feat(semantic): allow unqualified calls to a procedural-typed field

Inside a method, an unqualified FFn(args) -- no 'Self.' prefix -- where
FFn is a procedural-typed field of the current class failed with
"Undeclared function/procedure 'FFn'".  Only the explicit Self.FFn(args)
form resolved.

Both call analysers now fall through to an implicit-Self field lookup
after the method lookup fails: a procedural-typed field of the current
class is dispatched through its stored pointer, validating argument count
and types (and the l-value requirement for var/out arguments) against the
field signature, exactly like the explicit-receiver path.

Codegen:
  * QBE: a shared EmitImplicitSelfProcFieldCall loads Self, indexes the
    field, and dispatches; var/out arguments are passed by reference and
    'of object' fields pass the captured Data as the implicit first arg.
    Used by both the statement and expression forms.
  * Native x86-64: both forms reuse EmitProcFieldCall with Self as the
    receiver.

The two new AST flags (TProcCall.IsProcFieldCall,
TFuncCallExpr.IsProcFieldCall) are recorded in the bif-coverage status
inventory as semantic-only (safe, not serialised), matching the existing
IsIndirectCall flags.

Covered by an IR test (loads Self, indirect dispatch) and end-to-end
tests on both backends for the expression and statement forms.
This commit is contained in:
Andrew Haines 2026-06-24 15:48:36 -04:00 committed by Graeme Geldenhuys
parent aa33103659
commit 6eea55bc59
7 changed files with 283 additions and 0 deletions

View file

