← board

An imported generic routine is spliced before the program's own type section

A generic function declared in a UNIT and specialized on a type declared in the importing PROGRAM does not compile: the specialized body is spliced at the end of the program's uses clause, which is ahead of every type the program itself declares, so the substituted type argument is not in scope yet.

tgenfunc19.pp:32 is the corpus row. The program declares TTest2, calls specialize DoTest<TTest2>, and the spliced Result := TTest2.Test is refused with undefined variable (TTest2) — a scope diagnostic pointing into ugenfunc19.pp, because the template text came from the unit.

CONTROL, measured at compiler 1764cc174080, not reasoned about. The same program with the type argument declared IN THE UNIT compiles and prints the right answer (9):

unit uA;   { TBase, TInUnit, generic function DoTest<T: TBase> }
program pA;  uses uA;  begin WriteLn(specialize DoTest<TInUnit>); end.

So the substitution machinery, the arity check and the call-site rewrite are all correct. It is the splice POSITION alone — the one variable that differs between the two programs is where the argument type is declared relative to the uses clause.

Why the splice is there, and why moving it is not a one-liner

SpecializeImportedGenericFuncUses runs at the end of ParseUsesClause for a reason its own comment states at length: ahead of TokPos is the only safe direction to edit in, because RemoveTokens shifts token indices and AdjustPass2Spans is a no-op outside the body pass, so a removal BEHIND TokPos invalidates TokPos and every DeclItem span already recorded. The uses clause is the earliest point at which "every template this clause imported is registered, and every use lies ahead" is true.

That argument is about the rewrite of the use sites (specialize F<C> -> F_C), which genuinely must happen ahead of TokPos. It is not an argument about where the body is spliced. The likely shape of the fix is to split the two: rewrite the call sites at the uses clause as today, and defer the SpecializeStream + ParseSubroutine of the specialized body until a point after the program's declarations — the same relative position a locally declared template already gets, which is why the local case has never had this bug.

Not attempted here: it moves a splice site that four call paths share (ParseGenericFunctionDef, ParseTopLevelSpecialize, SpecializeImportedGenericFuncUses, ExpandGenericMethod), and the local and imported cases would stop sharing one routine. Worth doing as a piece of work, not on the way past.

The FIRST wall on this row was a different defect and is fixed: the call-site predicate required a trailing (, so specialize DoTest<TTest> — a parameterless call, which Pascal spells without parentheses — was declined and left in the stream to be read as an expression (undefined variable (specialize)). Fixture: test/test_a_parameterless_generic_routine_is_called_without_parentheses.pas. That fix also burned tgenfunc12.pp, whose skip reason named the same missing spelling.

FIXED 2026-09-08 at compiler d1d78434de8c — and the LOCAL case had it too

The concrete body is no longer spliced at the template's own position. SpecializeInlineGenericFuncUses QUEUES it (PendFunc*, the routine-side twin of PendingSpec*) and FlushPendingFuncSpecializations empties the queue where the declaration section ENDS — ParseProgram just before afterDeclTok, ParseUnit just before afterImpTok. That is the first token at which every top-level type of the compilation unit is declared, which is the only thing the concrete body needed.

Splicing there moves no recorded index: every DeclItem span was recorded BEHIND that token and Pass2Active is still False, so the hazard AdjustPass2Spans exists for is not live yet.

THIS TICKET'S "the local case has never had this bug" IS FALSE, AND IT IS THE FINDING

Measured at cd2d264c72df, one file, no unit, no import:

program g2;
generic function DoIt<T>(a: T): LongInt;
begin Result := a.Test; end;
type TA = record Test: LongInt; end;      { declared AFTER the template }
...  WriteLn(specialize DoIt<TA>(x));     { pxx: unknown type: TA   fpc: 9 }

Move the type above the generic function and it compiles. The local case worked only when the type happened to precede the template, which is the ordinary way anyone writes it — so the constraint was invisible and the defect read as an import problem. It is one defect with three faces: an imported template over a program type, a local template above its type, and a routine-local type argument, which is never in scope at top level at all.

That also retires this ticket's proposed target — "the same relative position a locally declared template already gets". That position is the bug.

tgenfunc19.pp DOES NOT BURN, AND ITS SKIP REASON IS NOW WRONG IN THE OTHER DIRECTION

The file compiles and links now. It still exits rc=1, on a SECOND wall the splice defect was hiding:

DoTest<TTest>  = 1   fpc 2      <- the unit's class HELPER is not applied
DoTest<TTest2> = 3   fpc 3      <- correct

Pre-existing, proved by stash-and-rebuild: cd2d264c72df, without this change, answers 1 for the same call. So the skip reason's "the reason here named a CLASS-HELPER rule that was already carved out … and was never this row's wall" was TRUE about the wall it was measuring and is FALSE as a claim about the file: the helper rule was the NEXT wall, standing behind the one being described. Filed as [[bug-p-a-generic-routine-body-does-not-see-its-own-units-class-helper]]; the row's reason is rewritten to name it.

DoTest<TTest2> answering 3 rather than the program helper's 4 is the harder half and it is already correct — the body resolves T.Test against the class, not against a helper declared in the specializing program.

Fixture

test/test_a_specialized_routine_body_lands_after_the_declarations.pas (test_gfsplice26) with test/units/ugenfuncsplice.pas, differential against fpc 3.2.2, four rows — and two of them are the controls that hid this: local-below and unit-unittype are the shapes that always worked, so the file prints which of the four spellings a future regression breaks. Each row returns a different tag (11, 22, 33, 44), so no row can pass by picking up another row's specialization, and none of those is a zero, a default or SizeOf(Integer). Positive control on pin v407: unknown type: TInProg, reported against the UNIT's file, which is the reported diagnostic's own shape.

Log