Status: Superseded by ADR-296 — its D1-D8 are D16-D23 there Implements: ADR-009 D1–D3 and D5 on the Nikaia side Related: ADR-001 D4 (Stage 0 is a transpiler), ADR-004 D1 (the one lowering this is the construct-by-construct half of), ADR-007 (the DSL protocol), ADR-008 (views and the input lifetime), ADR-012 (spans, which close half of §4)
Everything the parallel grammar protocol needs exists in the parser backend.
Nikaia could not reach any of it, because a grammar item in the AST was:
Grammar { name: Ident, content: String }
A string is a shape no compiler can do anything with. It cannot verify a frame,
cannot find the entry rule of a dsl … from …, cannot decide the driver’s
Parallelism from user_parallelism, and cannot even rewrite @frame to #[frame]
without parsing it first.
This ADR records what the lowering is, and — more importantly — what it deliberately refuses to do.
grammar Name { … } becomes a structure: rules, alternatives, patterns, the
commit point, repetition bounds, fold/par_fold with their expressions, and
the keyed @frame attribute. dsl Name from input becomes an expression naming
a grammar in the same file.
Keeping the body as text fails at the first question anyone asks of it. Which
rule does dsl Measurements from data enter through? Is it a par_fold, so
that a piece driver exists? Does this grammar declare a frame at all? All three
are answered by looking at the rules and none by a string.
Every action block, every init/step/merge, every function body is emitted
as written, name for name. Where Nikaia and Rust disagree about what a name
means, the emitted program says so by failing to compile; the printer does not
invent semantics to paper over it.
The case this rules on: a par_fold whose step is written
fn(acc, m) { acc.record(m) }, where record is a &mut self method. A
monoid’s step is (Acc, Item) -> Acc, so making it fit means emitting
|mut acc, m| { acc.record(m); acc } — inferring from the shape of the tail
expression that the method mutates and the accumulator should be threaded.
Rejected: acc.merged(m), which returns a new accumulator, has the same shape
and would be silently mis-lowered. The adapter is a typing question, answerable
once the compiler lowers impl blocks and knows the receivers — never in an
emitter.
@frame lowers key for key, and Nikaia does not re-implement its check.@frame → #[frame], @frame(boundary: "\n") → #[frame(boundary = "\n")],
and unchecked passes through. The boundary keeps its escape text, so what the
source wrote is what the backend is handed.
The check that a boundary cannot occur inside a frame stays upstream. Two implementations of ADR-009 D1’s “checked, never rewritten” would eventually disagree about what is safe, and the version of that disagreement that matters is where Nikaia accepts a grammar the backend rejects — a compile error against generated code, blaming a line the user never wrote. One verdict, one place.
unchecked passes through for the same reason: it is the user’s assertion to
the checker, and a second opinion is only a second thing to get wrong.
user_parallelism chooses the Parallelism, and nothing else.dsl Name from input lowers to the piece driver with Parallelism::Auto under
auto and the identical call with Parallelism::Off at 0. That is the whole
of ADR-009 D2’s degradation: same accumulator, same
merge, same result, no threads.
The two emitted programs differ in one token. Nothing else in the lowering reads either switch, because nothing else may: a switch selects a build, not a dialect (ADR-037 D1), and the moment it changes what a program means the language has two of them.
A grammar whose entry rule is not a par_fold has no pieces to cut and no merge
to apply, so it gets the rule’s own parser, run once over the whole input.
par_fold gets its binding from the compiler.ADR-009 D2 requires a par_fold to be the whole body of its rule,
so there is nothing an action block could add: the value of the rule is the
value of the fold. Nikaia accepts the rule without one and supplies the binding
the backend wants.
Everywhere else an action is required, which is also what makes a rule’s pattern
unambiguous: the pattern ends at its -> rather than wherever the next rule
happens to begin.
&str in a rule’s return type is emitted as &'a str, 'a being the lifetime
the backend gives its input. A struct with a view field is emitted with <'a>
and named with it wherever it appears, transitively.
How it is spelled follows the position and nothing else: named inside the grammar module and on the structs it builds, where the two must be tied together; elided in a function signature, which declares no lifetime and would not compile with one named.
This is ADR-008 in the only place a bootstrap compiler can express
it — the source never writes a lifetime, because & is a view marker there.
The tether-state lattice is not implemented: every view is emitted as
Borrowed, which is correct for the grammars written so far and wrong the
moment one escapes its buffer. A named limitation, not a design.
WS = (WSE | COMMENT)* is the only place comment skipping can be said, since
the backend inserts WS between the tokens of every syntactic rule.
The backend’s built-in string recognises \\ and \" and no other escape —
one short of "\n", which is the boundary every frame in the corpus is written
with. So the compiler brings its own lexical STRING, which keeps escapes as
written. Keeping them is what makes D3’s “the backend is handed the same "\n"”
true.
The lowering emits grammar! { … } and lets the parser backend expand it, name
for name with what the .nika file wrote. A grammar item is never dropped
silently, which would compile a program with its parser missing.
The grammar half of the flagship lowers end to end. Its emitted Rust is checked
in, and the test guarding it does not only compare text: it include!s the
file, so the backend has to accept every line, and it runs the generated driver
at five different cuts to check that the answer does not depend on how the input
was divided — on an input it accepts and on one it rejects.
Named so that “the compiler lowers the grammar protocol” is not read as more than it is:
impl blocks and methods — without them a fold’s new, record and
merge have no definitions to check against, which is the other half of D2.
(Closed by ADR-013.)throws/catch/?? and string interpolation. (Closed by
ADR-013 and ADR-023.)sync checking — the keyword parses and is recorded; nothing verified
that a parallel section contains no pause. (Closed by
ADR-027.)std mapping — (closed by ADR-013 D1: std is a crate.)Input provenance, and therefore the hasher. (Closed by ADR-020.) Worth recording why it could not be done here: choosing the hasher means knowing that a map’s keys are views into a buffer from a trusted source, which is dataflow through a grammar’s return types and a struct field — a semantic inference, which D2 forbids an emitter to make.
So Stage 0 did the only correct thing available to it and behaved as though every buffer were untrusted, which is ADR-010 D1’s own rule applied to the widest barrier there is: an analysis that did not exist yet.
With the ledger it does exist, and the hash the answer picks is worth 120.0 M → 89.1 M instructions on 200 000 rows — 26 % of the flagship, the largest single item this section ever listed. Stage 0’s analysis is exactly one buffer, because ADR-008 gives a compilation unit one input lifetime; the join over a program’s sources is not a coarsening of a per-buffer analysis, it is the per-buffer analysis for the one buffer this representation can express.
Still not true: ADR-010 D5 requires the build to fail on a
target with no entropy source rather than seed predictably. The underlying
RandomState makes no such promise, so neither does Nikaia.
@frame → #[frame] alone requires parsing (D1)..nika lines —
the right fix for an error’s provenance is spans, not a second checker whose
verdicts can drift from the first (D3).Reading { name, temp } and the brace that
opens a block are told apart by parsing if/for heads with a brace-free
expression rule, the same restriction Rust imposes. A struct literal in such a
head must be parenthesised, and the error if it is not will be about the
block, not the literal.