@ -6365,6 +6365,14 @@ begin
Self.Emit(#9'movq %xmm0, %rax');
Exit;
end;
{ Unqualified call to a procedural-typed field of the current class
(implicit Self.Field) used as an expression. }
if FC.IsProcFieldCall then
begin
Self.EmitProcFieldCall(nil, 'Self', False, FC.ProcFieldInfo,
TProceduralTypeDesc(FC.ResolvedProcType), FC.Args, FC.ResolvedType);
Exit;
end;
if FC.IsIndirectCall then
begin
{ Bare function-pointer call: load the pointer from the variable slot
@ -11549,6 +11557,14 @@ begin
Self.Emit(#9'callq _ProcessFree');
Exit;
end;
{ Unqualified call to a procedural-typed field of the current class
(implicit Self.Field) used as a statement. }
if PC.IsProcFieldCall then
begin
Self.EmitProcFieldCall(nil, 'Self', False, PC.ProcFieldInfo,
TProceduralTypeDesc(PC.ResolvedProcType), PC.Args, nil);
Exit;
end;
if PC.IsIndirectCall then
begin
if (PC.ResolvedProcType <> nil) and

View file

@ -326,6 +326,13 @@ type
function EmitInheritedCallExpr(ACall: TInheritedCallExpr): string;
procedure EmitCaseStmt(AStmt: TCaseStmt);
procedure EmitProcCall(ACall: TProcCall);
{ Dispatch a call through a procedural-typed field of the current class
reached via implicit Self (an unqualified FFn(args) inside a method).
Loads Self, indexes the field, and calls through the stored pointer;
var/out arguments are passed by reference. Returns the QBE result
temp, or '' for a procedure (no return value). }
function EmitImplicitSelfProcFieldCall(AFieldInfo: TFieldInfo;
APT: TProceduralTypeDesc; AArgs: TObjectList): string;
{ Helper for string-mutator built-ins (Delete, SetLength).
ARtlName is the RTL function (e.g. '_StringDelete') and
AExtraArgCount is the number of trailing Integer args after the
@ -8615,6 +8622,74 @@ begin
TGenericFuncInstance(AProg.GenericFuncInstances.Items[I]).MethodDecl);
end;
function TCodeGenQBE.EmitImplicitSelfProcFieldCall(AFieldInfo: TFieldInfo;
APT: TProceduralTypeDesc; AArgs: TObjectList): string;
var
SelfTemp: string;
SlotAddr: string;
FPtrTemp: string;
ArgTemp: string;
DataTemp: string;
ArgLine: string;
QType: string;
T: string;
I: Integer;
begin
{ Self pointer from the implicit Self parameter slot. }
SelfTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %%_var_Self', [SelfTemp]));
if AFieldInfo.Offset > 0 then
begin
SlotAddr := AllocTemp();
EmitLine(Format(' %s =l add %s, %d', [SlotAddr, SelfTemp, AFieldInfo.Offset]));
end
else
SlotAddr := SelfTemp;
FPtrTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [FPtrTemp, SlotAddr]));
{ 'of object' field: Data (Self) sits at +8 and becomes the first arg. }
if APT.IsMethodPtr then
begin
ArgTemp := AllocTemp();
EmitLine(Format(' %s =l add %s, 8', [ArgTemp, SlotAddr]));
DataTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [DataTemp, ArgTemp]));
ArgLine := Format('l %s', [DataTemp]);
end
else
ArgLine := '';
for I := 0 to AArgs.Count - 1 do
begin
if ArgLine <> '' then ArgLine := ArgLine + ', ';
{ var/out parameters pass the argument's l-value address by reference. }
if TProcParamInfo(APT.Params.Items[I]).IsVarParam then
begin
ArgTemp := EmitLValueAddr(TASTExpr(AArgs.Items[I]));
ArgLine := ArgLine + Format('l %s', [ArgTemp]);
end
else
begin
ArgTemp := EmitExpr(TASTExpr(AArgs.Items[I]));
QType := QbeTypeOf(TProcParamInfo(APT.Params.Items[I]).TypeDesc);
ArgTemp := CoerceArg(ArgTemp, TASTExpr(AArgs.Items[I]), QType);
ArgLine := ArgLine + Format('%s %s',
[QbeParamTypeOf(TProcParamInfo(APT.Params.Items[I]).TypeDesc), ArgTemp]);
end;
end;
if APT.ReturnType <> nil then
begin
QType := QbeTypeOf(APT.ReturnType);
T := AllocTemp();
EmitLine(Format(' %s =%s call %s(%s)', [T, QType, FPtrTemp, ArgLine]));
Result := T;
end
else
begin
EmitLine(Format(' call %s(%s)', [FPtrTemp, ArgLine]));
Result := '';
end;
end;
procedure TCodeGenQBE.EmitProcCall(ACall: TProcCall);
var
UCaseName: string;
@ -8635,6 +8710,15 @@ var
CallTgt: string;
begin
PMark := PendingReleaseMark();
{ Unqualified call to a procedural-typed field of the current class
(implicit Self.Field) used as a statement. }
if ACall.IsProcFieldCall then
begin
EmitImplicitSelfProcFieldCall(ACall.ProcFieldInfo,
TProceduralTypeDesc(ACall.ResolvedProcType), ACall.Args);
FlushPendingReleases(PMark);
Exit;
end;
{ Indirect call through a procedural-typed variable: load the function
pointer from the variable and call through it. For 'of object' types
the variable's slot is a 16-byte (Code, Data) block; load both halves
@ -10444,6 +10528,12 @@ begin
Exit(T);
end;
{ Unqualified call to a procedural-typed field of the current class
(implicit Self.Field) used as an expression. }
if FC.IsProcFieldCall then
Exit(EmitImplicitSelfProcFieldCall(FC.ProcFieldInfo,
TProceduralTypeDesc(FC.ResolvedProcType), FC.Args));
{ Indirect call through a procedural-typed variable: F() / F(args)
where F is declared 'var F: TProcType'. Load the function pointer
from F and call through it. Must precede the ResolvedDecl=nil

View file

@ -466,6 +466,10 @@ type
IsIndirectCall: Boolean; { set by uSemantic — Name is a procedural-typed variable }
IndirectCallIsGlobal: Boolean; { set by uSemantic — when IsIndirectCall, variable is global }
[Unretained] ResolvedProcType: TObject; { TProceduralTypeDesc — not owned; valid when IsIndirectCall }
IsProcFieldCall: Boolean; { set by uSemantic unqualified Name is a
procedural-typed field of the current class
(implicit Self.Field), called as a statement }
[Unretained] ProcFieldInfo: TFieldInfo; { not owned — the procedural field, when IsProcFieldCall }
constructor Create;
destructor Destroy; override;
end;
@ -489,6 +493,10 @@ type
IsBuiltinHasClassAttr: Boolean; { set by uSemantic — HasClassAttribute builtin }
HasClassAttrClass: string; { class name for arg 1 (class being queried) }
HasClassAttrAttr: string; { attribute class name for arg 2 }
IsProcFieldCall: Boolean; { set by uSemantic unqualified Name is a
procedural-typed field of the current class
(implicit Self.Field), used as an expression }
[Unretained] ProcFieldInfo: TFieldInfo; { not owned — the procedural field, when IsProcFieldCall }
constructor Create;
destructor Destroy; override;
end;

View file

@ -8717,6 +8717,7 @@ var
I: Integer;
PT: TProceduralTypeDesc;
PPar: TProcParamInfo;
FldInfo: TFieldInfo;
begin
{ Resolution order matches Delphi/FPC:
1. Local variables / parameters / var-parameters a `var Run:
@ -8767,6 +8768,38 @@ begin
ACall.IsImplicitSelfMethod := True;
Exit;
end;
{ No method of that name an unqualified procedural-typed field of the
current class (implicit Self.Field) dispatches through its stored
pointer, mirroring the explicit Self.Field() path. }
FldInfo := FCurrentClass.FindField(ACall.Name);
if (FldInfo <> nil) and (FldInfo.TypeDesc <> nil) and
(FldInfo.TypeDesc.Kind = tyProcedural) then
begin
PT := TProceduralTypeDesc(FldInfo.TypeDesc);
if ACall.Args.Count <> PT.Params.Count then
SemanticError(Format(
'Indirect call ''%s'' expects %d argument(s), got %d',
[ACall.Name, PT.Params.Count, ACall.Args.Count]),
ACall.Line, ACall.Col);
for I := 0 to ACall.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(ACall.Args.Items[I]));
PPar := TProcParamInfo(PT.Params.Items[I]);
if PPar.IsVarParam and
not IsVarArgLValue(TASTExpr(ACall.Args.Items[I])) then
SemanticError(
Format('var argument %d of ''%s'' must be a variable',
[I + 1, ACall.Name]),
ACall.Line, ACall.Col);
CheckTypesMatch(PPar.TypeDesc, ArgType,
Format('argument %d of ''%s''', [I + 1, ACall.Name]),
ACall.Line, ACall.Col);
end;
ACall.IsProcFieldCall := True;
ACall.ProcFieldInfo := FldInfo;
ACall.ResolvedProcType := FldInfo.TypeDesc;
Exit;
end;
end;
{ Try on-demand instantiation of a generic function }
if StrPos('<', ACall.Name) >= 0 then
@ -9019,6 +9052,7 @@ var
I: Integer;
PT: TProceduralTypeDesc;
PPar: TProcParamInfo;
FldInfo: TFieldInfo;
begin
{ HasClassAttribute(AClass, AAttrClass): Boolean runtime query of the custom
attribute RTTI stored in slot 7 of the class's typeinfo. Both arguments
@ -9237,6 +9271,40 @@ begin
AExpr.ResolvedType := Result;
Exit;
end;
{ No method of that name an unqualified procedural-typed field of the
current class (implicit Self.Field) called as an expression dispatches
through its stored pointer and yields the signature's return type. }
FldInfo := FCurrentClass.FindField(AExpr.Name);
if (FldInfo <> nil) and (FldInfo.TypeDesc <> nil) and
(FldInfo.TypeDesc.Kind = tyProcedural) then
begin
PT := TProceduralTypeDesc(FldInfo.TypeDesc);
if AExpr.Args.Count <> PT.Params.Count then
SemanticError(Format(
'Indirect call ''%s'' expects %d argument(s), got %d',
[AExpr.Name, PT.Params.Count, AExpr.Args.Count]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(AExpr.Args.Items[I]));
PPar := TProcParamInfo(PT.Params.Items[I]);
if PPar.IsVarParam and
not IsVarArgLValue(TASTExpr(AExpr.Args.Items[I])) then
SemanticError(
Format('var argument %d of ''%s'' must be a variable',
[I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
CheckTypesMatch(PPar.TypeDesc, ArgType,
Format('argument %d of ''%s''', [I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
end;
AExpr.IsProcFieldCall := True;
AExpr.ProcFieldInfo := FldInfo;
AExpr.ResolvedProcType := FldInfo.TypeDesc;
Result := PT.ReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
end;
{ Try on-demand instantiation of a generic function }
if StrPos('<', AExpr.Name) >= 0 then

View file

@ -50,6 +50,10 @@ type
{ Method-pointer (of object) field: assign @Obj.Method into a field, then
dispatch through it exercises the 16-byte (Code, Data) field store. }
procedure TestRun_MethodPtrField_AssignAndCall;
{ Unqualified call to a procedural-typed field via implicit Self (FFn(...)
with no 'Self.' prefix), as an expression and as a statement. }
procedure TestRun_ImplicitSelfProcField_Expr;
procedure TestRun_ImplicitSelfProcField_Stmt;
{ Default parameters }
procedure TestRun_DefaultParam_OmitLast;
@ -458,6 +462,52 @@ const
end.
''';
{ Unqualified (implicit-Self) call to a procedural-typed field, as an
expression FFn(...) with no 'Self.' prefix. }
SrcImplicitProcFieldExpr = '''
program Prg;
type
TFn = function(A, B, C: Integer): Integer;
TBox = class
FFn: TFn;
function Run(X: Integer): Integer;
end;
function Sum3(A, B, C: Integer): Integer;
begin Result := A + B + C end;
function TBox.Run(X: Integer): Integer;
begin Result := FFn(X, X + 1, X + 2) end;
var B: TBox;
begin
B := TBox.Create();
B.FFn := @Sum3;
WriteLn(IntToStr(B.Run(10)));
B.Free()
end.
''';
{ Unqualified (implicit-Self) call to a procedural-typed field, as a
statement. }
SrcImplicitProcFieldStmt = '''
program Prg;
type
TFn = procedure(const S: string);
TBox = class
FFn: TFn;
procedure Run;
end;
procedure Hi(const S: string);
begin WriteLn(S) end;
procedure TBox.Run;
begin FFn('hi') end;
var B: TBox;
begin
B := TBox.Create();
B.FFn := @Hi;
B.Run();
B.Free()
end.
''';
SrcDefaultParam = '''
program Prg;
function Add(A: Integer; B: Integer = 10): Integer;
@ -805,6 +855,18 @@ begin
AssertRunsOnAll(SrcMethodPtrField, 'T:hello' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_ImplicitSelfProcField_Expr;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcImplicitProcFieldExpr, '33' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_ImplicitSelfProcField_Stmt;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcImplicitProcFieldStmt, 'hi' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_DefaultParam_OmitLast;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;

View file

@ -51,6 +51,9 @@ type
expression (Result := Self.FFn(S)) must type to the field's return
type and dispatch through the loaded pointer, not a direct call. }
procedure TestCodegen_ProcFieldCallExpr_IndirectNotDirect;
{ An unqualified procedural-field call via implicit Self (Result := FFn(S),
no 'Self.' prefix) must resolve and dispatch through Self's field. }
procedure TestCodegen_ImplicitSelfProcFieldCall_LoadsSelf;
end;
implementation
@ -586,6 +589,40 @@ begin
IRContains(IR, 'call $FFn('));
end;
procedure TProcTypesTests.TestCodegen_ImplicitSelfProcFieldCall_LoadsSelf;
var
IR: string;
begin
{ Regression: an unqualified FFn(...) where FFn is a procedural field used
to fail with "Undeclared function". It must now resolve as an implicit
Self.Field call: load Self, then dispatch through a temp. }
IR := GenIR(
'''
program Test;
type
TFn = function(const S: string): Integer;
TBox = class
FFn: TFn;
function Run(const S: string): Integer;
end;
function TBox.Run(const S: string): Integer;
begin
Result := FFn(S)
end;
var
B: TBox;
begin
end.
'''
);
AssertTrue('Implicit-Self field call must load Self',
IRContains(IR, 'loadl %_var_Self'));
AssertTrue('Implicit-Self field call must dispatch through a temp',
IRContains(IR, 'call %'));
AssertFalse('Implicit-Self field call must not be a direct call to $FFn',
IRContains(IR, 'call $FFn('));
end;
initialization
RegisterTest(TProcTypesTests);

View file

@ -214,6 +214,7 @@ TProcCall.Args serialise
TProcCall.IsImplicitSelfMethod safe
TProcCall.IsIndirectCall safe
TProcCall.IndirectCallIsGlobal safe
TProcCall.IsProcFieldCall safe
# TFuncCallExpr (uAST.pas:465)
TFuncCallExpr.Name serialise
@ -224,6 +225,7 @@ TFuncCallExpr.IndirectCallIsGlobal safe
TFuncCallExpr.IsBuiltinHasClassAttr safe
TFuncCallExpr.HasClassAttrClass safe
TFuncCallExpr.HasClassAttrAttr safe
TFuncCallExpr.IsProcFieldCall safe
# TIndirectFuncCallExpr (uAST.pas:491)
TIndirectFuncCallExpr.CalleeExpr serialise