← board

TypeRef: migrate consumers lane by lane

Follow-up to [[feature-a-typeref-handle]], which landed TTypeRef (compiler/defs.inc) additively — the record exists, nothing reads it yet. This ticket is the actual payoff: replacing the ~90+ parallel-array sites (SymPtrBaseTk/SymPtrBaseRec, UFldPtrElemTk/UFldPtrElemRec, ProcRetPtrElemTk, LiftCapPtrTk/LiftCapProcSig, ...) with one TTypeRef field per entity, one lane at a time.

See devdocs/dev/type-identity-as-substrate.md for the full design and why this matters (the "one of six parallel arrays not written" bug class — four such bugs landed in one session per that note's evidence table).

The candidate lane order below is OVERTAKEN by this ticket's own later sections — do not plan off it. Measured 2026-08-30: lane 1 (TSymbol) is substantially landed, and lane 4 (proc returns) and record/class fields are DONE. Lanes 2 and 3 are not the live work. The live item is step 3 (the TTypeRef fold), whose Track U blocker [[decide-typeref-gains-a-pointer-depth-field]] was answered on 2026-08-25 (28c19f214) and sat unnoticed for five days — PtrDepth has since landed, readers-free. What remains is one coordinated commit that needs ir.inc. See the 2026-08-30 section at the bottom.

Landing rule (unchanged from the parent ticket)

Each lane migrates ONE entity kind's parallel arrays to a single TTypeRef field, lands under the self-host byte-identical gate, and does not touch any other lane's arrays in the same change. Candidate lane order (smallest/ lowest-risk first, largest blast radius last):

  1. TSymbol (Syms[]) — the largest single consumer (~26 parallel fields per the design note), but also the most self-contained: read/write sites are concentrated in symtab.inc/ir.inc.
  2. AliasPtrBaseTk/AliasPtrBaseRec (type aliases).
  3. UFldPtrElemTk/UFldPtrElemRec (class/record fields).
  4. ProcRetPtrElemTk/ProcRetProcSig (routine return types).
  5. LiftCapPtrTk/LiftCapProcSig (closure capture types).
  6. CTypeFnRetPBaseTk/CTypeFnRetPProcSig (C frontend type declarations).

Each lane: add a TTypeRef field alongside the existing parallel arrays, populate it everywhere the old arrays are populated (do not remove the old arrays yet — they stay the source of truth until every READ site is migrated too), then migrate reads, then delete the old arrays once nothing references them. Land incrementally — this is explicitly NOT a one-sitting change; the parent ticket's Blocks [[feature-a-abi-oracle]] note means the oracle work cannot start until at least enough lanes are migrated that a type's identity can be read from one place instead of per-backend Syms[] probing.

Gate

make test + self-host byte-identical per lane; make test-nilpy too for any lane NilPy code touches (2 and 3 especially, given the design note's NilPy divergence evidence).

2026-08-01 — lane 1 (TSymbol) in progress; REVERTED two backend read-site

migrations, real sync gap found

SymTR (additive, 54b5684bc) landed, and several read sites migrated cleanly: ir.inc's managed-record-copy and char-pointer-check (93ebb9377, e9d1522c6) — both safe because the fields they read (RecName/PtrElemTk for those specific symbols) are only ever written at the symbol's creation chokepoint, so SymSyncTypeRef (called there) keeps SymTR correct.

Then a real gap was caught before more damage landed: SymSyncTypeRef is only called at the 5 symtab.inc creation chokepoints (AllocVar/AllocParam/AllocArray/AllocDynArray/AddConst), but parser.inc/cparser.inc/pyparser.inc/ir.inc also mutate the SAME old fields after creation — e.g. compiler/parser.inc:26006 (and :20565, :21058) does idx := AllocArray(...) then immediately Syms[idx].ElemRecName := LastTypeRecId for any array of <record> parameter or local — a hot, common pattern, not an edge case. None of those ~132 post-creation write sites call SymSyncTypeRef, so SymTR goes stale for any symbol they touch.

Two already-merged backend migrations were exposed to exactly this: 573e1c4e7 (x86-64 ir_codegen.inc, migrated ElemRecName/ElemType/ RecName reads for anon-dynarray registration + managed-array-element copy) and e3497fc73 (i386/arm32/aarch64/riscv32/xtensa RecName reads for managed-record dynarray-append growth) — both read SymTR[...].ElemRec/ .RecId for exactly the array-of-record symbols the parser.inc sites above mutate post-creation. Reverted (9b73ff4d6, e484fde67) rather than leave a known, plausibly-widely-triggered stale-read gap live on master — this is the same bug shape as the pconst-shift regression from the night before (a parallel-array sync miss that a happy-path test corpus doesn't happen to exercise), and that one only surfaced in a full test-core run, not fixedpoint or testmgr --tier quick, so "tests passed" was not sufficient evidence to keep it.

Next: before migrating any more reads (especially anything touching RecName/ElemRecName/TypeKind/PtrElemTk/PtrElemRec/SymProcSig), add SymSyncTypeRef calls after every post-creation write site — full inventory (grep for Syms[<idx>].<field> := outside symtab.inc's Alloc*/AddConst) is roughly: RecName/ElemRecName ~58 sites (ir.inc 3, pyparser.inc ~42, parser.inc ~13), TypeKind ~9 (parser.inc), PtrElemTk/PtrElemRec ~17+17 (cparser.inc, parser.inc), SymProcSig ~11, ElemType/IsArray/ArrLen ~16. That write-side sync is real, distinct work — do it before trusting any more SymTR reads, and re-land the two reverted backend migrations only after it's done and re-verified.

2026-08-03 — moved working/ -> unfinished/ (board maintenance)

working/ is a live lock: a ticket sits there only while an agent is actively on it. This one had not been touched in three days, so the lock was stale and next was reserving a Track A file-lane nobody held.

Not a revert, and nothing is half-applied: lane 1 (the SymTR parallel array plus the full write-side sync) is landed and green — the self-host fixedpoint has been rebuilt many times since against it. What remains is the ~68 read sites and the old-field deletion, which is why this is unfinished/ rather than done/. Re-claim it to continue.

Triage 2026-08-19 (Track D re-triage pass, pin v364)

Genuine feature, still wanted — no lane migrated, and no partial state. Measured by counting consumers rather than reading the ticket: TTypeRef appears 4 times in compiler/defs.inc (the additive declaration its parent landed) and zero times in compiler/symtab.inc and compiler/ir.inc. So lane 1 has not started, and nothing is half-applied.

Queue hygiene: this is in unfinished/, and it should not be. That folder means work halted with the ticket incomplete, and a Track A ticket there is flagged CRITICAL precisely because a half-applied compiler change can break the self-host gate. Nothing is half-applied here — the parent landed cleanly and this is untouched follow-up work. It belongs in backlog/. Not moved by this read-only triage pass; flagged for whoever holds the A slot.

2026-08-24 — the 2026-08-19 triage is out of date, and lane 4 has a measured prerequisite

Correction first. The triage above says "TTypeRef appears 4 times in compiler/defs.inc … and zero times in compiler/symtab.inc and compiler/ir.inc. So lane 1 has not started." Measured today:

compiler/symtab.inc : 59 mentions      compiler/ir.inc : 19
SymSyncTypeRef call sites : 105  (pyparser 54, pasparser_stmt 17, cparser 8,
                                  ir 8, symtab 7, pasparser_decl 5, ...)
SymTR read sites already migrated : ir.inc, ir_codegen386.inc, ir_codegen_xtensa.inc

Lane 1 is substantially landed — write-side sync and a first set of reads. The triage counted a stale checkout or grepped the type name rather than the array name; either way, do not plan off it.

Lane 4 (proc return types) is now WANTED by a filed bug

[[bug-p-dereferencing-a-function-result-of-pointer-to-pchar-loses-the-shape]]: GetQ^ where GetQ: ^PChar is wrong in the four contexts that refuse to guess, because a proc records ProcRetPtrElemTk/Rec — the immediate pointee — and nothing about the return pointer's DEPTH or ultimate BASE, so PChar and ^PChar are indistinguishable as return types. The lane is small: 10 write sites, 7 read sites.

Two things block it, both measured today, both in this ticket's own subject

1. TTypeRef cannot express a pointer's depth. As declared (defs.inc:1559) it has PtrBaseTk/PtrBaseRec and DynDepthdynamic array nesting — and no pointer-level count. Symbols carry theirs OUTSIDE SymTR, in SymPtrDepth. So a TTypeRef today is strictly less expressive than the parallel arrays it is meant to replace, for exactly the case that motivates the migration.

2. And the field is fed the wrong half. SymSyncTypeRef does

  SymTR[idx].PtrBaseTk  := Ord(Syms[idx].PtrElemTk);   { the IMMEDIATE pointee }

into a field whose name and comment say base. Measured with PXXDBG=a.symptr:

declaration depth PtrElemTk (immediate) SymPtrBaseTk (ultimate)
pc: PChar 1 tyChar tyChar
ppc: ^PChar 2 tyPointer tyChar
pr: ^TRec 1 tyRecord tyRecord (rec 29)
raw: Pointer 0 tyUnknown tyUnknown

They coincide at depth ≤ 1, which is why the single existing reader (ir.inc:2506, the char-pointer check) is correct today and why nothing has caught it.

The obvious fix is NOT safe yet — this is the part worth recording

The clean shape is: TTypeRef gains PtrDepth, and PtrBaseTk/PtrBaseRec are fed from SymPtrBaseTk/SymPtrBaseRec so they mean what they are named (the immediate pointee stays derivable: depth > 1 ⇒ tyPointer, else the base — which is exactly how the deref chain in pasparser_lval.inc already reasons). The one existing reader then guards on PtrDepth = 1, which is a correctness improvement in its own right.

It cannot land until the old arrays are themselves in lockstep, and they are not:

Syms[..].PtrElemTk := ...  outside symtab.inc : 21 sites
  ast_syminfer 6 · cparser 9 · pasparser_decl 2 · pasparser_proc 1
  · pasparser_stmt 1 · pyparser 2
SymPtrDepth[..] := ...     outside symtab.inc :  9 sites  (all cparser)

So twelve post-creation sites set the immediate pointee and never touch depth or base. Feeding PtrBaseTk from SymPtrBaseTk today would make it read tyUnknown at every symbol those twelve touch, and the char-pointer check would silently stop firing. That is the same shape as the two backend migrations reverted on 2026-08-01 (9b73ff4d6, e484fde67) — a parallel-array sync miss that a happy-path corpus does not exercise — and the same lesson: sync the write side first, then migrate reads.

Concrete next step for whoever takes this

  1. Make the pointer triple (PtrElemTk/PtrElemRec, SymPtrDepth, SymPtrBaseTk/SymPtrBaseRec) written together at all 21 post-creation sites — a SetSymPointerType(idx, elemTk, elemRec, depth, baseTk, baseRec) helper, so there is one place to forget instead of five fields. That is independently valuable and is the actual root cause behind the filed bug.
  2. Then add PtrDepth to TTypeRef and re-point PtrBaseTk/Rec at the ultimate base, updating ir.inc:2506 to guard on depth.
  3. Then lane 4 proper: ProcRetTR, populated at the 10 sites, read by a new AN_CALL arm in the deref chain — which is what closes the filed bug.

Each step under make compiler/pascal26 + tools/gate.sh quick, and re-run the 88-pair PChar differential recorded in [[refactor-centralize-managed-string-pchar-conversion]] (currently 5 diverging, all of them the GetQ^ shape) as the acceptance check.

Also: this ticket is in backlog/, which the 2026-08-19 triage asked for. That move happened. No action needed.

2026-08-24, step 1's first instalment — SetSymPointerType, and the inference site

The step-1 plan recorded above ("make the pointer triple written together at all 21 post-creation sites — a SetSymPointerType helper, so there is one place to forget instead of five fields") now has its helper and its first converted call site.

The helpers

The depth = 0 escape is deliberate and is the honest half of the design: a caller that does not know is at least visibly declining instead of silently forgetting, which is the whole failure mode.

Converted: InferSymTypeFromNode (ast_syminfer.inc)

Six branches, and they split exactly along whether the source records depth:

inferred from source result
another SYMBOL SymPtrDepth/SymPtrBaseTk/Rec exact
a pointer ALIAS AliasPtrDepth/AliasPtrBaseTk/Rec exact
PChar(x) definition exact (1 over tyChar)
@x x's own type derived, declines if x is a pointer
a record FIELD UFldPtrElemTk/Rec only — no UFldPtrDepth derived
a function RESULT ProcRetPtrElemTk/Rec only — no depth derived

The last two are the shortfalls this ticket exists to remove; the function-result one is filed as [[bug-p-dereferencing-a-function-result-of-pointer-to-pchar-loses-the-shape]].

What it fixed, and how it was proven not to break anything else

var q := pp where pp: ^PChar lost the char-ness one level in, because only the pointee was copied:

before:  132814934638624      'x' + q^ -> 132814934638744     q^ = 'alpha' -> FALSE
after:   alpha                              xalpha                              TRUE

Proven neutral by an A/B binary comparison, which is the technique this ticket's 2026-08-01 revert lacked: the compiler built before the change and the compiler built after it were each used to compile the same sources, and the resulting binaries diffed.

compiler.pas                         BINARY IDENTICAL
test_pchar_pointer_to_pchar.pas      BINARY IDENTICAL
test_pchar_array_of_pointer_to_pchar BINARY IDENTICAL
test_not_operand_type_matrix.pas     BINARY IDENTICAL
test_basic_comprehensive.bas         BINARY IDENTICAL
c_builtin_bits.c                     BINARY IDENTICAL

That is stronger than "the tests pass" — it is "no emitted byte moved, in any frontend" — and it is the standard the remaining conversions should be held to.

Test

test/test_inferred_pointer_keeps_its_depth.pas. FPC cannot compile it (inline var in a statement block is a pxx/Delphi form, not objfpc), so the oracle is the explicitly typed twin printed beside each inferred row: every line must have two equal halves, and the explicit half is separately pinned against fpc 3.2.2 by test_pchar_pointer_to_pchar.pas. test-core asserts both the recorded output and the halves-are-equal invariant on its own, because a pair that drifted apart would still match a regenerated .expected. Verified to FAIL on the pre-change compiler, on all three affected rows. Cross-checked on i386 / aarch64 / arm32 / riscv32.

Step 1 is DONE — every post-creation site now goes through the helper

grep 'Syms\[.*\].PtrElemTk :=' outside symtab.inc returns nothing. The 15 remaining sites landed in two A/B-verified batches after the ast_syminfer one: pasparser_decl 2, pasparser_proc 1, pasparser_stmt 1, pyparser 2, then cparser 9. Reference binary compiled the same sources before and after each batch; compiler.pas, three PChar/not/depth Pascal tests, test_basic_comprehensive.bas, three NilPy tests and eight C tests (VLA, 2-D row length, decay stride, fn-pointer array, ** return, called result, ptr-array field) were binary identical every time. The compiler's own code section shrank 1,306 bytes in the cparser batch alone — the helper replaces five stores at nine sites.

Two findings worth carrying forward:

Lane 4 is DONE too, and it went the other way round

Proc returns got the triple (ProcRetPtrDepth / ProcRetPtrBaseTk / Rec) at all nine declaration sites, and record/class FIELDS got theirs (UFldPtrDepth / UFldPtrBaseTk / Rec) right after, because the 72-program cross product found them one after the other. Both closed bug-p-dereferencing-a-function-result-of-pointer-to-pchar-loses-the-shape; that ticket asked for TTypeRef FIRST and got parallel arrays instead. The reasoning is written out in the resolved ticket -- in short, the same five fields are populated at the same sites either way, folding arrays into a record is mechanical, and waiting cost silently wrong values in the field.

The reader is the expensive half, every single time. Populating proc-return depth fixed nothing on its own: c := GetQ^; WriteLn(c) printed the string while WriteLn(GetQ^) printed the address, same binary, same declaration. ApplyCallResultPtrSuffix is a FOURTH copy of the pointer walk (with the main deref chain, pasparser_expr's parenthesised tail, and the inherited-call tail) and it stamped none of the node tags the rest of the compiler reads. Four tickets running now have ended "the metadata was there / easy, the reader was missing" -- when a pointer shape is wrong, look at the reader before the table.

Cross-product acceptance line: 72/72 agree with fpc 3.2.2 (10 source shapes x 8 contexts). Before this session's fixes: 67/72, and before the array fix, 36 divergences out of 88.

Still open, in order

  1. The other 20 post-creation sites. DONE
  2. Lane 4 (proc returns). DONE, plus fields, which were not on the list.
  3. TTypeRef gains PtrDepth and PtrBaseTk/Rec are re-pointed at the ultimate base, with ir.inc guarding on PtrDepth = 1. DONE (972b8be8e, plus a296024e9 for ir.inc's remaining readers). The Track U fork [[decide-typeref-gains-a-pointer-depth-field]] was answered in 28c19f214; PtrDepth landed readers-free in the 2026-08-30 frankA pass, and the fold is the 2026-08-30 frankwasm section at the bottom. The recorded Pascal-parameter prerequisite measured STALE and cost nothing.
  4. The remaining reader duplication: four copies of the pointer walk, none of which can be deleted without the other three agreeing on the node tags. That is a bigger, better-value refactor than the table fold and it is not filed yet -- file it before starting, with the four sites named.

2026-08-30 (frankA) — PtrDepth landed, and the recorded blocker was stale

Took this under a coordinator constraint: no compiler/ir.inc (held for the cir.inc carve-out). That turned out to select exactly the right slice.

The lane assessment, since the dispatch asked for one

The ticket's lane list is largely overtaken by its own later sections. Lane 1 (TSymbol) is substantially landed; lane 4 (proc returns) and record/class FIELDS are DONE. The live item is step 3 — the TTypeRef fold — and decide-typeref-gains-a-pointer-depth-field is answered (28c19f214), so it is no longer blocked. The decision's own step 1 is "add PtrDepth to TTypeRef first, readers-free", which touches defs.inc + symtab.inc and not ir.inc. That is what landed here. Step 2 — re-pointing PtrBaseTk/Rec at the ultimate base — does need ir.inc, in the same change, and is deliberately not started.

The correction, and it is the reason this was worth doing now

symtab.inc's SetSymPointerType comment stated, in the present tense, that "twelve symbols carried an immediate pointee over depth 0 and base tyUnknown … and it is why feature-a-typeref-migrate-consumers cannot re-point TTypeRef.PtrBaseTk at the real base yet." That was the stated reason the migration could not proceed, and it is out of date — it was written 2026-08-24 when 12 of 21 sites were unconverted, and step 1 finished afterwards.

Re-measured 2026-08-30:

Syms[..].PtrElemTk :=   outside symtab.inc :  0
SymPtrDepth[..]    :=   outside symtab.inc :  0
SetSymPointerType/To call sites outside it : 21
   ast_syminfer 6 · cparser 9 · pasparser_decl 2 · pyparser 2
   · pasparser_proc 1 · pasparser_stmt 1

The 21 is the denominator that makes the two zeros mean converted rather than no such sites exist. The write side is in lockstep. The remaining blocker is purely the reader: ir.inc's char-pointer check reads PtrBaseTk as the immediate pointee and must begin guarding on PtrDepth in the same commit. The comment is corrected in place, with the old text kept as history.

Landed

TTypeRef.PtrDepth (defs.inc), populated from SymPtrDepth[idx] in SymSyncTypeRef as a literal copy like every other line there. Readers-free — nothing consults it yet, which is what makes it safe mid-migration. The two Base-named fields now carry a note saying they hold the immediate pointee despite their names, so the next reader is not misled by the same gap that made ^PChar indistinguishable from PChar.

A/B binary comparison — the standard this ticket sets, eight sources, four frontends, all BINARY IDENTICAL: compiler.pas, test_pchar_pointer_to_pchar.pas, test_inferred_pointer_keeps_its_depth.pas, test_not_operand_type_matrix.pas, test_basic_comprehensive.bas, c_builtin_bits.c, test_nilpy_configparser.npy, test_nilpy_float_methods.npy. Compiler built before and after, same sources, diffed. Self-host fixedpoint 1 round 3b00f387f0df; gate.sh quick GREEN.

Item 4's count is wrong, measured — filed as its own ticket

The "still open" list says "four copies of the pointer walk". Listed rather than counted: nine, of which six are the Pascal frontend (pasparser_lval.inc:4770, pasparser_expr.inc:930/6391/6740, pasparser_stmt.inc:6444/6559) and three are NilPy's (pyparser.inc:42567/47378/47524), which are legitimately duplicated across languages per the-substrate-is-ast-and-ir-not-the-parser.md. So the refactor is 6 Pascal-side copies, not 4. Filed as [[refactor-a-the-pointer-suffix-walk-has-six-copies-in-the-pascal-frontend]] as this ticket asked ("file it before starting, with the four sites named").

Next, for whoever holds ir.inc

One coordinated commit: re-point PtrBaseTk/PtrBaseRec at SymPtrBaseTk/SymPtrBaseRec, and change ir.inc's char-pointer check to require PtrDepth = 1. PtrDepth is already there and already populated. Note the outstanding caveat recorded 2026-08-24: Pascal's parameter table still has no depth sibling, so a Pascal ^PChar parameter arrives at depth 1 rather than 2 — check whether that makes the PtrDepth = 1 guard wrong for parameters before relying on it.

2026-08-30 (frankwasm) — step 2 landed: the fold, with the guard it needed

TTypeRef.PtrBaseTk/PtrBaseRec now hold the ultimate base (they were holding the immediate pointee under a name that said base), and ir.inc's char-pointer predicate gained the PtrDepth = 1 guard in the same commit. Two edits, exactly as step 3 of the "Still open" list specified.

The recorded prerequisite was stale — measured, not assumed

The caveat carried since 2026-08-24 — "Pascal's parameter table still has no depth sibling, so a ^PChar parameter arrives at depth 1 rather than 2; check before relying on the PtrDepth = 1 guard" — is false at HEAD, and the earlier note that extending the param table was "a prerequisite for step 2 … do it as the first half of step 2" is therefore discharged with no work. ptypesPtrDepth / ptypesPtrBaseTk / ptypesPtrBaseRec exist in pasparser_proc.inc, are assigned per parameter group, and reach the symbol through SetSymPointerType. Measured:

Mixed(aInt: Integer; bPc: PChar; cPtr: Pointer; dPpc: ^PChar; ePb: ^Byte)
  bPc  depth=1 basetk=3 (tyChar)     dPpc depth=2 basetk=3 (tyChar)
  cPtr depth=0 basetk=0              ePb  depth=1 basetk=8 (tyUInt8)

Order-independent — PcThenPpc and PpcThenPc both record correctly.

The probe that would have answered this in August could not see parameters at all. PXXDBG=a.symptr fires from the Alloc* chokepoints, i.e. BEFORE SetSymPointerType, so for a parameter it prints the recycled sym slot's previous occupant. Read at face value that output says "every parameter in a proc gets the last pointer parameter's shape" — a dramatic bug that is not there. This is the second hole in that probe, and the same one its own header comment already warns about for arrays: "a probe with a hole in it is worse than no probe, because the silence reads as an answer." Fixed forward instead of worked around: PXXDBG=p.ptrparam, which fires at the right moment, now prints depth and ultimate base too (53f9b82e0). It printed only the immediate pointee before, which is precisely the field that cannot tell PChar from ^PChar.

The guard is load-bearing, and the first measurement that said so was wrong

Removing the guard and rebuilding appeared to change the compiler's own output. It did not: the working tree still had the guard removed, so compiler.pas {$I}-included a different ir.inc. That compared two SOURCES, not two behaviours — the contamination this ticket already warns about under "Do not edit sources while a gate runs", reached by a different route. Re-measured on one fixed tree, the three compilers agree on compiler.pas byte for byte, and an instrumented build shows the guard rejects nothing while compiling it.

The honest discriminator is a bare depth-2 identifier in a PChar context:

var p: PChar; q: ^PChar;  p := @s[0];  q := @p;  WriteLn('A=', q);

  pre-fold compiler   A=4352408      (the address)
  folded + guard      A=4352408      byte-identical binary
  folded, no guard    A=<raw bytes>  q read as a PChar

So the guard is exactly what keeps the fold behaviour-neutral, which is this ticket's own standard — not an extra.

What is NOT pinned, deliberately

The guard's only reachable observable is that bare-identifier arm, and fpc 3.2.2 rejects every shape that reaches it (WriteLn(q), 'x' + q over a typed pointer). There is no oracle, and by CLAUDE.md's compat table "we accept a form FPC rejects" is not a defect — so the answer is a pxx dialect call, not a conformance fact, and no test asserts it. The guard is held to A/B behaviour-neutrality instead. test/test_ptr_depth2_bases.pas is new coverage of the adjacent real gap (depth 2 over ^Byte and ^ShortInt, which nothing tested — ^PChar had two tests and its two siblings had none) and its header says in as many words that it does not pin the guard.

Verification

A/B, compiler built before and after the fold, same sources, binaries diffed — all identical: compiler.pas (and it equals the fixedpoint sha), test_pchar_pointer_to_pchar.pas, test_inferred_pointer_keeps_its_depth.pas, test_not_operand_type_matrix.pas, test_basic_comprehensive.bas, c_builtin_bits.c, test_nilpy_configparser.npy, test_nilpy_float_methods.npy, test_cross_write_pchar.pas. Self-host fixedpoint verified (80f0bf81b554); gate.sh quick green. test_ptr_depth2_bases.pas agrees with fpc 3.2.2 line for line.

Still open after this — nothing on the list

Step 3 is DONE, and so is step 4: frankA had already split it out earlier the same day as [[refactor-a-the-pointer-suffix-walk-has-six-copies-in-the-pascal-frontend]], correcting the count to six on the way, and that ticket is in done/. I nearly filed a duplicate of it before checking — the "still open" list reads as open because nobody struck item 4 through when it moved.

ir.inc's own two remaining raw readers (the depth-2 stride case and arm 4's q[i]) were migrated in a296024e9 rather than left: both want the ultimate base, so they were unmigratable until the fold above, which is why they were still there and why they belong to this change and not to a later sweep. See the scope section below for what is left elsewhere and why it is not a ticket.

Scope, since two sessions have now asked whether this ticket is still legible

It is, but only its "Still open" list is live. The survey sections above (the site counts, and every mention of parser.inc) are a RECORD of a survey run before the pasparser_*.inc split, and parser.inc has not existed since. Per CLAUDE.md's precedence rule those are history and are deliberately left unrewritten rather than "fixed" — but they should not be read as instructions, and their counts are superseded by the measured ones below.

Re-measured at HEAD, raw SymPtrDepth/SymPtrBaseTk/SymPtrBaseRec readers left outside symtab.inc/defs.inc:

pasparser_lval.inc 9   cparser.inc 6   pyparser.inc 4
pasparser_decl.inc 3   ast_syminfer.inc 2          ir.inc 0

24 sites, all mechanical now that SymTR carries the ultimate base. ir.inc is at zero, which is the file that mattered — it held the only reader whose migration had semantic content.

So the ticket does not need re-scoping so much as closing: items 1-3 of its own list are done, and item 4 was already split out by frankA earlier the same day as [[refactor-a-the-pointer-suffix-walk-has-six-copies-in-the-pascal-frontend]] (which also corrected the count from four to six, by listing rather than counting) and is itself in done/. Nothing on the list is left, so this ticket resolves rather than carries forward.

The 24 remaining raw-array readers above are NOT a residue of this ticket's list — they are the mechanical tail of lane 1, safe to sweep whenever the file that holds them is free, and worth nothing on their own. A follow-up ticket for them would be make-work at prio 10 that the ranker scans forever, which is the exact leak CLAUDE.md's compat table warns about; whoever next edits pasparser_lval.inc or cparser.inc for a real reason should convert the readers in front of them and no more.

Log