← board

tools/gate.sh fixedpoint does not iterate — false RED on any codegen change

What happens

tools/gate.sh's fixedpoint() is:

"$PINNED" compiler/compiler.pas "$a"
"$a"      compiler/compiler.pas "$b"
"$b"      compiler/compiler.pas "$c"
cmp -s "$a" "$b" && cmp -s "$b" "$c"

A is built from NEW source by the OLD pinned binary, so it carries the new codegen; B is built by A. Whenever the change alters the code emitted for compiler.pas itself, A != B — legitimately, and by exactly one generation. B == C is the convergence. The gate requires A == B and so reports RED.

Measured on the ticket above: gate.sh quickFAIL self-host fixedpoint, while tools/selfhost_fixedpoint.sh on the identical tree → "converged after 2 round(s) from pinned: the compiler reproduces itself / agrees with compiler/pascal26", and testmgr --tier quick 15/15 green. The failure log is empty (fixedpoint.log is 0 bytes), so the summary line is the only signal and it names no reason — which is what makes this cost a detour rather than a glance.

Why this is the known-wrong shape

The Makefile's $(COMPILER) rule already carries the fix and the reasoning, in chore-makefile-selfhost-iterate-to-convergence:

this rule used to demand byte-identical convergence in exactly ONE pass from whatever local seed happened to be on disk. […] "a stale seed legitimately needs an extra round (stage2 came from the OLD compiler, stage3 from the new one) — demanding one pass is what made a normal bootstrap look like a failure." testmgr already iterates for exactly this reason.

tools/selfhost_fixedpoint.sh iterates to MAX_ROUNDS=4 and additionally checks the hermetic fixedpoint equals compiler/pascal26 (the Thompson-trap property). gate.sh reimplemented the check inline and reintroduced the bug.

Fix

Have fixedpoint() call tools/selfhost_fixedpoint.sh instead of open-coding three rounds — it is the authoritative implementation, it iterates, and it already enforces the stronger second property. If gate.sh must stay self-contained, iterate to MAX_ROUNDS and accept the first round where two consecutive stages agree, exactly as the Makefile rule does.

Either way, write the reason into fixedpoint.log on failure — an empty log behind a FAIL line is what forced the manual bisect that produced this ticket.

Severity

Every Track A codegen fix trips it, and the prescribed per-fix loop in CLAUDE.md is make compiler/pascal26 + tools/gate.sh quick. A gate that is red for the normal case trains agents to ignore it, which is worse than not having it.