feat(generics): generic methods (method-level type parameters)

A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:

    type TUtil = class
      function Pick<T>(cond: Boolean; a, b: T): T;
        begin if cond then Result := a else Result := b end;
    end;
    ...
    u.Pick<Integer>(True, 7, 9);     // -> 7
    u.Pick<string>(False, 'a', 'b'); // -> 'b'

Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.

Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
  method name (Pick<Integer>), using the same two-token '<' lookahead as
  generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
  by Owner.Method and skipped from signature/vtable/body analysis;
  InstantiateGenericMethod clones the template, substitutes the type
  params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
  records a TGenericMethodInstance (deduplicated per owner+args). The
  call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
  methods (Self param + mangled ResolvedQbeName).

Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.

Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line  function TOwner.Method<T>(...)  form is not
yet parsed.
This commit is contained in:
Graeme Geldenhuys 2026-06-16 16:13:12 +01:00
parent 42ca9d7535
commit 3490a03aa7
9 changed files with 481 additions and 6 deletions

View file

@ -240,7 +240,8 @@ type
the supplied generic-instance lists. }
procedure EmitClassMethods(ATypeDecls: TObjectList;
AGenericInstances: TObjectList;
AGenericRecordInstances: TObjectList);
AGenericRecordInstances: TObjectList;
AGenericMethodInstances: TObjectList);
{ Resolve a class/interface name to its assembly symbol form, adding the
unit prefix when the type is defined in a non-system unit. Mirrors the
@ -3162,7 +3163,8 @@ end;
procedure TX86_64Backend.EmitClassMethods(ATypeDecls: TObjectList;
AGenericInstances: TObjectList;
AGenericRecordInstances: TObjectList);
AGenericRecordInstances: TObjectList;
AGenericMethodInstances: TObjectList);
var
I, J: Integer;
TD: TTypeDecl;
@ -3170,6 +3172,7 @@ var
RD: TRecordTypeDef;
GI: TGenericInstance;
GRI: TGenericRecordInstance;
GMI: TGenericMethodInstance;
Decl: TMethodDecl;
SavedUnit: string;
begin
@ -3183,6 +3186,8 @@ begin
begin
Decl := TMethodDecl(CD.Methods.Items[J]);
if Decl.Body = nil then Continue;
{ Generic-method templates emit per instantiation, not as a template. }
if Decl.TypeParams <> nil then Continue;
Self.EmitFunctionDef(Decl);
end;
end
@ -3193,6 +3198,7 @@ begin
begin
Decl := TMethodDecl(RD.Methods.Items[J]);
if Decl.Body = nil then Continue;
if Decl.TypeParams <> nil then Continue;
Self.EmitFunctionDef(Decl);
end;
end;
@ -3228,6 +3234,16 @@ begin
Self.EmitFunctionDef(Decl);
end;
end;
{ Generic METHOD instances (method-level <T>): each monomorphised body. Its
ResolvedQbeName encodes <Owner>_<Method><args> and OwnerTypeName is set, so
EmitFunctionDef emits it with the implicit Self like any method. }
for I := 0 to AGenericMethodInstances.Count - 1 do
begin
GMI := TGenericMethodInstance(AGenericMethodInstances.Items[I]);
if GMI.MethodDecl.Body <> nil then
Self.EmitFunctionDef(GMI.MethodDecl);
end;
end;
{ ------------------------------------------------------------------ }
@ -14169,7 +14185,7 @@ begin
{ Class method bodies before standalone procedures. }
Self.EmitClassMethods(AProg.Block.TypeDecls, AProg.GenericInstances,
AProg.GenericRecordInstances);
AProg.GenericRecordInstances, AProg.GenericMethodInstances);
{ Array-typed constants emit data sections so const arrays are defined
as assembly labels before being referenced in code. Covers block-level
@ -14313,7 +14329,7 @@ begin
generic class/record instances declared in this unit. After
LinkClassMethodImpls the definition's TMethodDecl nodes hold the bodies. }
Self.EmitClassMethods(AUnit.IntfBlock.TypeDecls, AUnit.GenericInstances,
AUnit.GenericRecordInstances);
AUnit.GenericRecordInstances, AUnit.GenericMethodInstances);
{ Array-typed constants from both interface and implementation blocks. }
Self.EmitArrayConstData(AUnit.IntfBlock, '');

