Skip to content

fix(zig): declare assert_eq, and give the prelude one emitter (+60 build-obj / +28 deep, 0 regressions) - #2953

Merged
gHashTag merged 3 commits into
masterfrom
w791
Aug 30, 2026
Merged

gHashTag merged 3 commits into
masterfrom
w791

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

The defect

assert_eq(a, b) in a test or invariant body reaches Zig as a call to a function
nothing declares.

The Zig statement emitter special-cases assert and @compileAssert and lowers
them itself (compiler.rs:9641), but assert_eq is not in that list, so it
falls through to the generic call path and is written out verbatim:

test "maj_p_p_n" {
    assert_eq(maj3(2, 2, 0), 2);   // error: use of undeclared identifier
}

Zig resolves identifiers in AstGen — file-wide, before any Sema — so the
undeclared name is a hard error even inside a test block that build-obj
never analyses
.

The C backend hit the identical defect and fixed it with a macro; its own comment
names it (compiler.rs:17693, W583: "the backend emits calls to assert_eq in
test bodies (59 headers) and never defined it"
). Rust does not emit the call at
all. Zig was the one backend left out — which is why the same specs pass cc and
rustc and fail zig.

Measured: 60 of 581 generated files call assert_eq and all 60 are rejected.
One distinct message text across 176 diagnostic lines — the construct is
message-homogeneous.

The fix

Declare the shim in the Zig prelude, next to __t27_assert_fail, under the same
has_tests guard. Faithful, not a stub: build-obj never analyses a test body,
so a shim that compared nothing would clear the gate while writing a lie into
every test the corpus ships.

The prelude was duplicated, so the fix had two homes

The has_tests prelude existed as two byte-identical copies — gen_zig
(what t27c gen and the corpus harness use) and gen_zig_project (reached
through Compiler::compile_project_file). Adding the shim to the site the
measurement names would have left the project emitter emitting the old, broken
prelude, and nothing that drives the CLI would have caught it. One emitter, two
call sites now, with a structural test asserting exactly that.

Measurement — two rulers, and they disagree

Per spec, both directions, 581 generated files, zig 0.16.0, isolated per-worker
cache, no timeouts:

ruler before after new regressions
zig build-obj -fno-emit-bin (what the corpus harness runs) 222 282 +60 0
zig test --test-no-exec -femit-bin=… 105 133 +28 0

The gap is the honest part of this change. 60 files clear the gate this repo
measures; only 28 survive a ruler that actually analyses test bodies. The other
32 are held by a separate emitter defect — 1 << n lowered as
@as(u32, 1) << … regardless of the declared type, e.g.
specs/ternary/gft_add_rne.t27 emits var half: i32 = @as(u32, 1) << @intCast(d);
inside fn on_comb, not inside a test. 33 generated files carry that shape and
all 33 sit inside these 60, so the undeclared identifier was masking it — and
after this change build-obj's laziness will keep masking it. Filed separately.

Deleting the assert_eq lines instead of declaring them scores 60/60 on the deep
ruler, which looks better and means less: with the calls gone the functions are
unreferenced and Zig never analyses them at all. That is the inflation trap, not
a better fix.

Mutation testing

mutant killed by
M1 drop the shim 3 of 4 tests
M2 stub shim, _ = a; _ = b; the_shim_actually_compares_its_arguments
M3 restore the duplicated prelude the_zig_prelude_has_a_single_emitter
M4 emit the prelude with no tests present a_spec_without_tests_gets_no_shim

Two mutants first came back as "no output", which is not a kill: the M5 freeze
build script rejects a compiler.rs whose hash no longer matches FROZEN_HASH,
so nothing built and nothing ran. Each mutant is resealed before it is judged, and
the verdict separates three outcomes — killed, survived, never built.


Closes #2951 · Refs #2952

🤖 Generated with Claude Code

`assert_eq(a, b)` in a test body reached Zig as a call to a function
nothing declares. The statement emitter special-cases `assert` and
`@compileAssert` and lowers them itself; `assert_eq` is not in that list,
so it falls through to the generic call path and is written verbatim.

Zig resolves identifiers in AstGen -- file-wide, before any Sema -- so an
undeclared name is a hard error even inside a `test` block that build-obj
never analyses.

The C backend hit this exact defect and fixed it with a macro (W583, and
its comment says so); Rust never emits the call. Zig was the one backend
left out, which is why the same specs pass cc and rustc and fail zig.
Measured: 60 of 581 generated files call it and all 60 are rejected.

The prelude was duplicated -- byte-identical copies in `gen_zig` and
`gen_zig_project` -- so the shim had two homes and only one of them is
what the corpus harness runs. Adding it to the measured site would have
left `compile_project_file` emitting the old prelude with nothing to
catch it. Both now call one `write_zig_test_prelude`, with a structural
test asserting exactly that, because the project path needs a whole
repository on disk to drive.

The shim compares. build-obj never analyses a test body, so a stub would
clear the gate while writing a lie into every test the corpus ships.

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

    zig build-obj -fno-emit-bin    222 -> 282   +60   regressions 0
    zig test --test-no-exec        105 -> 133   +28   regressions 0

The gap is the honest part. 60 clear the gate this repo measures; 28
survive a ruler that analyses test bodies. The other 32 are held by a
separate defect -- `1 << n` lowered as `@as(u32, 1)` regardless of the
declared type -- which the undeclared identifier was masking and which
build-obj's laziness will keep masking. Filed as #2952.

Deleting the assert_eq lines instead scores 60/60 on the deep ruler and
means less: with the calls gone the functions are unreferenced and Zig
never analyses them. That is the inflation ceiling, not a better fix.

Four mutants, each killed by its own test: drop the shim; stub it;
restore the duplicated prelude; emit the prelude with no tests present.
Two first came back as "no output", which is not a kill -- the M5 freeze
rejects a compiler.rs whose hash no longer matches FROZEN_HASH, so
nothing built and nothing ran. Each mutant is resealed before it is
judged and the verdict separates killed, survived, never built.

Seals: 411 files rewritten, in this one commit with the emitter and the
FROZEN_HASH reseal, so no commit in this branch leaves the tree
inconsistent.

Closes #2951
Refs #2952

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

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-30 14:37:46 UTC

Summary

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

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=59a96dbf35b6 != 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).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

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

Refs #2951

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 14:55:18 UTC

Summary

Status Count
Total Open PRs 10
PRs with Failing Checks 7
PRs with All Checks Green 3
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=59a96dbf35b6 != 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).

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-30 15:08:44 UTC

Summary

Status Count
Total Open PRs 10
PRs with Failing Checks 7
PRs with All Checks Green 3
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=59a96dbf35b6 != 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).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

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

@gHashTag
gHashTag merged commit 133b1f5 into master Aug 30, 2026
34 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 backend: assert_eq is emitted but never declared (60 of 581 files)

1 participant