fix(codegen-native): spill implicit-Self call args beyond six registers to the stack

An implicit-Self method call — a bare `Method(...)` inside another method that
resolves to a method of the current class — loaded Self plus every argument
into the System V integer argument registers unconditionally. When Self and the
arguments together needed more than the six available integer registers,
codegen aborted with "System V integer arg register index 6 out of range".

The explicit-receiver path (obj.Method() / Self.Method()) already split on slot
count and spilled the overflow to the stack; only the implicit-Self expression
path was missing it. Add the same split: keep the push/pop-into-registers fast
path for <= 6 slots, and for more, evaluate the arguments into a stack slot
block, load the first six slots into registers, and spill the rest per the ABI
(EmitImplicitSelfCallOverflow, mirroring EmitMethodCallExpr's overflow branch).

Tests: an assembly-level test asserts the slot-block spill shape and dispatch,
and an end-to-end test runs a program with an 8-argument non-virtual and a
7-argument virtual implicit-Self call and verifies every argument — including
the spilled ones — arrives intact.
This commit is contained in:
Andrew Haines 2026-06-25 23:12:22 -04:00 committed by Graeme Geldenhuys
parent 7a95fd0472
commit 18b215835c
3 changed files with 171 additions and 13 deletions

View file

@ -566,6 +566,13 @@ type
{ Emit a TMethodCallExpr (class method call, with explicit receiver).
Loads Self into %rdi and evaluates scalar args; result in %rax/%xmm0. }
procedure EmitMethodCallExpr(ACall: TMethodCallExpr);
{ Implicit-Self regular (non-sret) method call whose register slots
(Self + args) exceed 6: evaluate args into a stack slot block (slot 0 =
Self), load the first six into registers and spill the rest, then dispatch
(vtable-aware) with Self in %rdi. Mirrors EmitMethodCallExpr's >6-slot
branch; the implicit-Self expression path (EmitExprToEax) lacked it. }
procedure EmitImplicitSelfCallOverflow(MD: TMethodDecl;
AArgs: TObjectList; const ASym: string);
{ Emit a TMethodCallStmt (class method call in statement position). }
procedure EmitMethodCallStmt(ACall: TMethodCallStmt);
{ Emit a TInheritedCallStmt (`inherited Method[(args)]`). Direct static
@ -6466,19 +6473,27 @@ begin
if FC.IsImplicitSelfMethod then
begin
MD := TMethodDecl(FC.ResolvedDecl);
Self.BeginCallArgs(MD.Params, FC.Args);
for I := 0 to FC.Args.Count - 1 do
Self.PushCallArg(TMethodParam(MD.Params.Items[I]),
TASTExpr(FC.Args.Items[I]), I);
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
Self.EmitPopMethodArgsToRegs(MD.Params, FC.Args, 1);
Self.Emit(#9'movq %r10, %rdi');
{ Implicit-Self call to a virtual method must dispatch through the vtable
(Self may be a more-derived instance), exactly as Self.Method() does;
a static callq would bind to the declaring class and break
polymorphism (and link-fail for an abstract base). }
Self.EmitSelfDispatch(MD, FuncSymbolOf(FC));
Self.EndCallArgs();
{ Register slots = Self (%rdi) + the arg slots. When they fit in the six
integer registers, use the push/pop-into-registers fast path; otherwise
spill the overflow to the stack per the SysV ABI. }
if Self.CountArgSlots(MD.Params) + 1 <= 6 then
begin
Self.BeginCallArgs(MD.Params, FC.Args);
for I := 0 to FC.Args.Count - 1 do
Self.PushCallArg(TMethodParam(MD.Params.Items[I]),
TASTExpr(FC.Args.Items[I]), I);
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
Self.EmitPopMethodArgsToRegs(MD.Params, FC.Args, 1);
Self.Emit(#9'movq %r10, %rdi');
{ Implicit-Self call to a virtual method must dispatch through the vtable
(Self may be a more-derived instance), exactly as Self.Method() does;
a static callq would bind to the declaring class and break
polymorphism (and link-fail for an abstract base). }
Self.EmitSelfDispatch(MD, FuncSymbolOf(FC));
Self.EndCallArgs();
end
else
Self.EmitImplicitSelfCallOverflow(MD, FC.Args, FuncSymbolOf(FC));
if FC.ResolvedType <> nil then
Self.EmitNarrowToType(FC.ResolvedType);
Exit;
@ -8345,6 +8360,61 @@ begin
end;
end;
procedure TX86_64Backend.EmitImplicitSelfCallOverflow(MD: TMethodDecl;
AArgs: TObjectList; const ASym: string);
var
I, AllocSz, CleanUp, Dest: Integer;
Arg: TASTExpr;
HD, HK: TList<Integer>;
HTotal: Integer;
begin
{ Slot block: slot 0 = Self, slots 1.. = one per logical arg (matching
EmitMethodOverflowLoad's reader). 16-aligned. }
AllocSz := (((Self.CountArgSlots(MD.Params) + 1) * 8 + 15) and (-16));
HD := TList<Integer>.Create();
HK := TList<Integer>.Create();
HTotal := Self.EmitArgHoist(MD.Params, nil, True, '', AArgs, HD, HK);
Self.Emit(Format(#9'subq $%d, %%rsp', [AllocSz]));
for I := 0 to AArgs.Count - 1 do
begin
Arg := TASTExpr(AArgs.Items[I]);
Dest := (I + 1) * 8;
if HK.Get(I) >= akRecCall then
begin
Self.Emit(Format(#9'movq %d(%%rsp), %%rax', [AllocSz + HTotal - HD.Get(I)]));
Self.Emit(Format(#9'movq %%rax, %d(%%rsp)', [Dest]));
end
else if TMethodParam(MD.Params.Items[I]).IsVarParam then
begin
Self.EmitVarArgAddrToRax(Arg);
Self.Emit(Format(#9'movq %%rax, %d(%%rsp)', [Dest]));
end
else if Self.OverflowArgIsFloat(MD.Params, I) then
begin
Self.EmitExprToXmm0(Arg);
Self.EmitXmm0WidthAdjust(Arg.ResolvedType,
TMethodParam(MD.Params.Items[I]).ResolvedType.Kind = tySingle);
Self.Emit(Format(#9'movsd %%xmm0, %d(%%rsp)', [Dest]));
end
else
begin
Self.EmitExprToEax(Arg);
Self.Emit(Format(#9'movq %%rax, %d(%%rsp)', [Dest]));
end;
end;
{ Self into slot 0. }
Self.Emit(Format(#9'movq %s, %%rax', [Self.VarOperand('Self')]));
Self.Emit(#9'movq %rax, 0(%rsp)');
{ Load slot 0 -> %rdi (Self) and the arg slots into %rsi.. / %xmm0.., spilling
the >6th integer slot to the stack; returns bytes to reclaim after the call. }
CleanUp := Self.EmitMethodOverflowLoad(MD.Params, AArgs, AllocSz);
{ Self is now in %rdi — dispatch (vtable-aware) exactly like Self.Method(). }
Self.EmitSelfDispatch(MD, ASym);
Self.EmitHoistEpilogue(AArgs, HD, HK, HTotal, CleanUp, True);
HD.Free();
HK.Free();
end;
{ Leave the ADDRESS of a var/out argument in %rax. Shared by every
call-emission path that passes a var param — loading the variable's
VALUE here (the old EmitSretCall behaviour) hands the callee a garbage

View file

@ -386,6 +386,10 @@ type
{ Record value assigned into a record-typed field of the sret Result
must be a full ARC-aware copy, not an 8-byte pointer store. }
procedure TestRun_Native_SretResult_NestedRecordFieldAssign;
{ Regression: implicit-Self method calls whose Self+args exceed six integer
registers must spill the overflow to the stack (the expression path used
to abort codegen with 'arg register index 6 out of range'). }
procedure TestRun_Native_ImplicitSelfCall_RegisterOverflow;
{ Subscripting a string-typed FIELD (Rec.S[I] / Obj.Data[I]) must be
0-based like every other Blaise subscript (regression: QBE
subtracted 1, native read garbage through the unhandled path). }
@ -1417,6 +1421,43 @@ const
end.
''';
{ Implicit-Self method calls whose register slots (Self + args) exceed the
six System V integer registers must spill the overflow to the stack — the
implicit-Self expression path previously aborted codegen. Covers a
non-virtual call (Self + 8 args = 9 slots, 3 spilled) and a virtual/vtable
call (Self + 7 args = 8 slots, 2 spilled); the values verify that every
arg, including the spilled ones, arrives intact. }
SrcImplicitSelfArgOverflow = '''
program Prg;
type
TWorker = class
function Sum8(a, b, c, d, e, f, g, h: Integer): Integer;
function VSum7(a, b, c, d, e, f, g: Integer): Integer; virtual;
function Drive(): string;
end;
function TWorker.Sum8(a, b, c, d, e, f, g, h: Integer): Integer;
begin
Result := a + b + c + d + e + f + g + h;
end;
function TWorker.VSum7(a, b, c, d, e, f, g: Integer): Integer;
begin
Result := a * 1 + b * 2 + c * 3 + d * 4 + e * 5 + f * 6 + g * 7;
end;
function TWorker.Drive(): string;
begin
WriteLn(Sum8(1, 2, 3, 4, 5, 6, 7, 8));
WriteLn(VSum7(1, 1, 1, 1, 1, 1, 1));
Result := 'ok';
end;
var
W: TWorker;
begin
W := TWorker.Create();
WriteLn(W.Drive());
W.Free();
end.
''';
const
SrcStringFieldCharRead = '''
program Prg;
@ -1472,6 +1513,13 @@ begin
'1' + LE + '5' + LE + '7' + LE + '70' + LE + 'x' + LE + '9' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_ImplicitSelfCall_RegisterOverflow;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcImplicitSelfArgOverflow,
'36' + LE + '28' + LE + 'ok' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_EmptyProgram_ExitsZero;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;

View file

@ -68,6 +68,10 @@ type
{ Inline asm: a nostackframe asm-body function emits the verbatim block and
NO compiler prologue (no pushq %rbp / frame subq). }
procedure TestInlineAsm_VerbatimBody_NoPrologue;
{ Regression: an implicit-Self call whose Self+args exceed the six integer
argument registers spills the overflow to the stack (it used to abort
codegen). Asserts the slot-block overflow shape and the dispatch. }
procedure TestImplicitSelfCall_RegisterOverflow_Spills;
end;
implementation
@ -682,6 +686,42 @@ begin
AssertTrue('no pushq %rbp prologue', Pos('pushq %rbp', Region) < 0);
end;
procedure TNativeConstArgTests.TestImplicitSelfCall_RegisterOverflow_Spills;
const
Src = '''
program P;
type
TW = class
function C(a, b, c, d, e, f, g: Integer): Integer;
function D(): Integer;
end;
function TW.C(a, b, c, d, e, f, g: Integer): Integer;
begin
Result := a;
end;
function TW.D(): Integer;
begin
Result := C(1, 2, 3, 4, 5, 6, 7);
end;
begin
end.
''';
var
Region: string;
begin
{ Implicit-Self call C(...): Self (%rdi) + 7 args = 8 integer slots. Six load
into registers; the last two spill to the stack. The slot-block overflow
path stores Self into 0(%rsp) and fills all six integer arg registers —
unlike the <=6 push/pop fast path. Before the fix this aborted codegen with
'arg register index 6 out of range'. }
Region := FuncRegion(GenAsm(Src), 'TW_D');
AssertTrue('overflow call dispatched', Pos('callq TW_C', Region) >= 0);
AssertTrue('Self stored into slot 0 (overflow slot-block path)',
Pos('movq %rax, 0(%rsp)', Region) >= 0);
AssertTrue('arg registers filled through %r9',
(Pos('%r8', Region) >= 0) and (Pos('%r9', Region) >= 0));
end;
initialization
RegisterTest(TNativeConstArgTests);