ADR-087: A Head Holds Every Expression; Only a Brace-Led One Needs Parentheses

Status: Superseded by ADR-301 — its D1-D4 are D4-D7 there, its D5 is D8 Date: September 14, 2026 Target Version: Nikaia 0.0.8 Supersedes: ADR-086 D4, which left ?? out of the head for want of a program that had run into it. The rule that record states — D3, a head parses the same language as a body, minus exactly the forms a { begins — is untouched and is what this applies; what is withdrawn is the exception D4 made to it Related: ADR-086 D2 and D3 (the brace argument and the rule), ADR-066 D4 (?? is right-associative), ADR-054 (as names a type), ADR-052 (null), Part I 2.3 and 3.2


1. Context

ADR-086 D3 wrote the rule down and D4 immediately made an exception to it: ?? cannot begin a block either, so the rule reaches it, and it was left out because no program had run into it and “adding a construct on a principle alone, in a record whose other half is evidence, mixes the two kinds of change.”

That reasoning was sound about evidence and wrong about the size of what it was excepting. Asked what else the head refuses, the answer is not one level:

if a ?? 0 > 3 { … }      expected `{`; found unexpected token `?`
if a as i64 > 3 { … }    expected `{`; found unexpected token `as`
if a == null { … }       expected one of: `!`, `-`, `false`, `true`;
                         found unexpected token `null`
if (a, b) == pair { … }  expected `{`; found unexpected token `,`

Four forms, none of them brace-led, each refused in the position Part I 3.2 describes with no restriction at all — and null is the one that says what kind of defect this is. null is not an operator or a precedence level; it is a literal, and a head that takes false and refuses null is not a restriction anybody could have deduced. It is a list that was copied and came up short.

D4’s test — has a program run into it — is the wrong test for this, and §2 D3 says what the right one is.

2. Decisions

D1 — The head chain holds every level and every primary that is not brace-led. There is no exception.

Added: head_coalesce (??), head_cast (as), and null, a tuple and dsl … from … among the primaries. With ADR-086’s head_or and head_and the chain is now, top to bottom: ??, range, ||, &&, comparison, +, *, as, unary, postfix, primary — which is the ordinary chain, level for level, with the same associativity and the same precedence.

head_primary is primary_expr minus a list that can be written out: struct_lit, block_expr, if_expr, match_expr, overlap_expr, a dsl … { … } eod, and spawn, whose lambda is a brace. Every one of those begins with, or reaches, a { that the body would claim.

D2 — The excluded forms are excluded from a spelling, not from the position: parentheses reach them, and always did.

if p == (P { x: 1 }) { … }
if (fs::read_to_string(p) catch { "" }).len() > 0 { … }
if (match n { 1 => 10, else => 20 }) > 15 { … }

All three parse today and did before this record. paren_expr takes a whole expr, so the head’s restriction stops at the outermost token: a head may hold anything the language has, and a form that starts with a brace has to be parenthesised so that the body’s { is still the body’s.

That is worth stating as a decision rather than left as a fact about the implementation, because it changes what the restriction is. Not “these constructs are unavailable in a condition” — which would be a language restriction needing a justification — but “the token that opens a body is reserved for the body”, which is a syntactic necessity with a one-character way round it.

D3 — The test for what belongs in the head is the rule, not a reproduction.

This is the part that supersedes ADR-086 D4, and it is a decision about how to decide.

A reproduction is the right evidence for adding a construct to the language: something that did not exist, that has a cost, and whose absence somebody has to have felt. It is the wrong evidence for an inconsistency in where an existing construct may stand. Waiting for a program to run into one of those does not gather evidence — it just means the next person meets it, and there is nothing to learn from their meeting it that is not already known.

The difference is whether the question is should this exist or does this already exist everywhere it should. D4 applied the first test to the second question. The head is now defined by the rule, and a level absent from it is a defect rather than a decision waiting for a witness.

D4 — Where a new alternative goes in head_primary is measured, not tidy.

Two placements were measured and neither is where the ordinary grammar puts them.

dsl … from … goes last. In primary_expr it stands near the front because the brace-led forms around it constrain the order; in head_primary those are gone and nothing constrains it, since dsl is a reserved word and no other alternative can take one. At the front it was a failed keyword match in front of every primary in every head, and it cost the one thing a parse error has: if { } answered “expected one of !, -, dsl, false, true, identifier”, leading with the rarest of the six.

null goes after the literals rather than beside false. primary_expr lists the two together because they read as a pair; that is not an ordering constraint, because null is reserved too. Third in the list it was one failed match in front of every name in every head — worth 0.45 % of a compile on a head-dense file, measured.

Both follow ADR-084 D8’s rule for the statement alternation, and this record adds the reason it generalises: a PEG alternation is an ordered list, so where a rare alternative sits is a cost paid by every common one in front of which it is put.

D5 — What it costs.

file ADR-086 head complete head change
2,000 while heads 290,373,660 300,405,725 +3.455 %
examples/json/src/main.nika 41,179,514 41,682,708 +1.222 %
examples/n-body/src/main.nika 22,911,397 23,141,242 +1.003 %
examples/1brc.nika 16,226,828 16,359,891 +0.820 %
examples/k-nucleotide/src/main.nika 16,699,393 16,835,512 +0.815 %
examples/tally/src/main.nika 9,106,188 9,151,692 +0.500 %

5,016 instructions per head, against ADR-086’s 2,536 for the two connectives — roughly double for roughly double the additions, and the reason the ratio is not lower is worth knowing: head_cast and the new primaries are tried per operand, not per head. A head of two operands pays for them twice.

0.5 % to 1.2 % of a compile on real programs. That is more than ADR-086 cost and it is accepted for D3’s reason: what is bought is not four constructs but the end of a class of surprise, and a position that cannot be described in one sentence is a position every reader has to test by hand.

3. Consequences

Part I 3.2 says what a head holds, in one sentence and with the parenthesis form beside it, replacing the sentence ADR-086 put there — which named &&, || and ! and was therefore a list of the same kind that produced this record.

A parse error in a head is two frames deeper and its expectation list is longer. That is the standing trade of a deeper chain and it is the same one every level of the ordinary grammar already makes.

?? binds looser than a comparison, in a head exactly as everywhere else. a ?? 0 > 3 is a ?? (0 > 3) — which reads oddly, was found by this record’s own first test being wrong about it, and is not the head’s business: the head mirrors the body, and whether the body’s precedence is right is a question about ADR-066 D4 that this does not touch. It is on [#214](https://github.com/Nikaia-Language/Nikaia/issues/214) as a suspicion with its reproduction.

Two defects were found and neither is this one’s. A null passed inside the second hole of an f"…" is emitted as Some(None) — a double wrap that rustc refuses — where the same call outside a hole, or in a hole of its own, is emitted correctly. Reproduced identically on the build before this change, so it predates it; it is on [#225](https://github.com/Nikaia-Language/Nikaia/issues/225). And the measurement method itself turned one up: the first baseline built for §D5 was wrong, because the script that removed the head’s new alternatives matched the first occurrence of the same text in primary_expr instead. It produced a binary that could not parse a tuple anywhere, and it produced numbers — a −92 % that was a parse failure wearing a percentage. §4 says what replaced it.

4. What is built

D1 and D4 in the parser: head_expr is the coalesce level, head_range below it, head_cast between head_mul and head_unary, and head_primary carries null_lit, tuple_expr and dsl_from_expr in the positions D4 measured. The comments carry the reasons.

Five tests in crates/nikaia/tests/jumps.rs, four of them run rather than read. The one that is not is the important one:

a_head_parses_what_a_body_parses lowers the same expression in both positions and compares the two lowerings. A missing level fails it; so does a level that mirrors its counterpart badly, which a per-construct test would not catch. Six expressions, including a ?? 0 > 3 — the case that caught this record’s own first draft assuming a precedence the language does not have.

The baseline for D5 is git show 5a1e99a:crates/nikaia/src/parser/mod.rs, the file as ADR-086 left it, rather than a hand-edited revert. §3 says why that matters: the hand-edited one was silently wrong, and an A/B against a broken baseline produces numbers that look like findings.

This site is open source. Improve this page.