ADR-086: A Head Parses the Same Language as a Body, Minus the Forms a { Begins — So && and || Join It

Status: Superseded by ADR-301 — its D1-D3 keep their numbers there, its D5 is D8; its D4 is not carried (withdrawn by ADR-087 D3) Date: September 14, 2026 Target Version: Nikaia 0.0.8 Amends: ADR-084 §4’s first table row, whose −25 % for a while that stops mid-body turns out to have measured this gap rather than the one it was attributed to — see §3. Its decision is untouched, and §7 of break-continue-cost.md had already said why Related: ADR-084 (during which this was found), ADR-011 D2, Part I 3.2 and 3.3, [#206](https://github.com/Nikaia-Language/Nikaia/issues/206)’s entry on the connectives missing from a head (the reproduction, now closed)


1. Context

The head of an if, a while and a for is parsed by a grammar of its own — head_expr and the chain below it — and that chain exists for one reason: in for d in whole { … } the { opens the body, so a head may not contain the forms that begin with a brace. A struct literal is the one that matters; while n < 5 { … } read as a variable called while, a comparison thrown away, a bare block is the trap the rule’s own comment records from the day the rule did not exist.

That chain was built by copying the ordinary one and stopping at head_cmp. What stopped there was not only the brace-led forms:

fn f(a: bool, b: bool) -> i64 {
    if a && b {
        return 1
    }
    return 0
}
Parse error:
expected `{`; found unexpected token `&` at line 2, column 10
   2 |     if a && b {
                ^

Part I 3.2 writes a condition as “a bool” with no restriction on what may produce one, and && is in the expression grammar. It was in every position but the one a condition is written in.

Found by writing a benchmark, which is worth recording because it is the reason this was not found earlier: ADR-084 needed the loop a language without break has to write, and that loop is while i < n && running. So the workaround for one missing construct ran straight into another, and [#206](https://github.com/Nikaia-Language/Nikaia/issues/206) carried the reproduction, as its entry on the connectives missing from a head, until this closed it.

2. Decisions

D1 — head_or and head_and join the head chain, mirroring or_expr and and_expr exactly.

Two rules and two tails, in the same places and with the same precedence: || binds looser than &&, which binds looser than a comparison, and a range binds looser than all of them. if a || b && c is a || (b && c), which is what it is everywhere else in the language.

D2 — They are safe because the brace problem is about what stands immediately before the {.

That is the whole argument and it is worth stating rather than assuming. Neither && nor || can begin a block, and neither ends an expression — after each comes another operand, which descends through the same chain to the same brace-free head_primary. So the token before the body’s { is drawn from exactly the set it was drawn from before, and the property the head chain exists to keep is untouched.

D3 — The rule the chain follows: a head parses the same language as a body, minus exactly the forms a { begins.

This is the decision, and D1 is an instance of it.

A head that differs from the ordinary grammar in any other way is a trap rather than a restriction: the program reads as valid Nikaia, and the compiler either refuses it for a reason no rule states — this case — or, worse, accepts it and means something else, which is what the missing while rule did. A reader cannot be expected to hold a second grammar in their head for the inside of a loop head, and nothing is bought by making them.

So the chain is a copy with one difference, in head_primary, and every level above it mirrors its ordinary counterpart. Where a level is missing, it is missing for the brace reason or it is a defect.

D4 — ?? is the one level the rule still reaches, and this record does not take it.

Applied strictly, D3 says head_coalesce should exist too: a ?? b cannot begin a block either, so the brace reason does not exclude it. catch genuinely is excluded — catch { … } is a brace-led form — and the brace-led primaries stay out by construction.

It is not taken here for the reason this record exists at all: no program has run into it. ADR-084’s benchmark produced a reproduction for &&; nothing has produced one for ?? in a head, and adding a construct on a principle alone, in a record whose other half is evidence, mixes the two kinds of change.

The polarity makes waiting free, which is the same argument ADR-050 D7 made about un-reserving a word: widening what parses breaks nothing and narrowing it does. The day a program writes if a ?? false { … }, the level is added and no program that ran before stops running. If the reverse were true this record would take it now.

D5 — What it costs, measured.

Two more precedence levels are descended for every head in every program, whether or not it uses them.

file without the levels with them change
2,000 while heads 285,291,601 290,363,639 +1.778 %
examples/json/src/main.nika 41,014,193 41,180,196 +0.405 %
examples/k-nucleotide/src/main.nika 16,660,957 16,703,351 +0.254 %
examples/1brc.nika 16,193,052 16,232,667 +0.245 %
examples/n-body/src/main.nika 22,877,060 22,911,369 +0.150 %

2,536 instructions per head, so about 1,268 per level — a failed match of the operator and the bookkeeping that records it could have matched. On a real program that is a tenth to four tenths of one percent of a compile, against a construct that was simply missing from the position it is most used in. Taken.

The method is ADR-084 D8’s: two builds whose source differs in nothing but these rules, five runs each, medians — because an unrelated eighty lines move a whole-compiler total by up to 0.8 % through code layout, and an identical binary run twice differs by 0.11 %.

3. Consequences

[#206](https://github.com/Nikaia-Language/Nikaia/issues/206)’s entry on the connectives closes, and it is the second of the three defects ADR-084 turned up to do so.

ADR-084 §4’s first row measured this gap and not the one it names. This is the part of the record worth reading twice.

That row says a while that learns it is finished mid-body costs −25.0 % with a break against the shape that replaces it. The shape that replaced it had to nest an if inside the loop, because while i < n && running did not parse. It parses now, and the same benchmark with a third member says:

a `while` that stops mid-body - n = 2000000
  a flag, and a nested `if`         8,501,129 baseline
  a flag, and `&&` in the head      6,500,940 -23.5%
  `break`                           6,501,105 -23.5%

The flag and the break cost the same — 165 instructions apart over two million turns, which is nothing. The −25 % was the cost of the nesting the head grammar forced, and the jump was never what it measured.

Two things follow and they point in opposite directions, so both are said.

It does not touch ADR-084’s decision. That record does not rest on this row, and says so in its own words: what decided it was the for row — a loop that cannot be stopped at all, where the gap is unbounded in n — and break-continue-cost.md §7 already wrote “a record leading with the two percentages would have made a weaker case out of stronger evidence.” The stronger evidence is untouched.

And it is exactly the mistake a measurement can make. An A/B measures the difference between two programs, and it is only about the construct if the construct is the only difference. Here the baseline carried a second handicap that the same investigation had already filed as a defect three sections away, and nothing connected them. The benchmark now holds all three shapes, and stop_with_flag is kept as it was rather than rewritten: a benchmark edited under a published number is a number nobody can check.

An if statement’s head is parsed twice, which this measured by accident and is recorded because it is a real compile-time cost with nothing to do with this change. 2,000 while heads cost 2,536 instructions each; 2,000 if heads cost 5,072 — exactly double — because assign_stmt comes before expr_stmt in the statement alternation and parses the whole if … as a candidate assignment target before failing to find a = and backtracking. The ordering is load-bearing (x = 1 needs it), so this is not a line to change; it is a thing to know before anyone reads a per-head number again.

The error corpus gains two entries and two frames. A head’s “also possible here” now offers && and ||, and a parse error inside a head carries in head_and and in head_or in its rule trace — two more lines in a message that was already long. That is the honest cost of a deeper chain, and it is the same trade every precedence level in the ordinary grammar already makes.

Part I 3.2 gains a sentence saying what a head may hold, because the restriction that remains — no brace-led form — was nowhere written down, and an unstated restriction is what produced this record.

4. What is built

D1: head_or, head_or_tail, head_and and head_and_tail in the parser, with head_expr and head_range_tail descending to head_or where they descended to head_cmp. The comment at the chain carries D2’s argument and D3’s rule.

Tested by crates/nikaia/tests/jumps.rs: the two connectives in a while head and in an if head, run rather than read; the precedence a || b && c, also run; and a range head unchanged, because head_range_tail moved.

Measured by crates/nikaia/tests/measure.rs, whose first group now has three members for §3’s reason:

cargo test -p nikaia --test measure -- --ignored --nocapture what_a_jump
This site is open source. Improve this page.