fix(semantic): scope same-named nested routines to their enclosing body

A nested function or procedure is lexically scoped to the routine that
contains it, so two sibling routines may each declare a nested helper of
the same name without conflict. Two independent gaps broke this:

  * Nested-in-METHOD decls were still entered in the global overload index
    (FProcIndex / FProcGroups). The registration guard in
    AnalyseStandaloneDecls only excluded nested-in-standalone-proc decls
    (FCurrentEnclosingDecl set); AnalyseMethodDecl never sets that field, so
    a nested helper in a method body leaked into the global index and two
    same-named siblings collided as "Ambiguous overload".

  * Nested-in-PROC functions called as an expression could not be resolved.
    AnalyseFuncCallExpr's tail consulted only FProcIndex.IndexOf and errored
    with "Cannot find declaration for function 'X'" without ever checking the
    scoped symbol it had already looked up. (The statement-position path,
    AnalyseProcCall, already handled scoped nested procs, which is why only
    the function-as-expression form failed.)

Fixes: also exclude a decl from global registration when FCurrentMethodOwner
is set (inside a method body); and in AnalyseFuncCallExpr, when the global
index misses but the scoped symbol carries a backing TMethodDecl, resolve the
call directly against that single decl.

Adds IR and e2e regression tests for both the proc and method enclosing forms,
and removes the MovRaxImm4b/4c rename workaround in cp.test.linker.pas — three
sibling test methods now legitimately share a nested MovRaxImm helper.

Verified: QBE, native, internal-asm and warm-cache fixpoints all green; full
suite passes under both the QBE-built and native-built test runner (4020 tests).
This commit is contained in:
Graeme Geldenhuys 2026-07-01 10:14:30 +01:00
parent 464cc113b6
commit a1ba257da7
4 changed files with 233 additions and 14 deletions

View file