View file

@ -7743,6 +7743,7 @@ var
RD: TRecordTypeDef;
GI: TGenericInstance;
GRI: TGenericRecordInstance;
GMI: TGenericMethodInstance;
MDecl: TMethodDecl;
Methods: TObjectList;
SavedUnit: string;
@ -7763,7 +7764,10 @@ begin
else
Continue;
for J := 0 to Methods.Count - 1 do
if TMethodDecl(Methods.Items[J]).Body <> nil then
if (TMethodDecl(Methods.Items[J]).Body <> nil) and
{ Generic-method templates (method-level <T>) are emitted per
instantiation via GenericMethodInstances, not as a template body. }
(TMethodDecl(Methods.Items[J]).TypeParams = nil) then
EmitMethodDef(TD.Name, TMethodDecl(Methods.Items[J]));
end;
@ -7799,6 +7803,16 @@ begin
EmitMethodDef(QBEMangle(GRI.TypeName), MDecl);
end;
end;
{ Generic METHOD instances (method-level <T>) emit each monomorphised body.
Its ResolvedQbeName already encodes <Owner>_<Method><args>, so EmitMethodDef
uses that label and adds the implicit Self param. }
for I := 0 to AProg.GenericMethodInstances.Count - 1 do
begin
GMI := TGenericMethodInstance(AProg.GenericMethodInstances.Items[I]);
if GMI.MethodDecl.Body <> nil then
EmitMethodDef(GMI.OwnerType, GMI.MethodDecl);
end;
end;
procedure TCodeGenQBE.EmitFuncDef(ADecl: TMethodDecl; AExported: Boolean);

View file

@ -894,6 +894,19 @@ type
destructor Destroy; override;
end;
{ A monomorphised generic METHOD instance (method-level <T>): a method that
declares its own type parameters, instantiated at a call site. OwnerType
is the class/record the method belongs to; the instance keeps an implicit
Self and is emitted as OwnerType_Method_<args>. }
TGenericMethodInstance = class
public
OwnerType: string; { the class/record name }
InstName: string; { method name with type args, e.g. 'Pick<Integer>' }
MethodDecl: TMethodDecl; { owned — concrete method decl with substituted types }
constructor Create;
destructor Destroy; override;
end;
{ One concrete instantiation produced by monomorphization stored on TProgram.
Codegen iterates this list to emit typeinfo, vtables, and method bodies. }
TGenericInstance = class
@ -977,6 +990,7 @@ type
SymbolTable: TSymbolTable; { owned after semantic analysis; nil before }
GenericInstances: TObjectList; { owned TGenericInstance — populated by uSemantic }
GenericFuncInstances: TObjectList; { owned TGenericFuncInstance — populated by uSemantic }
GenericMethodInstances: TObjectList; { owned TGenericMethodInstance — populated by uSemantic }
GenericIntfInstances: TObjectList; { owned TGenericInterfaceInstance — populated by uSemantic }
GenericRecordInstances: TObjectList; { owned TGenericRecordInstance — populated by uSemantic }
constructor Create;
@ -999,6 +1013,7 @@ type
program (program owns the table). }
GenericInstances: TObjectList;
GenericFuncInstances: TObjectList;
GenericMethodInstances: TObjectList;
GenericIntfInstances: TObjectList;
GenericRecordInstances: TObjectList;
constructor Create;
@ -1535,6 +1550,17 @@ begin
inherited Destroy();
end;
constructor TGenericMethodInstance.Create;
begin
inherited Create();
end;
destructor TGenericMethodInstance.Destroy;
begin
{ Owned class fields released by ARC field cleanup. }
inherited Destroy();
end;
{ TGenericInterfaceDef }
constructor TGenericInterfaceDef.Create;
@ -1692,6 +1718,7 @@ begin
UsedUnits := TStringList.Create();
GenericInstances := TObjectList.Create(True);
GenericFuncInstances := TObjectList.Create(True);
GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True);
end;
@ -1721,6 +1748,7 @@ begin
FinalStmts := nil;
GenericInstances := TObjectList.Create(True);
GenericFuncInstances := TObjectList.Create(True);
GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True);
end;

