← board

gate.sh quick is structurally blind to a broken pinned RTL

What happened

97b1812fe (Track A, [[feature-a-emitted-nil-checks]]) added PXXNilRefHook to compiler/builtin/builtinheap.pas and installed it from lib/rtl/sysutils.pas. Both files are live sources, the change was coherent, and the full per-fix loop was green: make compiler/pascal26 reached its fixedpoint and tools/gate.sh quick passed.

It still broke master for three other lanes. make pin freezes the builtin RTL sources into stable_linux_amd64/default/builtin/, so pinned was still compiling the pre-hook builtinheap.pas against the post-hook sysutils.pas:

pascal26:4750: error: undefined variable (PXXNilRefHook)
  near: PXXIoErrorHook   SysRaiseIoError  PXXNilRefHook >>>   SysRaiseAccessViolation

Every $(PXX_STABLE) build — Track B, D, E, make lib-test, make demos — failed from that commit until the v369 pin. It was found by accident, by a Track A session compiling an unrelated scratch program with pinned.

Why the gate cannot see it, and why that is not an accident

gate.sh quick runs the self-host fixedpoint and the quick testmgr tier. Both build with the freshly built compiler, which has the new builtin in it. The pinned binary — the thing three lanes actually build against — is never invoked. So the gate is not weak here, it is looking somewhere else entirely: this break lives exactly in the seam between the two, and nothing currently stands in that seam.

Track T's own sweep would have caught it eventually (its wider tiers build lib), but "eventually, one lane over" is how three tracks lose an evening.

The fix is one cheap step, not a new tier

Add a canary to gate.sh (all modes — it is ~1 second):

./stable_linux_amd64/default/pinned <a program that `uses SysUtils`> /tmp/...

Compile only; running it is not the point. Any live-RTL-vs-frozen-builtin mismatch is a compile error, which is the whole failure mode. test/ already has plenty of uses SysUtils programs to point it at, so it needs no new fixture.

Consider asserting the reverse direction too — that the frozen builtin set is byte-identical to compiler/builtin/ when the pin is current — but that is a progress.sh check-shaped assertion and would fire (correctly, but noisily) on every Track A commit between a builtin change and its pin. The compile canary is the one that reports exactly the thing that is actually broken. Prefer it; do not grow both.

Gate

T's own gate for tooling changes, plus: revert to 97b1812fe's tree state in a scratch clone and confirm the new canary goes RED there, then green at the v369 pin. A canary that has never been seen to fail is not yet a canary.

Resolved 2026-08-26 (pxx-aa, Track T)

gate.sh grows pinned_rtl_canary(): compile test/test_uses_sysutils.pas with stable_linux_amd64/default/pinned. ~1s, compile only.

Wired in ONE place, before the case, so it covers quick, lib and full alike. The first draft put a step line in each branch; a check that must be remembered in every new mode is the check that will be missing from the next one.

The gate asked for a canary that has been seen to fail, so it was

Not by reverting a clone to 97b1812fe, which is slower and no more faithful. In a scratch tree (pinned + the frozen builtin/ + a copy of lib/rtl, laid out so ExeDir/../lib/rtl resolves), one injected reference in lib/rtl/sysutils.pas to a builtin the frozen set does not define reproduces the original error shape exactly:

pascal26:5306: error: undefined variable (PXXCanaryProofHook)
  in: ./stable_linux_amd64/default/../lib/rtl/sysutils.pas
  near: PXXNilRefHook   SysRaiseAccessViolation  PXXCanaryProofHook >>>

and through gate.sh's own step wrapper it prints FAIL pinned builds live lib/rtl and returns 1, i.e. the gate goes RED.

The hole the fix would otherwise have opened

The canary SKIPs when the fixture or the pin is absent — correct on a fresh clone, and a permanent silent green in-repo if another lane ever renames test/test_uses_sysutils.pas. That is the same failure this ticket is about, one level up, and it is the third instance found today (the fgl rung guarded on an absent /usr/share/fpcsrc; fpjson enrolled in no tier at all).

So tools/gate_pinned_rtl_canary_devtest.py asserts the fixture exists, still contains uses sysutils, that gate.sh still defines and invokes the canary, and — the real one — that the canary is green on a sound tree and red on an injected mismatch. ~2s.

Not done, deliberately

The reverse assertion the ticket floats (frozen builtin/ byte-identical to compiler/builtin/ when the pin is current) stays unbuilt, for the reason the ticket gives: it fires correctly but noisily on every Track A commit between a builtin change and its pin. The compile canary reports the thing that is actually broken. Do not grow both.

Log