Why this exists and what it replaces
Track P closed roughly twenty bugs tonight, ranked by ready --track P. That
ranking is a backlog order, not a usage order. This umbrella is the
instrument that produces a usage order: point pxx at FPC's source, and the first
failure is by definition the thing most in the way.
It does not replace the P backlog — it re-ranks it. Tickets the attempt hits
inherit this umbrella's prio through effective_prio; tickets it never reaches
were, by measurement, not blocking real-world Pascal.
Notation — ONE spelling for the corpus totals, and it is a guard
Corpus totals are written BOTH-OK / ORACLE-NO / PXX-FAIL as N / N / N,
everywhere in this file, including in prose and in the summary: field. Two
earlier spellings (21 BOTH-OK, 10 ORACLE-NO, 176 PXX-FAIL and the same with
· separators) were normalised to it on 2026-09-16 without changing a number.
This is not tidiness; it is the only guard that would have caught the
wall-13/14 error. From b1b61eeff (2026-09-16) this document stated the
totals both ways at once — the summary: field said 21 BOTH-OK, 10 ORACLE-NO, 176 PXX-FAIL and the wall-13 table said 22 / 10 / 175 — and the
two notations meant no grep could ever put them side by side. git log -S says
it outright: -S'21 BOTH-OK' reaches back to 7f8d00b0d, while
-S'21 / 10 / 176' returns only the 2026-09-16 correction as the oldest
commit introducing it. Read literally, the second says the right answer had
never been written down and was invented that day; the first says it had been
there all along. Both are true about their own spelling and neither is true
about the quantity. The record was not stale and was not silent — it held the
right answer and the wrong one simultaneously, and every reader who looked at
one region saw something internally coherent.
That is this repo's wrong-population rule arriving in a document rather than in
an instrument: a search for a NAME enumerates one spelling, and a quantity with
two spellings cannot be checked against itself. So: one notation, and when you
record the totals, record all three numbers and read them off the probe's own
SUMMARY line. Never derive one from an adjacent row — 207 − 22 − 10 = 175
is exactly how the wrong pair got in, and the arithmetic checking out is what
kept anyone from re-deriving it.
(The one place the old spelling still appears in this file is the quotation two paragraphs up, so a grep for it counts this note — the observer inside the namespace it scans, one level up from the case the note is about. Expect one hit; a second is a real one.)
The check, with its baseline, so a later reader knows what clean looks like:
grep -o '[0-9]* / [0-9]* / [0-9]*' <this file> | sort | uniq -c. That command
could not be written at all while the totals had two spellings, which is the
entire point.
AND ITS BASELINE HAD ROTTED BY THE NEXT DAY — re-measured 2026-09-17 (frankB)
and recorded as ERAS, not as a corrected/superseded PAIR. The original
baseline here named two counts, thirteen and five, and read them as the right
triple against the wrong one. Walls sixteen and seventeen then moved the
totals legitimately, and a third era arrived, so the pair framing no longer
describes the file: today the command answers eighteen, twelve and
one across three distinct triples, and none of that is an error. A reader
obeying the old sentence would hunt a live mistake that does not exist — the
born-red failure the note one paragraph up warns about, in the note's own
baseline, one day later. The invariant that survives is the NOTATION, not any
count: every triple in this file is N / N / N, each is true of the tree it
was measured at, and the era is named beside it. Counts are spelled in words
here so this sentence cannot match the pattern it describes.
Those two counts are spelled as WORDS deliberately, and the reason is that the first version of this sentence spelled them as digits and was BORN RED. It quoted one instance of each triple, so it was a member of the population it was counting: the documented command answered fourteen and six while the sentence claimed thirteen and five, and it could never have passed for any reader after the commit that wrote it. A later reader would have run it, got the extra hit, applied the criterion one line above, and gone hunting for a live wrong claim that does not exist -- and a guard that cries wolf on its first outside run teaches the reader to ignore it, which is worse than having no guard. Caught by frankuser running the check instead of reading it, 2026-09-16. Note the depth: the paragraph above already applies the observer-in-the-namespace rule to the QUOTATION, and missed it on the BASELINE NUMBERS, which were measured before the sentence stating them existed. Words take the sentence out of the set, so the command answers as documented for every later reader and keeps answering that way if someone re-words this paragraph -- the same move as putting the convention at the top of the file rather than beside the tables: change the structure so the failure cannot recur, instead of correcting the instance.
The target
FPC's compiler source, as shipped. Do not vendor it, do not reduce it, do not fix it — this is a corpus, and the cheat licence that applies to lekkerzeilen explicitly does not apply here: FPC is not ours, and bending it would destroy what the measurement is for.
How to grow this (the only supported method)
- Point
compiler/pascal26at a translation unit of FPC's source. - Record the FIRST failure. Reduce it to a minimal repro before filing — a first-error reading is a LOWER BOUND, not a work estimate, and tonight produced three cases where reducing changed what the bug WAS.
- File it in
backlog-pascal, wire it here withblocked-by, move to the next unit. - Say in each ticket which FPC unit and line produced it.
Do not triage the existing P backlog into this umbrella. If an existing ticket turns out to be what the attempt hit, wire that one — membership is an edge, not a folder, and one ticket can sit under several umbrellas.
Two things measured 2026-09-09 that set expectations
- The self-host fixedpoint proves nothing here.
compiler.pasis a deliberately procedural subset; Track P's coverage of it is partial, which is worse than none because it looks total. FPC's source uses the whole language. - Do not chase FPC parity, chase compiling FPC. Us accepting what FPC rejects is not a defect. The question this umbrella asks is only ever "does the source compile and run correctly", never "does our diagnostic match".
Sibling targets, same mode
umbrella-compile-and-run-dosbox (prio 50, zero blockers — nobody has
attempted it) and the busybox family, which has eleven open tickets across
five folders and no umbrella of its own. Both are the same instruction:
attempt the target, let the failures rank themselves.
Attempts
2026-09-09, frankH — attempt 1: uses cutils
First failure: constexp.pas:164, undefined variable (internalerrorproc).
Reduced to 8+9+4 lines and filed as
[[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]]
(p60). A unit cycle closed through an implementation uses clause cannot see
the other unit's interface. 25 of 162 units in fpc-trunk/compiler close
such a cycle, so this is structural in the corpus, not incidental — and nothing
behind it can be measured until it moves.
Pre-existing, not a regression: pin v399 gives the identical error.
2026-09-09 — the owner's framing, which bounds this umbrella
"the challenge is just to compile FPC as a proof of pudding. we don't target any advanced compatibility. i can see what FPC is doing, sortof. we don't care. FPC is a great compiler and we have other goals, the common thing is pascal and that we sayd we target FPC's dialect as de-facto standard."
This is a PROOF, not a compatibility programme, and the distinction is the one most likely to be lost by whoever attempts it. Pointing pxx at 400k lines of another compiler's source will surface a great many differences. Almost none of them are ours.
A finding belongs under this umbrella only if it stops correct Pascal compiling or running. Not because FPC does it differently, not because a diagnostic differs, not because an intermediate has a different type. If the source compiles and the program behaves, there is nothing to file — and the temptation to file it anyway is exactly what turned 5035 tickets into 467 open ones.
Binary interop with FPC is NOT a goal — settled the same day in
decide-how-a-hand-built-com-interface-becomes-callable, now in done/. Do not
rank anything here on exchanging objects with FPC-compiled code, sharing its
representations, or linking against its output.
2026-09-09, frankH — attempt 2: THE WHOLE CORPUS, and the invocation was wrong
READ THIS BEFORE ANY NUMBER IN ATTEMPT 1. Attempt 1 was invoked as
pascal26 -Mobjfpc -Fu<compiler> -Fi<compiler>, and that is not how FPC's
compiler is built. Under it, globtype.pas:115 unknown type: PInt looked like a
frontend bug and was on its way to being filed. It is not one: PInt is
declared only inside {$ifdef cpu64bitaddr} arms, fpcdefs.inc derives that
from the CPU define, and FPC's own Makefile.fpc:381 passes -d$(CPC_TARGET).
pxx already has the flag for exactly this — --mimic-fpc-compiler
(feature-mimic-fpc-compiler-define-profile), which supplies the FPC identity
defines plus the one CPU define fpcdefs.inc derives the other forty from.
The corpus-attempt rule this produces, and it is the transferable half:
compile the corpus the way its own BUILD compiles it, and prove it by running
that build's compiler as an ORACLE on the same invocation. Four of the ten first
failures in attempt 1 were mine, not pxx's, and one of them (ccharset) fpc
itself refuses under that invocation — a unit the oracle cannot compile can
never be evidence about us. Every row below is therefore fpc first, pxx
second, same flags:
fpc -Mobjfpc -dx86_64 -Fu{C} -Fu{C}/x86_64 -Fu{C}/systems -Fu{C}/x86 \
-Fi{C} -Fi{C}/x86_64 -Fi{C}/x86
pascal26 -Mobjfpc --mimic-fpc-compiler <the same -Fu/-Fi set>
Driver: one program d; uses <unit>; begin end. per unit, all 207 .pas in
fpc-trunk/compiler. Script committed as tools/fpc_compiler_corpus_probe.sh, so the method is
runnable rather than described; it refuses to run at all when fpc is absent,
because without the oracle a PXX-FAIL row is not evidence.
The distribution, measured at 24dbb0b37
| units | 207 |
| compile under both | 9 — compinnr dbgdwarfconst dwarfbase fpchash globtype macho symconst version wasmbase |
| oracle refuses (not evidence about us) | 10 |
| pxx stops | 188 |
First failures, by count. These are first-failure counts and therefore LOWER BOUNDS on nothing and UPPER BOUNDS on nothing — a unit that stops on the cycle may stop on the next cause after it moves, and 144 units clearing does not mean 144 units compiling. What the count IS good for is ORDER.
| n | first failure | ticket |
|---|---|---|
| 144 | undefined variable (internalerrorproc) |
[[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]] |
| 19 | {$if} over a source const or a source type alias |
[[bug-p-a-conditional-directive-cannot-read-a-constant-or-a-type-the-source-declares]] |
| 8 | an object type cannot have a constructor |
[[feature-p-legacy-value-object-types]] |
| 3 | undefined variable (bitsizeof) |
none — see below |
| 2 | expected field name in record constant |
none yet |
| 3 | uses: unit source not found (unixcp, heaptrc, charset) |
RTL units, not frontend |
| 2 | conditional directive: malformed expression / expected operator |
none yet |
| 1 each | undefined variable (align), undefined variable (IsATTY) |
none yet |
What moved this session, attributed rather than assumed
Two fixes landed. 588f18717 folds FPC's Sar* intrinsics; 24dbb0b37 lets a
subrange bound be a folded call (low(TCGLoc)..pred(LOC_CREFERENCE),
cgbase.pas:63). Re-running the whole probe before and after the second one, the
diff is exactly five rows and no others: aasmcfi, cgbase, nbas, ncgmem and
ncgnstmm move from cgbase.pas:63 unknown type: low to cgbase.pas:381 an object type cannot have a constructor, 318 lines further into the same file.
No unit newly compiles, and saying that is the point: a corpus delta is easy
to quote as progress and the honest unit of progress here is a wall, not a line
number.
bitsizeof, banked rather than filed, with the whole answer
bitsizeof(x) is an FPC intrinsic like Sar* — compinnr.pas:85,
in_bitsizeof_x — and it stops constexp, cgobj and ppu. It is exactly
SizeOf(x) * 8 for every type pxx can express, and that is measured, not
assumed: fpc answers 32/64/32/8 for bitsizeof of a LongInt, an Int64, the
LongInt type name and Byte, and the one place the two would differ is a
bitpacked field (fpc says 2 for a: 0..3), which pxx has no bitpacked at
all to reach — type T = bitpacked record is unknown type: bitpacked.
So the desugar has no wrong answer available today and WILL when bitpacked
lands; whoever writes it should say so in the comment.
It is not a one-line fix like Sar* for one reason worth recording: SizeOf's
arm in pasparser_expr.inc runs ~700 lines with six separate
AllocNode(AN_INT_LIT) exits, so * 8 is six edits, which is the shape this
repo keeps finding a missed copy in. The honest version unifies those exits
first.
What the attempt says about the two big walls
The cycle is not merely the largest, it is 77% of the corpus, and it is
structural: FPC splits uses into two clauses precisely so units can be
mutually recursive. Nothing behind it can be measured. The conditional-directive
family is second and is the one that is shaped like work already done here —
two forwarded questions of exactly its kind already exist beside it.
2026-09-09, frankH — attempt 3: the conditional-directive family, and what it uncovered
Two commits, abc681636 (read a source const, follow a type ALIAS through
sizeof) and fcbe280b7 (the two probe defects underneath it). The whole
probe was re-run before and after each, so both deltas below are per-unit
diffs and not category arithmetic.
| 24dbb0b37 | abc681636 | fcbe280b7 | |
|---|---|---|---|
| compile under both | 9 | 9 | 9 |
| oracle refuses | 10 | 10 | 10 |
| pxx stops | 188 | 188 | 188 |
| — of those, the conditional-directive family | 26 | 3 | 3 |
| — of those, the unit cycle | 144 | 155 | 158 |
NO UNIT NEWLY COMPILES, across both commits. That is the third time this umbrella has had to say it and it stays the honest unit of progress: a corpus delta is a wall moving, not a program building, and everything behind the 26 rows was standing in the unit cycle's queue.
abc681636 — 25 units changed and every one of them was a
conditional-directive first failure. No row moved that was not in that
family, which is what an attributed delta looks like. 11 went to the unit
cycle, 5 to ALU not defined (a wall further into the same files), 5 to
Unsupported tcompilerwidechar size, 2 to bitsizeof, 1 to unterminated conditional directive and 1 (ncnv) from one conditional row to another.
fcbe280b7 — 12 units changed, all forward. ALU not defined (5 units)
and unterminated conditional directive (1) cleared entirely.
The two probe defects, because they are the transferable half
Neither was new with the const/alias questions; both are older and
{$if declared(X)} had the first one first.
- A probe launched inside a conditional unbalanced the
{$if}stack. The walk scans the WHOLE token stream foruses, so a unit being lexed throughLexAppendfinds its PARENT'suses <thisunit>and re-lexes the file it is currently inside, from the outer file's conditional depth. At depth 0 the arithmetic balances and nothing shows; one conditional deep, the outer file is refused at its own last line. FPC'sentfile.pas— 2131 lines, 170 openers, 170{$endif}, perfectly balanced — reduced to 14+5+3 lines. - The probe never expanded the probed unit's
{$I}includes, which its own header said it did and is the reason it LEXES rather than scanning text.ParseUsesUnitBodycallsExpandIncludesbefore the realLexAppend; the probe calledLexAppendalone. So a name in an.incwas invisible, and so was every name behind a define an.incSETS:globtype.pasdeclaresPUint = qwordinside{$ifdef cpu64bitaddr},fpcdefs.incderivescpu64bitaddr, and the walkTConstPtrUInt -> PUint -> qwordtherefore stopped one hop short. The 18sizeofrows of the 26 named five different types -- 11bestreal, 3TConstPtrUInt, 2tcompilerwidechar, 1aintmax, 1bestrealrec-- and only theTConstPtrUIntchain needed the includes; the rest were answered by the const/alias commit alone. Said that way because "18 rows of one kind" is exactly the summary that would have hidden the split.
Unsupported tcompilerwidechar size is the charset row, not a frontend bug
7 units stop there and they are ONE cause, measured rather than inferred:
ncon.pas:968 is FPC's own {$error} in the {$else} arm of
{$if sizeof(tcompilerwidechar) = 2}; tcompilerwidechar = word lives in
widestr.pas:35; widestr uses charset, which is an FPC RTL unit
and is not on the probe's unit path. Two controls:
- a local stand-in unit declaring the identical
tcompilerwidechar = wordanswerstwo, so the alias walk is not the problem; and - with
fpc-trunk/rtl/incadded to-Fu, the{$if}resolves and the first failure moves INTOcharset.pp, atDirectorySeparator— an FPCSystemconstant that appears nowhere in pxx'slib/orcompiler/.
So these 7 belong on the same row as uses: unit source not found: charset,
and the next measurable step for them is not the frontend: it is whether pxx
compiles FPC's RTL at all. That is a different corpus and it should be
decided as one before anybody starts adding System constants one error at a
time.
What is left of the conditional-directive family: 3 units, 2 shapes
Filed as
[[bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal]]
— set membership over a set-valued const (nld.pas:700, and ncnv reaches the
same directive through uses nld) and a const whose value is a folded call
(RS_INVALID = high(tsuperregister), cgbase.pas:400, asked by
rgobj.pas:1728). Both are behind the unit cycle anyway.
make test-fpc IS NOT THIS TARGET, and it has never produced a verdict row
Two separate traps, and the second is the one that will catch a later
attempt. test-fpc runs FPC's TEST SUITE; this umbrella compiles FPC's
COMPILER. They share a name and nothing else — the suite is thousands of
small conformance programs, the compiler is 207 mutually-recursive units, and
a green on one says nothing about the other. And test-fpc is in NO TIER
(frankuser, 2026-09-09, filed as
bug-t-six-real-program-jobs-are-in-no-tier-so-they-never-run, T p65, with
duktape, quickjs, chess-perft, sqlite-parity and wasm32): it is not skipped
and not failing, no tier invokes it, so it has produced zero rows ever. A
green from running it by hand is a claim about that one run on that one tree.
So: do not reach for make test-fpc to measure this umbrella, and do not read
its absence from the tstate archive as a pass.
2026-09-10, frankH — attempt 4: the two biggest walls, and neither bought a unit
Two fixes, each with the WHOLE probe re-run before and after it, so both deltas below are per-unit joins and not category arithmetic.
590e7c100 |
6e8a821db (cycle) |
7e4f69a34 (bitsizeof) |
|
|---|---|---|---|
| units | 207 | 207 | 207 |
| compile under both | 9 | 9 | measured below |
| oracle refuses | 10 | 10 | |
| pxx stops | 188 | 188 |
| first failure | 590e7c100 |
after the cycle fix |
|---|---|---|
undefined variable (internalerrorproc) — the unit cycle |
158 | 0 |
undefined variable (bitsizeof) |
5 | 163 |
an object type cannot have a constructor |
8 | 8 |
Unsupported tcompilerwidechar size |
7 | 7 |
expected field name in record constant |
2 | 2 |
conditional directive: expected operator |
2 | 2 |
uses: unit source not found (unixcp, heaptrc, charset) |
3 | 3 |
undefined variable (align) / (IsATTY) |
1 each | 1 each |
RS_INVALID has no integer value |
1 | 1 |
Exactly 158 units moved, every one of them from the cycle to bitsizeof, and
nothing else moved at all. A join on the unit name says so.
The number to distrust here is the one that did NOT move
BOTH-OK stayed at 9 across the largest single fix this umbrella has had. 76% of the corpus was standing behind the unit cycle and the next wall was immediately behind it, so a 158-row delta bought no unit. That is the shape to expect from a corpus attempt and it is why this file reports walls rather than line numbers: a per-unit diff of 158 rows is easy to quote as progress, and the honest unit of progress is a wall.
bitsizeof was banked here with its whole answer and it held up
The banked note said SizeOf(x) * 8, no wrong answer reachable because pxx has
no bitpacked, and not a one-liner only because SizeOf's arm has six
AllocNode(AN_INT_LIT) exits — "the honest version unifies those exits
first". All three were right. The exits are now one (EmitSizeOfResult) and
the multiply exists once.
Banking the whole answer rather than filing a ticket was the right call for a
five-line desugar behind a six-copy refactor, and it is worth saying why it
worked: the note carried the MEASUREMENT (fpc's four answers), the BOUNDARY
(bitpacked, and that it is unreachable) and the COST (six exits). A ticket
saying "implement bitsizeof" would have carried none of them.
What the attempt says now
The corpus is no longer dominated by one structural wall. After 7e4f69a34
the distribution is measured in the row below this section; whatever it says,
the next causes are all SMALL — the largest before this attempt was 8 — which
means the next rung of this umbrella is a different kind of work from the last
two: several unrelated causes rather than one that everything queues behind.
2026-09-10, frankH — attempt 5: the System routines, and a THIRD consecutive null row
Two more walls cleared and BOTH-OK did not move at all, for the third attempt running. That is the number this section is about.
What landed
| sha | wall | units it was the first failure of |
|---|---|---|
959468420 / 0dfa0b298 |
unknown type: PSizeUint |
150 |
dbb96cdb6 |
undefined variable (IndexQWord) |
96 |
Both are FPC System-unit surface, not dialect features — the same class as
Prefetch, which is already in builtin.pas for exactly this reason. The
Index/Compare family went in beside it: IndexByte, IndexWord, IndexDWord,
IndexQWord, CompareWord, CompareDWord. TFPList.IndexOf
(cclasses.pas:890-895) selects the DWord or QWord arm by pointer width under
a {$if}, so both arms must resolve for either to compile — a family with
holes in it is not half-working, it is not working.
The measurement
Whole probe re-run at each sha, fpc as oracle, per-unit join rather than
category arithmetic.
probe #8 @ 0dfa0b298 |
probe #9 @ dbb96cdb6 |
|
|---|---|---|
| BOTH-OK | 15 | 15 |
| ORACLE-NO | 10 | 10 |
| PXX-FAIL | 182 | 182 |
Join: exactly 96 units changed their row, every one of them
IndexQWord -> unaligned, and nothing else moved. The BOTH-OK set is
byte-identical between the two runs, not merely the same size:
cdynset compinnr constexp cstreams cutils dbgdwarfconst dwarfbase fpchash
globtype macho optbase symconst systems version wasmbase
First failures now
| n | first failure |
|---|---|
| 150 | undefined variable (unaligned) — cclasses.pas:1327 |
| 6 | Unsupported tcompilerwidechar size |
| 7 | an object type cannot have a constructor (two line numbers) |
| 4 | expected field name in record constant |
| 3 | unknown type: d |
| 1 each | IsATTY, align, swapendian, unixcp, RS_INVALID |
The number to distrust, again, is the one that did NOT move
Three attempts in a row have cleared the single largest wall and bought zero
units: 158 units moved for the unit cycle, 96 for the Index family, and BOTH-OK
went 9 -> 15 -> 15 -> 15. The 15 was bought by bitsizeof alone.
The reading is not "the fixes did nothing". It is that in this corpus the
walls are STACKED, and the depth is what nobody has measured. Every unit that
cleared IndexQWord landed on unaligned — the same file, four hundred lines
further down. Clearing wall N reveals wall N+1 in the same dependency, so the
first-failure histogram is a picture of ONE unit's contents (cclasses.pas,
which nearly everything uses) far more than of the corpus's difficulty.
So stop ranking this umbrella's blockers by how many units name them. 150
units naming unaligned is one call site in one shared unit; it says almost
nothing about how much work stands between here and a compiling corpus. The
honest instrument for that question is a probe that reports EVERY failure in a
unit rather than the first, and nobody has built one. Until then, the count is
a queue position, not a size.
Filed from this attempt:
[[feature-p-unaligned-is-a-transparent-lvalue-not-a-function]] — and note it is
NOT a builtin.pas job like the last two looked: 275 call sites in the corpus
and some are assignment targets, so a function-shaped fix clears all 150 units
and still fails on ogomf, owomflib and entfile for the identical spelling.
2026-09-10, frankH — attempt 6: the prediction, tested the same hour it was written
b9c8fc160 landed unaligned. Probe #10, whole corpus re-run, per-unit join:
exactly 150 units changed their row, every one unaligned -> Finalize(x, n),
nothing else moved, BOTH-OK byte-identical at 15 for the third run running.
That is the FOURTH consecutive null row, and it is the sharpest one, because
the section above predicted it in this form: the walls are stacked inside
cclasses.pas and the count measures queue position rather than size. Three
consecutive walls, one file:
| probe | wall | line in cclasses.pas |
units |
|---|---|---|---|
| #8 | undefined variable (IndexQWord) |
895 | 96 |
| #9 | undefined variable (unaligned) |
1327 | 150 |
| #10 | Finalize(x, n) — the element-count form |
1726 | 150 |
TFPHashList.Clear calls Finalize(FItems^, FCount). Each fix delivered its
entire population intact to a wall a few hundred lines further down the same
file.
This is now the umbrella's most reliable finding and it should shape how the
next seat works it. Fixing the top of the histogram is nearly free of value
per fix while cclasses is unfinished — the honest unit of work is "make
cclasses.pas compile", not "clear the 150". The instrument that would say
how far that is reports EVERY failure per unit rather than the first, and it
still does not exist; building it is worth more than the next four walls.
Promoted to CLAUDE.md from here, on the recurrence test: frankB's lekkerzeilen umbrella produced the same shape independently (six import walls across two passes, modules-compiling moved by zero both times) — two corpora, no shared code, five null rows between them.
2026-09-10 — the ten ORACLE-NO units, named, because the category reads as a scandal
The owner asked, reasonably, whether this umbrella was claiming "fpc can't
compile its own units". It is not, and the category name invites that reading.
Measured by running only the oracle half of
tools/fpc_compiler_corpus_probe.sh (207 fpc compiles, no pxx):
| unit | fpc's own message | why |
|---|---|---|
pp |
Syntax error, "UNIT" expected but "PROGRAM" found |
it is a PROGRAM — FPC's compiler main. uses pp is meaningless |
cg64f32 |
Identifier not found "tcg64" |
32-bit codegen; tcg64 exists only on a 32-bit target |
ogomf |
RELOC_ABSOLUTE16 |
16-bit x86 object format |
ogrel |
RELOC_ABSOLUTE_HI8 |
AVR |
ogwasm |
TWasmBasicType |
wasm |
oglx |
tobjectinput |
platform-conditional |
browcol |
TCallbackFunBoolParam |
platform-conditional |
impdef |
dirstr |
platform-conditional |
cepiktimer |
Cannot open include file "../../epiktimer/epiktimer.pas" |
external package, not installed |
ccharset |
fpcdefs.inc(1,2) Mode switch "OBJFPC" not allowed here |
the probe's own -Mobjfpc colliding with the include |
Nothing here is fpc failing to build fpc. The probe fixes -dx86_64; build
FPC for i386 and cg64f32 compiles, for AVR ogrel, for wasm ogwasm. The
exclusion exists so a unit the ORACLE rejects under OUR flags can never be
counted against us — which is the right design and is why attempt 1 had four
false findings before it existed.
Two of the ten are OURS, not the corpus's
pp.pas is a program and should be excluded by name: counting it inflates
the denominator by one and it can never pass, so it is a permanent
can-never-be-green row sitting in the corpus total. ccharset is a flag
collision the probe creates itself — -Mobjfpc on the command line against an
include that sets its own mode switch — and may well compile once the probe stops
forcing the mode it is already getting from fpcdefs.inc.
So the honest shape is 206 units: 8 genuinely out of scope for an x86-64 probe, 15 BOTH-OK, 182 PXX-FAIL. Worth fixing in the probe rather than re-explaining every time someone reads the category — a number that needs a paragraph of defence each time it is quoted is a number with the wrong denominator.
2026-09-11, frankS — attempt 7: cclasses.pas compiles, and the null row ends at four
Probe #11 at e013c4344, binary 79b76b2cc67f, whole corpus, fpc as oracle.
probe #10 @ b9c8fc160 |
probe #11 @ e013c4344 |
|
|---|---|---|
| BOTH-OK | 15 | 18 |
| ORACLE-NO | 10 | 10 |
| PXX-FAIL | 182 | 179 |
Join against attempt 6's BOTH-OK set, which was listed there by name: all
fifteen are still present and the three added are cclasses, crefs,
rabase. crefs is uses globtype, cclasses; rabase is uses cclasses, systems. So the delta is exactly the unit that was unblocked plus its two direct
dependents, and nothing else moved — the same per-unit join the earlier
attempts used, and the reason it is worth the extra minute is that a +3 with a
different membership would have meant something else entirely.
What it took, and it was two bugs in one file
The instrument attempt 6 asked for — one that reports EVERY failure per unit
rather than the first — turned out not to be a wrapper. Finalize(x, n) was
raised through Error(), which HALTS, so every later wall in the same file was
structurally invisible. Converting that one site to ErrorRecover took
cclasses from ONE reported failure to THREE, and the second was real and
pre-existing:
- [[bug-p-a-method-parameter-typed-through-a-forward-pointer-alias-never-matches-its-own-body]]
(
ee560d0ad) — a method parameter typed through a forward pointer alias never matched its own body, so the body died at codegen withunresolved forwardnaming a file the author never wrote. - [[feature-p-the-element-count-form-of-initialize-and-finalize]]
(
d095cb08d) — the wall itself, which was a DELIBERATE refusal recorded indone/feature-a-implement-initialize-and-finalize-over-the-arc-helpers. That refusal is right about ignoring the form and is not an argument against implementing it.
ok: dpxx [code=536232B data=109348B bss=93392B procs=1487].
The four null rows were a queue and the queue emptied — but NOT for free
Attempt 6's finding stands and this attempt is its confirmation, not its refutation. The 150 did not become 150 BOTH-OK; they became 119 units stopped at the next wall, in a different file. Three of 150 is what "clearing the wall the 150 were on" was actually worth, and that is the number to quote against any future proposal to rank a blocker on its unit count.
First failures now
| n | first failure |
|---|---|
| 119 | unknown type: TExecuteFlags — cfileutl.pas:136, in its INTERFACE |
| 18 | an object type cannot have a constructor |
| 10 | expected field name in record constant — tokens.pas:378 |
| 10 | Unsupported tcompilerwidechar size |
| 6 | uses: unit source not found: charset |
| 4 | unknown type: d — sizeof(d) of a PARAMETER inside a local type decl |
| 4 | unknown type: TDoubleRec — x86_64/cpuinfo.pas:36 |
| 2 | conditional directive: expected operator |
| 1 each | unixcp, heaptrc, swapendian, align, IsATTY, RS_INVALID |
TExecuteFlags is not a parser gap — it is sysutils, and it was already
filed by frankH on 2026-09-09 as
[[feature-b-sysutils-has-no-executeprocess-and-no-texecuteflags]], back when it
was the frontier for a handful of units rather than for 119. Every line of that
diagnosis still holds and none of it needed re-deriving; it has a dated note
with the new count and nothing else. The hard half (ExecutePipeline /
PalVforkAndExec) is already written in the same unit, and a type-only stub is
worse than nothing — cfileutl's implementation calls ExecuteProcess, so the
type alone just moves the failure to link time.
Do not rank it on 119: this umbrella's own most reliable finding says that
number is a queue position, and attempt 7 just measured the conversion rate at
three units per wall.
One row is already gone, and it is recorded here so the next join is honest
expected field name in record constant was reduced and fixed the same hour —
[[bug-p-a-set-valued-record-field-cannot-be-written-in-a-record-constant]]
(138604b5e): FPC's tokens.pas writes keyword:[m_none] in ~400 record
constants and the set arm of TryParseInitValForm had been written out by hand
in the const-ARRAY loop and given to no other caller. tokens.pas compiles.
This table is probe #11's. Probe #12 is below and it is the current one.
Probe #12 @ 4c7c88d36, the same evening: 18 -> 20, and the ratio held a third time
#10 @ b9c8fc160 |
#11 @ e013c4344 |
#12 @ 4c7c88d36 |
|
|---|---|---|---|
| BOTH-OK | 15 | 18 | 20 |
| ORACLE-NO | 10 | 10 | 10 |
| PXX-FAIL | 182 | 179 | 177 |
Join again: all eighteen of #11's BOTH-OK units are still there and the two
added are tokens and rescmn — the two units that carried a set-valued
record field themselves. rescmn is uses Systems and opens with
res_elf_info : tresinfo = (...). The other eight of the ten that stopped at
expected field name in record constant moved to unknown type: TExecuteFlags,
which is why that row went 119 -> 127 while nothing about TExecuteFlags
changed.
Three walls, three joins, three small numbers: 3, then 2. A wall's unit count has now predicted its yield wrongly five times running, and the two measurements in this attempt are the first where the yield was positive at all — which makes them the strongest form of the same finding, not a counterexample to it. The useful reading: a wall's population tells you how many units are QUEUED behind it; the units that become BOTH-OK are the ones for which it was the LAST wall. Those are near-disjoint sets, and only the second is worth a number.
First failures now (probe #12)
| n | first failure |
|---|---|
| 127 | unknown type: TExecuteFlags — cfileutl.pas:136, in its INTERFACE |
| 18 | an object type cannot have a constructor |
| 10 | Unsupported tcompilerwidechar size |
| 6 | uses: unit source not found: charset |
| 4 | unknown type: d — sizeof(<a PARAMETER>) in a const expression |
| 4 | unknown type: TDoubleRec |
| 2 | conditional directive: expected operator |
| 1 each | unixcp, heaptrc, swapendian, align, IsATTY, RS_INVALID |
unknown type: d is entfile.pas:371, array[0..sizeof(d)-1] where d is the
enclosing function's PARAMETER. ConstEvalFactor's sizeof arm resolves a TYPE
NAME only and says so in its own comment; the expression path handles a variable
and SizeOf(d) in a statement answers 8 correctly. Reduced to ten lines and
left unfixed here rather than folded in — the honest fix is narrow but it is in
a 300-line intrinsic and deserves its own pass.
2026-09-11, frankH — two blockers closed, and the wall census gets a fourth null row
Both were taken from this umbrella's blocked-by, both are Track P, and the
combined corpus movement is one unit advanced to the next wall and zero units
compiling. That is the fourth independent time this umbrella has measured that
shape, and it is worth one line rather than another paragraph: the finding is no
longer news, it is the baseline expectation.
bug-p-sizeof-of-a-variable-is-not-folded-in-a-constant-expression —
sizeof(<a variable>) in a const expression (FPC entfile.pas:371). Fixed; 4
units stopped there and none of them compiles now either.
bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal
— this one was two defects plus a fourth hop the ticket had not traced, and
that is the part worth carrying forward:
- pxx did not SHORT-CIRCUIT
andin a{$if}. FPC'sdeclared(X) and (X<>Y)idiom exists BECAUSE it short-circuits, and a shunting-yard applies the parenthesised comparison first. Nobody had filed it. It is a real bug with an oracle and it moves zero units, because 17 of FPC's 18 cpubase files DO declareRS_STACK_POINTER_REG— the portable idiom is portable, so the portable path is nearly never taken in this corpus. - The const door then needed four hops, because the LEFT operand chains too:
RS_STACK_POINTER_REG = RS_RSPacross two units, andhigh()over a distinct-type alias. rgobj CLEARS this wall and stops at cfileutl.pas:136, which isTExecuteFlags— the 127-unit wall already at the top of this umbrella. nldandncnvdid NOT move; they are the set-membership shape, now split out as [[bug-p-a-conditional-directive-cannot-evaluate-in-over-a-set-constant]]. They are also one directive, not two — ncnv's interfaceuses nld.
THE SPLIT IS THE TRANSFERABLE PART. One ticket carried both shapes and their
sizes differ by an order of magnitude: the half that landed reused walks that
already existed and added no value kind, while the half split out needs a fourth
value kind on the directive stack, set-union folding, enum-member resolution and
an in operator the grammar does not have. A summary cannot be true about both,
and this umbrella ranks by blocked-by edges — so a mixed ticket prices the
cheap half at the expensive half's cost, in the one place where the price is what
gets read.
The every-failure census, 2026-09-16 (binary e790ec9e027f at 9281da35b)
Run it as PXX_CORPUS_DETAIL=<dir> tools/fpc_compiler_corpus_probe.sh. The
probe has had that flag and errs=N since 2026-09-11 — this umbrella's own
headline still said nobody had built the instrument, and a seat nearly rebuilt
it. Fixed above.
Totals: 21 / 10 / 176 (BOTH-OK / ORACLE-NO / PXX-FAIL). BOTH-OK unchanged at 21, as expected — nothing landed for P between probes.
First-failure histogram (still ranks by queue position — do not rank on it):
| n | first failure |
|---|---|
| 132 | unknown type: TDoubleRec |
| 18 | an object type cannot have a constructor |
| 10 | Unsupported tcompilerwidechar size |
| 6 | uses: unit source not found: charset |
| 4 | undefined variable (entryreal_bytes) |
| 2 | conditional directive: expected operator |
The result the histogram cannot show, and the reason for the detail flag:
of the 132 units whose FIRST failure is TDoubleRec, 0 have it as their ONLY
failure and all 132 also hit too many array initializer elements — and
nothing else. Error counts per unit: 38 units report 1, 132 report exactly
2, and only one unit anywhere reaches MAX_REPORTED_ERRORS. So the two-error
finding is real and not a truncation artefact.
Both walls are x86_64/cpuinfo.pas, lines 36 and 281. That is the
cclasses.pas pattern again — one file's contents wearing the shape of a
population — and it is the third time this umbrella has recorded it.
Prediction for the next run, stated before it: clearing BOTH lands the 132 on
a third wall in the same file, not on 132 compiling units. A complete
reported failure set is not a guarantee of compilation — ErrorRecover carries
past SEMANTIC failures, and a halting diagnostic later in the unit would never
appear. That is the honest caveat and it should not be dropped when this is
quoted.
2026-09-16 (frankS) — the seventh null row, and the third wall is REAL but NOT in the same file
bug-p-an-array-constant-with-a-set-element-type-cannot-be-initialised is fixed
(14df2066b). Measured what that delivered, and measured the prediction that was
put on record before the work, which is the part worth keeping.
Three arms, same probe, 207 units, pxx-only (no fpc oracle — read the DELTA between columns, never the absolute count):
| arm | compiler | cpuinfo walls | units OK | total errors |
|---|---|---|---|---|
base |
pre-fix | both present | 21 | 396 |
fix |
post-fix | TDoubleRec only | 21 | 396 |
stub |
pre-fix | both stubbed out | 21 | 256 |
UNITS COMPILING MOVED BY ZERO IN BOTH DIRECTIONS — including with BOTH walls
removed. That is the seventh null row and it is consistent with every previous
one. 138 units report exactly two errors, TDoubleRec first.
THE FIX WORKED AND THE COUNT CANNOT SEE IT. base and fix are identical
unit-by-unit and identical in total error count, which reads as "nothing
happened". It is not what happened. Checked one unit directly:
aasmbase, pre-fix : pascal26:36 unknown type: TDoubleRec
pascal26:281 too many array initializer elements
aasmbase, post-fix: pascal26:36 unknown type: TDoubleRec
pascal26:35 an object type cannot have a constructor
The array error is gone and a different error took its slot. All 138 units
traded one wall for the next, so the count held at 2 and the first-error
histogram — which is what the PXX_CORPUS_DETAIL summary reports — held at
TDoubleRec. An error COUNT is not an error IDENTITY, and a wall census
compared on counts will report a successful fix as a null result. Compare the
message sets, not the totals.
The prediction, scored honestly. It was: "clearing both lands the 132 on a third wall in the same file." Half right, and the wrong half matters.
- RIGHT that a third wall swallows the population. With both walls stubbed,
156 units hit
an object type cannot have a constructor(bug-p-object-value-types-standard-meaning, already ticketed) — up from 18, so 138 arrived there exactly as predicted. - WRONG that it is in the same file. With both stubbed,
cpuinfo.pascompiles clean. There is no third wall in that file; the next one is further down each unit's own dependency chain.
That distinction is worth keeping because it changes what the next fix should
be. "Another wall in cpuinfo.pas" would mean keep grinding that file; "one
shared wall one file further on" means the next lever is
bug-p-object-value-types-standard-meaning, which now gates 156 of 207 units
and is the largest single wall this corpus has.
And the ranking caveat still applies to that 156, unchanged. It counts units QUEUED, not work, and this umbrella has now converted seven walls at a yield of zero additional compiling units. Predict before the next re-run; a null row is only information to someone who said what they expected.
ErrorRecover caveat, carried forward. A complete reported failure set is
a claim about what the diagnostic emitted, not about what compiles. A halting
diagnostic later in a unit never reaches the detail file at all, so "two errors"
and "two fixes from compiling" are different statements — and the zero-yield rows
above are consistent with that being true of some units here.
SCORING A WALL BY THIS CENSUS WILL REPORT A WORKING FIX AS A NULL RESULT
Measured 2026-09-16 (franks-ee), and it lands on the section above. Three arms over all 207 units:
| arm | compiler | cpuinfo walls | units OK | total errors |
|---|---|---|---|---|
| base | pre-fix | both present | 21 | 396 |
| fix | post-fix | TDoubleRec only | 21 | 396 |
| stub | pre-fix | both stubbed out | 21 | 256 |
base and fix are identical unit-by-unit, identical by count, and identical in
the first-error histogram — and the fix WORKED. All 138 units traded the array
wall for a different error in the same slot, so the count held at 2 and the first
error stayed TDoubleRec:
aasmbase pre-fix : :36 TDoubleRec / :281 too many array initializer elements
aasmbase post-fix: :36 TDoubleRec / :35 an object type cannot have a constructor
AN ERROR COUNT IS NOT AN ERROR IDENTITY. PXX_CORPUS_DETAIL's summary reports
counts and first errors, so scoring a wall with them alone reports a landed fix as
nothing. Diff the detail files unit-by-unit, on the error TEXT, not on errs=N
and not on the histogram. Same family as CLAUDE.md's line-31 finding: the
instrument is honest and is answering a narrower question than the reader supplies.
SEVENTH NULL ROW — and the third wall is NOT in cpuinfo.pas
Stubbing BOTH walls out of a copy of cpuinfo.pas moved units-compiling by ZERO
(21 -> 21). The prediction that a third wall would swallow the population was right;
the prediction that it would be in the same file was wrong — cpuinfo.pas compiles
clean with both stubbed. The 138 land one file further on, at an object type cannot have a constructor, which goes 18 -> 156 of 207 and is now the largest
single wall this corpus has recorded.
That distinction is the whole value of the measurement: "another wall in
cpuinfo" means keep grinding one file; "one shared wall one file on" means the next
lever is feature-p-legacy-value-object-types / bug-p-object-value-types-standard-meaning.
Ranking caveat fully intact — 156 is units QUEUED, and this umbrella has now
converted seven walls at a yield of zero.
STUB BEFORE YOU FIX. It answered the size question for almost nothing and BEFORE the work, and it is the only reason we know the next wall is elsewhere.
2026-09-16 (frankS, later) — the wall ORDER matters, and I got it wrong before measuring
fa397c761 fixes var s: string = <named const> (globals.pas:502). It
changes nothing here today, and the reason is worth more than the fix.
I claimed this wall gated 138 units and was the cheaper lever than the object
wall. It does not, and it is not in front of it. globals.pas:502 sits
BEHIND the object-constructor wall: those units stop at the object
constructor and never reach the var initialiser. Measured on the real tree,
before and after:
| units OK | total errors | first-error histogram | unit-by-unit | |
|---|---|---|---|---|
before fa397c761 |
21 | 396 | unchanged | identical |
| after | 21 | 396 | unchanged | identical |
It only appeared to be in front because I found it with the object wall STUBBED OUT. The stub was there to reveal what was behind; I then read its output as an ordering over the unstubbed corpus. A stub tells you what is BEHIND a wall — it cannot tell you a wall is in FRONT of one, because in the stubbed world that wall does not exist. The stub answers "what is next", and I read it as "what is first".
That is the same family as the count-versus-identity finding above and it arrives from the other side: there, an instrument that could not see a real change; here, an instrument that showed a real change in a world I had built. Both are honest and both are about a narrower question than the reader supplies.
What the fix does deliver, measured where it is reachable (stubbed tree, cpuinfo ×2 + versioncmp's ctor):
- all 138 move from
not a constanttounknown type: TSystemTime - units compiling 22 → 22
Wall six is TSystemTime — an RTL type gap, same family as TDoubleRec,
Track B. With that, the walls this umbrella has hit now alternate between
Track P parser gaps and Track B RTL type coverage:
1 TDoubleRec RTL type (B, ticketed)
2 array-of-set var init parser (P, FIXED 14df2066b)
3 object constructor language (DECIDED against; see decide-old-style-object-types)
4 var = named string const parser (P, FIXED fa397c761)
5 TSystemTime RTL type (B, unticketed)
So the binding constraint is not one subsystem. Two of the five are RTL types we simply do not declare, and neither needs a compiler change or a decision — which makes them the cheapest remaining lever, and the first structural read this umbrella has had that is not "one more wall in one more file".
Ninth null row. Five walls cleared or stubbed across the day, units
compiling 21 → 22. The standing caveat holds and has now been demonstrated
twice in one session: a wall's population is a queue position, the arms are
pxx-only (read deltas, not the absolute 21), and an error count is not an
error identity — fa397c761 is invisible in every summary quantity and is
still a real fix.
A STUB ORDERS THE STUBBED WORLD ONLY — it says what is BEHIND a wall, never that a wall is in FRONT
Measured 2026-09-16 (franks-ee), correcting itself, and it corrects the advice in the section above. "Stub before you fix" is right and it is what found walls 4 and 5 at all. What it does NOT do was never stated: in the stubbed world the stubbed wall does not exist, so the ordering you read off it cannot tell you that wall sits in FRONT of what you found.
globals.pas:502 was found with the object wall stubbed and reported as gating 138
units and as the cheaper lever. It gates nothing on the real corpus — those 138 stop
at the object constructor and never reach the var initialiser. Before and after on the
unstubbed tree: 21 units OK, 396 total errors, identical unit-by-unit AND by error
identity.
The discipline, with the missing line: stub to find the next wall, then RE-RUN UNSTUBBED before claiming an ordering. One command.
Pair this with the count-vs-identity section above — they are the same family in opposite directions, twelve hours apart. There the instrument could not see a real change; here it showed a real change in a world the measurer had built. Both honest, both answering something narrower than the reader supplied.
And fa397c761 is the count-vs-identity rule biting a SECOND time: a real fix,
invisible in every summary quantity — same units, same counts, same first-error
histogram. Two independent instances now, not one.
THE WALLS, and the first structural read that is not "one more wall in one more file"
| # | wall | kind | state |
|---|---|---|---|
| 1 | TDoubleRec |
RTL type | Track B, ticketed |
| 2 | array-of-set var init |
parser | FIXED 14df2066b |
| 3 | object constructor |
language | DECIDED AGAINST (decide-old-style-object-types, option A) |
| 4 | var = named string const |
parser | FIXED fa397c761 |
| 5 | TSystemTime |
RTL type | FIXED c52d5b31b (franks-ee) |
| 6 | sizeof(files[0]) on a pointer-indexed element (finput.pas:544) |
parser | FIXED a931bef4d (franks-ee) |
| 7 | parameterless call spelled WITHOUT parens when the name is OVERLOADED (comphook.pas:386) |
parser | FIXED 3926a4098 (franks-ee) |
| 8 | StdErr is an fd, not a Text (comphook.pas:397, :399) |
RTL type | FIXED d66f128a1 (franks-ee) — live without a pin |
| 9 | SysUtils.FileAge (comphook.pas:474) |
RTL | FIXED 42e127d6d (franks-ee) — live without a pin |
| 10 | TRawByteSearchRec + FindFirst (cfileutl.pas:282) |
RTL type | FIXED 34e3a2fa8 (franks-ee) — live without a pin |
| 11 | GetDir + the sysunixh.inc const block (cfileutl.pas:518, :543, :696) |
RTL | FIXED d7bf36ce0 (franks-ee) — live without a pin |
| 12 | MkDir/RmDir/ChDir + IOResult plumbing (cfileutl.pas:714) |
RTL | OPEN — the head of the STUBBED arm, Track B |
THE NUMBER IN COLUMN 1 IS A ROW POSITION, NOT AN IDENTITY — CITE THE file:line.
This heading said FIVE while the table held SEVEN rows, and on 2026-09-16 two seats
disagreed in writing about which wall comphook.pas:386 is: this table calls it 7,
franks-ee calls it 8, and both counts are honest — the umbrella's prose above
counts walls CLEARED historically, and this table numbers its own rows. Neither is
wrong and neither is quotable. Nothing downstream may key on the number; the
subjects are unambiguous and the two seats never disagreed about those —
finput.pas:544 is FIXED at a931bef4d, comphook.pas:386 is OPEN. Name walls by
file:line in tickets, commits and messages.
Two of the five are RTL types we simply do not declare — no compiler change, no
decision, nothing to reverse. That makes them the cheapest remaining lever by some
distance, and it is the first time this umbrella's blockers have sorted into kinds
rather than into files.
Wall 3 is the one to understand before ranking anything: it is decided AGAINST, so walls 4 and 5 sit behind a wall nobody is authorised to remove. Do not rank 1 or 5 on the 138 — that count comes from the stubbed world. Nine null rows in a row; the yield of walls 1 and 5 stays unknown until wall 3 moves, and moving wall 3 requires a decision recorded below the existing one.
WALL 5 IS FIXED AND IT IS THE FIRST ONE THAT IS LIVE WITHOUT A PIN — rank RTL walls above compiler walls for that reason alone
Landed 2026-09-16 by franks-ee, c52d5b31b: SysUtils.TSystemTime, GetLocalTime,
DateTimeToSystemTime, SystemTimeToDateTime. Wall 5 of the five-wall table above is
cleared. The new head is finput.pas:544 — ReallocMem(files,afiles*sizeof(files[0])),
sizeof of an element reached by INDEXING A POINTER. That is a parse gap, not a type
gap, so it is Track P and it does not join the two RTL rows.
The property that makes the kind-sort in the table ACTIONABLE, and it was not stated when the table was written. A wall's kind said what it would COST to fix. It also says when the fix becomes REAL, and the two RTL rows differ from every other row on that axis:
- A compiler fix (walls 2 and 4, and anything in
compiler/**) is inert until the next pin.$(PXX_STABLE)consumers keep the old behaviour until Track A pins. - An RTL fix is live the moment it lands. Verified here, independently of the
author's report: pin v410's directory contains no RTL at all (
builtinand the binary, nosysutils),TSystemTimeenters the tree in exactly one commit —c52d5b31b, not an ancestor of the pin — and the pinned v410 binary compiles and runs a program usingTSystemTime,DateTimeToSystemTimeandSystemTimeToDateTime, answering1899 12 30forTDateTime(0.0)and round-tripping to0.0000. The pin snapshots the compiler and its builtin units;lib/rtlis read from the tree.
So "cheapest lever" understated it. An RTL wall costs no compiler change, no decision,
AND no pin — it is the only class of umbrella blocker whose fix is worth something to
every other seat on the same day. Wall 1 (TDoubleRec) is the remaining member and should
be ranked accordingly.
What this does NOT do is move the yield question. Walls 4, 5 and 6 still sit behind wall 3, which is decided against, so nine null rows in a row remain nine. Live-without-a-pin is an argument about WHEN a fix pays, never about WHETHER this umbrella's count moves.
Two conventions franks-ee read out of FPC's source rather than guessing, recorded so
nobody re-derives them: TSystemTime.DayOfWeek is 0-based where SysUtils.DayOfWeek
is 1-based (a fixture must assert the DIFFERENCE, or it passes under either convention);
and SystemTimeToDateTime composes with a sign-correct ComposeDateTime, not by adding
— 1899-12-29 06:00 is -1.25 composed and -0.75 added. GetLocalTime returns UTC
deliberately.
No ticket, and that is correct. CLAUDE.md: filing instead of fixing is the error. The table above is the bookkeeping.
WALL 6 FIXED, AND ITS CENSUS IS THE SIXTH NULL ROW — PREDICTED AS ZERO IN ADVANCE, FOR THE SIXTH TIME
a931bef4d (franks-ee): a FindSym MISS kept SizeOf on the name path, which cannot
index. SizeOf(<field>[index]) is accepted; 33/33 on the extended fixture, with a control
that can fail — pin v410 and a purpose-built pre-fix binary both refuse it with
expected ')' before '[', and TR is 12 bytes in that fixture so a pointer-width answer
cannot pass for a correct one.
Four-arm census, against an expectation written down BEFORE the arms ran:
| arm | result |
|---|---|
| stubbed | 105 units first-failed at the sizeof wall; afterwards zero detail files name expected ')' before '[' anywhere. Cleared, not moved. Units-OK 22 → 22. |
| unstubbed | not one row changed. 21 → 21. No unit lost in either pair, compared unit by unit. |
The unstubbed null is the half that carries the ordering claim, and it is the discipline
this umbrella wrote for itself working: finput sits BEHIND the cpuinfo and versioncmp
walls on the real corpus, so the fix is worth nothing today and worth the whole 105 the
moment those clear. Sixth null row, predicted as zero in advance for the sixth time —
which is the only thing that makes a null row information.
The absence instrument is the right one here. "Zero detail files name the error string anywhere" cannot be produced by accident; a units-OK count can. Cleared rather than moved is a claim about error IDENTITY, not about a count, and this umbrella has been burned by the difference before.
WALL 7 — a one-cell defect, and this seat FAILED TO REPRODUCE IT (which refutes nothing)
comphook.pas:386, system.str(getrealtime-starttime:0:3,hs2). franks-ee read the site
off the detail file's own in: line rather than inferring it — necessary, because the
diagnostic prints no file name and the symbol leads a reader to globals.pas:386, which
is a comment. (Verified here: it is { contains tpackageentry entries }.) That is CLAUDE.md's
same-line-number rule paying for itself a second time in this umbrella.
Its reduction, eleven lines, with the two controls that make it one cell:
| shape | verdict | |
|---|---|---|
| A | overloaded, bare | REFUSED undefined variable (grt) |
| B | overloaded, empty parens | COMPILES |
| C | overloaded, bare, plain RHS | REFUSED |
| D | the parameterful overload | COMPILES |
| E | control — NOT overloaded, bare | COMPILES |
| F | control — NOT overloaded, parens | COMPILES |
So only the intersection fails: a parameterless call spelled without parentheses when the name is overloaded. Same shape as wall 6 — a door wired for one spelling of a reference and not the other, and the passing spelling is the one everybody writes in a test.
THIS SEAT COULD NOT REPRODUCE IT AND THAT IS A FACT ABOUT THIS SEAT'S FIXTURE. Six
reconstructed rows plus four more in the real site's str(...:0:3, s) shape ALL COMPILE at
a931bef4d. The reconstruction is worthless as a refutation, and the control says why: it
compiles under PIN v410 too — a binary that predates both walls. A fixture that passes on
every compiler ever built cannot distinguish a fixed defect from one that was never there,
which is this file's own "if the machinery did nothing at all, would this row still pass?"
answering yes. Recorded so nobody reads it as a contradiction of franks-ee's table; the
difference is in something the reconstruction did not copy, and the fixture to trust is the
one with the failing rows in it. Asked for its eleven lines rather than guessing further.
The method note franks-ee volunteered is the one to keep: its FIRST run of that table reported all six rows REFUSED, controls included, because the harness broke on spaces in a tag and it was reading "no error line printed" as success. The controls caught it — E and F are not decoration, they are what separates a finding from an instrument. It nearly shipped a table in which the instrument was the finding, and said so unprompted.
WALLS 6 AND 7 WERE THE SAME DEFECT CLASS IN TWO TABLES, AND WALL 8 IS ELEVEN LINES BELOW WALL 7
Measured 2026-09-16, unstubbed AND stubbed, expectation recorded before each run.
finput.pas:544(a931bef4d, written up in full above):SizeOf's operand scan askedFindSymabout the identifier before the[and only left the name path on a POSITIVE identification of a non-array symbol. A field is not a symbol in scope, so the lookup MISSED and the operand kept the path that cannot index what it cannot find.comphook.pas:386(this commit):FindProcreturns the REPRESENTATIVE of a same-named set, and both parenless call doors tested that one proc's arity and gave up — so a parameterful overload declared FIRST made the bare spelling unreachable.
Both are a lookup answering about the wrong member of a set and the caller reading that as a fact about the whole set, once through a miss and once through a representative. Neither is a missing check; both fired a confident diagnostic about the wrong thing. That is a sharper grouping than "two parser walls" and it predicts where to look next: any door that resolves a name ONCE and then reasons about arity, kind or type from the single answer.
The yield is still zero and it was predicted as zero both times. Wall 6
cleared 105 units onto wall 7; wall 7 cleared the same 105 onto wall 8, which is
comphook.pas:397 — eleven lines below wall 7 in the same file. That is the
cclasses.pas shape this umbrella already recorded (895 → 1327 → 1726): a
first-failure census walking one file's contents and reporting each step as a
new population. Units-compiling: 22 → 22 → 22 stubbed, 21 throughout unstubbed,
where not one row changed either time because everything still dies earlier at
walls 1 and 3.
A caution for whoever reads the next histogram. comphook.pas:397 nearly did not appear
in it: the compiler mints a per-instantiation suffix (WriteMsgTypeColored$151860), so grouping the raw message split one 105-unit wall into 105 singletons, each
ranking below every genuine small wall, and the visible rows were IDENTICAL to
the previous run. Normalise $[0-9]+ before grouping, and check the buckets sum
to the population. Worked up in debugging-playbook.md.
Eleven null rows in a row now. Walls 6, 7 and 8 all sit behind wall 3, which
is decided against, so the count of units compiling cannot move by clearing them
and nobody should rank them on it. What they buy is that the queue behind wall 3
is being drained in advance, and each one is a real defect that real Pascal hits
outside this corpus — the overload one in particular is reachable by any program with an
overloaded parameterless routine, which is why it landed with fixtures and a
must-refuse control rather than as corpus bookkeeping.
Named by file:line throughout, per the caution in the table above — I called
comphook.pas:386 "wall eight" in a commit message and in messages to a peer while this
table calls it row 7, and both counts were honest. The subjects were never in dispute.
FOUR CONSECUTIVE WALLS IN ONE FILE, AND THE KINDS ALTERNATE — comphook.pas 386 → 397 → 399 → 474
Measured 2026-09-16. finput.pas:544 delivered its 105 units to comphook.pas:386,
which delivered the same 105 to :397, which delivered the same 105 to :474. Three
fixes, three null rows, one file walked from line 386 to line 474. This umbrella
already recorded that shape for cclasses.pas (895 → 1327 → 1726); it is now the
dominant pattern here and not an anecdote, and it is the reason the first-failure
census cannot be read as a work estimate.
The kinds alternate, which is the part worth acting on:
| wall | kind | inert until a pin? |
|---|---|---|
finput.pas:544 |
parser (Track P) | yes |
comphook.pas:386 |
parser (Track P) | yes |
comphook.pas:397 / :399 |
RTL type (Track B) | NO — live immediately |
comphook.pas:474 |
RTL (Track B) | NO |
cfileutl.pas:282 |
RTL type (Track B) | NO |
cfileutl.pas:518 / :543 / :696 |
RTL (Track B) | NO |
cfileutl.pas:714 |
RTL (Track B) | NO |
So the head of this umbrella is currently an RTL wall, and RTL walls are worth
strictly more per hour than compiler walls — the umbrella's own earlier finding,
now with a third instance (TSystemTime, then StdErr, and FileAge next). A
lib/** fix is verifiable against the pin in place and reaches every other seat the
moment it is pushed; a compiler/** fix sits inert until somebody pins, and nobody
may pin while the owner is away. Whoever picks this up next should take
comphook.pas:474 (FileAge) rather than looking for a parser wall.
StdErr was misclassified as a parser wall for about an hour, by me, in the table
above. It reads like one — the diagnostic is an overload-resolution failure, which
is what the two parser walls before it also were. It is an RTL type gap: StdErr was
an integer fd constant with a parser special case, where FPC's is a Text. The
diagnostic names the door that refused, not the thing that is missing, and three
walls in a row arriving through overload resolution is exactly the run that makes the
fourth look like more of the same.
WALL TEN — SysUtils.FileAge, and the FIFTH consecutive wall is where the pattern BREAKS
Measured 2026-09-16, franks-ee. FileAge, FileDateToDateTime and
DateTimeToFileDate implemented in lib/rtl/sysutils.pas. Library-only, so it is
live under pin v410 and inert for nobody.
Expectation, recorded before the run (w10_expectation.txt, and it was written
before the first attempt, not after): the 105 leave the wall; units-OK does NOT move
from 22; the likeliest next head is comphook.pas:1012 or something further down the
same file, because that file had produced four walls in a row and
FileDateToDateTime was already visible at :1012 with 12 units on it.
The first half was right and the second half was wrong, which is the finding.
| arm | units-OK | the wall | the new head |
|---|---|---|---|
| stubbed | 22 → 22 | 105 → 0 | cfileutl.pas:282 TRawByteSearchRec, 120 units |
| unstubbed | 21 → 21 | 105 → 0 | x86_64/cpuinfo.pas:36 TDoubleRec, 132 units |
CLEARED, NOT MOVED, in the strong form: zero detail files name FileAge
anywhere — not merely as a first error — across all 207 units in both arms. The
same is true of FileDateToDateTime, whose own 12-unit wall at :1012 went with it,
because the one fix supplied both names. No unit compiled before and fails now,
and none newly compiles, in either arm, compared unit by unit. The ORACLE-NO set is
identical. Ninth null row, ninth time predicted as zero in advance.
comphook.pas is now out of the picture entirely — it holds zero first errors and
appears in zero detail files as a location. Four consecutive walls in one file, and
then the file cleared rather than yielding a fifth. So the "one file walks
downward" shape is real but it TERMINATES, and the section above it should not be
read as predicting where a head goes next: it predicted :1012, the fix took :1012
out along with :474, and the head left the file. The cclasses.pas precedent
(895 → 1327 → 1726) says the same thing in hindsight — a file stops producing walls
when you run out of its distinct RTL dependencies, not when you reach its end.
THE UNSTUBBED HEAD IS ROW 1 OF THE TABLE ABOVE. TDoubleRec has been wall #1 in
this umbrella from the beginning and is still open, and on the REAL corpus it is what
132 of 207 units hit first. Everything the stubbed arm reports is downstream of
somebody agreeing to look past it.
And the stub is TWO files, not one — versioncmp.pas and x86_64/cpuinfo.pas.
This seat wrote "only one file differs" after diffing fpcsrc/*.pas, which is the
corpus ROOT only; the probe also passes -Fu$F/x86_64 -Fu$F/systems -Fu$F/x86, and
cpuinfo.pas — the file that carries the whole 132-unit difference between the two
arms — lives in one of those. A glob scoped to one directory answering about a
tree is the ordinary shape of this mistake, and the tell was free: the two arms
reported different heads, which one stubbed file in a unit almost nothing imports
cannot explain.
Instrument note, for whoever runs this next: two full background corpus runs were
KILLED at ~150 of 207 units, both reported as system memory pressure while the box
had 35GB free and 56GB available. The run was confirmed genuinely dead rather than
detached-and-still-running by checking whether the output file was still GROWING —
not by a process scan, which cannot tell a finished run from one sampled between two
compiler invocations. The corpus now takes a PXX_CORPUS_LIST of unit paths, and
three ~2.5-minute FOREGROUND chunks complete where one background run does not. The
three lists are asserted to be an exact partition of the glob before the run, and the
assembled output is checked at 207 rows / 207 distinct units after it.
WALL ELEVEN — FindFirst, and the walk-downward shape RECURS in a second file
Measured 2026-09-16, franks-ee. FindFirst / FindNext / FindClose,
TSearchRec / TRawByteSearchRec, the nine fa* attribute constants and
AllFilesMask, in lib/rtl/sysutils.pas. None of it existed — not the record
type, the whole family. Library-only, live under pin v410.
Expectation, recorded before the run: the 120 units leave cfileutl.pas:282
and the whole procedure clears rather than the head advancing a few lines (:287
findfirst and :290 sat right behind it, and one fix supplies all three names);
units-OK stays at 22, "and I am saying so for the tenth time in a row", with the
one shape that could move it named in advance — cfileutl is a DEPENDENCY, not a
leaf, so if it compiled, everything blocked only on it would compile too. Next
head deliberately NOT guessed, on the strength of wall ten's finding.
| before | after | |
|---|---|---|
| units-OK (stubbed) | 22 | 22 |
| the wall | 120 | 0 |
| new head | — | cfileutl.pas:518 GetDir, 120 units |
Both halves held. No unit lost, none gained. Tenth null row.
THE WALK-DOWNWARD SHAPE IS BACK, IN A SECOND FILE: 282 → 518, the same 120
units, cfileutl.pas both times. Wall ten's section says that shape terminates
and does not predict the next head, and both statements survive this: it did
terminate in comphook.pas, and it has now started afresh in cfileutl.pas 236
lines lower, in a different procedure. So the shape recurs without being
predictive — which is why the expectation above declined to name a next head and
was right to. A file stops producing walls when its distinct RTL dependencies run
out, and cfileutl.pas has more of them.
The fixture is where the real risk was, and the first version of it passed while
measuring nothing. Run against test/, faAnyFile and faDirectory return the
IDENTICAL answer — there is no dotfile, no symlink, no unwritable file and no
device node in a checked-in tree, so every attribute-filter row is vacuous and a
filter that ignores its argument entirely scores full marks. The fixture therefore
builds its own directory: a dotfile, an unwritable file, a symlink to a file, a
symlink to a directory, and a dangling symlink.
The strongest control is differential, and it was checked for sensitivity. The
same probe compiled under fpc and under pxx against one directory produces
byte-identical output across ten rows — names, counts and the raw attribute
integers. That is only worth something if it can go red, so the filter's polarity
was deliberately inverted (any bit instead of all bits) in a scratch copy of
the RTL: the diff reddens on six of ten rows. A guard that cannot fail prints
PASS, and this one was made to fail on purpose before its green was quoted.
A claim written into the source comment was disproved by that control, in the
same hour it was written. The comment said faAnyFile being $1FF rather than
the widely-quoted $3F matters because $3F "silently drops faNormal and
faSymLink". Measured: on unix the two select exactly the same entries, in every
arrangement probed — faNormal is never set by the unix attribute mapping, and
faSymLink only appears when the caller already requested it, which puts the bit
in the filter either way. The reason to carry FPC's value is that a caller may
COMPARE against it, not that the other one filters. The comment now says the
measured thing.
Two behaviours that look like bugs and are FPC's, both asserted: . and ..
are returned (fpc's own cfileutl.pas:287 filters them by hand, which is only
necessary because they arrive), and Attr is a PERMISSIVE filter rather than a
requirement — faDirectory still returns ordinary files, because every unix entry
carries faArchive and the filter accepts an entry when every bit it HAS is
covered. faArchive and faReadOnly are forced into the filter unconditionally,
as FPC does, or the obvious call returns nothing at all.
Found on the way, NOT fixed here, and it is a silent wrong value:
PChar(AnsiString('literal')) yields one garbage byte under this compiler where
fpc yields the string. It was found because the fixture used that spelling to create
a symlink and got a dangling link with a one-byte target — which then made four
unrelated assertions fail and looked exactly like an RTL defect. PChar(literal)
and PChar(variable) are both correct; only the double cast is wrong. Compiler-side
and therefore inert until a pin, so it is going in as its own change rather than
riding along with an RTL fix.
WALL TWELVE — GetDir, and the corpus catching a HALF-DONE GROUP mid-change
Measured 2026-09-16, franks-ee. GetDir plus fpc's sysunixh.inc:29-53 const
block in lib/rtl/sysutils.pas. Library-only, live under pin v410.
| before | after | |
|---|---|---|
| units-OK (stubbed) | 22 | 22 |
| the wall | 120 | 0 |
| new head | — | cfileutl.pas:714 rmdir, 120 units |
Eleventh null row, predicted as zero in advance, with the dependency caveat named again and again not firing.
THE FIRST VERSION OF THIS CHANGE WAS HALF A GROUP, AND THE CORPUS SAID SO IN
ONE RUN. GetDir and AllowDirectorySeparators are the two names the wall at
:518 and :543 sat on, so those are what got written. The re-run moved the
head to cfileutl.pas:696 — DriveSeparator, a member of the same const block
in the same fpc include file, 178 lines further down. The fix was one line.
That is normalise-dont-special-case's sibling rule arriving as a declaration
group rather than as a code path: one const block in fpc's source is one group
here, and taking the two names a diagnostic happened to name is the same mistake
as fixing one arm of a double case. The whole block went in on the second pass —
DriveSeparator, ExtensionSeparator, PathSeparator, LFNSupport,
maxExitCode, MaxPathLen, UnusedHandle, and the two writable case flags.
The cost of getting this wrong is a full corpus re-measurement for a one-line
constant, which is what it cost here.
DriveSeparator is an EMPTY STRING on unix, not a character, and the row
that matters is what it DOES: Pos(DriveSeparator, s) is real fpc code at
cfileutl.pas:696 and must answer 0 — that is how a drive-letter scan finds
nothing on a platform with no drives. Asserting Length = 0 alone would pass for
a constant that Pos then choked on, so both are asserted.
PLACEMENT IS A STATED DIVERGENCE, NOT AN OVERSIGHT. fpc declares all of this
in SYSTEM, visible with no uses. pxx's System is compiler/builtin/builtin.pas,
whose own header says it contains no syscalls — and GetDir is getcwd. Putting
it there would mean layering the PAL into the one unit documented as having none,
and it would be inert until the next pin: measured, the pinned compiler
resolves lib/rtl from the LIVE tree and builtin from its OWN snapshot
(stable_linux_amd64/default/builtin/). So it lives in SysUtils, and the cost is written into
the source: GetDir(0, s) without uses SysUtils compiles under fpc and does not
compile here.
Behind this wall there are only TWO errors left in cfileutl.pas, and the
second one is ours: rmdir at :714 (which needs MkDir/RmDir/ChDir and
IOResult plumbing — fpc's caller wraps it in {$I-} and reads ioresult), and
then internal parser bug: statement made no progress in block (would hang) at
cfileutl.pas:1495 — a Track P defect, not an RTL gap. The detail file is what
shows this: it records every failure per unit, not just the first, which is the
instrument this umbrella spent five null rows wishing for.
Values checked against fpc, harness checked for sensitivity. The same probe
under both compilers from one directory diffs byte-identical across eight rows,
then ten more for the const block. The separator set was then deliberately
narrowed to ['/'] in a scratch RTL copy and the diff reddens — and the FIRST
attempt at that control was itself wrong: the sed did not match, nothing was
built, and diff failed on missing files while the shell reported "CAUGHT IT".
A guard reporting a verdict for rows where nothing was built is the exact failure
this umbrella keeps recording; it was re-run with the build asserted and branched
on before the green was believed.
CORRECTION, 2026-09-16, to the paragraph above as first written. It said the
live and pinned builtin/ trees were "byte-identical right now" and offered that
as the trap a reader should not fall into. The claim was false, and it was
false in the shape it was warning about: it came from cmp on ONE file
(builtin.pas) with the conclusion asserted about the whole DIRECTORY — one
location sampled, all locations asserted. diff -rq answers three:
| file | last changed |
|---|---|
builtinheap.pas |
445ce3e25, 09-14 |
pyeval.pas |
dea6cf762, 09-16 |
pylib.pas |
334680199, 09-16 |
All three after pin v410. So the divergence is LIVE in three files rather than hypothetical, and the placement caution the paragraph argues for is better founded than its own evidence was. Found by frankuser re-measuring it rather than reading it — which is the only thing that catches this class, since the sentence was wrong in a way that made the surrounding argument sound MORE careful.
And the ledger has THREE states, not two: lib/rtl/** reaches from the live
tree and is live on push; compiler/** is inert until a pin; compiler/builtin/**
is inert until a pin AND is currently three files behind it, so two nilpy fixes
that landed today reach nothing running under the pin.
WALL THIRTEEN — cfileutl.pas:1495, and it is not a wall, it is a BLINDFOLD
internal parser bug: statement made no progress in block (would hang). Taken
out of order, ahead of rmdir, on frankuser's argument that characterising is
not subject to the inert-until-pin discount: a reduction and a ticket land live
whatever the pin does, so the RTL-first rule simply does not apply to them.
That argument was right, and it turned out to be right twice over, because the
thing behind this error was not a wall at all.
What it is
fpc's own cfileutl.pas:142 declares
{ hide Sysutils.ExecuteProcess in units using this one after SysUtils}
const
ExecuteProcess = 'Do not use' deprecated 'Use cfileutil.RequotedExecuteProcess instead, ...';
— a constant whose entire job is to SHADOW a function — and then calls the real
one qualified, twice, in its own body (:1494, :1496). We consulted the unit
qualifier in the proc table ((qUnit < 0) or (ProcUnitIdx[i] = qUnit)) and in
neither constant table, so the constant took the call.
Three faces, and the parser message is the least of them:
| shape | what pxx did | fpc |
|---|---|---|
| assignment, const type fits | printed the constant's text, arguments discarded, no diagnostic | the function's result |
| set-const shadow | the baked mask's ADDRESS printed as a string — a memory dump | the function's result |
| argument position | expected ')' before '(' |
the function's result |
inside an if arm |
statement made no progress in block (would hang) |
the function's result |
The boundary was measured, not assumed: only an untyped string const of length
≠ 1 captures the reference. A char const ('S'), a typed const
(Thing: AnsiString = '…'), an integer const and a float const all resolve
correctly — because only the string and set constants live in separate name
tables keyed without a unit. const T = '' is the nastiest row: it prints
nothing at all, which is indistinguishable from a blank.
Fixed at dc3fedb0a (one body per table, the existing name a thin wrapper, so
the scoping rules cannot drift between the qualified and unqualified forms),
pinned by a test at a29892c8c. Both are compiler/**, so both are INERT
UNTIL THE NEXT PIN — the test runs under ./$(COMPILER) and so is live for
anyone who builds, but nothing on $(PXX_STABLE) sees the fix.
The expectation, and the half of it that was wrong
Recorded before the re-run, as required. Right about the verdicts:
| before | after | |
|---|---|---|
| units-OK (stubbed) | 22 | 22 |
| BOTH-OK / ORACLE-NO / PXX-FAIL | 21 / 10 / 176 | 21 / 10 / 176 |
| units whose verdict moved | — | 0 |
Corrected 2026-09-16 — this row read
22 / 10 / 175when written, in this table and in wall 14's, and both were wrong on arrival. The corpus totals were never 22/175 at any tree this ticket has measured.207 − 22 − 10 = 175is the whole mechanism: the BOTH-OK count was derived from theunits-OK (stubbed)number directly above it and the PXX-FAIL count from the subtraction, instead of being read off the probe's own SUMMARY line — a conclusion written as a caption, in the row a reader is least likely to re-derive because the arithmetic checks out. The two metrics are not the same quantity and must not be inferred from each other. Established by a before/after A/B on 2026-09-16 at6bc01579f, three foreground chunks of an asserted partition each time (rows = distinct units = 207, both runs): BEFORE (lib/rtl/textfile.pasreverted toHEAD~2) 21 / 10 / 176, AFTER 21 / 10 / 176, 0 units whose row changed across all 207. Nothing between the wall-14 measurement and that A/B could have moved it — the only commits touchinglib/orcompiler/are the two being A/B'd, the probe changee0e2baac8added counters with byte-identical verdict logic, and the corpus is unchanged since 09-01 — so 21 / 10 / 176 was the true value at walls 13 and 14 as well. The ticket's ownsummary:field said 21 / 10 / 176 all along and was right; these two tables were the outlier. Theunits-OK (stubbed)row is left as written: it is a different measurement, it was not re-run here, and nothing in this A/B says anything about it.
A twelfth consecutive null row, and this one null BY CONSTRUCTION rather
than by surprise: :1495 was never a HEAD. It sits behind :714 rmdir in the
same file, so no unit ever reported it first.
What I got wrong is the finding. I predicted the give-up appeared in ONE
detail file, because I had only looked at the head. It was in 134 — one wall
seen through 134 importers, which the in: line confirms is cfileutl.pas
every single time.
THE PART THAT MATTERS: A PARSER GIVE-UP IS A SECOND BLINDNESS, INSIDE THE INSTRUMENT BUILT TO CURE THE FIRST
PXX_CORPUS_DETAIL exists because a first-error census ranks by queue position
and hides everything behind the head. It reports EVERY failure per unit. That is
true and it is not sufficient, because statement made no progress in block
is the parser ABANDONING THE BLOCK, and everything after the abandoned block
is invisible to a per-unit error list just as thoroughly as it is to a head.
Total error lines across the 207 detail files:
| before | after | |
|---|---|---|
| error lines | 419 | 776 |
| detail files naming the give-up | 134 | 0 |
357 error lines were behind that one give-up, including three error KINDS
that had never been seen in this corpus at all (cannot override: no virtual method found in parent chain: freeinstance, no overload of tostr,
undefined variable (ppureal_bytes)).
The control is what makes that attributable rather than alarming: of the 41 detail files that never carried the give-up, grew=0, shrank=0, same=41. Every one byte-for-byte unchanged. The growth is confined, without exception, to the files where the parser previously stopped reading — so this is the instrument getting its sight back, not a regression.
So the corpus number has a third caveat, alongside the two already recorded
(a head is a queue position; a units-blocked count is not a work count): a
unit's error LIST is truncated wherever the parser gave up, and nothing in the
output says so. Any future census of "how much is left" that predates
dc3fedb0a undercounts, and undercounts invisibly.
Behind the head, now
cfileutl.pas has exactly one error left before it: rmdir at :714. That is
the next wall and it is RTL — MkDir/RmDir/ChDir plus IOResult plumbing,
fpc's caller wrapping it in {$push}{$I-} … RemoveDir:=(ioresult=0), with
IOResult/LastIOResult living in lib/rtl/textfile.pas:131/:314, i.e.
cross-unit state.
Banked separately, and NOT caused by this fix
n := F('x') for a string-returning F and an Integer n compiles with no
diagnostic and prints a pointer as a number. It was the one row of a 17-row
differential matrix that still diverged from fpc after the fix, which is
precisely the shape that reads as a confession — so it was attributed to a
RANGE before being attributed to the change, and the pinned pre-fix compiler
reproduces it on a program containing neither a qualifier nor a shadowing
const. It had been MASKED here by the very defect above. A pre-existing bug
can sit behind a second bug, and fixing the front one looks exactly like
causing the back one.
bug-p-a-string-function-result-assigned-to-an-integer-compiles-silently.
WALL FOURTEEN — cfileutl.pas:714 rmdir, and cfileutl.pas is now CLEAR
System's MkDir / RmDir / ChDir, added to lib/rtl/textfile.pas rather
than SysUtils because they report through IOResult and nothing else — they
raise nothing and return nothing — and IOResult/LastIOResult were already
there. fpc's caller is cfileutl.pas:709:
{$push}{$I-}
rmdir(d);
{$pop}
RemoveDir:=(ioresult=0);
Landed 387e1d4cc. RTL only, so it is LIVE ON PUSH — built and verified
under the pinned compiler, which resolves lib/rtl from the live tree.
The measurement that stopped a wrong reuse
Every IOResult code was produced under fpc 3.2.2 by making the condition happen and reading it back. fpc documents none of them, and two rows disagree with the table this unit's FILE routines already use:
| condition | errno | RmDir | MkDir | ChDir |
|---|---|---|---|---|
| does not exist | ENOENT 2 | 2 | 2 | 3 |
| name too long | ENAMETOOLONG 36 | 3 | 3 | 3 |
| permission denied | EACCES 13 | 5 | 5 | 5 |
| already exists | EEXIST 17 | — | 5 | — |
| a plain file, not a directory | ENOTDIR 20 | 5 | 5 | 5 |
| not empty | ENOTEMPTY 39 | 5 | — | — |
ENOENT is 2 for RmDir/MkDir and 3 for ChDir. fpc separates "file not found"
from "path not found" by the operation, not by the errno — the same errno,
two answers — which no single errno→code map can express. And ENAMETOOLONG is 3
here where the file table answers 2. So DirErrnoToIO is a second table on
evidence, not a second path out of convenience, which is the distinction
normalise-dont-special-case.md actually asks for.
The empty path is a no-op returning 0 in all three. That is the row that
matters most: passing '' to the syscall reports ENOENT for a call meant to do
nothing, and an implementation resolving '' to the current directory would
delete or enter it. test/lib_dirio.pas asserts the no-op's absence of
effect as well as its code.
FPC runs test/lib_dirio.pas itself and also answers 31 / 31, so the test
is an oracle-agreeing spec rather than a spec of our own behaviour. Two positive
controls, asserted and branched on: the pre-change textfile.pas refuses the
probe, and a one-code perturbation (chdir ENOENT 3 → 2) drops it to 30 / 31
naming exactly that row.
The corpus, against the expectation recorded first
| before | after | predicted? | |
|---|---|---|---|
| units-OK (stubbed) | 22 | 22 | yes |
| BOTH-OK / ORACLE-NO / PXX-FAIL | 21 / 10 / 176 | 21 / 10 / 176 | yes |
| units whose verdict moved | — | 0 | yes |
the :714 wall |
120 | 0 | yes |
detail files naming rmdir |
134 | 0 | yes (strong form) |
new head globals.pas:1095 Replace |
12 | 132 | yes, 12 + 120 |
| detail files without the wall, unchanged | — | 41 of 41 | yes (control) |
The BOTH-OK row is corrected from
22 / 10 / 175, 2026-09-16 — see the same correction under wall 13 for the mechanism and the A/B that settled it. Thepredicted?column still readsyes: the prediction was that the row would not MOVE, and it did not. What was wrong is the level, in both the before and the after cell, which is why a null row can be right and its own table wrong.
A thirteenth consecutive null row, and the fourth in a row where clearing a shared dependency hands its whole population to the next wall in the same import chain.
cfileutl.pas now names itself ZERO times across all 207 detail files. Four
consecutive RTL walls (FileAge, the FindFirst family, GetDir, and these
three) plus one compiler fix cleared the file entirely.
THE SHRINK, WHICH MY OWN EXPECTATION CALLED A REGRESSION TO EXPLAIN
Total error lines went 776 → 642, and the expectation said a shrink would
have to be explained rather than accepted. It is exactly explained: 776 − 642 =
134, all 134 wall files lost exactly one line each, that line was the
rmdir line, and every remaining error in every one of them is byte-identical
to before. Nothing was hidden behind this wall.
And that BOUNDS the wall-thirteen finding instead of leaving it open. An
undefined variable error does not truncate a unit's error list; a parser
give-up does, because it abandons the block. So the detail instrument's
blindness is specific to the give-up class, not to errors in general — which
is a much smaller caveat than "any count may be low", and it is checkable: grep
the detail files for statement made no progress.
The two, which was exact after all
The pre-dc3fedb0a reading that "only two errors remain behind that wall" was
recorded as a lower bound once the truncation was found. Measured across the
three runs, cfileutl.pas's own distinct error LINES were:
| run | cfileutl.pas's own error lines |
|---|---|
| before the const fix | :714, :1495 |
| after the const fix | :714 |
after rmdir |
(none) |
So the two was exact, not a lower bound. The give-up at :1495 was the last
thing in the file, so nothing of cfileutl's own was hidden behind it; the 357
lines it concealed were all in OTHER units of the import chain. The caveat is
real for the corpus-wide count and did not apply to that particular two — worth
recording, because a correction that is itself imprecise costs the next reader
the same measurement.
Wall fifteen — charset, and the strongest null row this ticket has recorded
uses: unit source not found: charset was the sixth-largest head of the 207 and
the largest genuinely LIVE one, named directly by 8 units. FPC's charset.pp is
its codepage mapping registry, so this is Track B surface, not a compiler gap:
the unit simply did not exist here.
Landed 6bc01579f (lib/rtl/charset.pas, 814 lines — the full public surface:
loadunicodemapping, three loadbinaryunicodemapping rows, registermapping,
registerbinarymapping, both getmap, mappingavailable, getunicode and
getascii pairs) and 69ba50571 (BlockRead/BlockWrite at every count-out
width, which the binary loader needed and which turned out to be memory
corruption in its own right — see
bug-p-a-var-parameter-accepts-a-narrower-actual-and-writes-past-it). Byte-
identical to fpc 3.2.2 across an 86-line differential probe, with three
deliberate divergences, all measured, all in places where fpc corrupts or
crashes: bounded hex scans, a >= growth check moved outside the hex branch,
and a nil-guard in getascii's buffer form (fpc writes ABuffer^ unchecked, so
getascii(c, p, nil, 0) is a runtime error 216 for any unrepresentable
character). test/lib_charset.pas pins it, 98 rows, with a parity mode that
halts before the two divergence rows so fpc can run the same file: fpc 95/95,
pxx 95/95 in parity mode, pxx 98/98 full, and a control at 91/98.
Both are lib/rtl/**, which reaches from the LIVE tree — they are live on
push, not inert until the next pin.
The corpus, A/B rather than inferred
| before | after | predicted? | |
|---|---|---|---|
| BOTH-OK / ORACLE-NO / PXX-FAIL | 21 / 10 / 176 | 21 / 10 / 176 | movement yes, LEVEL no |
| units whose row changed | — | 0 of 207 | yes |
| regressions | — | none | yes |
| gains | — | none | yes |
A fourteenth consecutive null row, and in an unusually strong form: not
"units-OK is unchanged" but every one of the 207 rows is byte-identical,
verdict and first error alike. That is an A/B and not an inference — the BEFORE
leg reverted lib/rtl/textfile.pas to HEAD~2 and re-ran the whole corpus, both
legs in three foreground chunks of an asserted partition (rows = distinct units =
207 each time).
The prediction was right about the movement and wrong about the level, and the
reason is this ticket's own tables. I predicted "units-OK stays at 22" — the
movement was zero, as predicted, but the baseline was never 22; it was 21, and it
was 21 before the change too. I had taken it from the wall-13 and wall-14 tables,
which is where the 22 / 10 / 175 error above was sitting. A stale row in a
record is not inert: it was consumed as a premise by the next measurement that
cited it, and the only reason it surfaced is that this row was measured by A/B
instead of by reading the previous number forward. Both tables are corrected
above.
The corpus is a corpus: an fpc loader bug found and NOT fixed
Bisected exactly while writing the differential probe, in fpc's own
loadunicodemapping. After a line like 0x8E<tab>#DBCS LEAD BYTE (verbatim from
Microsoft's CP932.TXT), a shorter following line 0x8E41<tab>0x4E00 leaves AD
in the ShortString buffer; fpc's hex scan is unbounded, eats it, parses
$4E00AD and truncates to a word — so U+4E00 registers as 173. Well-formed
input, no diagnostic. Recorded here, not filed and not repaired: per the probe's
own header, FPC is the corpus and the question is only ever whether it compiles.
Our bounded scan is one of the three deliberate divergences above.
2026-09-17, frankB — the 09-17 probe @ 17b8561f2: the globals.pas:502 prediction is measured, and it is the FIFTEENTH null row
Measured at 17b8561f2, binary 93200e4b45db, three foreground chunks of a
partition ASSERTED to union back to the glob exactly (69 + 69 + 69, rows =
distinct units = 207, no overlap, nothing dropped). The binary was rebuilt after
the pull and before the run — converged after 1 round(s) — per PUSH → LET THE
PULL SETTLE → REBUILD → MEASURE.
Why this run happened, and what it was testing
decided/decide-old-style-object-types.md prices the object feature at +1 unit
and then names a cheaper lever: the 138 blocked units "do not scatter — they
land as one group on a fifth wall, globals.pas:502", the var-spelling of a
string initialiser, with the instruction "That is Track P, needs no decision,
and gates the same 138 — so it is the cheaper lever by a wide margin, and it
should be taken before anyone reopens this page."
That lever was taken on 2026-09-16 by frankS (fa397c761, "a named string
constant is a string initialiser too"), and fa397c761 is not an ancestor
of 9281da35b, the tree the last recorded corpus totals were measured on. So
the prediction had never been re-measured and this run is a test of it rather
than a re-confirmation. 25 commits touching compiler/** or lib/** sit
between the two trees, including the lever and a run of Track B RTL additions
(charset, FindFirst/FindNext, MkDir/RmDir/ChDir, GetDir, StdErr/StdOut as real
Text files, the SysUtils date converters).
THE EXPECTATION WAS RECORDED BEFORE THE RUN, verbatim: "Given this umbrella's FOURTEEN consecutive null rows, my actual expectation is ANOTHER NULL — the 138 land together on a sixth wall. I am recording that so the result is information either way."
The result
wall 15 @ 9281da35b |
09-17 @ 17b8561f2 |
|
|---|---|---|
| BOTH-OK | 21 | 21 |
| ORACLE-NO | 10 | 10 |
| PXX-FAIL | 176 | 176 |
A fifteenth consecutive null row, and the prediction it was testing is now
measured stale in this umbrella's own recorded shape. globals.pas:502 is the
first failure of zero of 207 units — the lever is taken, and both spellings
of the declaration compile under fpc and pxx when probed directly. The 138 were
QUEUED behind it, not gated by it. They landed on the next wall.
(Scope: "zero units" is a statement about FIRST failures, which is what the probe reports. It is not a claim that no unit reaches that line behind an earlier error.)
Where the population went, and it is one file again
| first failure | probe #12 | 09-17 |
|---|---|---|
unknown type: TDoubleRec — x86_64/cpuinfo.pas:36 |
4 | 140 |
an object type cannot have a constructor |
18 | 18 |
unknown type: PUnicodeChar |
— | 7 |
unknown type: TExecuteFlags |
127 | 0 |
Unsupported tcompilerwidechar size |
10 | 0 |
uses: unit source not found: charset |
6 | 0 |
unknown type: d (sizeof(<a PARAMETER>)) |
4 | 0 |
conditional directive: expected operator |
2 | 0 |
| conditional directive, three named operands | — | 3 |
Five walls cleared between the two probes and units-OK moved by zero. The
whole delivered population is now standing on x86_64/cpuinfo.pas:36, which is
the cclasses.pas picture for the fourth time: a histogram of one file's
contents. TDoubleRec has gone 4 → 132 → 140 across three measurements while buying
nothing, which is the cleanest available statement of this umbrella's own
finding — a wall's population counts units QUEUED behind it, never work.
Attributions for the cleared walls, so the delta is not read as one seat's:
tcompilerwidechar is 0728155f2 (the pre-pass must not raise an {$error}),
charset is 6bc01579f, sizeof(<a PARAMETER>) is a931bef4d, expected operator is eaf776dd8. Only the first and last are mine.
The join instrument is the PREVIOUS SWEEP'S ROWS — keep them
Recorded here because this section gets the join half right and the other half wrong, and the next reader should not repeat it. The by-name BOTH-OK set below lets a later run join the units that COMPILE. The first-failure table above is an aggregate, so it cannot see two units swapping inside one error class — equal totals are satisfiable by a regression and a gain that cancel. The instrument that closes that is the previous sweep's 207 per-unit rows, joined unit by unit, which reports zero verdict changes in either direction rather than an unchanged count.
That is strictly stronger and it is nearly free, but only if the rows still
exist: the probe writes them to a session scratchpad under /tmp, which
/etc/tmpfiles.d/tmp.conf cleans with D /tmp ... 6h.
CHECK, DO NOT ASSUME, IN EITHER DIRECTION — and this sentence is a correction
of the one that stood here for four hours. I first wrote "reaped at six
hours" flatly, from the configured age. Measured 2026-09-17 at 18:45 on plexus:
my own three chunk files, written 08:29-08:35, were all still present with 207
rows intact at TEN hours, and the oldest surviving file anywhere under
/tmp/claude-1000 was 24 hours old. systemd-tmpfiles requires atime, mtime and
ctime all to be older than the age, so an active session's tree outlives the
number. Six hours is the configured age, not an observed deadline, and
reading it as one costs an unnecessary eight-minute sweep — which is the same
error as trusting it the other way, just in the direction nobody notices
because the work still gets done. One box, one tree, one reading: ls the rows
before re-running, and ls them before relying on them. Note also that seven's
/tmp is a tmpfs on a different regime, so none of this travels there.
So keep the rows when you finish a sweep — franks-ee did the per-unit join on the first attempt after the [55] fixes purely because a pre-fix sweep was still sitting in the same directory from an hour earlier, and said plainly that coming to it cold he would have compared three numbers like anyone else. Not judgement; kept rows. I ran this total three times on 2026-09-17 and joined per unit on none of them.
AND ALIVE IS NECESSARY, NOT SUFFICIENT — A SECOND CORRECTION TO THIS SAME
SECTION, AND IT IS THE ONE THAT WOULD HAVE MISATTRIBUTED A PIN. Everything
above establishes that the rows still EXIST; it says nothing about what tree
produced them, and I wrote it while holding an assignment whose whole value
depends on that. The rows above were produced at 17b8561f2 at 08:26. Measured
2026-09-17: nine commits touching compiler/ or lib/ have landed since,
every one of them a Track P fix and three of them mine —
c86e8b29d 6eb1db8b4 efe06a903 170197a54 160684761
2de677672 5c1db8c6b 5ce561a11 ec8a4d88c
— and the corpus totals have moved 21 / 10 / 176 -> 22 / 10 / 175 across two later sweeps by another seat. So these rows are a valid partner for exactly one question and not for the other:
| question | partner | valid against these rows |
|---|---|---|
| what did the fleet's HEAD fixes buy since 08:26? | a HEAD run now | yes, with the nine-commit range named |
| what does the FLEET'S FLOOR become when the pin moves? | a PINNED-binary run | no |
The second is the assignment, and against these rows it would credit the PIN
with nine commits of HEAD movement — the favourable direction, which CLAUDE.md
names as the one nobody checks. The instrument for a pin question is a
same-tree two-binary A/B, PXXBIN=stable_linux_amd64/.../stable_pinned
against compiler/pascal26, both sweeps in one session at one tree, which is
the shape wall fifteen and the 401c00f2b row already used. It costs a second
eight-minute sweep and that is the price of the question; the alive rows do not
buy it, and believing they did is what this correction is for.
The BOTH-OK set, BY NAME — recorded because it never has been at 21
Every previous attempt that moved reported the join by name and it was the valuable half; the 21 has only ever been a count. The next run now has something to join against:
cclasses cdynset compinnr comptty constexp crefs cstreams cutils
dbgdwarfconst dwarfbase fpchash globtype macho optbase rabase rescmn
symconst systems tokens version wasmbase
Three named walls this run produced in Track P's own conditional evaluator
eaf776dd8 replaced the blunt conditional directive: expected operator with
refusals that name the operand, and the corpus turned that into three precise
one-unit walls rather than two anonymous ones:
| unit | directive | refusal |
|---|---|---|
nld |
nld.pas:700 |
right operand of in is not a set constant this pass can read: supported_optimizerswitches |
nadd |
nadd.pas:1352 |
sizeof cannot size this type here: bestrealrec |
hlcgobj |
hlcgobj.pas:4156 |
sizeof cannot size this type here: aintmax |
supported_optimizerswitches is x86_64/cpuinfo.pas:205, a set constant built
by +-joining other named set constants across units — the next step of
eaf776dd8's own feature, not a new subsystem. bestrealrec and aintmax are
both x86_64/cpuinfo.pas again.
AND THE LINE NUMBERS IN THAT TABLE ARE A FIX, NOT A READING. The probe
originally reported these at 1334, 1112 and 1821 — two pointing backwards, one
forwards, all three at ordinary statements. Error prints the CURRENT TOKEN's
line and the evaluator runs from the directive handler. Fixed in ec8a4d88c,
after this run; the directives above were grepped out of FPC's sources
independently, before anything was changed. Nothing about the verdicts moved —
the fix changes only where a diagnostic points — so the totals in this section
stand for 17b8561f2 as measured.
2026-09-17 (frankS) — WALL SIXTEEN, object CONSTRUCTORS, and the SIXTEENTH NULL ROW KILLS THE INSTRUMENT THIS FILE ASKED FOR
Fixed at efe06a903, binary 1e1d3ce55597. Totals 21 / 10 / 176 -> 22 / 10 /
175. One unit. Zero regressions.
That is an ordinary null row and it is not why this section is long. The finding is that the every-failure census — the instrument this umbrella spent six null rows asking for, built on 2026-09-16, and called "the first proposal with a complete failure set behind it instead of a queue position" — predicted this wall's yield NO BETTER THAN THE FIRST-FAILURE CENSUS IT REPLACED. It said 18. The answer was 1.
The prediction, recorded before the run, and it was wrong
Written out in full before the corpus was touched:
The 18 units with
{object-constructor}as their COMPLETE set should convert nonzero — between 5 and 18 (BOTH-OK 26..39), the first non-null wall this umbrella has proposed; the 141{object-constructor, TDoubleRec}units are a pair population and should deliver zero. If the yield is 0, the complete-failure-set instrument is no better than the first-failure census — a negative finding about the method itself.
The pair half was right and bought nothing, as expected. The half the whole census existed to answer was wrong by a factor of 18, and the caveat stated beside it ("a hard-erroring type declaration suppresses downstream analysis, so a complete set of one is necessary, not sufficient") turns out to have been the entire story rather than a hedge.
Why, exactly — and this is the transferable half
A complete-failure-set census is complete only for the SUBJECT unit. Every unit it IMPORTS is still truncated at that unit's own first failure, so the census inherits first-failure blindness ONCE PER IMPORT.
The object-constructor diagnostic is raised in three files and none of them is
usually the subject: versioncmp.pas (144 of the 161 reports), cgbase.pas
(14), cmsgs.pas (3). When an imported unit stops, everything after that point
in that same file is never analysed — so for 17 of the 18 subjects, the "complete
set" was complete about a truncated import, not about the program.
The join is exact and leaves nothing to interpretation:
| before | after | |
|---|---|---|
versioncmp — declares its object IN ITS OWN SOURCE |
PXX-FAIL {object-ctor} |
BOTH-OK |
14 units importing cgbase |
PXX-FAIL {object-ctor} |
PXX-FAIL cgbase.pas:401 not a constant |
3 units importing cmsgs |
PXX-FAIL {object-ctor} |
PXX-FAIL cmsgs.pas:124 undefined variable (fail) |
18 = 1 + 14 + 3. The one unit that converted is the one where the wall was in
its own source and there was nothing behind it. The other seventeen moved
25 lines further down one file (cgbase.pas 376 -> 401) and 77 lines
further down another (cmsgs.pas 46 -> 123).
So this is the cclasses.pas 895 -> 1327 -> 1726 shape for the fifth time,
arriving in two new files — and this time it defeated the instrument built
specifically to detect it. The census does not see a second wall in an imported
file; it sees a truncated import and reports one error with total confidence.
What would ACTUALLY answer the size question
Not a deeper census. Stub the wall and re-run — apply the proposed lowering
by hand over a copy of the tree, which is what the 2026-09-16 stub3 arm in
decided/decide-old-style-object-types.md did. That arm predicted 22 units
for this change. The real fix delivered 22. The stub was right and the census
was wrong, because a stub removes the wall for the importing units too and
therefore lets their next failure become visible.
Read that as the method, not as a score: a stub answers "what is behind this wall for everyone", a census answers "what did the compiler happen to reach". Only the first is a prediction. Where a stub is cheap, it beats any amount of diagnostic recovery — and this umbrella now has one measurement where the two disagreed by 18x and the stub won.
New first-error distribution, all 175
141 unknown type: TDoubleRec (x86_64/cpuinfo.pas:36)
14 not a constant (cgbase.pas:401) <- NEW
7 unknown type: PUnicodeChar (+ uses cp1251)
4 undefined variable (entryreal_bytes)
3 undefined variable (fail) (cmsgs.pas:124) <- NEW
1 uses: unit source not found: unixcp
1 uses: unit source not found: heaptrc
1 undefined variable (swapendian)
1 undefined variable (align)
1 conditional directive: the right operand of `in` ...
1 conditional directive: sizeof cannot size this type here
The two new walls, both named with their in: line rather than guessed
Both first lines carry NO file name — CLAUDE.md's line-31 hazard — and the first
reading of 401 here was wrong twice before the in: line settled it.
cgbase.pas:401, 14 units.NR_INVALID = tregister($ffffffff). An enum typecast in a constant expression. Isolated to three lines, and it is NOT about the explicitly-ranged enum it was found on:
Filed astype TE = (a, b, c); const K = TE(2); { pascal26:3: error: not a constant; fpc: fine }
bug-p-an-enum-typecast-is-not-a-constant-expression, wired here.cmsgs.pas:124, 3 units.fail;— the standard procedure that exists ONLY inside an old-style object's constructor, and therefore a wall this very fix created the reachability for. Two units in the whole corpus use it (cmsgs,symtable). Behind it in the same file sitsSetCodePageat :459. Recorded infeature-p-legacy-value-object-types, which is already wired here.
The RTL half of the residual, which nobody has called out
Of the 17 units that are neither TDoubleRec nor the object wall, 10 are pure
RTL gaps, not frontend walls: PUnicodeChar + cp1251 (7), unixcp (1),
heaptrc (1), swapendian (1). All five names were checked against lib/rtl
rather than assumed — none is present, and the three units are absent outright.
CORRECTED IN PLACE, SAME EVENING: this paragraph first said 12, and the
arithmetic beside it summed to 14. It counted entryreal_bytes (entfile, pcp)
as an RTL gap. It is not one — entfile.pas:371 declares it as a LOCAL type
inside a function, array[0..sizeof(d)-1] of byte over the enclosing function's
own parameter, and uses it as a typecast. That is a Track P gap, and it is a
TYPECAST gap, which is the same family as the two fixed tonight. So those two
units are RTL plus one Track P wall, not RTL. The rest of the 17: nadd and
nld (Track P conditional evaluator), fpcp and ppu (mixed with the object wall),
machoutils (its own). 10 + 2 + 2 + 2 + 1 = 17.
Per this file's own WALL 5 note, an RTL wall is LIVE without a pin and a compiler wall is not — so if anyone wants delivered units rather than cleared walls, that is where the cheap ones are.
2026-09-17 (frankS, later) — WALL SEVENTEEN, the enum typecast: the SEVENTEENTH null row, and this time it was PREDICTED CORRECTLY
6eb1db8b4 + c86e8b29d, binary bc884808fda5 at c86e8b29d. Totals
22 / 10 / 175 -> 22 / 10 / 175. Unchanged.
The prediction, recorded before the run and in the form the previous section's
mistake taught: "another null row; cgbase.pas is a large shared file, so I
expect the 14 to land on its next wall; at most cgbase itself converts
(23 / 10 / 174), most likely unchanged."
All 14 moved to TDoubleRec. Not one converted, cgbase included. So the
14 that appeared behind the object wall six hours earlier were, once again, a
queue — which is now recorded seventeen consecutive times.
The run before this one was CONTAMINATED and is not reported here
I rebuilt compiler/pascal26 while a corpus sweep was in flight, so chunk 0 was
measured against one binary and chunks 1 and 2 against another. That run
(bgc5b75su) completed with a plausible-looking tally and is discarded. The
numbers above are from a clean sweep started after the tree settled, from a
checkout equal to origin, with the binary sha and commit recorded before the
first chunk. CLAUDE.md names this exact hazard and I walked into it by treating
"just rebuild to test a small fix" as separate from "the instrument"; a rebuild
IS the instrument here, because the probe invokes the binary per unit.
The state this leaves the corpus in, which is the actionable part
155 unknown type: TDoubleRec (x86_64/cpuinfo.pas:36) <- 89% of failures
7 unknown type: PUnicodeChar (+ uses cp1251)
4 undefined variable (entryreal_bytes)
3 undefined variable (fail) (cmsgs.pas:124)
1 uses: unit source not found: unixcp
1 uses: unit source not found: heaptrc
1 undefined variable (swapendian)
1 undefined variable (align)
1 conditional directive: the right operand of `in` ...
1 conditional directive: sizeof cannot size this type here
TDoubleRec is now the first failure of 155 of 175 failing units. The
argument for taking it next is NOT that count — this file has seventeen rows
saying a count is a queue position. The argument is that the corpus is blind
behind it: at 89% there is almost nothing left to learn from another sweep
until it moves, and every other wall's population is now too small to be
informative about anything but itself.
It is feature-b-rtl-has-no-tdoublerec, already in blocked-by, and it is
Track B / lib/rtl, which reaches from the live tree — so it delivers without
waiting for a pin, unlike both fixes landed tonight. That combination (the
only wall wide enough to see past, and the only kind of wall that is live
immediately) is the strongest case this umbrella has had for a specific next
lever, and it does not rest on a unit count.
2026-09-17 (frankB) — THE PIN FLOOR, v410 -> v411, and the FIRST PER-UNIT JOIN this file has ever carried
Run after pin v411 landed, to answer a question the null rows cannot: not what
HEAD can do, but what the fleet's FLOOR becomes when the pin moves. Every
$(PXX_STABLE) consumer stands on the floor, not on HEAD.
The instrument, and the trap it walked into first
frankuser specified PXXBIN=stable_pinned against PXXBIN=compiler/pascal26.
That A/B is a tautology and the harness refused it: v411 was minted from this
tree, so both are bc884808fda5 and the two arms had collapsed into one before
the instruction was written. The measurer's own earlier step — a PIN — was the
contaminant. Recorded because it is the cheapest possible instance of a rule
this file keeps paying for, and because the refusal came from a guard rather
than from a reader: tools/fpc_corpus_ab.sh declines when the two legs have the
same sha256, on the grounds that an A/B which cannot tell its legs apart cannot
fail.
So the legs are the OLD floor against the NEW one, everything else held:
| leg | binary | sha256 | provenance |
|---|---|---|---|
| A | pin v410 | c599e8546121 |
git show 764ee2ed2:stable_linux_amd64/default/stable_pinned |
| B | pin v411 = HEAD | bc884808fda5 |
compiler/pascal26 at 8d9d69bdc |
Both pin identities checked against their own commit subjects and
sha256sum, never a --grep. One tree (8d9d69bdcbb8+e3b0c442), one lib/rtl,
one corpus, one oracle, one flag set; tree identity re-taken after both legs and
unmoved; truncated=0 on both, which is what makes an errs= count a count.
The result
The floor moved 21 / 10 / 176 -> 22 / 10 / 175. That is the canonical
notation this file requires; the block below is the probe's own SUMMARY lines
quoted verbatim, and its both-ok= spelling is a QUOTATION and not a live
claim -- the first draft of this section stated the totals in that spelling and
in no other, which would have made this row invisible to the very grep the
notation rule exists to enable. Caught by running the file's own check.
A SUMMARY both-ok=21 oracle-no=10 pxx-fail=176 (v410)
B SUMMARY both-ok=22 oracle-no=10 pxx-fail=175 (v411)
JOIN identical=43 wall-moved=29 errs-only=134 (up 134 / down 0)
gained=1 regressed=0 other-verdict=0 only-in-one=0
ZERO REGRESSIONS ACROSS 207 UNITS, and that is the sentence only a join can
produce. The three-number reading is +1 unit and is silent on whether
anything broke; equal or near-equal totals are satisfiable by a gain and a
regression that cancel, and until tonight nobody here had ever checked. The
gain is versioncmp.
The headline understates it, and the reason is the interesting part
+1 unit is true and is the least of what moved. 29 units changed their first
error — the wall transitions, most common first:
| n | from | to |
|---|---|---|
| 14 | an object type cannot have a constructor |
unknown type: TDoubleRec |
| 8 | Unsupported tcompilerwidechar size |
unknown type: TDoubleRec |
| 3 | an object type cannot have a constructor |
undefined variable (fail) |
| 1 | Unsupported tcompilerwidechar size |
conditional directive: sizeof cannot size this type here: bestrealrec |
| 1 | conditional directive: expected operator |
the right operand of 'in' is not a set constant this pass can read |
And 134 units sat at the identical first error with errs= risen, 134 up and
0 down. The obvious reading is that more is broken. It is the opposite.
A RISING errs= IS NEWLY VISIBLE GROUND — measured, on 5 of 5 sampled
Detail-dumped under both binaries, aasmbase plus four drawn from the
errs-only set (aasmdef, comprsrc, nmem, pexpr):
| unit | v410 errors / files reached | v411 errors / files reached | too many array initializer elements |
|---|---|---|---|
| aasmbase | 2 / 1 | 8 / 4 | present in A, gone in B |
| aasmdef | 2 / 1 | 8 / 4 | present in A, gone in B |
| comprsrc | 2 / 1 | 8 / 4 | present in A, gone in B |
| nmem | 2 / 1 | 8 / 4 | present in A, gone in B |
| pexpr | 2 / 1 | 5 / 4 | present in A, gone in B |
Under v410 every one of these units reported two errors, both inside
x86_64/cpuinfo.pas, and saw exactly one file. v411 fixes the second of them
(cpuinfo.pas:281), and the compiler then walks on into three or four files
it had never reached. So the count rose because a wall fell.
THIS IS THE cclasses.pas SHAPE ARRIVING FROM THE OTHER SIDE. Every
previous row here recorded a cleared wall delivering its population to the next
wall a few hundred lines down the SAME file, converting nothing. This one
clears a wall and the population walks into DIFFERENT FILES — which is why it
shows up as errs= rather than as units, and why no first-failure census could
have seen it. The corpus did not merely gain a unit tonight; it became more
informative, and the new information is the stratum below.
THE QUANTIFIER IS NOT MEASURED AND IS NOT CLAIMED: 5 of 5 sampled, not 134 of 134. The mechanism is unanimous in the sample and the sample is small.
The newly visible stratum, BY NAME — none of it had a wall row before
From the sampled units' detail, in files the corpus could not previously reach:
globals.pas:1095—no overload of Replace matches these arguments, for(AnsiString, ShortString, Integer)against candidates(ShortString, ShortString, ShortString)and(AnsiString, ShortString, AnsiString)globals.pas:1742and:1755—init/doneis not a procedure or function, so it cannot be called(a procedure variable invoked afterassigned(...))cmsgs.pas:124—undefined variable (fail)cmsgs.pas:459/:460—undefined variable (SetCodePage)and the call on itmsgtxt.inc:347—too many array constant elements
Per this file's own rule these are NOT to be ranked on how many units name them. They are recorded because they are new ground, not because they are large.
The instrument over-reported itself, and that is corrected in the tree
The first join printed moved=163. Only 29 of those were a different
first error; the other 134 were the identical wall with a changed errs=. The
join keyed on the whole row, errs=N included, so it answered honestly about a
question nobody asked — a 5.6x overstatement of wall movement, one hour after I
wrote the thing. tools/fpc_corpus_ab.sh now splits WALL-MOVED from ERRS
and reports them separately; re-joining the same two leg files with the
corrected awk reproduces the split exactly, and an independent Python pass over
the same rows agrees. Two implementations that fail differently, which is the
only kind of agreement this file accepts.