The compiler's own source means two different things to fpc and to pxx
Measured 2026-09-06 at compiler bb002bffcb5c.
program cs2;
{$CASESENSITIVE ON}
var PyImportLang: AnsiString;
procedure P; var pyImportLang: AnsiString;
begin pyImportLang := PyImportLang; PyImportLang := ''; ... end;
fpc 3.2.2 Warning: Illegal compiler directive "$CASESENSITIVE"
local="" global="pas" { one name; the local shadows }
pxx local="pas" global="" { two names }
compiler/pasparser_proc.inc:4471 is exactly that shape and is not a reduction:
a local pyImportLang claims the global PyImportLang and clears it. Under pxx
that is the intended claim-and-clear. Under the fpc seed both spellings are the
local, so the global is never claimed and never cleared and an explicit import
language leaks into the next import.
Why nothing has failed
compiler.pas has no NilPy import, so PyImportLang is '' on both readings
and the bootstrap converges. Every other pair of the 21 is a local shadowing a
global where the routine only ever spells one of the two, so both readings agree
by accident of usage rather than by construction.
The census, and the instrument
PXXDBG=a.casedup (added the same day) reports every case-only pair at its
declaration. Over compiler/compiler.pas: 21 pairs, all samescope=0 --
code/Code nine times, fixCount/FixCount four, frameSize, astTk,
curBlockId, symBlockId, dynamicOff, dynamicSize, N/n,
pyImportLang. Zero same-scope duplicates, which is the good news.
The check that would hold this closed is a lint, not a compiler change: no two
identifiers in compiler/** differing only in case. It belongs beside the
existing FPC-seed forward-decl lint in tools/gate.sh, which exists for the
identical reason -- paslexer.inc:463 calling LowerCase before we declare it
resolves to FPC's OWN system-unit routine, and the gate's note already says the
seed build and the self-hosted build run different implementations there.
Neighbour
[[bug-p-a-parameter-and-a-local-that-differ-only-in-case-are-two-symbols]] is the same collision INSIDE one scope, where fpc refuses the file outright. This one is across scopes, where fpc accepts it and means something else.
A census that reports ZERO is conditioned on the input reaching the site
(frankA, 2026-09-06 — recorded here at frankB's request, because the instrument this ticket ships is the one it bites.)
a.casebind and a.casedup are delta instruments: their null output is a
number. Zero is a legitimate value of that number, so it does not look like
the "nothing ran" case — it looks like an answer.
The measured instance is frankB's. Their first a.casebind run over
test_parallel_for_private and test_critsec_once reported zero moved
bindings, which reads as "the resolution change did not cause these". Both
fixtures need --threadsafe; without it the compile dies inside palthread.pas
before the program body is parsed, so FindSym never reached a single
identifier the instrument had been asked about. The instrument was correct and
answered a question nobody asked: how many bindings moved in the part of the
compile that ran.
My own part in this belongs on the record accurately: I supplied the shape and
the wrong instance. I framed it after arguing that the same census had missed
a real defect in pasparser_proc.inc:4471. It had not. The census was right,
the defect did not exist, and the thing I mistook for one is {$CASESENSITIVE ON} — this ticket. So the section rests on frankB's measurement, not on mine.
CLAUDE.md already carries the general rule: "a guard must also be AIMED and READ … a comparison whose inputs were never proven to exist cannot fail." The wrinkle a census adds is that there is no comparison to inspect — only a count, and the count is well-formed either way.
The cheap fix is in the instrument, not in the discipline. Have
a.casebind report the resolutions it SAW beside the ones that MOVED.
0 of 0 and 0 of 4127 are different answers and only the second one is
evidence; today both print as zero.