fix(codegen): spill overflow args in native inherited calls (>6 registers)

An `inherited Method(args)` whose Self plus arguments occupy more than the
six System V integer argument registers aborted native codegen with
"System V integer arg register index 6 out of range". EmitInheritedCallSeq
only implemented the all-in-registers case: it pushed every argument and
popped each into a register, walking past the six available once Self + the
args exceeded six slots. There was no stack-spill path like the regular
method-call and constructor emitters already have.

Give EmitInheritedCallSeq the same >6-slot handling: when Self + args exceed
six register slots, build the six-register prefix plus an overflow region in
one frame, place Self in slot 0, evaluate each argument into its slot, load
registers 0..5, drop the prefix so the overflow sits at the callee's
[rsp+0..], then static-dispatch to the parent. Up to six register slots
keeps the existing push/pop path. QBE was already correct.

This is independent of the call position and of metaclass dispatch — any
inherited call with seven or more register slots (e.g. a method or
constructor with six+ scalar/aggregate args calling its parent) was
affected.

Adds an e2e regression (cp.test.e2e.classes2) exercising an inherited call
with Self + 7 integer args (8 register slots), asserting correct dispatch
and result on every backend.

(cherry picked from commit b1fc1285ff3984eedaf6a68d58e2d568f1e4c793)
This commit is contained in:
Andrew Haines 2026-06-17 15:54:08 -04:00 committed by Graeme Geldenhuys
parent b5573e5377
commit 7a663c561f
2 changed files with 101 additions and 12 deletions

View file

@ -8640,21 +8640,73 @@ var
I: Integer;
Sym: string;
Par: TMethodParam;
UserSlots, TotalSlots, OverflowSlots, AllocSz, CleanUp, Dest, HTotal: Integer;
Arg: TASTExpr;
HD, HK: TList<Integer>;
begin
Sym := MethodEmitNameNative(MD, MD.OwnerTypeName, AName);
Self.BeginCallArgs(MD.Params, AArgs);
for I := 0 to AArgs.Count - 1 do
Sym := MethodEmitNameNative(MD, MD.OwnerTypeName, AName);
UserSlots := CountArgSlots(MD.Params);
TotalSlots := UserSlots + 1; { + Self }
if TotalSlots <= 6 then
begin
Par := TMethodParam(MD.Params.Items[I]);
Self.PushCallArg(Par, TASTExpr(AArgs.Items[I]), I);
{ All of Self + args fit in the 6 integer arg registers: push then pop. }
Self.BeginCallArgs(MD.Params, AArgs);
for I := 0 to AArgs.Count - 1 do
begin
Par := TMethodParam(MD.Params.Items[I]);
Self.PushCallArg(Par, TASTExpr(AArgs.Items[I]), I);
end;
{ Self is the current method's Self slot. }
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
for I := UserSlots - 1 downto 0 do
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %r10, %rdi');
Self.Emit(#9'callq ' + Sym);
Self.EndCallArgs();
end
else
begin
{ More than 6 register slots (Self + args): the surplus must spill to the
stack. Build the 6 register slots + overflow region in one frame, place
Self in slot 0, evaluate each arg into its slot, load regs 0..5, drop the
6-register prefix so the overflow sits at the callee's [rsp+0..], then
static-dispatch to the parent. Mirrors EmitMethodCallStmt's >6 path
(the inherited call is always a direct callq, never vtable). }
OverflowSlots := TotalSlots - 6;
CleanUp := ((OverflowSlots * 8 + 15) and (-16));
AllocSz := 6 * 8 + CleanUp;
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]);
if I + 1 < 6 then
Dest := I * 8 + 8
else
Dest := 6 * 8 + (I + 1 - 6) * 8;
if HK.Get(I) >= akRecCall then
Self.Emit(Format(#9'movq %d(%%rsp), %%rax',
[AllocSz + HTotal - HD.Get(I)]))
else if TMethodParam(MD.Params.Items[I]).IsVarParam then
Self.EmitVarArgAddrToRax(Arg)
else
Self.EmitExprToEax(Arg);
Self.Emit(Format(#9'movq %%rax, %d(%%rsp)', [Dest]));
end;
{ Self into slot 0 (the implicit first integer arg). }
Self.Emit(Format(#9'movq %s, %%rax', [Self.VarOperand('Self')]));
Self.Emit(Format(#9'movq %%rax, 0(%%rsp)', []));
for I := 0 to 5 do
Self.Emit(Format(#9'movq %d(%%rsp), %s', [I * 8, SysVArg64(I)]));
Self.Emit(Format(#9'addq $%d, %%rsp', [6 * 8]));
Self.Emit(#9'callq ' + Sym);
Self.EmitHoistEpilogue(AArgs, HD, HK, HTotal, CleanUp, True);
HD.Free();
HK.Free();
end;
{ Self is the current method's Self slot. }
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
for I := CountArgSlots(MD.Params) - 1 downto 0 do
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %r10, %rdi');
Self.Emit(#9'callq ' + Sym);
Self.EndCallArgs();
end;
procedure TX86_64Backend.EmitInheritedCall(ACall: TInheritedCallStmt);

View file

@ -40,6 +40,9 @@ type
procedure TestRun_Is_WrongClass_False;
procedure TestRun_As_DowncastCallsMethod;
procedure TestRun_Inherited_CallsParentMethod;
{ inherited call whose Self + args exceed 6 integer registers must spill
the overflow args to the stack (native arg-register overflow). }
procedure TestRun_Inherited_ManyArgs_SpillsToStack;
procedure TestRun_Inherited_VarParam_PassesByReference;
procedure TestRun_Virtual_OverrideDispatch;
procedure TestRun_MultiLevel_Inheritance_Chain;
@ -399,6 +402,34 @@ const
end.
''';
{ Regression: an `inherited Method(args)` call whose Self + args occupy more
than the 6 integer argument registers. The native inherited-call sequence
only handled the all-in-registers case (it popped every slot into a
register, walking past the 6 available), so an inherited call with 6+ args
aborted codegen ("arg register index 6 out of range"). Here Self + 7 ints
= 8 register slots, forcing the overflow to spill to the stack. TBase.Sum
= 1+..+7 = 28; TChild.Sum doubles it via inherited => 56. }
SrcInheritedManyArgs = '''
program Prg;
type
TBase = class
function Sum(a, b, c, d, e, f, g: Integer): Integer; virtual;
end;
TChild = class(TBase)
function Sum(a, b, c, d, e, f, g: Integer): Integer; override;
end;
function TBase.Sum(a, b, c, d, e, f, g: Integer): Integer;
begin Result := a + b + c + d + e + f + g end;
function TChild.Sum(a, b, c, d, e, f, g: Integer): Integer;
begin Result := inherited Sum(a, b, c, d, e, f, g) * 2 end;
var o: TChild;
begin
o := TChild.Create();
WriteLn(o.Sum(1, 2, 3, 4, 5, 6, 7));
o.Free()
end.
''';
{ Regression: `inherited Foo(X)` where the parent method has a var/out param.
The inherited-call arg loop had no var-param branch, so it passed the
loaded VALUE (a `w`) where the callee expects an address (`l`) — the parent
@ -1109,6 +1140,12 @@ begin
AssertEquals('15', '15' + LE, Output);
end;
procedure TE2EClasses2Tests.TestRun_Inherited_ManyArgs_SpillsToStack;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcInheritedManyArgs, '56' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_Inherited_VarParam_PassesByReference;
var Output: string; RCode: Integer;
begin