← board

Mutually recursive generic specialization hangs the compiler

The measurement

type
  generic TP<A, B> = record
    l: A; r: B;
    function Swap: specialize TP<B, A>;
  end;
function TP.Swap: specialize TP<B, A>;
begin Result.l := r; Result.r := l; end;
type TIP = specialize TP<Integer, string>;
result
FPC 3.2.2 rejects (syntax error at the return type)
pxx at e18d01cef spins — killed at 3 minutes, 97% CPU, 117 MB and climbing

A hang is the worst failure mode a compiler has: no location, no message, no progress, and in a build script no way to tell it from a slow file.

Root cause

ParseSpecialization handles a specialization whose body names a nested one that is not a type yet by deferring: it re-emits its own declaration behind its prerequisites' declarations, into the token stream, and re-parses. That terminates only while the unmet-prerequisite set shrinks.

TP$Integer$string needs TP$string$Integer; TP$string$Integer needs TP$Integer$string, which has not been registered because it deferred. Neither set ever shrinks, and each round inserts a fresh copy of both declarations, so the token stream grows without bound.

The self-referential case already worked, and shows the shape of the missing guard — the prerequisite scan skips an alias whose name equals the one being declared:

if (not CaseEqual(aliasNm, specName)) and (not NestedSpecKnown(aliasNm)) then

so generic TNode<T> with a next: specialize TNode<T> field (the ordinary linked list) is fine. A swapped-argument self reference mints a different alias name, so it misses that check and falls into the cycle.

The fix

A deferral is only progress while the unmet set shrinks, and it cannot legitimately take more rounds than there are specializations in the whole program. So count deferral rounds per specialization NAME and error past MAX_SPECIALIZATIONS:

error: circular generic specialization: TP$string$Integer requires
       TP$Integer$string, which requires TP$string$Integer back

Both sides are named, because the cycle — not either declaration alone — is what the author has to break. 0.5 s to the diagnostic.

This makes the failure loud; it does not make the program compile. Two mutually recursive specializations are finite under memoisation (there are exactly two of them), so a future change could register a specialization before streaming its prerequisites and let the cycle close. FPC does not accept this program either, so that would be a dialect extension rather than parity work, and it is not what this ticket is for.

Not affected

A wider generics differential probe run alongside this matches FPC line for line: generic classes with typed fields and constructors, a generic class with a array of T field driven by SetLength, specializations over Integer / string / Boolean, a nested specialization (specialize TList<TIBox>), generic records, and a self-referential generic class.

Test

test/test_generic_cycle_fail.pas — a negative test: the compile must fail and the log must contain circular generic specialization.

Gate

make compiler/pascal26 fixedpoint converged after 1 round; tools/gate.sh quick GREEN.