ADR-177: A Grammar Runs While the Program Is Built by Compiling the Parser It Generates

Status: Accepted Date: September 21, 2026 Answers: #178, which carried this as work and as the argument against the other way of doing it Related: ADR-310 Q4 (the second compilation, named as a cost), ADR-310 and ADR-310 D3 (asset("…"), where the bytes come from), ADR-287 D6 and ADR-287 (the build-time evaluator this runs inside, and what bounds it), ADR-311 D1 (what a result has to be to cross), ADR-296 D24 and D25 (a grammar is entered by an ordinary call, and every entry rule is an entry), ADR-021 (the build cache that turns the cost from every build into when the grammar changes), Part II 10.1 and 10.2


1. Context

Part II 10.2 promises one grammar, two stages: the same syntax and the same meaning at build time and at run time. The run-time half has worked since grammars did. The build-time half — comptime CONFIG = Json::value(asset("config.json")) — needs something behind it.

There is an obvious way to build it, and it is the wrong one.

2. Decisions

D1 — A grammar is not interpreted here, and the reason is not the size of the work.

The compiler holds the grammar tree already. Walking it and matching against the input is the short path, and it is refused.

winnow-grammar is a code generator: its model crate parses the grammar language, validates it and analyses it, and hands the result to a macro that writes a parser. Its own summary says so — intended to be used by procedural macros that generate parsers. There is no interpreter in it to borrow.

So interpreting here would be a second implementation of the same semantics, and the two would have to agree exactly. Part II 10.2’s promise would stop being a property and become a hope. The disagreements would not land where anybody looked for them; they would land in the corners — implicit whitespace, repetition bounds, the commit point, frames and resynchronisation, interning, spans — and would present as this file parsed while the program was built and fails while it runs, for the same file and the same grammar.

D2 — What happens instead is that the generated parser is compiled and run.

The compiler writes a small Cargo project holding the grammar’s own lowering, the types its rules produce and a driver, builds it, and runs it over the bytes. Then there is one implementation of the grammar language and the agreement is a tautology rather than a claim.

It needs no new security model. A grammar’s action blocks are Nikaia and ADR-287 already says what a build-time body may do; the bytes come from asset("…") and ADR-310 already bounds which. What is new here is neither the permission nor the input.

D3 — The cost is a second compilation, and the cache is what makes it bearable.

ADR-310 Q4 named this cost. The sub-project is keyed on the program text that went into it — the grammar’s lowering, the types, the dump — so it is written and built when the grammar changes and reused otherwise. The compiler already emits Rust and already drives Cargo, so the machinery is not new.

D4 — The result has to cross, and a result that cannot is refused by name.

Changed by ADR-311 D1: what crosses is that record’s set — an integer, a finite float, a bool, text, a list, a pair, a struct and an enum variant, each in its view form — and a rule whose declared result is outside it is refused here.

What a comptime hands the program is a value the build carries and the language below can write (ADR-311 D1). A rule that hands back something outside that set has nothing to arrive as.

Refused before a parser is compiled, from the rule’s declaration rather than from what it produced: there is no point building a parser to learn that its answer has nowhere to go. NK1178 names the type and says why it cannot cross.

D5 — Invalid input fails the build, in the parser’s own words.

Part II 10.2 A says it and this keeps it literally: the parser’s diagnostic is relayed whole rather than re-worded. It is in the grammar’s own vocabulary and counts its line and column against the bytes that were parsed, which is the one place they mean anything.

3. Consequences

4. What this does not decide

5. The measurement

Over the corpus as first declared, neither grammar’s result crosses: examples/json’s Json is an enum with Vec payloads, and examples/config’s Section holds a Vec[Setting], a field that owns memory a const cannot hold. The mechanism runs and the crossing is what stops both, which is the question ADR-179 answers with ref Array[T].

This site is open source. Improve this page.