Conversation
C requires a type's name before its use. Every struct was emitted as an
anonymous typedef, so a struct naming itself had no name in scope until
the line that closed it:
typedef struct {
BTreeNode** children; <- not in scope
} BTreeNode; <- until here
That is a cycle of length one. The topological sort landed in #2941 cannot
order a declaration before itself -- powerless by construction, not by
oversight. Measured: 24 corpus specs write one.
Emit a forward-declaration block, then tagged bodies. An anonymous typedef
cannot be forward-declared at all, so the body had to become tagged for the
declaration to refer to anything; the two edits are one change.
The sort stays. A forward declaration gives C the NAME, enough for a pointer
member and not enough for a by-value member, so the two mechanisms cover
different halves of the same requirement. Mutant M4 disables the sort and is
still killed.
Measured per spec, both directions, idle machine, 650 specs:
cc -fsyntax-only -std=gnu11 accepts 264 -> 268
newly accepted 4 markup, b_tree, kd_tree, rtree
regressions 0 (empty set)
Four of 24 carrying specs, and the honest reading is that the other 20 are
blocked further down: b_tree still fails on `BTreeNode(K, V)* children`, a
generic-parameterised type name reaching a language with no generics. That
is a separate defect and is not fixed here.
Mutation testing, four mutants, each killed by the test that should catch it:
drop the forward block; restore the anonymous body; declare only recursive
structs (leaves a mutual L/R pointer cycle unnamed); disable the sort.
Two rulers were broken on the way and are fixed here:
A mutant the freeze rejects is not a measurement. The first three runs
printed nothing because the M5 build script refused a compiler.rs whose
hash no longer matched FROZEN_HASH -- no build, no test, and neither a
`test result` line nor an `error:` line. Absence of "ok" is not death.
The test helper keyed its temp directory by (pid, src.len()) and each test
removed the whole directory. Two tests with equal-length sources shared it;
under the parallel runner one erased the spec another was mid-read of and
the assertion read `got []`. It passed the first time it ran. Keyed by an
atomic counter now; three consecutive runs green.
Seals: drift is 0 on master and 356 with this change -- the C shape changed
for every spec with a struct, which is what this change does. The 334 seal
files, FROZEN_HASH and the emitter are resealed in this one commit, so no
commit in this branch leaves the tree inconsistent.
Closes #2948
Refs #2941
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Refs #2948 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR DashboardGenerated at: 2026-08-30 14:54:16 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
`_core_c` sliced the struct out of the generated C with a regex keyed to
the anonymous shape:
typedef struct { ... } TernaryWeight;
The emitter now writes a forward declaration and a tagged body, so the
regex matched nothing, `_core_c` returned None, and all three C arms
reported "C backend failed to build/run" -- which reads like an
arithmetic disagreement and is not one.
Both shapes are accepted now, so the tool judges the arithmetic rather
than the spelling and keeps working on either side of the change. The
exhaustive FNV-1a digest is unchanged at 6b2724c5 over all 65,536 inputs:
the struct change is spelling, and this is the measurement that says so.
Two things this cost, both worth writing down:
emit-bitexact does not run on master (`pull_request` with a `paths:`
filter and no `push:`), so there was no baseline to compare against.
The baseline came from OTHER branches instead -- green on w791, w792,
and three other agents' branches, red only on this one, which is what
convicted this change.
Locally the tool passed on this branch, twice. Both readings came from
a t27c built before the emitter change: `git checkout` does not rebuild.
The reader was keyed to the old shape the whole time.
The sibling reader at line 441 slices the HOISTED tuple typedef, which is
emitted by a different path and is genuinely still anonymous -- checked
against the generated output rather than assumed.
Refs #2948
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR DashboardGenerated at: 2026-08-30 14:59:35 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
382 a mutant the M5 freeze rejects is not a measurement -- three arms,
not two, because a build refusal prints neither a test result nor an
error line
383 a shape change breaks every reader keyed to the old shape; the
dangerous one goes vacuously green, not red
384 a gate that never runs on master has no baseline -- borrow one from
sibling branches rather than reading 'no data' as 'no problem'
385 git checkout does not rebuild; the stale binary is usually the old
one, so a change measures as harmless exactly when it is not
386 a race that has not fired is not a test that passed -- 12 green runs
while the collision happened on every one; probe the identity
387 two rulers that disagree: report both, name which the ratchet uses,
and measure the inflation ceiling on purpose
388 fix the emitter once, then count its call sites; a structural test
when the second path needs a repository on disk
389 a detector needs a counterexample, not a review -- two holes, both
invisible by reading the rule, both frozen into --self-check
Refs #2948
Refs #2951
Refs #2954
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This branch also carries the skill update for all three of today's changes ( Sections 383, 384 and 385 are this PR's own lessons: a shape change breaks every reader keyed to the old shape (one of them went vacuously green, not red); a gate that never runs on master has no baseline, so the baseline came from sibling branches; and |
PR DashboardGenerated at: 2026-08-30 15:03:12 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
…the class (#2955) * fix(tests): give each test its own scratch directory, and a gate for the class Four test binaries computed their scratch directory from a key every test in the binary shares, and each test deletes the WHOLE directory on the way out: let dir = temp_dir().join(format!("t27-scaffold-{}-{}", process::id(), src.len())); ... let _ = std::fs::remove_dir_all(&dir); One process per binary, so the pid is constant and the key is really `src.len()`. Two tests whose sources are the same length get one directory, and under the parallel runner one erases the spec another is mid-read of: t27c prints nothing and the assertion reports an empty result. verilog_imported_enum interpolates only the pid, so all three of its tests shared one directory. Measured rather than inferred. Printing the paths of one scaffold_c run: 6 tests produced THREE directories before, six after. A probe asserting the directory was fresh fired 8 runs out of 8 on const_width, pointer_param_c and verilog_imported_enum -- the collision happens every run and only the timing of the delete decides whether a test dies. scaffold_c failed about one run in three; the others had not fired yet. It passed the first time it was written. A green run does not clear a race. Keyed by an AtomicUsize counter now: unique per call, which is what a directory its own user deletes requires. struct_order_c is the fifth carrier and is fixed in #2949; touching it in both branches would only make a conflict. `tri harness scratch` (--gate, --self-check) keeps the class from coming back. It looks for the conjunction -- more than one #[test], a scratch path under temp_dir(), a remove_dir_all of it, and a key with no per-call component -- because any one alone is fine. Against master it reports exactly the five the probe convicted and nothing else. Two false readings it had to survive, both found by counterexample: Its first rule asked whether the format! call contained a `{`, true of every single-line format call, so a pid-only key looked variable and it reported nothing on verilog_imported_enum while the probe fired 8 of 8. It judges the arguments now. Judging only the arguments then convicted backend_behaviour, whose key is format!("...-{tag}") -- an inline capture, distinct per test, with no argument list at all. Inline captures are read as arguments now. Grepping the symptom rather than the construct also convicts verilog_r_si_1, where src.len() sizes a String and never touches a path. The self-check is five-way: a planted collision and a pid-only key must be seen; counter-keyed, inline-capture and single-test files must not be flagged. A failing leg exits non-zero and says the clean run claims nothing. --gate is deliberately NOT wired into CI here: it stays red until #2949 removes the fifth carrier. Wiring it belongs in the commit after both. Closes #2954 Refs #2949 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(now): entry for the scratch-directory race Refs #2954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-08-30 15:13:20 UTC
Summary
Seal Status
|
# Conflicts: # .claude/skills/ci-gates/SKILL.md
Three conflicts, and each one is resolved by regenerating rather than by choosing a side: FROZEN_HASH -- recomputed from the merged compiler.rs, which now carries both this branch's forward declarations and master's Zig prelude. 338 seal files -- the merged emitter's output is what they must describe, so they are recomputed. `tri seals drift` reads 0 across three runs with a binary the tool itself reports FRESH; the first reading of 338 was taken before the rebuild finished and is not the measurement. .claude/skills/ci-gates/SKILL.md -- master had taken 382-386 for another agent's sections while this branch used 382-389. Master's numbering is kept EXACTLY as it stands and mine are appended as 387-394. The last time a skill conflict was resolved here the other side was renumbered, which broke every cross-reference into it; the rule is that the side already on master does not move. The commit messages and PR body on this branch still say 382-389. They describe the sections by title, and the titles did not change. `tri harness scratch` now reports NONE: struct_order_c was the fifth carrier of the shared-scratch-directory class and it is fixed here, so #2955's gate can be wired in a follow-up as its PR body said. t27c 0 failing test binaries, tri 391 passed. Refs #2948 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-08-30 15:25:47 UTC
Summary
Seal Status
|
…ratch gate (#2958) * fix(ci): count the files this gate does not read, and wire the scratch gate `check_pr_branch_filters.py` printed merge-critical workflows checked: 15 workflow files present: 49 explicitly not merge-critical: 4 CLEAN: no merge-critical workflow filters pull_request by branch. 15 + 4 against 49, printed side by side and never subtracted. Thirty files were read by nothing and the last line still said CLEAN. Two of the thirty carried `pull_request: branches: [master]` -- the very defect this check exists to detect. A branches filter on pull_request means the gate does not run at all when a PR targets any other base, so a stacked PR shows a green check list with that gate absent from it. One of the two is corpus-ratchet, the expected-failure ledger. Three parts: the two filters are removed -- `paths:` selects by what changed and stays, `branches:` selects by where the change is headed and is a hole; the third bucket is printed with its arithmetic closing against the file count, and the clean line says how many files remain unread instead of promising more than it checked; MAX_UNCLASSIFIED = 27, down only. Twenty-seven files cannot be classified in the commit that finds them, and a gate red on the day it lands teaches everyone to ignore red. The ceiling buys that the NEXT workflow cannot land unread. The same read runs over the unclassified files and is REPORTED, not failed: whether one should block a merge is a human call, whether anybody looked is not. That count is 0 and is printed as a zero rather than omitted. Controls, each seen failing on purpose: a 28th unclassified workflow gives UNCLASSIFIED ROSE 27 -> 28; a name in both lists gives IN BOTH LISTS; restoring the filter on corpus-ratchet gives BRANCH-FILTERED MERGE-CRITICAL WORKFLOWS. All restored, exit 0. Also here: harness-scratch.yml, which #2955 held back because the gate stayed red until #2949 removed the fifth carrier of that class. Both have landed -- on master the command reports `none` and `--gate` exits 0. It carries no `paths:` filter and a push trigger on master, and its header says why: emit-bitexact-gate.yml is pull_request with a paths filter and no push, so it has never run on master, and when a change made it fail there was no baseline to compare against. Historical control: against the tree before #2955 the gate exits 1 and names all five carriers. The remaining 27 are a work list, not a verdict; l1-traceability.yml is the one worth deciding first. Closes #2957 Refs #2954 Refs #2919 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skill): 395 -- a guard's clean line is a claim about a population The two list sizes and the file count were printed three lines apart and never subtracted: 15 + 4 against 49 present, so 30 workflow files were read by nothing and the summary still said CLEAN. Two of the thirty carried the defect the check exists to detect. Make the parts sum out loud; name the remainder as a third state; run the same read over it and report rather than fail; ceiling not refusal so the next addition cannot land unread; and let the final line state the scope it actually earned. Refs #2957 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Correction — a claim in this text is wrong and is withdrawn in #2959. This issue says The 2026-08-28 success is a usable baseline from two days before the change it was invoked about. I produced the wrong reading with What survives, and is the part this issue actually turns on: no The commit message on this branch carries the same wrong sentence. It cannot be edited without a force-push, so the correction lives in #2959, in the rewritten skill section 389, and here. |
The defect
C requires a type's name before any use of it. The C backend emitted every struct
as an anonymous typedef:
A struct that names itself is a cycle of length one. The topological sort
landed in #2941 orders declarations by dependency, and no ordering can put a
declaration before itself — the sort is powerless here by construction, not by
oversight.
Measured: 24 corpus specs write a self-referencing struct.
The fix
Emit a forward-declaration block, then tagged bodies:
An anonymous
typedef struct { ... } Name;cannot be forward-declared at all —there is no tag to declare — so the body had to become tagged for the forward
declaration to mean anything. The two changes are one change.
The topological sort stays: a forward declaration gives C the name, which is
enough for a pointer member and not enough for a by-value member. The two
mechanisms cover different halves of the same requirement, and a mutant that
disables the sort is still killed by its own tests (M4 below).
Measurement
Per spec, both directions, on an idle machine, 650 corpus specs:
cc -fsyntax-only -std=gnu11acceptsencoding/markup,trees/b_tree,trees/kd_tree,trees/rtreebefore - after)All four are tree structures — the family the defect was found in. This is a
small number against 24 carrying specs, and the honest reading is that the other
20 are blocked further down;
b_treeitself still fails onBTreeNode(K, V)* children— a generic-parameterised type name reaching C, which has nogenerics. That is a separate defect and it is not fixed here.
Mutation testing
Four mutants, each killed by the test that should catch it:
a_self_referencing_struct_is_forward_declaredevery_struct_is_forward_declared_not_only_the_recursive_onesa_struct_used_before_it_is_declared_is_emitted_firstM3 is the interesting one: declaring only the recursive structs leaves a mutual
pointer cycle (
LnamesR,RnamesL) with no name in scope for either.A mutant the freeze rejects is not a measurement
The first three mutation runs printed nothing and I nearly read that as "killed".
They were rejected by the M5 freeze build script —
bootstrap/stage0/FROZEN_HASHno longer matched — so
cargo testnever built, never ran, and produced neither atest resultline nor a plainerror:line. Absence of "ok" is not death.Mutating
compiler.rsrequires resealingFROZEN_HASHper mutant, and the verdicthas to separate three outcomes, not two.
A flaky test found on the way
bootstrap/tests/struct_order_c.rskeyed its temp directory by(pid, src.len())and each testremove_dir_alls it. Two tests whose sourceshappen to be the same length shared one directory; under the default parallel
runner one erased the spec another was mid-read of,
t27cprinted nothing, andthe assertion read
got []. It passed the first time it ran. Now keyed by anatomic counter; 3 consecutive runs green.
Seals
tri seals driftis 0 on master and 356 with this change — the C outputshape changed for every spec containing a struct, which is exactly what this
change does. 334 seal files rewritten in the same commit as the emitter and the
FROZEN_HASHreseal, so no commit in this branch leaves the tree inconsistent.Closes #2948 · Refs #2941
🤖 Generated with Claude Code