View file

@ -4030,6 +4030,23 @@ begin
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
SecondName := FCurrent.Value;
Advance();
{ Generic-method call: IDENT '.' IDENT '<' TypeArgs '>' '(' ... ')'.
Fold the explicit type arguments into the method name
(Pick<Integer>), mirroring the generic free-function call site; the
semantic pass instantiates on the '<' in the name. }
if Check(tkLessThan) and (PeekKind() = tkIdent) and
(PeekKind2() in [tkGreaterThan, tkComma, tkLessThan]) then
begin
Advance(); { consume '<' }
SecondName := SecondName + '<' + ParseTypeName();
while Check(tkComma) do
begin
Advance();
SecondName := SecondName + ',' + ParseTypeName();
end;
Expect(tkGreaterThan);
SecondName := SecondName + '>';
end;
if Check(tkLParen) then
begin
{ IDENT '.' IDENT '(' ... ')' — method call expression }

View file

@ -50,6 +50,7 @@ type
lifetime; the destructor frees this list, releasing the groups. }
FGroupKeepAlive: TObjectList;
FGenericFuncTemplates: TStringList; { base name → TMethodDecl template (not owned) }
FGenericMethodTemplates: TStringList; { 'OwnerType.Method' → TMethodDecl template (not owned) — generic methods with method-level <T> }
FLoopDepth: Integer; { depth of enclosing while/for — Break only legal if > 0 }
FScopeDepth: Integer; { mirrors FTable scope depth; used to detect main-level globals }
FCurrentClass: TRecordTypeDesc; { class being analysed (set in AnalyseMethodDecl) }
@ -131,6 +132,10 @@ type
{ Generic function instantiation: resolves 'Identity<Integer>' on demand. }
function InstantiateGenericFunc(const AInstName: string): TMethodDecl;
{ Generic method instantiation: resolves 'Pick<Integer>' on an owner class
on demand, preserving the implicit Self. Returns the monomorphised
TMethodDecl (with ResolvedQbeName set), or nil if not a generic method. }
function InstantiateGenericMethod(const AOwnerType, AInstName: string): TMethodDecl;
procedure AnalyseBlock(ABlock: TBlock; AIsProgramTop: Boolean = False);
procedure AnalyseConstDecls(ABlock: TBlock);
@ -468,6 +473,8 @@ begin
FGroupKeepAlive := TObjectList.Create(False);
FGenericFuncTemplates := TStringList.Create();
FGenericFuncTemplates.CaseSensitive := False;
FGenericMethodTemplates := TStringList.Create();
FGenericMethodTemplates.CaseSensitive := False;
FCurrentUsesChain := TStringList.Create();
FCurrentUsesChain.CaseSensitive := False;
FUnitIfaces := TStringList.Create();
@ -486,6 +493,7 @@ begin
FUnitIfaces.Free();
FCurrentUsesChain.Free();
FGenericFuncTemplates.Free();
FGenericMethodTemplates.Free();
{ Releasing the keep-alive releases every group list; the group string
lists themselves only hold raw pointers. }
FGroupKeepAlive.Free();
@ -3050,6 +3058,165 @@ begin
end;
end;
function TSemanticAnalyser.InstantiateGenericMethod(
const AOwnerType, AInstName: string): TMethodDecl;
{ AInstName is the method name with type args, e.g. 'Pick<Integer>'. Mirrors
InstantiateGenericFunc but preserves the implicit Self: the instance is
analysed as a method of AOwnerType and emitted as <Owner>_<Inst>. }
var
BaseName: string;
ArgsStr: string;
TemplIdx: Integer;
BracPos: Integer;
Templ: TMethodDecl;
Args: TStringList;
NewMDecl: TMethodDecl;
NewPar: TMethodParam;
OldPar: TMethodParam;
ParTypeName: string;
RetTypeName: string;
SubstType: TTypeDesc;
OwnerSym: TSymbol;
OwnerRT: TRecordTypeDesc;
GMI: TGenericMethodInstance;
I, J: Integer;
begin
Result := nil;
BracPos := StrPos('<', AInstName);
if BracPos < 0 then Exit;
BaseName := StrHead(AInstName, BracPos);
ArgsStr := StrCopyFrom(AInstName, BracPos + 1, Length(AInstName) - BracPos - 2);
TemplIdx := FGenericMethodTemplates.IndexOf(AOwnerType + '.' + BaseName);
if TemplIdx < 0 then Exit; { not a known generic method on this owner }
Templ := TMethodDecl(FGenericMethodTemplates.Objects[TemplIdx]);
OwnerSym := FTable.Lookup(AOwnerType);
if (OwnerSym = nil) or not (OwnerSym.TypeDesc is TRecordTypeDesc) then Exit;
OwnerRT := TRecordTypeDesc(OwnerSym.TypeDesc);
{ Idempotent: a second call site with the same type args reuses the existing
instance rather than emitting a duplicate symbol. }
if FCurrentUnit <> nil then
begin
for I := 0 to FCurrentUnit.GenericMethodInstances.Count - 1 do
begin
GMI := TGenericMethodInstance(FCurrentUnit.GenericMethodInstances.Items[I]);
if SameText(GMI.OwnerType, AOwnerType) and SameText(GMI.InstName, AInstName) then
Exit(GMI.MethodDecl);
end;
end
else
for I := 0 to FProg.GenericMethodInstances.Count - 1 do
begin
GMI := TGenericMethodInstance(FProg.GenericMethodInstances.Items[I]);
if SameText(GMI.OwnerType, AOwnerType) and SameText(GMI.InstName, AInstName) then
Exit(GMI.MethodDecl);
end;
Args := TStringList.Create();
try
while Length(ArgsStr) > 0 do
begin
BracPos := StrPos(',', ArgsStr);
if BracPos >= 0 then
begin
Args.Add(Trim(StrHead(ArgsStr, BracPos)));
ArgsStr := Trim(StrCopyTail(ArgsStr, BracPos + 1));
end
else
begin
Args.Add(Trim(ArgsStr));
ArgsStr := '';
end;
end;
if Args.Count <> Templ.TypeParams.Count then
SemanticError(
Format('Generic method ''%s.%s'' expects %d type parameter(s) but got %d',
[AOwnerType, BaseName, Templ.TypeParams.Count, Args.Count]),
0, 0);
for I := 0 to Args.Count - 1 do
if (Templ.TypeParamConstraints <> nil) and
(I < Templ.TypeParamConstraints.Count) then
CheckTypeParamConstraint(Templ.TypeParams.Strings[I], Args.Strings[I],
Templ.TypeParamConstraints.Strings[I],
Format('generic method ''%s.%s''', [AOwnerType, BaseName]));
NewMDecl := TMethodDecl.Create();
NewMDecl.Name := AInstName;
NewMDecl.OwnerTypeName := AOwnerType;
NewMDecl.IsRecordMethod := Templ.IsRecordMethod;
{ Mangled symbol: <unit><Owner>_<Method><args> -> QBEMangle drops the <>
and joins with '_', e.g. TUtil_Pick_Integer. }
NewMDecl.ResolvedQbeName := CurrentUnitPrefix() + AOwnerType + '_' + AInstName;
if Templ.Body <> nil then
begin
NewMDecl.Body := CloneBlock(Templ.Body);
NewMDecl.OwnBody := True;
for I := 0 to NewMDecl.Body.Decls.Count - 1 do
TVarDecl(NewMDecl.Body.Decls.Items[I]).TypeName :=
Self.SubstTypeParam(TVarDecl(NewMDecl.Body.Decls.Items[I]).TypeName,
Templ.TypeParams, Args);
end
else
begin
NewMDecl.Body := nil;
NewMDecl.OwnBody := False;
end;
RetTypeName := Templ.ReturnTypeName;
for I := 0 to Templ.TypeParams.Count - 1 do
if SameText(RetTypeName, Templ.TypeParams.Strings[I]) then
RetTypeName := Args.Strings[I];
NewMDecl.ReturnTypeName := RetTypeName;
if RetTypeName <> '' then
begin
SubstType := FindTypeOrInstantiate(RetTypeName);
if SubstType = nil then
SemanticError(Format('Unknown type ''%s'' in generic method instance ''%s.%s''',
[RetTypeName, AOwnerType, AInstName]), 0, 0);
NewMDecl.ResolvedReturnType := SubstType;
end;
for I := 0 to Templ.Params.Count - 1 do
begin
OldPar := TMethodParam(Templ.Params.Items[I]);
NewPar := TMethodParam.Create();
NewPar.ParamName := OldPar.ParamName;
NewPar.IsVarParam := OldPar.IsVarParam;
ParTypeName := OldPar.TypeName;
for J := 0 to Templ.TypeParams.Count - 1 do
if SameText(ParTypeName, Templ.TypeParams.Strings[J]) then
ParTypeName := Args.Strings[J];
NewPar.TypeName := ParTypeName;
SubstType := FindTypeOrInstantiate(ParTypeName);
if SubstType = nil then
SemanticError(Format('Unknown type ''%s'' for parameter ''%s'' in ''%s.%s''',
[ParTypeName, NewPar.ParamName, AOwnerType, AInstName]), 0, 0);
NewPar.ResolvedType := SubstType;
NewMDecl.Params.Add(NewPar);
end;
{ Analyse the body with Self (the owner) and concrete types in scope. }
AnalyseMethodDecl(NewMDecl, OwnerRT);
GMI := TGenericMethodInstance.Create();
GMI.OwnerType := AOwnerType;
GMI.InstName := AInstName;
GMI.MethodDecl := NewMDecl;
if FCurrentUnit <> nil then
FCurrentUnit.GenericMethodInstances.Add(GMI)
else
FProg.GenericMethodInstances.Add(GMI);
Result := NewMDecl;
finally
Args.Free();
end;
end;
procedure TSemanticAnalyser.AnalyseBlock(ABlock: TBlock; AIsProgramTop: Boolean = False);
var
I: Integer;
@ -4054,6 +4221,16 @@ begin
for J := 0 to MethodList.Count - 1 do
begin
MDecl := TMethodDecl(MethodList.Items[J]);
{ Generic method (method-level <T>): params/return reference the
method's own type params, unknown until a call site instantiates it.
Register the template keyed by OwnerType.Method and skip resolution. }
if MDecl.TypeParams <> nil then
begin
MDecl.OwnerTypeName := RT.Name;
if FGenericMethodTemplates.IndexOf(RT.Name + '.' + MDecl.Name) < 0 then
FGenericMethodTemplates.AddObject(RT.Name + '.' + MDecl.Name, MDecl);
Continue;
end;
for K := 0 to MDecl.Params.Count - 1 do
begin
Par := TMethodParam(MDecl.Params.Items[K]);
@ -4079,6 +4256,9 @@ begin
for J := 0 to MethodList.Count - 1 do
begin
MDecl := TMethodDecl(MethodList.Items[J]);
{ Generic-method templates take no vtable slot they cannot be
virtual (each instantiation is a distinct monomorphised body). }
if MDecl.TypeParams <> nil then Continue;
MangledKey := MDecl.Name;
if MDecl.IsOverload then
MangledKey := MangledKey + '$' + MangleParamSig(MDecl);
@ -4453,7 +4633,21 @@ var
Par: TMethodParam;
Sym: TSymbol;
SavedClass: TRecordTypeDesc;
TKey: string;
begin
{ Generic method (method-level <T>): its params/body reference the method's
own type parameters, which are not in scope until a call site instantiates
it. Register the template keyed by OwnerType.Method and defer analysis to
InstantiateGenericMethod. (A method of a generic CLASS uses OwnerTypeParams,
not TypeParams, and is NOT skipped here.) }
if AMethod.TypeParams <> nil then
begin
TKey := AClassType.Name + '.' + AMethod.Name;
if FGenericMethodTemplates.IndexOf(TKey) < 0 then
FGenericMethodTemplates.AddObject(TKey, AMethod);
AMethod.OwnerTypeName := AClassType.Name;
Exit;
end;
SavedClass := FCurrentClass;
FCurrentClass := AClassType;
FTable.PushScope();
@ -8850,6 +9044,26 @@ begin
Fall back to a plain chain lookup for the no-overload / single case. }
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args.Items[I]));
{ Generic method call obj.Pick<Integer>(...): the parser folded the explicit
type args into AExpr.Name. Instantiate the monomorphised method and use it
directly (it is not part of the overload set). }
if StrPos('<', AExpr.Name) >= 0 then
begin
MDecl := InstantiateGenericMethod(RT.Name, AExpr.Name);
if MDecl = nil then
SemanticError(Format('Class ''%s'' has no generic method ''%s''',
[RT.Name, AExpr.Name]), AExpr.Line, AExpr.Col);
AExpr.ResolvedClassType := RT;
AExpr.ResolvedMethod := MDecl;
AExpr.IsGlobal := ObjSym.IsGlobal;
AExpr.IsVarParam :=
(ObjSym.Kind = skVarParameter) or
((ObjSym.Kind = skParameter) and (ObjSym.TypeDesc <> nil) and
(ObjSym.TypeDesc.Kind in [tyRecord, tyStaticArray]));
Result := MDecl.ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
MDecl := ResolveMethodOverload(RT.Name, AExpr.Name, AExpr.Args,
AExpr.Line, AExpr.Col);
if MDecl = nil then

