Canonicalise type-alias generic arguments to their underlying type

Instantiating a generic with a type alias as the argument (e.g. TList<TAlias>)
produced a different instantiation identity than using the alias's underlying
type, because one path substituted the alias name literally while another
resolved it. Inside the generic body this made TList<T>.GetEnumerator's
declared result type (TListEnumerator<TAlias>) differ from the constructed
value (TListEnumerator<Underlying>), failing an internal assignment — so
TList<AnyAlias> was unusable.

Resolve each generic argument through its alias chain to the canonical
underlying-type name before forming the instantiation key (mirroring the
canonicalisation already done for array-of / pointer / set-of / class-of
element types), only when the resolved name genuinely differs (preserving
existing string/String mangled names). Adds a regression test.

3744 compiler tests (20 pre-existing toolchain failures unchanged), stdlib OK.
This commit is contained in:
Graeme Geldenhuys 2026-06-23 19:36:04 +01:00
parent 676cee0cae
commit c904bf321d
2 changed files with 139 additions and 3 deletions

View file

@ -151,6 +151,15 @@ type
FindTypeOrInstantiate. Has no effect on names without '<'. }
function ResolveScopeBoundTypeParams(const ATypeName: string): string;
{ Canonicalises the type arguments of a generic instantiation name by
resolving each argument through any alias chain to its underlying type's
canonical name. 'TList<TIntAlias>' (where TIntAlias = Integer) becomes
'TList<Integer>', so an instantiation using a transparent type alias and
one using the alias's underlying type share a single identity. Has no
effect on names without '<' or on arguments that are unresolved type
parameters. }
function CanonGenericArgs(const ATypeName: string): string;
{ Generic function instantiation: resolves 'Identity<Integer>' on demand. }
function InstantiateGenericFunc(const AInstName: string): TMethodDecl;
{ Generic method instantiation: resolves 'Pick<Integer>' on an owner class
@ -2333,6 +2342,61 @@ begin
Result := BasePart + '<' + OutArgs + '>';
end;
function TSemanticAnalyser.CanonGenericArgs(const ATypeName: string): string;
var
BrOpen, BrClose, I: Integer;
BasePart, ArgsPart, OutArgs, Arg: string;
ArgList: TStringList;
ArgType: TTypeDesc;
begin
Result := ATypeName;
BrOpen := StrPos('<', ATypeName);
if BrOpen < 0 then Exit;
BrClose := Length(ATypeName); { closing '>' is always the last char }
BasePart := StrHead(ATypeName, BrOpen);
ArgsPart := StrCopyFrom(ATypeName, BrOpen + 1, BrClose - BrOpen - 2);
ArgList := TStringList.Create();
try
while ArgsPart <> '' do
begin
I := StrPos(',', ArgsPart);
if I >= 0 then
begin
ArgList.Add(Trim(StrHead(ArgsPart, I)));
ArgsPart := Trim(StrCopyTail(ArgsPart, I + 1));
end
else
begin
ArgList.Add(Trim(ArgsPart));
ArgsPart := '';
end;
end;
OutArgs := '';
for I := 0 to ArgList.Count - 1 do
begin
Arg := ArgList.Strings[I];
{ Resolve the argument to its underlying type and use that type's
canonical name. A transparent type alias (type TIntAlias = Integer)
shares the underlying type's descriptor, so its .Name is the
canonical 'Integer' — collapsing 'Foo<TIntAlias>' and 'Foo<Integer>'
onto one instantiation. Leave the argument untouched when it does not
resolve (e.g. an in-scope generic type parameter T) so substitution
still applies later. }
ArgType := FindTypeOrInstantiate(Arg);
if (ArgType <> nil) and (ArgType.Name <> '') and
(not SameText(ArgType.Name, Arg)) then
Arg := ArgType.Name;
if OutArgs = '' then
OutArgs := Arg
else
OutArgs := OutArgs + ',' + Arg;
end;
finally
ArgList.Free();
end;
Result := BasePart + '<' + OutArgs + '>';
end;
function TSemanticAnalyser.SynthAnonEnum(const AMemberList: string): TEnumTypeDesc;
{ Synthesise (or reuse) an enum type from an inline member list encoded as
'(a,b,c)'. Members become skConstant symbols, exactly as a named enum's do.
@ -2631,11 +2695,21 @@ begin
end;
if StrPos('<', AName) >= 0 then
begin
Result := InstantiateGeneric(AName);
{ Canonicalise type-alias arguments to their underlying type's name so a
generic instantiated with a transparent alias (Foo<TIntAlias>) shares one
identity with the underlying-type form (Foo<Integer>). Re-check the
table under the canonical name before instantiating. }
CanonName := CanonGenericArgs(AName);
if not SameText(CanonName, AName) then
begin
Result := FTable.FindType(CanonName);
if Result <> nil then Exit;
end;
Result := InstantiateGeneric(CanonName);
if Result = nil then
Result := InstantiateGenericRecord(AName);
Result := InstantiateGenericRecord(CanonName);
if Result = nil then
Result := InstantiateGenericInterface(AName);
Result := InstantiateGenericInterface(CanonName);
end;
end;

View file

@ -90,6 +90,11 @@ type
procedure TestSemantic_Generic_TwoInstances_PointerFieldDistinctTypes;
procedure TestCodegen_Generic_TwoInstances_MethodCallsResolveToOwnInstance;
{ ------------------------------------------------------------------ }
{ Type-alias type argument resolves to one canonical instantiation }
{ ------------------------------------------------------------------ }
procedure TestSemantic_GenericInstantiatedWithAlias_Compiles;
{ ------------------------------------------------------------------ }
{ Diamond operator: TFoo<> infers type args from LHS }
{ ------------------------------------------------------------------ }
@ -981,6 +986,63 @@ begin
Pos('call $TBox_String_SetValue', StrBody) > 0);
end;
{ ------------------------------------------------------------------ }
{ Type-alias type argument }
{ ------------------------------------------------------------------ }
const
{ Regression: a generic instantiated with a transparent type alias
(TIntAlias = Integer) must share ONE identity with the underlying-type
form, otherwise a generic method whose body both declares a nested
generic return type (TInner<T>) and constructs it (TInner<T>.Create())
sees two divergent instantiations. Previously one path kept the alias
spelling (TInner<TIntAlias>) while the other resolved it (TInner<Integer>),
so the internal assignment 'Result := TInner<T>.Create()' failed with
"Type mismatch ... expected 'TInner<TIntAlias>' but got 'TInner<Integer>'".
The two instantiation paths must now agree. }
SrcGenericInstantiatedWithAlias =
'''
program Prg;
type
TIntAlias = Integer;
TInner<T> = class
FV: T;
end;
TOuter<T> = class
function Make: TInner<T>;
begin
Result := TInner<T>.Create()
end;
end;
var
O: TOuter<TIntAlias>;
N: TInner<TIntAlias>;
begin
O := TOuter<TIntAlias>.Create();
N := O.Make()
end.
''';
procedure TGenericsTests.TestSemantic_GenericInstantiatedWithAlias_Compiles;
var
Prog: TProgram;
Inner: TRecordTypeDesc;
begin
{ Analysing without a semantic error is the regression check — the buggy
compiler raised a type-mismatch in TOuter<T>.Make's body. }
Prog := AnalyseSrc(SrcGenericInstantiatedWithAlias);
try
{ The alias and underlying-type forms must collapse onto one canonical
instantiation keyed by the underlying type name (Integer). }
Inner := TRecordTypeDesc(Prog.SymbolTable.FindType('TInner<Integer>'));
AssertNotNull('TInner<Integer> is the canonical instantiation', Inner);
AssertNull('no separate TInner<TIntAlias> instantiation exists',
Prog.SymbolTable.FindType('TInner<TIntAlias>'));
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ Diamond operator tests }
{ ------------------------------------------------------------------ }