Skip to content

fix(zig): a statement that lowered to nothing is not a semicolon (+25 deep, +26 build-obj, 0 regressions) - #2984

Merged
gHashTag merged 5 commits into
masterfrom
w801
Aug 30, 2026
Merged

fix(zig): a statement that lowered to nothing is not a semicolon (+25 deep, +26 build-obj, 0 regressions)#2984
gHashTag merged 5 commits into
masterfrom
w801

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Zig has no empty statement

/tmp/x.zig:3:5: error: expected statement, found ';'
    ;
    ^

A statement that lowered to nothing became a bare ;. That does not merely add noise — one
such line kills the whole file, and every check inside it with it.

Measured: 491 bare ; lines across 81 generating specs.

Two source forms, one emitted construct

The notice now distinguishes them, and the corpus split is exact:

label count source
empty statement 475 a bench body's prose line — target: < 100ms — arriving as a childless StmtExpr through gen_bench_block's fallback
StmtExpr 16 defer <call>; in a test body, arriving with a child that gen_expr renders as nothing

475 + 16 = 491. One notice for every ; removed — nothing lost, nothing invented.

The defer case is deliberate on the parser's side: it keeps the node because "a dropped
defer silently removes a release, a close or a free"
. It reached an emitter that then
dropped it anyway.

The remedy is the one already used beside it

gen_invariant_block writes // invariant: X NOT CHECKED -- body was not lowered (T43)
when its body was discarded. The Verilog backend already emits benches as comments. Zig
was the only backend where a bare ; is fatal rather than merely useless
— in C it is a
legal null statement, which is why the same emission escapes consequence there.

So: render the child first (render_expr, an idiom already in this file), and if it comes
back empty, emit the same kind of notice instead of a statement that claims to be one.

Measurement — per spec, both directions, zig 0.16.0, no timeouts

ruler before after new regressions
zig test --test-no-exec 165 190 +25 0
zig build-obj -fno-emit-bin 282 308 +26 0

+25 is exactly the number an adversarial sweep attributed to this line when it split the
class: 32 for the whole remedy, 25 for the bare-; site. An independent implementation
reaching precisely 25 is the confirmation that split was real.

The other 7 are bench prose that does parse into an expression, so it renders to something
and this guard leaves it alone. Surveying the 120 still-rejected specs, the remaining error
families are unrelated — undeclared identifiers, initializer syntax, shadowing.

The deletion-equivalence, re-derived rather than inherited

Replacing a bench body with a notice is equivalent to deleting it, which is normally the
inflation trap. It is sound here, and I measured the justification myself rather than taking
it from the issue:

bench_* functions defined in the generated corpus : 780
                              ... with a call site:   0

zig test runs only test {} blocks, so emptying a bench_* cannot lose an executed check.

Mutation testing — and two mutants that survived the first round

mutant killed by
M1 restore the bare ; 4 of 6 tests
M2 emit the notice as code rather than a comment the_notice_names_…, zig_accepts_the_generated_file
M3 drop the ; from every statement a_call_statement_keeps_its_semicolon_and_is_not_a_notice
M4 label both forms the same the_defer_form_is_covered_too_and_labelled_differently

M3 and M4 survived the first run, and that is the useful part.

  • The original "a statement that lowers keeps its semicolon" test used y = x + 1, which
    reaches StmtAssign and never touches the arm under test. A test of an adjacent path
    that happens to hold. A bare call statement is what goes through StmtExpr with a
    non-empty rendering, and it is what the mutant destroys.
  • Nothing asserted that the two labels differ, so a label saying empty statement for a
    statement that has a child — sending the reader to the wrong construct — passed.

The fixture was also wrong to start with. I invented bench "b" { measure: ... } from the
issue's prose. It does not parse, t27c exits 1, and the test asserted on an empty string.
The real spelling is bench NAME with no braces (compiler/cli/gen.t27:499). Read the
construct out of the corpus, not out of the report about it.

Closes #2974


Closes #2974

🤖 Generated with Claude Code

Zig has no empty statement -- it answers "expected statement, found ';'"
-- so a bare `;` does not add noise, it kills the whole file and every
check inside it. Measured: 491 bare `;` lines across 81 generating specs.