View file

@ -49,6 +49,12 @@ type
interface class(IVal) on a generic template must wire AddImplements. }
procedure TestRun_GenericClassImplementsInterface;
procedure TestRun_GenericClassImplementsInterface_MethodArgs;
{ Generic METHODS (method-level <T>): a method declaring its own type
parameter, instantiated per call site (obj.M<Integer>(...)). }
procedure TestRun_GenericMethod_Pick;
procedure TestRun_GenericMethod_TwoInstantiations;
procedure TestRun_GenericMethod_UsesSelfField;
procedure TestRun_GenericMethod_TwoTypeParams;
end;
implementation
@ -343,6 +349,93 @@ begin
AssertRunsOnAll(SrcGenericImplementsIntfArgs, '42' + LE, 0);
end;
const
SrcGenMethodPick = '''
program Prog;
type
TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T;
begin if cond then Result := a else Result := b end;
end;
var u: TUtil;
begin
u := TUtil.Create;
WriteLn(u.Pick<Integer>(True, 7, 9));
WriteLn(u.Pick<Integer>(False, 7, 9));
u := nil
end.
''';
SrcGenMethodTwo = '''
program Prog;
type
TUtil = class
function Echo<T>(x: T): T; begin Result := x end;
end;
var u: TUtil;
begin
u := TUtil.Create;
WriteLn(u.Echo<Integer>(42));
WriteLn(u.Echo<string>('hi'));
u := nil
end.
''';
SrcGenMethodSelf = '''
program Prog;
type
TBox = class
FBase: Integer;
constructor Create(b: Integer); begin FBase := b end;
function Combine<T>(x: T): T; begin Result := x end;
function Offset: Integer; begin Result := FBase end;
end;
var b: TBox;
begin
b := TBox.Create(100);
WriteLn(b.Combine<Integer>(5) + b.Offset());
b := nil
end.
''';
SrcGenMethodTwoParams = '''
program Prog;
type
TUtil = class
function First<A, B>(x: A; y: B): A; begin Result := x end;
end;
var u: TUtil;
begin
u := TUtil.Create;
WriteLn(u.First<Integer, string>(7, 'ignored'));
u := nil
end.
''';
procedure TE2EGenericsTests.TestRun_GenericMethod_Pick;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcGenMethodPick, '7' + LE + '9' + LE, 0);
end;
procedure TE2EGenericsTests.TestRun_GenericMethod_TwoInstantiations;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcGenMethodTwo, '42' + LE + 'hi' + LE, 0);
end;
procedure TE2EGenericsTests.TestRun_GenericMethod_UsesSelfField;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcGenMethodSelf, '105' + LE, 0);
end;
procedure TE2EGenericsTests.TestRun_GenericMethod_TwoTypeParams;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcGenMethodTwoParams, '7' + LE, 0);
end;
initialization
RegisterTest(TE2EGenericsTests);

