{ Begins — So && and || Join ItStatus: 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)
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.
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.
{.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.
{ 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.
?? 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.
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 %.
[#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.
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