ADR-092: A Grammar Fold’s init, step and merge Are Code

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

1. Context

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.

2. Decisions

D1 — The three expressions are walked by the ordinary expression walk.

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.

D2 — Inside the frame the rule’s own pattern makes.

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.

D3 — Outside 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.

D4 — ADR-084 D4’s walk is removed rather than kept beside this one.

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.

3. Consequences

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.

4. What this does not decide

5. What is built

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.

This site is open source. Improve this page.