ADR-013: Running the Flagship — std, Methods, and What Stage 0 Refuses to Infer

Status: 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)

1. Context

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.

2. Decisions

D1 — Nikaia’s 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).

D2 — The fold adapter is a lookup in the program’s declarations.

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.

D3 — throws, catch and ?? lower to what they already mean.

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.

D4 — The anonymous constructor is 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.

D5 — A lambda takes the arguments it names.

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.

D6 — An interpolated hole may say how it is written.

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.

D7 — Where Stage 0 does not infer, the source says it.

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.

3. What this makes of the corpus

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.

This site is open source. Improve this page.