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
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?
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.
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.
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.
sync, which is nine programs in
examples/.sync is demanded rather than inferred, and the list of
them is short and stated: an overlap branch, a par_iter lambda, an
interrupt handler (ADR-119), a grammar’s action.async. It is winnow-grammar’s
question before it is this language’s, and no program has asked.keeps,
touches and locks — which are still ? and still want the derivation
#134 names.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.