View file

@ -53,6 +53,9 @@ type
{ ------------------------------------------------------------------ }
procedure TestCodegen_GenericFunc_BodyEmitted;
procedure TestCodegen_GenericFunc_CallEmitted;
{ Generic METHOD (method-level <T>) — monomorphised body + call site. }
procedure TestCodegen_GenericMethod_BodyEmitted;
procedure TestCodegen_GenericMethod_CallEmitted;
end;
implementation
@ -382,6 +385,36 @@ begin
Pos('call $Identity_Integer', IR) > 0);
end;
const
SrcGenericMethodUsage =
'''
program Prog;
type
TUtil = class
function Echo<T>(x: T): T; begin Result := x end;
end;
var u: TUtil; r: Integer;
begin u := TUtil.Create(); r := u.Echo<Integer>(42); WriteLn(r) end.
''';
procedure TGenericFuncTests.TestCodegen_GenericMethod_BodyEmitted;
var
IR: string;
begin
IR := GenIR(SrcGenericMethodUsage);
AssertTrue('generic-method body emitted with mangled owner_method_type name',
Pos('$TUtil_Echo_Integer', IR) > 0);
end;
procedure TGenericFuncTests.TestCodegen_GenericMethod_CallEmitted;
var
IR: string;
begin
IR := GenIR(SrcGenericMethodUsage);
AssertTrue('generic-method call emitted with mangled name',
Pos('call $TUtil_Echo_Integer', IR) > 0);
end;
initialization
RegisterTest(TGenericFuncTests);
end.

