fix: inherit a non-generic class from a generic-class instance

A non-generic class could not inherit from a generic-class instance:

    TBox<T> = class ... end;
    TIntBox = class(TBox<Integer>) ... end;   // rejected, then bad IR

Two bugs:

1. Semantic: AnalyseTypeDecls assumed any generic name in the first
   heritage slot was an interface — it cast FindTypeOrInstantiate's
   result to TInterfaceTypeDesc and moved it to the implements list, so a
   generic CLASS parent was rejected as "Unknown interface 'TBox<Integer>'
   in implements list". It now classifies the instantiated descriptor: a
   tyInterface result is an implements entry, anything else stays as the
   parent class for normal class-parent resolution.

2. QBE codegen: the inheriting class's typeinfo referenced its parent as
   $typeinfo_TBox<Integer> via ClassSymName (which does not mangle <>),
   but the generic instance's typeinfo is defined under the QBEMangle'd
   symbol $typeinfo_TBox_Integer — producing invalid QBE IR ("invalid
   character <"). The parent reference now uses QBEMangle for a generic
   instance, matching its definition. The native backend already mangled
   correctly and only needed the semantic fix.

Verified inherited method + field access and a virtual method declared on
the generic base and overridden in the derived class, on both backends.
Adds TE2EGenericsTests.TestRun_InheritFromGenericInstance_{MethodAndField,
VirtualOverride} and a note in docs/language-rationale.adoc.
This commit is contained in:
Graeme Geldenhuys 2026-06-16 14:08:59 +01:00
parent d91be47103
commit 659071f136
4 changed files with 86 additions and 6 deletions

View file

@ -7233,7 +7233,16 @@ begin
CD := TClassTypeDef(TD.Def);
if RT.Parent <> nil then
ParentStr := '$typeinfo_' + ClassSymName(RT.Parent.Name)
begin
{ A generic-instance parent (TBox<Integer>) is defined under the
QBEMangle'd symbol (typeinfo_TBox_Integer) and carries no unit prefix,
so reference it the same way. Non-generic parents keep ClassSymName
(which adds the unit prefix). }
if StrPos('<', RT.Parent.Name) >= 0 then
ParentStr := '$typeinfo_' + QBEMangle(RT.Parent.Name)
else
ParentStr := '$typeinfo_' + ClassSymName(RT.Parent.Name);
end
else
ParentStr := '0';
if RT.ImplementsCount() > 0 then

View file

@ -3576,6 +3576,7 @@ var
RT: TRecordTypeDesc;
ParentRT: TRecordTypeDesc;
ParentSym: TSymbol;
GenParentDesc: TTypeDesc;
FldType: TTypeDesc;
FldName: string;
Sym: TSymbol;
@ -3944,18 +3945,25 @@ begin
if TClassTypeDef(TD.Def).ParentName <> '' then
begin
ParentSym := nil;
{ If name looks generic, try instantiating as interface first }
{ A generic name as the first heritage entry (class(TBox<Integer>) or
class(IFoo<T>)) must be instantiated, then classified: a generic
CLASS instance is the parent class, a generic INTERFACE instance is
an implements entry. Earlier this assumed interface unconditionally,
so inheriting from a generic class was rejected as an unknown
interface. }
if StrPos('<', TClassTypeDef(TD.Def).ParentName) >= 0 then
begin
IntfDesc := TInterfaceTypeDesc(
FindTypeOrInstantiate(TClassTypeDef(TD.Def).ParentName));
if IntfDesc <> nil then
GenParentDesc := FindTypeOrInstantiate(TClassTypeDef(TD.Def).ParentName);
if (GenParentDesc <> nil) and
(GenParentDesc is TInterfaceTypeDesc) then
begin
{ Treat it as an interface to implement — move to implements list }
{ Generic interface — move to the implements list. }
TClassTypeDef(TD.Def).ImplementsNames.Insert(
0, TClassTypeDef(TD.Def).ParentName);
TClassTypeDef(TD.Def).ParentName := '';
end;
{ Otherwise (generic class instance, or unresolved) leave it as the
ParentName for the class-parent resolution below. }
end;
if TClassTypeDef(TD.Def).ParentName <> '' then
begin

View file

@ -41,6 +41,10 @@ type
rejected as shadowing a visible type. }
procedure TestRun_GenericClass_LocalNamedLikeTypeParam;
procedure TestRun_GenericRecord_LocalNamedLikeTypeParam;
{ Non-generic class inheriting from a generic-class instance
(class(TBox<Integer>)) — parent classification + symbol mangling. }
procedure TestRun_InheritFromGenericInstance_MethodAndField;
procedure TestRun_InheritFromGenericInstance_VirtualOverride;
end;
implementation
@ -240,6 +244,56 @@ begin
AssertRunsOnAll(SrcRecordLocalLikeParam, '55' + LE, 0);
end;
const
SrcInheritGenericMethodField = '''
program P;
type
TBox<T> = class
FVal: T;
procedure SetIt(v: T); begin FVal := v; end;
function GetIt: T; begin Result := FVal; end;
end;
TIntBox = class(TBox<Integer>) end;
var b: TIntBox;
begin
b := TIntBox.Create;
b.SetIt(7);
WriteLn(b.GetIt());
WriteLn(b.FVal);
b := nil
end.
''';
SrcInheritGenericVirtual = '''
program P;
type
TBase<T> = class
function Describe: string; virtual; begin Result := 'base' end;
function Wrap: string; begin Result := '[' + Self.Describe() + ']' end;
end;
TIntD = class(TBase<Integer>)
function Describe: string; override; begin Result := 'derived' end;
end;
var d: TBase<Integer>;
begin
d := TIntD.Create;
WriteLn(d.Wrap());
d := nil
end.
''';
procedure TE2EGenericsTests.TestRun_InheritFromGenericInstance_MethodAndField;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcInheritGenericMethodField, '7' + LE + '7' + LE, 0);
end;
procedure TE2EGenericsTests.TestRun_InheritFromGenericInstance_VirtualOverride;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcInheritGenericVirtual, '[derived]' + LE, 0);
end;
initialization
RegisterTest(TE2EGenericsTests);

View file

@ -2461,6 +2461,15 @@ The codegen emits itab and impllist data for generic class instances in
Both class and interface names in itab symbols are passed through `QBEMangle`
to produce valid QBE identifiers (e.g. `$itab_TDictionary_Integer_Integer_IMap_Integer_Integer`).
A non-generic class may also *inherit* from a generic-class instance — e.g.
`TIntBox = class(TBox<Integer>)`. The first heritage entry is instantiated and
then classified: a `tyInterface` result becomes an implements entry (above),
any other (a class instance) is the parent class. Because the generic instance
is emitted under its `QBEMangle`'d symbol (`typeinfo_TBox_Integer`), the
inheriting class's typeinfo must reference the parent through `QBEMangle` too,
not the unmangled name — otherwise the emitted QBE identifier contains the
illegal `<`/`>` characters.
=== Alternatives Rejected
* *Structural duck-typing without interfaces* — Blaise already uses