init, step and merge Are CodeStatus: Superseded by ADR-296 — its D1-D4 are D28-D31 there
Date: September 15, 2026
Target Version: Nikaia 0.0.8
Related: ADR-084 D4 and D6 (which closed the half a jump can
reach, and said why it went no further), ADR-023 (the grammar
these stand in), ADR-011 D2 (a name written is a name lowered, so
an unchecked name is rustc’s)
grammar Nums {
rule N -> i64 = d:dec[i64](../../specification/adr/digit+) -> { d }
pub rule file -> i64 = fold(N, zero, fn(acc, m) { nothing_declares_this })
}
lowered without a word, and |acc, m| { nothing_declares_this } reached
rustc. A fold’s three lambdas are expressions inside a pattern, and the
checker’s grammar walk takes a rule’s action block and nothing else — so
NK1117 never saw the body, and neither did anything else.
Why that is a defect and not a gap in coverage: the same walk is what refuses an undeclared name everywhere else in the language. A lambda the compiler does not look inside is a lambda whose errors are the backend’s, which is Part III C.1.
ADR-084 D4 had already closed the half a jump can reach, with a walk of its own over these same three expressions, and said why it stopped there: walking them with the whole checker would newly refuse programs for reasons that have nothing to do with the construct that found it. That is the question this answers, by measuring rather than by arguing.
Not a
second walk with rules of its own: self.expr on each of init, step and
merge, which is what every other expression in the language gets.
A fold can stand beside a binding in a sequence —
pub rule mixed -> i64 = head:N rest:fold(N, zero, fn(acc, m) { acc + m + head }) -> { rest }
— and that parses. Without the frame, head is NK1117, which was measured
before the two lines were written.
expected.The rule’s declared type is what an action block
builds, and the walk asserts it there. A fold’s init and step build the
parser backend’s running value on the way to that type, and holding them to it
would refuse the arithmetic every fold is made of.
The Expr::Closure arm already crosses a boundary and counts loops from
zero, so a jump in a fold’s step meets NK1132 through the path every other
lambda’s does. Two walks over one construct would be two chances to disagree,
and the compiler told us the first was redundant: with D1 in place the old
function was dead code.
The message improves in the bargain, which is the argument for doing it this
way round. The old walk ran with no barrier set, so it said “break … and this
is not in a loop” and offered return as the way out — wrong advice inside a
grammar action. Through the lambda path it says “the nearest loop is outside
this lambda”, and the help is to hand a bool back.
ADR-084 D6’s backstop in the lowering is untouched and stays: a walk cannot answer whether it is complete, and the emitter’s refusal does not have to.
Nothing in the corpus is newly refused. That is the measurement D4 of
ADR-084 asked for and could not do at the time, and it is the
whole of why this could be taken now: examples/1brc.nika and
examples/access-log/src/main.nika both write
par_fold(RULE, Type::new, fn(acc, m) { acc.record(m) }, Type::merge) — a path
as a value in two positions and a method on a ? in the third — and all of it
goes through untouched.
What a fold’s lambdas now get is every rule the language has, because they go
through the same walk: NK1117 for an undeclared name, NK1132 for a jump,
NK1128 for a name the language below cannot escape, and whatever is added
next. None of that had to be written for the construct.
#237’s last defect closes — a grammar fold’s init, step and
merge are not checked at all.
?
(ADR-023), so acc and m are too, and a type error inside a
step is still the backend’s. This closes the half about names, which is
the half a walk can answer without a type.init is plainly the wrong type is not refused here.
That wants what the parser backend does with the value, which is
ADR-023’s and not this record’s.Checker::folds_in replaces jumps_in_a_fold, and jumps_outside_a_loop — the
walk D4 removes — is gone with it. Called from the grammar walk, inside the
pattern’s frame and outside expected. Exercised by seven programs in
crates/nikaia/tests/fold_lambdas.rs: the reproduction, the init and the
merge of a par_fold, the two shapes the examples write, the sibling binding
that needs the frame, the jump and the loop written inside the step, and a sweep
of every .nika in examples/ and tests/samples/ that writes a fold — which
asserts it found some, so that a corpus gone quiet cannot pass by checking
nothing.