Merged
Conversation
Zig has no `comptime_int << runtime`, so a shift with a runtime amount
must pin a width on its left operand. With nothing else to go on the
emitter read the literal's MAGNITUDE -- `1` fits u32, therefore u32 --
and produced
var half: i32 = @as(u32, 1) << @intcast(d);
a u32 expression initialising an i32. The type it needed was written two
lines above the expression that had to guess it.
58 emitted sites across 47 specs carry the shape. By position: 33 are a
declaration that states its type (all i32, the defect), 13 an untyped
declaration, 10 inside a larger expression, 2 a return. Only the 33 are
touched. #2952's "33 files" was the intersection with the 60 files #2951
unblocked; the population carrying the shape is 47 specs.
The declaration's integer type is now recorded for the length of its
initializer and preferred over the magnitude default. Precedence, each
step with a test: the literal's own suffix, then the declaration's type,
then magnitude. A declared type that is not an integer returns None and
falls back, so a f64 or a struct does not become `@as(SomeStruct, 1)`.
Measured per spec, both directions, zig 0.16.0, no timeouts:
zig test --test-no-exec 133 -> 165 +32 regressions 0
zig build-obj -fno-emit-bin 282 -> 282 +0 regressions 0
cc -fsyntax-only (control) 268 -> 268 +0 regressions 0
The second row was predicted before the fix and is the honest part: the
corpus column is measured with build-obj, which never Sema-analyses a
body nothing references, so it cannot see this defect and does not move
when it is repaired. The +32 is the number an adversarial verifier
derived independently a day earlier, before this code existed.
A contaminated baseline was thrown away: the first test-no-exec reading
came from a background job still running while the compiler was rebuilt.
It agreed with the clean re-measurement at 133 and was discarded anyway
-- a number that could have been wrong is not made right by turning out
correct.
Four mutants. M3 SURVIVED the first time, which is the useful part: the
leak test put a second DECLARATION after the first, and an untyped
declaration overwrites the hint with None on its way in, so removing the
explicit clear changed nothing it could see. What bites is a return --
`return 1 << e` in a u32-returning fn emits `@as(i32, 1)` when the
previous hint survives -- with a different shift amount, because with the
same amount twice CSE hoists one `_cse1` and there is only one shift left
to be wrong about.
Skill 399: `tail -N` reads the last section, not the summary. Reading
`tri seals drift --fix` that way produced two different wrong
conclusions on consecutive days, one of which reached a merge commit
message. Grep the label, and settle a count against the filesystem.
Closes #2952
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # .claude/skills/ci-gates/SKILL.md
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
This was referenced Aug 30, 2026
gHashTag
added a commit
that referenced
this pull request
Aug 30, 2026
…2967) Yesterday a change unblocked 32 specs under `zig test --test-no-exec` and moved this table's Zig column by exactly 0 (#2952, PR #2963). The zero is correct. `zig build-obj -fno-emit-bin` resolves identifiers and never Sema-analyses a function nothing references, so a defect inside a body is invisible to it and stays invisible when repaired. The consequence is that no ratchet can go red for that class, and no repair of it can be credited: a real fix reports as a fix that did nothing. The gap is not small. 282 accept under build-obj, 165 under the deeper ruler -- 117 files are counted as Zig-accepted today while carrying something the compiler would reject the moment it looked. So: a second row, `... and Zig ANALYSES it`, carried into --json (wave-to-wave comparison reads the JSON; a row only in the human table is a number nothing can diff) and into --per-spec, whose Zig field is three digits now with the header renamed to say so. `-femit-bin` is given a REAL path. `-femit-bin=/dev/null` fails on every input with "failed to invalidate kernel cache: PermissionDenied", which would report 0 of 589 and read as a catastrophic regression rather than as a broken ruler. That cost a re-run while measuring #2952 by hand and is written into the code beside the flag. Three tests, cheap on purpose, asserting the SHAPE and the INVARIANT rather than any count -- counts move whenever the emitter improves and a test pinned to one gets re-blessed rather than read: bodies <= build <= gen killed by `o.zig_bodies = true` the column reaches the JSON killed by dropping the field three digits in --per-spec killed by reverting the format The subset invariant is the load-bearing one: a file the deeper ruler accepts must pass the shallower one, because analysing a declaration is strictly more work than resolving its name. A violation means a broken ruler, not an improved compiler. Measured before being asserted -- it holds on a 150-spec sample. With zig absent the tests print SKIPPED and say so; a green meaning "not run" is what this change exists to prevent. Skill 401: a scratch file shared with your own background agents is a moving population. /tmp/specs.txt was regenerated by an agent between the moment a measurement printed 649 and the moment it used the file, which by then held 665. The A/B survived because both sides ran after the change -- luck, not design -- and the tell was on screen: GEN 589 where the previous day's identical command printed 581, beside a population line still reading 649. Also corrected there: the corpus population is 665, not the 650 several reports including mine have quoted, and cc acceptance is 268 of 589 generating rather than 268 of 650. Numerator right, denominator wrong, everywhere. Closes #2966 Refs #2952 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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
Zig has no
comptime_int << runtime, so a shift whose amount is not comptime-known mustcarry a width on its left operand. With nothing else to go on the emitter used
zig_int_literal_default_type, which reads the literal's magnitude:1fits in u32,therefore u32.
A u32 expression initialising an i32. Zig rejects it — and the type it needed was written
two lines above the expression that had to guess it.
Where the shape actually sits
58 emitted sites across 47 specs carry
@as(u32, 1) <<. Classified by position ratherthan by message:
i32— the defectwhilecondition, an operand)return(#2952 said 33 files. That was the intersection with the 60 files #2951 unblocked; the
whole population carrying the shape is 47 specs / 58 sites, and the sub-population this
change repairs is the 33 typed declarations.)
The fix
Record the declaration's own integer type for the length of its initializer, and prefer it
over the magnitude default. A declared width beats an inferred one — the same rule
zig_int_literal_default_typealready applies to a literal's suffix, applied one levelout.
Order of precedence, and each step has a test:
1u32) — most specific, unchanged;zig_declared_int_typereturnsNonefor anything that is not an integer, so af64or astruct falls back rather than emitting
@as(SomeStruct, 1)and trading one rejected filefor another.
Measurement — per spec, both directions, zig 0.16.0, no timeouts
zig test --test-no-exec -femit-bin=…zig build-obj -fno-emit-bincc -fsyntax-only -std=gnu11(control)The second row was predicted before the fix and is the honest part. #2952 says it
outright: the corpus acceptance column is measured with
build-obj, which resolvesidentifiers and never Sema-analyses a body nothing references, so it cannot see this defect
and does not move when it is repaired. Quoting only the +32 would overstate what this
repository's own column will show; quoting only the +0 would say a real repair did nothing.
The +32 is exactly the number an adversarial verifier derived independently on 2026-08-30,
before this code existed, when it measured how many of the 60 files unblocked by the
assert_eqshim were still held by something else.The C row is the control: the change is confined to the Zig expression emitter, and cc is
unmoved in both directions.
A contaminated baseline was thrown away. The first
test-no-execbaseline was taken bya background job that was still running while the compiler was rebuilt, so an unknown
suffix of its specs went through the new binary. It happened to agree with the clean
re-measurement at 133, and it was discarded anyway: a number that could have been wrong is
not made right by turning out correct.
Mutation testing
the_declared_type_pins_…,a_wider_declaration_wins_…a_suffixed_literal_beats_the_declarationthe_hint_does_not_leak_past_its_declarationa_non_integer_declaration_falls_back_…M3 survived the first time, and that is the useful part. The original leak test put a
second declaration after the first — and an untyped declaration overwrites the hint with
Noneon its way in, so removing the explicit clear changed nothing the test could see. Itasserted a true thing about a case the clear never affects.
What bites is a shift that is not a declaration:
return 1 << ein a u32-returning functionemits
@as(i32, 1)when the previous statement's hint survives. Verified against the mutant,which produces exactly that. The two shift amounts differ on purpose — with the same amount
twice, common-subexpression elimination hoists a single
_cse1and there is only one shiftleft to be wrong about.
Closes #2952
Closes #2952
🤖 Generated with Claude Code