@ -7113,11 +7113,14 @@ begin
ADecl.ResolvedQbeName := ADecl.Name;
{ Index for call resolution overloaded names appear multiple times.
Nested procs (those inside another proc's body) are resolved via the
Nested procs (those inside another routine's body) are resolved via the
scoped symbol table only; adding them to the global FProcIndex would
make same-named nested procs in different outer procs appear as
ambiguous overloads of each other. }
if FCurrentEnclosingDecl = nil then
make same-named nested procs in different outer routines appear as
ambiguous overloads of each other. A proc is nested when it sits
inside a standalone routine (FCurrentEnclosingDecl set) OR inside a
method body (FCurrentMethodOwner set) both cases must be excluded
from the global index. }
if (FCurrentEnclosingDecl = nil) and (FCurrentMethodOwner = nil) then
RegisterProcDecl(ADecl.Name, ADecl);
{ Register in symbol table }
@ -11259,6 +11262,43 @@ begin
Exit;
end;
{ Scoped nested routine: a function declared inside another routine's body
is registered in the symbol table only, never in the global FProcIndex /
FProcGroups (so same-named nested procs in sibling outer routines do not
collide as ambiguous overloads). When the call name misses the global
index but the scoped Sym carries its backing decl, resolve directly
against that single decl rather than the overload group. }
if (FProcIndex.IndexOf(AExpr.Name) < 0) and (Sym <> nil) and
(Sym.Decl <> nil) and (Sym.Decl is TMethodDecl) then
begin
MDecl := TMethodDecl(Sym.Decl);
HintBareEnumArgs(AExpr.Name, AExpr.Args);
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args.Items[I]));
if (AExpr.Args.Count < MinArity(MDecl)) or
(AExpr.Args.Count > MDecl.Params.Count) then
SemanticError(
Format('''%s'' expects %d argument(s), got %d',
[AExpr.Name, MDecl.Params.Count, AExpr.Args.Count]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params.Items[I]);
if Par.IsVarParam then
begin
ArgType := TASTExpr(AExpr.Args.Items[I]).ResolvedType;
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('var argument %d of ''%s''', [I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
end;
end;
RetypeSetLiteralArgs(AExpr.Args, MDecl);
AppendDefaultArgs(AExpr.Args, MDecl, AExpr.Name, AExpr.Line, AExpr.Col);
AExpr.ResolvedDecl := MDecl;
Result := MDecl.ResolvedReturnType;
Exit;
end;
Idx := FProcIndex.IndexOf(AExpr.Name);
if Idx < 0 then
SemanticError(

View file

@ -99,6 +99,15 @@ type
with one extra dereference; without it the element address resolved to a
global symbol and the writes/reads hit the wrong storage. }
procedure TestRun_NestedProc_CapturesVarArrayParam;
{ Two sibling outer routines each declare a same-named nested function.
Each call must resolve to the nested function of its own enclosing
routine. Previously the call fell through to the global overload index
(which does not list nested procs) and errored with "Cannot find
declaration". }
procedure TestRun_NestedProc_SiblingSameName_ResolvesPerScope;
{ As above but the enclosing routines are METHODS. Nested-in-method
functions must be scoped to their method body, not registered globally. }
procedure TestRun_NestedFunc_InSiblingMethods_ResolvesPerScope;
{ Diamond operator: TFoo<> infers type args from LHS }
procedure TestRun_Diamond_SingleArg_WorksAtRuntime;
@ -1206,6 +1215,83 @@ begin
'5' + LE + '15' + LE + '15' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_NestedProc_SiblingSameName_ResolvesPerScope;
const
Src =
'''
program Prg;
procedure First;
function Helper(X: Integer): Integer;
begin
Result := X + 1
end;
begin
WriteLn(IntToStr(Helper(10)))
end;
procedure Second;
function Helper(X: Integer): Integer;
begin
Result := X + 2
end;
begin
WriteLn(IntToStr(Helper(20)))
end;
begin
First();
Second()
end.
''';
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(Src, Output, RCode));
AssertEquals('exit code 0', 0, RCode);
AssertEquals('First.Helper(10)=11, Second.Helper(20)=22',
'11' + LE + '22' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_NestedFunc_InSiblingMethods_ResolvesPerScope;
const
Src =
'''
program Prg;
type
TFoo = class
procedure MethodA;
procedure MethodB;
end;
procedure TFoo.MethodA;
function Helper(X: Integer): Integer;
begin
Result := X + 1
end;
begin
WriteLn(IntToStr(Helper(10)))
end;
procedure TFoo.MethodB;
function Helper(X: Integer): Integer;
begin
Result := X + 2
end;
begin
WriteLn(IntToStr(Helper(20)))
end;
var F: TFoo;
begin
F := TFoo.Create;
F.MethodA();
F.MethodB()
end.
''';
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(Src, Output, RCode));
AssertEquals('exit code 0', 0, RCode);
AssertEquals('MethodA.Helper(10)=11, MethodB.Helper(20)=22',
'11' + LE + '22' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_NestedProc_CapturesVarRecordParam;
const
Src =

View file

@ -1247,7 +1247,7 @@ const
'mremap:' + LineEnding +
' movq $-1, %rax' + LineEnding + { MAP_FAILED — no FreeBSD mremap }
' ret' + LineEnding;
function MovRaxImm4b(AImm: Integer): string;
function MovRaxImm(AImm: Integer): string;
begin
Result := Chr($48) + Chr($C7) + Chr($C0) +
Chr(AImm and $FF) + Chr((AImm shr 8) and $FF) +
@ -1269,10 +1269,10 @@ begin
Lk.Free();
end;
{ getrandom uses the FreeBSD number 563 — NOT Linux's 318. }
AssertTrue('SYS_getrandom=563 movq imm present', Pos(MovRaxImm4b(563), Bytes) >= 0);
AssertTrue('Linux SYS_getrandom=318 NOT present', Pos(MovRaxImm4b(318), Bytes) < 0);
AssertTrue('SYS_getrandom=563 movq imm present', Pos(MovRaxImm(563), Bytes) >= 0);
AssertTrue('Linux SYS_getrandom=318 NOT present', Pos(MovRaxImm(318), Bytes) < 0);
{ mremap stub loads -1 (movq $-1,%rax = 48 C7 C0 FF FF FF FF). }
AssertTrue('mremap stub returns -1 (MAP_FAILED)', Pos(MovRaxImm4b(-1), Bytes) >= 0);
AssertTrue('mremap stub returns -1 (MAP_FAILED)', Pos(MovRaxImm(-1), Bytes) >= 0);
end;
procedure TLinkerE2ETests.TestLink_FreeBSDThreadLeaf_SyscallNumbers;
@ -1316,7 +1316,7 @@ const
' syscall' + LineEnding +
' ret' + LineEnding;
{ Encoded `movq $imm32, %rax` = 48 C7 C0 <imm32-LE>. }
function MovRaxImm4c(AImm: Integer): string;
function MovRaxImm(AImm: Integer): string;
begin
Result := Chr($48) + Chr($C7) + Chr($C0) +
Chr(AImm and $FF) + Chr((AImm shr 8) and $FF) +
@ -1340,12 +1340,12 @@ begin
Lk.Free();
end;
{ FreeBSD thread syscall NUMBERS present in the linked .text. }
AssertTrue('SYS_thr_new=455 movq imm present', Pos(MovRaxImm4c(455), Bytes) >= 0);
AssertTrue('SYS__umtx_op=454 movq imm present', Pos(MovRaxImm4c(454), Bytes) >= 0);
AssertTrue('SYS_thr_exit=431 movq imm present', Pos(MovRaxImm4c(431), Bytes) >= 0);
AssertTrue('SYS_thr_new=455 movq imm present', Pos(MovRaxImm(455), Bytes) >= 0);
AssertTrue('SYS__umtx_op=454 movq imm present', Pos(MovRaxImm(454), Bytes) >= 0);
AssertTrue('SYS_thr_exit=431 movq imm present', Pos(MovRaxImm(431), Bytes) >= 0);
{ NOT Linux's clone (56) / futex (202) — those numbers must be absent. }
AssertTrue('Linux SYS_clone=56 NOT present', Pos(MovRaxImm4c(56), Bytes) < 0);
AssertTrue('Linux SYS_futex=202 NOT present', Pos(MovRaxImm4c(202), Bytes) < 0);
AssertTrue('Linux SYS_clone=56 NOT present', Pos(MovRaxImm(56), Bytes) < 0);
AssertTrue('Linux SYS_futex=202 NOT present', Pos(MovRaxImm(202), Bytes) < 0);
{ _umtx_op's arg4 shuffle: movq %rcx, %r10 (49 89 CA). }
AssertTrue('movq %rcx,%r10 (_umtx_op arg4) present',
Pos(Chr($49) + Chr($89) + Chr($CA), Bytes) >= 0);

View file

@ -72,6 +72,18 @@ type
procedure TestCodegen_NestedProc_IsEmittedBeforeOuter;
procedure TestCodegen_NestedProc_CapturedVarPassedByPtr;
procedure TestCodegen_NestedProc_SameNameInTwoOuters_NoAmbiguity;
{ Same as above but the nested routines are FUNCTIONS called as
expressions (Helper(X) inside WriteLn), which exercises
AnalyseFuncCallExpr rather than AnalyseProcCall. The func-call path
previously consulted only the global FProcIndex and errored with
"Cannot find declaration for function 'Helper'". }
procedure TestCodegen_NestedFunc_SameNameInTwoOuters_ResolvesPerScope;
{ Two sibling METHOD bodies each declaring a same-named nested function.
Nested-in-method decls were registered in the global overload index
(the registration guard only excluded nested-in-standalone-proc decls),
so they collided as "Ambiguous overload". They must be scoped to their
enclosing method body like nested-in-proc decls. }
procedure TestCodegen_NestedFunc_InTwoMethods_ResolvesPerScope;
end;
implementation
@ -673,6 +685,87 @@ begin
AssertTrue('OuterB_Inner is emitted', StrPos('$OuterB_Inner', IR) >= 0);
end;
procedure TProcFuncTests.TestCodegen_NestedFunc_SameNameInTwoOuters_ResolvesPerScope;
const
Src =
'''
program TwinNestedFunc;
procedure OuterA;
function Helper(X: Integer): Integer;
begin
Result := X + 1;
end;
begin
WriteLn(IntToStr(Helper(10)));
end;
procedure OuterB;
function Helper(X: Integer): Integer;
begin
Result := X + 2;
end;
begin
WriteLn(IntToStr(Helper(20)));
end;
begin
OuterA();
OuterB();
end.
''';
var
IR: string;
begin
IR := GenIR(Src);
{ Each sibling nested function is emitted under its own enclosing-routine
qualified label proving they resolved to distinct decls per scope
rather than colliding as ambiguous overloads. (The call sites may be
inlined, so assert on the emitted function labels, not on a call.) }
AssertTrue('OuterA_Helper is emitted', StrPos('$OuterA_Helper', IR) >= 0);
AssertTrue('OuterB_Helper is emitted', StrPos('$OuterB_Helper', IR) >= 0);
end;
procedure TProcFuncTests.TestCodegen_NestedFunc_InTwoMethods_ResolvesPerScope;
const
Src =
'''
program TwinNestedMethod;
type
TFoo = class
procedure MethodA;
procedure MethodB;
end;
procedure TFoo.MethodA;
function Helper(X: Integer): Integer;
begin
Result := X + 1;
end;
begin
WriteLn(IntToStr(Helper(10)));
end;
procedure TFoo.MethodB;
function Helper(X: Integer): Integer;
begin
Result := X + 2;
end;
begin
WriteLn(IntToStr(Helper(20)));
end;
var F: TFoo;
begin
F := TFoo.Create;
F.MethodA();
F.MethodB();
end.
''';
var
IR: string;
begin
IR := GenIR(Src);
{ Compiling at all proves the nested-in-method funcs did not collide as
ambiguous overloads; both enclosing methods are emitted. }
AssertTrue('TFoo_MethodA is emitted', StrPos('$TFoo_MethodA', IR) >= 0);
AssertTrue('TFoo_MethodB is emitted', StrPos('$TFoo_MethodB', IR) >= 0);
end;
initialization
RegisterTest(TProcFuncTests);