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:
parent
7a95fd0472
commit
18b215835c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue