ADR-142: A Grammar’s Action May Not Pause

Status: Superseded by ADR-296 — its D1-D3 are D35-D37 there Date: September 19, 2026 Target Version: Nikaia 0.0.23 Related: ADR-082 (a grammar is entered by an ordinary call), ADR-009 (@frame and the parallel parse), ADR-092 (a fold’s init, step and merge are walked like any other code), ADR-010 D1 (a promise is taken away on doubt), ADR-050 and Part II 12.6 (the places that already demand sync), Part II 10.1 and 10.7

1. Context

A rule’s action is arbitrary Nikaia, so it may call something that pauses — and nothing refused it. The emitter wrote .await inside the synchronous parser the grammar! macro generates, and the backend answered:

error: `await` is only allowed inside `async` functions and blocks

Relayed onto the .nika line (ADR-056), and still the backend’s words about a construct this compiler let through — which is Part III C.1’s class for a shape the language allows.

Found by building ADR-140 D3, from the other end. Entering a grammar through a path made the entry a call by name, so the analyses read its contract instead of answering nothing about an unresolvable receiver — and the contract carries no sync, so every function that parses became async. The old answer was the analysis failing open: a caller kept a promise nobody had derived. The new one is honest and expensive, and both of them are answers to a question nobody had asked: may an action pause at all?

2. Decisions

D1 — An action may not pause, and a call that can is refused where it stands.

grammar Nums {
    pub rule number -> i64 = d:dec[i64](../../specification/adr/digit+) -> {
        let t = io::read_to_string()   // error[NK2209]
        return d
    }
}

The demand is the one ADR-050 makes of an overlap branch and Part II 12.6 makes of a par_iter lambda, in the place a parser needs it: sync, asked of every call an action makes and of every call a fold’s init, step and merge make (ADR-092 walks them already).

A parser is computation over bytes that are already there. That is not a restriction invented for the implementation — it is what makes ADR-009’s @frame sound: a parse that can be cut into pieces and run on several cores at once is one whose steps do not wait on the world. An action that reads a file is a second pass wearing a grammar.

D2 — So a grammar’s entry is sync, and it says so in the ledger.

[fn."Nums::number"]
pub = true
sync = "asserted"
throws = ["?"]
signature = "(input: ?) -> i64"

sync and not an inference, because D1 is a rule of the language rather than a property of this grammar: every entry has it, and NK2209 is what happens when an action contradicts it — which is NK2202’s shape one construct over.

This is what the column is for. A function whose body is one Nums::number(text) is sync again, so the async that ADR-140 D3 spread through every parsing program goes away — and it goes away because something is now known, not because the question is being avoided.

D3 — What this does not restrict.

An action may fail — a rule past a commit point can (ADR-023 D9), and the entry’s throws = ["?"] is unchanged. An action may call anything that does not pause, including into std and into the program’s own functions. And a grammar may be entered from a body that pauses; what is refused is pausing inside the parse, not around it.

3. Consequences

4. What this does not decide

5. What is built

All of it. NK2209 is raised where a call that can pause stands inside a rule’s action or inside a fold’s init, step or merge, and the entry’s contract carries sync = "asserted".

It is one flag and the walk was already there. ADR-092 put a fold’s lambdas through the ordinary expression walk and the action block was going through it anyway, so what this adds is a question asked at a_call_that_may_pause — the one place both the call path and the method path already meet.

The message names the rule, because a grammar is a page of rules and a caret on a call inside one of them is not enough to find it: number’s action calls io::read_to_string, which can pause.

Only where the ledger answered, which is every other sync rule’s convention (Part III C.4): a call whose callee nothing describes is not refused. That is the one place D1 is weaker than it reads, and it is the same weakness NK2206 and NK2202 have — a refusal on a guess is a correct program refused.

And the column took the async back — the column, and one leaf. Writing sync on the contract was not enough on its own: the fixpoint in contracts::sync reads this unit’s call graph, a grammar entry has no node in it, and holds.get(callee).unwrap_or(false) reads an absent node as pauses. That is the same trap ADR-109 D1’s comment names for a trait method, and the answer is the same one — the entry is inserted as a leaf that holds. examples/inventory’s read is pub fn again and its ledger says sync = "inferred".

keeps, touches and locks do not come back, and should not. D1 decided what an action may not do to time, and nothing about what it keeps or touches — so read still has locks = "?" and no keeps or touches, which is a restriction added on doubt (ADR-010 D1’s polarity, the right way round this time). Those three are the derivation #134 still names, and §4 above says so.

Five tests in crates/nikaia/tests/grammar_actions.rs.

This site is open source. Improve this page.