Merged
Conversation
`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>
Contributor
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>
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
PR DashboardGenerated at: 2026-08-30 14:55:18 UTC
Summary
Seal Status
|
Contributor
PR DashboardGenerated at: 2026-08-30 15:08:44 UTC
Summary
Seal Status
|
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
assert_eq(a, b)in a test or invariant body reaches Zig as a call to a functionnothing declares.
The Zig statement emitter special-cases
assertand@compileAssertand lowersthem itself (
compiler.rs:9641), butassert_eqis not in that list, so itfalls through to the generic call path and is written out verbatim:
Zig resolves identifiers in AstGen — file-wide, before any Sema — so the
undeclared name is a hard error even inside a
testblock thatbuild-objnever 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 toassert_eqintest 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
ccandrustcand failzig.Measured: 60 of 581 generated files call
assert_eqand 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 samehas_testsguard. Faithful, not a stub:build-objnever 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_testsprelude existed as two byte-identical copies —gen_zig(what
t27c genand the corpus harness use) andgen_zig_project(reachedthrough
Compiler::compile_project_file). Adding the shim to the site themeasurement 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:
zig build-obj -fno-emit-bin(what the corpus harness runs)zig test --test-no-exec -femit-bin=…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 << nlowered as@as(u32, 1) << …regardless of the declared type, e.g.specs/ternary/gft_add_rne.t27emitsvar half: i32 = @as(u32, 1) << @intCast(d);inside
fn on_comb, not inside a test. 33 generated files carry that shape andall 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_eqlines instead of declaring them scores 60/60 on the deepruler, 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
_ = a; _ = b;the_shim_actually_compares_its_argumentsthe_zig_prelude_has_a_single_emittera_spec_without_tests_gets_no_shimTwo mutants first came back as "no output", which is not a kill: the M5 freeze
build script rejects a
compiler.rswhose hash no longer matchesFROZEN_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