std, Methods, and What Stage 0 Refuses to InferStatus: Accepted
Closes: the fold-adapter question left open by ADR-296 D17, and
most of ADR-296
Superseded in part by: ADR-277 D1 and D2 (D5’s lambda arguments),
ADR-280 (D3’s throws)
Related: ADR-296 (the Stage 0 lowering),
ADR-014 (what D1’s fs::map became),
ADR-024 (the type checker §4 predicted)
ADR-296 got the flagship’s grammar onto the parser backend and
named the rest: impl blocks, throws/catch/??, string interpolation, and
the std names the program calls. This is that rest, and the measure of it is
not that the constructs parse — it is that the file compiles and prints what the
benchmark asks for.
Two questions had to be answered rather than deferred: where the meaning of
fs::map lives, and when the compiler may adapt a method it was handed.
std is a crate, not a table inside the emitter.fs::map, cli::args, HashMap and the list operations live in
crates/nikaia-std, and a program importing from std gets
pub use nikaia_std::prelude::*; in its preamble, so the names resolve as
written.
The alternative was a substitution table in the emitter — fs::map(x) prints as
std::fs::read_to_string(x). Rejected, because it makes the semantics of the
standard library into strings inside a printer: unreadable, untestable, and
impossible to document. As a crate, every function has a doc comment stating
what it guarantees and what it does not yet do, in the place a reader looks.
One name had to change. d.to_digit() cannot be a method: Rust’s char has
an inherent to_digit(radix), an inherent method beats a trait method, and a
call that resolves to the wrong thing is worse than a name that reads
differently. It is the free function digit_value(d).
ADR-296 D17 refused to thread a fold’s accumulator by inferring
from the shape of the step’s body, because acc.record(m) and acc.merged(m)
look identical and mean opposite things, and said the answer waits for impl
blocks. It does not have to wait any longer:
par_fold(MEASUREMENT, Summary::new, fn(acc, m) { acc.record(m) }, Summary::merge)
record is declared fn record(&mut self, m: Reading) sync — it takes its
subject by mutable reference and returns nothing, so the accumulator must be
threaded and the emitter writes |mut acc, m| { acc.record(m); acc }. A method
that returns a new accumulator is left exactly as written.
A method name two impl blocks disagree about is left alone rather than
resolved by preference. The emitted code then fails to compile, which is the
right outcome for an ambiguity the compiler cannot settle.
throws, catch and ?? lower to what they already mean.throws → Result<T, Box<dyn Error>>, boxed rather than lowered to Nikaia’s
own error types (ADR-023). A return inside such a function
carries the Ok, and a function returning nothing ends in Ok(()).catch → a match, with the error bound to error.?? → unwrap_or_else(|| fallback.into()) — the into() because the
fallback is written as the value it stands for, not as the type the option
holds.A lambda’s return leaves the lambda, so it never carries the enclosing
function’s Ok.
Changed by ADR-280: throws no longer lowers to Box<dyn Error>; a function’s failures are its error types, carried in ADR-280’s envelope.
new, and the call site says which form it is.An anonymous pub fn(…) -> Type has no name and Rust has no such thing, so it
is emitted as new. Type(positional) calls it; Type(named: value, …) is the
struct literal. They are told apart by requiring the named form’s first field to
carry a value — otherwise Stats(x) would read as a struct with one shorthand
field.
Changed by ADR-277 D2: a lambda’s arguments are the ones it names, and there are no automatic names; fn: expression is removed (ADR-277 D1).
fn(acc, m) { … } becomes a Rust closure of two arguments, fn { … } one of
none. Rust closures have fixed arity, and the names written are that arity.
f"{a}={s.mean():.1}". The first colon not inside a call or an index separates
the expression from the format spec; Stats(min: 1) keeps its colon, being
inside parentheses. {{ and }} are literal braces. Without this, the
benchmark’s own output format could not be written at all.
This is the decision the other six rest on: the lowering stays syntactic, so where a program relied on inference the compiler does not have, the program is what changes. The flagship’s edits, and what each teaches:
| Change | Why |
|---|---|
sum: first → sum: first as i64 |
Nikaia states no implicit numeric widening, so the conversion is written |
{s.min / 10.0} → {s.min as f64 / 10.0:.1} |
the widening again, and D6 for the decimal |
merge(&mut self, other: &Summary) → other: Summary |
the loop takes entries out of it, so the parameter is kept (ADR-094 D2) |
d.to_digit() → digit_value(d) |
D1 — a limitation, not a language statement |
Each of these is a sentence about the source rather than a workaround: a reader
learns that Nikaia does not widen integers silently. Whether an argument is lent
or kept is never the caller’s to write — ADR-094 D1 — so no row
here puts a & at a call.
A type checker (ADR-024) does not change this: a checker that quietly widened an integer would be checking a different language. What knowing the types buys is the diagnostic, not the coercion.
The flagship compiles and prints what the benchmark asks for:
$ ./brc measurements.txt
{Bulawayo=8.9/15.5/22.1, Hamburg=-3.4/2.9/12.0, Palembang=38.8/38.8/38.8}
A test lowers the real example, compiles it, runs it over an input small enough
to check by hand, and compares the line; it also runs it against a missing file,
because the catch in main is part of what the example claims.