View file

@ -861,9 +861,20 @@ FieldAssignment
;
MethodCall
= IDENT DOT IDENT LPAREN [ ArgList ] RPAREN
= IDENT DOT IDENT
[ LESS TypeArgList GREATER ] (* explicit args for a generic method *)
LPAREN [ ArgList ] RPAREN
;
(* Generic method call: Obj.Method<Integer>(args). The optional
* LESS TypeArgList GREATER supplies explicit type arguments for a method that
* declares its own type parameters (MethodDecl's [ LESS TypeParamList GREATER ]).
* The parser folds the type args into the method name (Method<Integer>) and the
* semantic pass monomorphises the method on first use. The leading-'<' is
* disambiguated from a comparison by the same two-token lookahead used for
* generic free-function calls. Only the INLINE-body method form is supported;
* the out-of-line impl function TOwner.Method<T>(...) is not yet parsed. *)
(* Semantic disambiguation of IDENT DOT IDENT in Stmt:
* - Followed by ASSIGN or LBRACKET ASSIGN FieldAssignment (plain or indexed)
* - Otherwise MethodCall

View file

@ -3417,6 +3417,55 @@ inside generic records are handled by the existing record-cleanup logic at
variable scope exit, not by class-style field cleanup.
== Generic Methods
=== Decision
A class or record method may declare its *own* type parameters, independent of
any type parameters on the enclosing type:
[source,pascal]
----
type
TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T;
begin if cond then Result := a else Result := b end;
end;
...
u.Pick<Integer>(True, 7, 9); // -> 7
u.Pick<string>(False, 'a', 'b'); // -> 'b'
----
The method is *monomorphised* on first use: each distinct set of explicit type
arguments at a call site produces one concrete method body, named
`<Owner>_<Method>_<Args>` (e.g. `TUtil_Pick_Integer`). The implicit `Self` is
preserved, so a generic method may read the receiver's fields and call its
other methods. Type arguments are explicit at the call site
(`u.Pick<Integer>(...)`); they are not inferred from the value arguments.
This is distinct from a method of a generic *class* (`TBox<T>` and its
`function Get: T`), where the type parameter `T` is bound by the class
instantiation, not the method. The two compose: a generic class may have a
method with its own additional type parameters.
=== Rationale
Generic methods reuse the existing generic free-function machinery: the same
on-demand instantiation, the same `<T>` type-parameter scoping during body
analysis, and the same `QBEMangle`/`NativeMangle` name mangling (`<`/`,` ->
`_`, `>` dropped). The only addition over a free function is carrying the
owner type and the implicit `Self` through to codegen, so the instance emits as
an ordinary method. Monomorphisation (rather than type erasure) keeps the
model uniform with generic classes and records and needs no runtime type
descriptors.
=== Current limitation
Only the *inline-body* declaration form is supported (the body written inside
the class). The out-of-line implementation form
`function TUtil.Pick<T>(...)` with the body separated from the class is not yet
parsed.
== Platform constants — one name per concept
Delphi and FPC carry multiple aliases for the same platform constant: