← board

A specialization ladder that gains an argument segment per round

Blocks [[feature-pascal-corpus-generics]] — this is the uses Generics.Collections wall on rung 3, at binary 5e00cec21466 (bab3814ad).

The ladder, and the row that names the pump

PXXDBG=p.mint:*, driver uses Generics.Collections, corpus staged at library_candidates/rtl-generics/packages/rtl-generics/src: 872 mints, and the distinct aliases bucket by how many TEnumerable$ segments they carry — 109 at zero, then exactly 10 aliases at each of rungs 1..54, 3 at rung 55.

PXXDBG=p.nspec:TCustomPointersEnumerator says why, one row per rung:

alias=TCustomPointersEnumerator$UInt32$PT
      under=TCustomListWithPointers$UInt32   nsub=1 subs=T->UInt32
alias=TCustomPointersEnumerator$TEnumerable$UInt32$PT$PT
      under=TQueue$TEnumerable$UInt32$PT     nsub=1 subs=T->TEnumerable$UInt32$PT
alias=TCustomPointersEnumerator$TEnumerable$TEnumerable$UInt32$PT$PT$PT
      under=TQueue$TEnumerable$TEnumerable$UInt32$PT$PT
                                             nsub=1 subs=T->TEnumerable$TEnumerable$UInt32$PT$PT

The substitution at rung N+1 is the alias minted at rung N. 55 rungs, and the only thing that stops it is MAX_SPECIALIZATIONS = 256.

Where it starts — one spelling, two bindings

TEnumerable<T> = class abstract
public type
  PT = ^T;                                                          // :131
...
TCustomPointersEnumerator<T, PT> = class abstract(TEnumerator<PT>);  // :144
...
  TPointersEnumerator = class(TCustomPointersEnumerator<T, PT>)      // :212

At :144 PT is the template's own SECOND PARAMETER. At :212 the bare PT is the nested type inherited from TEnumerable<T>. The seed row registers the :212 reference under subs=T->UInt32 and the second argument goes into the alias as the literal parameter name PT — the defect shape DbgMintTrace's own comment says the p.mint probe was written to catch: "an argument that is still a template PARAMETER name … means the mapping through SpecSub* in NestedSpecArg did not happen or ran under the wrong substitution set."

Correction (frankZ, same day): an earlier revision read nsub=1 as "one substitution for a two-argument reference". nsub is SpecSubCount, the substitution set in force, and 1 is CORRECT there — TCustomListWithPointers<T> has one parameter. Both arguments are recorded (na=2); the reference's own count is printed nowhere. Why PT misses is one layer down and is frankZ's: NestedSpecArg maps an argument through SpecSubNames (the enclosing template's own parameters) and HoistedNameFor (names it declares in its OWN body), and PT is declared in the ANCESTOR's body, so both miss.

THE ANSWER — why an unresolved argument became a runaway (fixed)

The unresolved PT alone gives a plain unknown type: PT. What turned it into a ladder was a SPURIOUS EDGE, and it is a name-for-identity defect one level from the one fixed at 3801a4d66:

constructor TQueue<T>.TEnumerator.Create(AQueue: TQueue<T>);

After DelphiRewriteGenericUses strips the <T> the stream reads constructor TQueue . TEnumerator . Create. ScanDelphiMethodImplsForNestedSpecs looked for <ident> . matching Templates[ti].Name with no test on what PRECEDES it, so for ti = the unit-level TEnumerator<T> the qualifier TEnumerator answered the match, and the captured range started at the DOT and ran through TQueue's nested class body — minting TQueue<X> from a program that never asked for one. That closes the cycle: TBase<X> needs TEnumerator<X.PT>, TEnumerator<Y> wrongly needs TQueue<Y>, TQueue<Y> needs TBase<Y>, and Y grows one segment each time.

BufferTemplateMethodsAhead had the header-shape test and was therefore correct; the scan was a second copy of the same shape with the test missing — a rule spelled per caller fails by an ABSENT copy. Both now call one DelphiMethImplHdrStart.

Measured, and the nested class is not a template of its own: a temporary PXXDBG=p.tmpl registry dump (kept — it is one guarded writeln at the single registration site) prints exactly TEnumerator and TQueue for the repro, so matching the path HEAD loses nothing.

31-line repro test/test_a_nested_class_method_impl_is_not_the_unit_level_template_of_that_name.pas, byte-matching fpc 3.2.2 -Mdelphi; the negative control is renaming the nested class, which compiles on the unfixed binary.

Why nothing bounds it

There IS a round cap and it cannot see this: SpecDeferRound[] is counted per specName, and circular generic specialization fires only when ONE name is deferred more than MAX_SPECIALIZATIONS times. Every rung is a new name, so each one is deferred once and the counter never climbs. What gives out is SpecDeferCount — the number of DISTINCT deferred specializations — reported as too many deferred specializations. A ladder is invisible to a per-name cap by construction; only a cap on total deferrals, or on argument-name depth, would see it.

Two things NOT to do

Relation to the nested-type ticket

[[bug-p-a-class-nested-type-as-a-specialization-argument-resolves-at-unit-scope]] is adjacent and deliberately not merged: that ticket's own summary says it is not this rung's blocker, and unknown type: PT here was cleared by 1c16d4523. This ticket does not depend on it owning anything. The question that would merge them: make the :212 PT resolve to TEnumerable<T>.PT and see whether the mint count drops to one rung.

Dead end already paid for: extending CollectHoistCandidates to walk the ancestor chain (so PT is found through TEnumerable<T>) HANGS this driver —

90s, 176 mint lines, then unknown type: TList$UInt32$PT.

Log

Conformance at the fix, with the delta attributed

tools/run_pascal_conformance.sh ./compiler/pascal26 at 9b4f55176, binary f22102f66298: 425 pass, 0 fail, 75 skip, 50 auto-gated (of 550); 41 gap.

Against this session's earlier 423/0/77/50 (41 gap -> 42 gap the other way: 42 before, 41 now) the delta is +2 pass, -2 skip, -1 gap, and NONE of it is mine. git diff on test/pascal-conformance/pxx.skip names both rows: tclassinfo1.pp removed by frankH's 5d29682a2 (TObject.ClassInfo answers the typinfo header) and tgeneric93.pp by frankZ's 004793f42 (declared() reads the generic arity spelling). Both arrived in the pull that banked this fix.

My change moved nothing in this corpus — the same result the arity fix had. That is expected: the ladder needs a nested class whose name collides with a unit-level template, which is an rtl-generics idiom and not something the conformance corpus exercises. The assertion for this fix is test_a_nested_class_method_impl_is_not_the_unit_level_template_of_that_name, which does not compile at all without it.