← board

The shape

generic TTest<T> = class
type
  TTestT = specialize TTest<T>;      { names its OWN template }
end;
TI = specialize TTest<LongInt>;      { -> expected '<' before ';' }

fpc 3.2.2 compiles and runs it. This is how a generic container names its own iterator or node type, and it is the first construct in ugeneric99.pp.

Two probes located it, and neither is the obvious one

A template carrying such a nested type but never SPECIALIZED parses fine. So nothing is wrong at capture; the failure is at specialization.

The identical self-reference in a USE position worksNxt: specialize TTest<T>; as a field compiled and ran correctly throughout, on this tree and on pin v407. So it is not the self-reference. It is the =.

NestedSpecGroup refuses any group whose preceding token is tkEq:

if (i > 0) and (TemplateTokens[ts + i - 1].Kind = tkEq) then Exit;

which is correct for TEnumSpec = specialize TEnum<T> — a declaration must MINT a class, and that is the deferral machinery's job, not a collapse. It is wrong for exactly one template: the one being streamed. Minting for a self-reference cannot be done, because the class does not exist yet.

Unrecognised, the group reached the verbatim copy, and the identifier arm immediately below renames the template's own name to the specialization's name. specialize TTest<LongInt> became specialize TI<LongInt>generic template TI not found. With the parameter still in it, the truncated parse reads expected '<' before ';'. Two messages, one exclusion, which is why they read as unrelated.

The fix

Move the name extraction above the = test and exempt the self-reference:

if (i > 0) and (TemplateTokens[ts + i - 1].Kind = tkEq) and
   (not CaseEqual(nm, SpecializeTemplateName)) then Exit;

Recognising the group is all it does. Whether the group COLLAPSES is still SpecializeToBuffer's decision, comparing the group's canonical name against StreamedSpecCanonName — the same key the alias-declared self-reference fix added earlier the same day. The exemption widens what is seen, not what is decided.

The case that still fails, and it is NOT a residual

TOtherArg = specialize TTest<Double> inside TTest<LongInt> — the same template at DIFFERENT arguments — is not a self-reference, does not collapse, and still fails. fpc 3.2.2 refuses that program too: Syntax error, "identifier" expected but ";" found. So a nested type may name its own template only at its own arguments, and ours is a differing diagnostic on code the language does not accept — deferred, not a gap.

The check therefore stays exact rather than being widened to the template NAME. Widening it would accept what fpc refuses and collapse two different specializations onto one class.

Fixture

test/test_a_nested_type_may_specialize_its_own_template.pas (test_selfnesttype26), five rows against fpc 3.2.2. Both broken spellings (via the parameter, and at the template's own argument) because they produced the two different messages; the USE-position control that locates the defect at the =; and an other-template row that must keep MINTING, so a regression reads as this change having widened rather than aimed.

That last row is built from INSIDE the template, which is where a nested minted type is actually used.

CORRECTED, and the correction is about my own measurement. This section said reaching a minted nested type through the OUTER specialization's name — TI.TMinted.Create — was "a separate gap (class method not found (TMinted)), still open". It is not, and this change is what closed it. The failure was real on the build BEFORE the fix, went into the fixture header and into this ticket, and was already false by the time the fix compiled. Re-measured at cd30ba1c7d5d: it compiles, runs, and matches fpc.

Worth naming beside the stale-binary class, because it is not that one: a stale MEASUREMENT — the tree moved under a note rather than under a run. A claim written mid-fix ages against the very binary being changed, and afterwards it reads as though it had been checked. Re-measure every "still broken" note AFTER the build that might have fixed it.

Residual

tgeneric99.pp does not burn. Its wall moves from line 11 to line 25, which is t: specialize TTest<LongInt>.TTestClass — a nested type reached THROUGH an inline specialization. The file's remaining forms are unit- and class-qualified specialize (ugeneric99.specialize TTest<LongInt>, TTestClass.specialize TTest<LongInt>), which is a separate syntax and a separate job.