Two source forms reach one emitted construct, and the notice now tells
them apart. 475 are a `bench` body's prose line arriving as a CHILDLESS
StmtExpr through gen_bench_block's fallback; 16 are `defer <call>;` in a
test body, arriving WITH a child that gen_expr renders as nothing. The
parser keeps that one deliberately -- "a dropped `defer` silently removes
a release, a close or a free" -- and the emitter dropped it anyway.
475 + 16 = 491: one notice for every semicolon removed.

The remedy is the one used beside it. gen_invariant_block already writes
"NOT CHECKED -- body was not lowered (T43)", and the Verilog backend
already emits benches as comments. Zig was the only backend where a bare
`;` is fatal rather than merely useless, which is why the same emission
escapes consequence in C.

Measured per spec, both directions, zig 0.16.0, no timeouts:

    zig test --test-no-exec    165 -> 190   +25   regressions 0
    zig build-obj              282 -> 308   +26   regressions 0

+25 is exactly what an adversarial sweep attributed to this line when it
split the class -- 32 for the whole remedy, 25 for this site -- so an
independent implementation reaching 25 confirms that split.

Deletion-equivalence re-derived rather than inherited: 780 bench_*
functions are defined in the generated corpus and 0 have a call site, and
`zig test` runs only test blocks, so emptying one cannot lose an executed
check.

Four mutants. M3 and M4 SURVIVED the first run, which is the useful part.
The "a statement that lowers keeps its semicolon" test used `y = x + 1`,
which reaches StmtAssign and never touches the arm under test -- a test
of an adjacent path that happens to hold; a bare CALL statement is what
goes through StmtExpr with a non-empty rendering. And nothing asserted
the two labels differ, so labelling a statement that HAS a child as
"empty" passed.

The fixture was wrong to start with too: I invented `bench "b" { measure:
... }` from the issue's prose, it does not parse, t27c exits 1, and the
test asserted on an empty string. The real spelling is `bench NAME` with
no braces. Read the construct out of the corpus, not out of the report
about it.

Closes #2974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-30 19:52:01 UTC

Summary

Status Count
Total Open PRs 8
PRs with Failing Checks 7
PRs with All Checks Green 1
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fd01b746e208 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

gHashTag and others added 2 commits August 31, 2026 02:52
An hour of work committed cleanly and then `gh pr create` answered
"No commits between master and w801" -- the branch did not exist. HEAD
had been detached, the commit landed on no branch, and nothing in the
commit path said so.

Verified in a throwaway repository rather than assumed: `git commit` on
a detached HEAD succeeds with no warning, `git branch --show-current`
returns empty, and only `git status` says it outright. `git push -u` is
silent too; the first objection comes from a tool three steps downstream,
about the wrong subject.

One command, beside the freeze check already run before every commit:

    test -n "$(git branch --show-current)" || echo DETACHED

Recovery is `git branch -f <name> <sha>`. The commit is not lost, it is
unreferenced -- which looks identical from every command that asks about
branches and nothing like it from `git log`.

What this is NOT evidence of: three background agents were running in the
same repository, and the obvious story is that one moved my HEAD. They
did not -- `git worktree list` shows each in its own worktree. The cause
is unestablished, and 'the agents did it' would have been a cause
invented to fit a symptom, which is the failure this skill records more
often than any other.

Refs #2974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	.claude/skills/ci-gates/SKILL.md
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-30 19:53:49 UTC

Summary

Status Count
Total Open PRs 9
PRs with Failing Checks 7
PRs with All Checks Green 2
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fd01b746e208 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

gHashTag and others added 2 commits August 31, 2026 03:08
Merging master moved one spec's generated output; `tri seals drift`
read 1 and `--fix` repaired it locally -- and the repair was never
committed. `coverage` went red on exactly that, and the neighbouring
session's PR was green, which is what said the drift was mine.

This repository's own lesson, twice recorded and broken again here: the
reseal belongs in the SAME commit as whatever moved the output, not in
a later one and certainly not only in the working tree. `git status` is
the check, and it costs one command.

Refs #2974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	.claude/skills/ci-gates/SKILL.md
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-30 20:17:12 UTC

Summary

Status Count
Total Open PRs 9
PRs with Failing Checks 7
PRs with All Checks Green 2
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fd01b746e208 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag merged commit 47557b2 into master Aug 30, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zig: bench prose lowers to a bare ; -- +32 on the deep ruler for about five lines

1 participant