fix(codegen): native passes float method/constructor args via xmm registers

The native x86-64 backend marshalled float arguments correctly for free
functions (SysV: floats -> %xmm0..7) but not for instance-method, constructor,
or inherited calls: those push every arg through the integer-register model, so
a Double/Single argument was either a codegen crash (literal -> "unsupported
expression form TFloatLiteral") or a silent ABI miscompile (variable bits
landed in an integer register while the callee read %xmm).

Fix, all in blaise.codegen.native.x86_64.pas:
- EmitMethodArgPush: a by-value float scalar arg is now materialised via
  EmitExprToXmm0 (handles literals through the .LF constant path), width-
  adjusted to the param type, and pushed as its 8-byte bit pattern.
- New EmitPopMethodArgsToRegs: replaces the integer-only `popq SysVArg64(I+1)`
  loops in the <=6-slot path of EmitMethodCallExpr, EmitMethodCallStmt, the
  constructor arg loop, EmitInheritedCallSeq, and the sret method path. It
  classifies each slot and routes floats to %xmm0.. and integers to the
  integer registers, advancing independent counters per SysV (Self still
  occupies integer slot 0/1).
- The rare >6-register-slot overflow path remains integer-only; a by-value
  float there now raises a clear "not yet supported" error (GuardNoFloatInOverflow)
  instead of crashing cryptically or silently miscompiling.

Tests: 6 e2e cases in cp.test.e2e.classes2 (method/ctor x literal/variable,
mixed int+float+string interleaving, multiple floats across xmm0/xmm1) run on
both backends, plus an IR assertion in cp.test.nativeconstarg that the call
site emits movsd into %xmm0.
This commit is contained in:
Graeme Geldenhuys 2026-06-22 14:16:34 +01:00
parent ef19c01cb7
commit 9607ca388e
3 changed files with 263 additions and 12 deletions

View file

@ -630,6 +630,21 @@ type
spilling any overflow to the stack. Returns overflow bytes to clean up spilling any overflow to the stack. Returns overflow bytes to clean up
after the call. Used by the interface/class sret call emitters. } after the call. Used by the interface/class sret call emitters. }
function EmitSretRegArgs(ASlots, ABase: Integer): Integer; function EmitSretRegArgs(ASlots, ABase: Integer): Integer;
{ Pop already-pushed method-call arg slots (slot 0 pushed first, on top is
the last slot) into the System V argument registers, routing float-family
params to %xmm0.. and everything else to the integer registers starting
at AIntBase (1 when only Self/%rdi is reserved). Mirrors EmitCall's SysV
classification so a Double/Single method or constructor argument lands in
its xmm register instead of being mis-passed in an integer register. }
procedure EmitPopMethodArgsToRegs(AParams, AArgs: TObjectList;
AIntBase: Integer);
{ Raise a clear error if a by-value float-family param appears in a call
whose total slots exceed the 6 integer arg registers. The >6-slot
method/ctor overflow emitters use an integer-only flat-slot layout; a
float arg there would be mis-classified into the integer overflow region
instead of an xmm register. The <=6-slot path handles floats correctly
(EmitPopMethodArgsToRegs); only the rare overflow path is unsupported. }
procedure GuardNoFloatInOverflow(AParams: TObjectList; const AWhat: string);
{ Emit a method-pointer (of-object) call: load Code from offset 0 and Data { Emit a method-pointer (of-object) call: load Code from offset 0 and Data
from offset 8 of the TMethod block at APtrOperand; call Code with Data as from offset 8 of the TMethod block at APtrOperand; call Code with Data as
Self (%rdi) and the remaining args shifted. } Self (%rdi) and the remaining args shifted. }
@ -7794,8 +7809,7 @@ begin
for I := 0 to ACall.Args.Count - 1 do for I := 0 to ACall.Args.Count - 1 do
Self.PushCallArg(TMethodParam(MD.Params.Items[I]), Self.PushCallArg(TMethodParam(MD.Params.Items[I]),
TASTExpr(ACall.Args.Items[I]), I); TASTExpr(ACall.Args.Items[I]), I);
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, ACall.Args, 1);
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %rbx, %rdi'); Self.Emit(#9'movq %rbx, %rdi');
if ACall.IsMetaclassDispatch and (MD.VTableSlot >= 0) then if ACall.IsMetaclassDispatch and (MD.VTableSlot >= 0) then
begin begin
@ -7816,6 +7830,7 @@ begin
HD := TList<Integer>.Create(); HD := TList<Integer>.Create();
HK := TList<Integer>.Create(); HK := TList<Integer>.Create();
HTotal := Self.EmitArgHoist(MD.Params, nil, True, '', ACall.Args, HD, HK); HTotal := Self.EmitArgHoist(MD.Params, nil, True, '', ACall.Args, HD, HK);
Self.GuardNoFloatInOverflow(MD.Params, 'constructor call');
OverflowSlots := TotalSlots - 6; OverflowSlots := TotalSlots - 6;
AllocSz := (((6 * 8 + OverflowSlots * 8) + 15) and (-16)) + 8; AllocSz := (((6 * 8 + OverflowSlots * 8) + 15) and (-16)) + 8;
CleanUp := AllocSz - 6 * 8; CleanUp := AllocSz - 6 * 8;
@ -7959,8 +7974,7 @@ begin
else else
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')])); Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, ACall.Args, 1);
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %r10, %rdi'); Self.Emit(#9'movq %r10, %rdi');
if MD.VTableSlot >= 0 then if MD.VTableSlot >= 0 then
begin begin
@ -7994,6 +8008,7 @@ begin
Self.EmitRecordCallSretAt(ACall.ObjExpr, '(%rsp)'); Self.EmitRecordCallSretAt(ACall.ObjExpr, '(%rsp)');
Self.Emit(#9'movq %rsp, %rbx'); Self.Emit(#9'movq %rsp, %rbx');
end; end;
Self.GuardNoFloatInOverflow(MD.Params, 'method call');
OverflowSlots := TotalSlots - 6; OverflowSlots := TotalSlots - 6;
CleanUp := ((OverflowSlots * 8 + 15) and (-16)); CleanUp := ((OverflowSlots * 8 + 15) and (-16));
AllocSz := 6 * 8 + CleanUp; AllocSz := 6 * 8 + CleanUp;
@ -8315,6 +8330,19 @@ begin
Self.Emit(#9'pushq %rax'); Self.Emit(#9'pushq %rax');
end; end;
end end
else if (APar <> nil) and IsFloatFamily(APar.ResolvedType) then
begin
{ Float scalar argument: materialise to %xmm0 (handles literals via the
.LF constant path, variables, expressions), adjust to the param's width,
and push its 8-byte bit pattern onto the stack as one slot. The pop loop
(EmitPopMethodArgsToRegs) routes this slot into an xmm argument register
per the SysV ABI instead of an integer register. }
Self.EmitExprToXmm0(AArg);
Self.EmitXmm0WidthAdjust(AArg.ResolvedType,
APar.ResolvedType.Kind = tySingle);
Self.Emit(#9'subq $8, %rsp');
Self.Emit(#9'movsd %xmm0, 0(%rsp)');
end
else else
begin begin
Self.EmitExprToEax(AArg); Self.EmitExprToEax(AArg);
@ -8449,6 +8477,110 @@ begin
Result := OverflowSlots * 8; Result := OverflowSlots * 8;
end; end;
procedure TX86_64Backend.EmitPopMethodArgsToRegs(AParams, AArgs: TObjectList;
AIntBase: Integer);
var
I, NParams, IntIdx, XmmIdx: Integer;
P: TMethodParam;
PT: TTypeDesc;
IsFloatSlot: TList<Integer>; { 1 = float (xmm) slot, 0 = integer slot,
in stack-slot order (slot 0 pushed first) }
Dest: TStringList; { target register per slot, parallel to above }
Slots: Integer;
begin
{ Determine the param backing each logical arg. AParams may be nil (no
declared params): then every arg is a plain integer slot. }
NParams := 0;
if AParams <> nil then NParams := AParams.Count;
IsFloatSlot := TList<Integer>.Create();
Dest := TStringList.Create();
try
{ Forward pass: one entry per stack slot, in push order (slot 0 first). }
for I := 0 to AArgs.Count - 1 do
begin
P := nil;
if I < NParams then P := TMethodParam(AParams.Items[I]);
PT := nil;
if P <> nil then PT := P.ResolvedType;
if (PT = nil) and (TASTExpr(AArgs.Items[I]).ResolvedType <> nil) then
PT := TASTExpr(AArgs.Items[I]).ResolvedType;
if (P <> nil) and P.IsOpenArray then
begin
IsFloatSlot.Add(0); IsFloatSlot.Add(0); { ptr + high }
end
else if (PT <> nil) and (PT.Kind = tyInterface) and
((P = nil) or not P.IsVarParam) then
begin
IsFloatSlot.Add(0); IsFloatSlot.Add(0); { obj + itab }
end
else if IsFloatFamily(PT) and ((P = nil) or
(not P.IsVarParam and not P.IsOpenArray)) then
IsFloatSlot.Add(1) { float scalar -> xmm }
else
IsFloatSlot.Add(0); { integer/ptr scalar }
end;
{ Assign a register to each slot in forward order: integers consume the
SysV integer registers from AIntBase, floats consume xmm0.. — the two
sequences advance independently, per SysV. }
IntIdx := AIntBase;
XmmIdx := 0;
for I := 0 to IsFloatSlot.Count - 1 do
begin
if IsFloatSlot.Get(I) = 1 then
begin
Dest.Add(SysVXmmArgRegs[XmmIdx]);
Inc(XmmIdx);
end
else
begin
Dest.Add(Self.SysVArg64(IntIdx));
Inc(IntIdx);
end;
end;
{ Reverse pass: the top of the stack is the LAST slot. Pop from the last
slot down to slot 0, moving each into its precomputed register. Every
slot is 8 bytes and consumed in turn, so the current slot is always at
0(%rsp) — a float reads it then raises %rsp by 8, an integer pops it. }
Slots := IsFloatSlot.Count;
for I := Slots - 1 downto 0 do
begin
if IsFloatSlot.Get(I) = 1 then
begin
Self.Emit(Format(#9'movsd 0(%%rsp), %s', [Dest.Strings[I]]));
Self.Emit(#9'addq $8, %rsp');
end
else
Self.Emit(#9'popq ' + Dest.Strings[I]);
end;
finally
Dest.Free();
IsFloatSlot.Free();
end;
end;
procedure TX86_64Backend.GuardNoFloatInOverflow(AParams: TObjectList;
const AWhat: string);
var
I: Integer;
P: TMethodParam;
begin
if AParams = nil then Exit;
for I := 0 to AParams.Count - 1 do
begin
P := TMethodParam(AParams.Items[I]);
if IsFloatFamily(P.ResolvedType) and not P.IsVarParam and
not P.IsOpenArray then
raise ENativeCodeGenError.Create(
'native backend: by-value float argument in a ' + AWhat +
' with more than 6 register slots is not yet supported (the >6-arg ' +
'overflow path is integer-only); reduce the argument count or pass ' +
'the float earlier in the list');
end;
end;
{ Emit a TMethodCallStmt (class method call in statement position). { Emit a TMethodCallStmt (class method call in statement position).
Same as EmitMethodCallExpr but for statement nodes. } Same as EmitMethodCallExpr but for statement nodes. }
procedure TX86_64Backend.EmitMethodCallStmt(ACall: TMethodCallStmt); procedure TX86_64Backend.EmitMethodCallStmt(ACall: TMethodCallStmt);
@ -8493,8 +8625,7 @@ begin
for I := 0 to ACall.Args.Count - 1 do for I := 0 to ACall.Args.Count - 1 do
Self.PushCallArg(TMethodParam(MD.Params.Items[I]), Self.PushCallArg(TMethodParam(MD.Params.Items[I]),
TASTExpr(ACall.Args.Items[I]), I); TASTExpr(ACall.Args.Items[I]), I);
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, ACall.Args, 1);
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %rbx, %rdi'); Self.Emit(#9'movq %rbx, %rdi');
if MD.VTableSlot >= 0 then if MD.VTableSlot >= 0 then
begin begin
@ -8515,6 +8646,7 @@ begin
HD := TList<Integer>.Create(); HD := TList<Integer>.Create();
HK := TList<Integer>.Create(); HK := TList<Integer>.Create();
HTotal := Self.EmitArgHoist(MD.Params, nil, True, '', ACall.Args, HD, HK); HTotal := Self.EmitArgHoist(MD.Params, nil, True, '', ACall.Args, HD, HK);
Self.GuardNoFloatInOverflow(MD.Params, 'constructor call');
OverflowSlots := TotalSlots - 6; OverflowSlots := TotalSlots - 6;
AllocSz := (((6 * 8 + OverflowSlots * 8) + 15) and (-16)) + 8; AllocSz := (((6 * 8 + OverflowSlots * 8) + 15) and (-16)) + 8;
CleanUp := AllocSz - 6 * 8; CleanUp := AllocSz - 6 * 8;
@ -8698,12 +8830,12 @@ begin
else else
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')])); Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, ACall.Args, 1);
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %r10, %rdi'); Self.Emit(#9'movq %r10, %rdi');
end end
else else
begin begin
Self.GuardNoFloatInOverflow(MD.Params, 'method call');
OverflowSlots := TotalSlots - 6; OverflowSlots := TotalSlots - 6;
CleanUp := ((OverflowSlots * 8 + 15) and (-16)); CleanUp := ((OverflowSlots * 8 + 15) and (-16));
AllocSz := 6 * 8 + CleanUp; AllocSz := 6 * 8 + CleanUp;
@ -8820,8 +8952,7 @@ begin
end; end;
{ Self is the current method's Self slot. } { Self is the current method's Self slot. }
Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')])); Self.Emit(Format(#9'movq %s, %%r10', [Self.VarOperand('Self')]));
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, AArgs, 1);
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %r10, %rdi'); Self.Emit(#9'movq %r10, %rdi');
Self.Emit(#9'callq ' + Sym); Self.Emit(#9'callq ' + Sym);
Self.EndCallArgs(); Self.EndCallArgs();
@ -8834,6 +8965,7 @@ begin
6-register prefix so the overflow sits at the callee's [rsp+0..], then 6-register prefix so the overflow sits at the callee's [rsp+0..], then
static-dispatch to the parent. Mirrors EmitMethodCallStmt's >6 path static-dispatch to the parent. Mirrors EmitMethodCallStmt's >6 path
(the inherited call is always a direct callq, never vtable). } (the inherited call is always a direct callq, never vtable). }
Self.GuardNoFloatInOverflow(MD.Params, 'inherited call');
OverflowSlots := TotalSlots - 6; OverflowSlots := TotalSlots - 6;
CleanUp := ((OverflowSlots * 8 + 15) and (-16)); CleanUp := ((OverflowSlots * 8 + 15) and (-16));
AllocSz := 6 * 8 + CleanUp; AllocSz := 6 * 8 + CleanUp;
@ -14533,8 +14665,7 @@ begin
{ Pop one register per SLOT (interface args occupy two), into the integer { Pop one register per SLOT (interface args occupy two), into the integer
registers after %rdi (sret) and %rsi (Self): %rdx, %rcx, %r8, %r9. } registers after %rdi (sret) and %rsi (Self): %rdx, %rcx, %r8, %r9. }
for I := UserSlots - 1 downto 0 do Self.EmitPopMethodArgsToRegs(MD.Params, ACall.Args, 2);
Self.Emit(#9'popq ' + SysVArg64(I + 2));
{ The saved dest sits just below the call frame's hoist region. } { The saved dest sits just below the call frame's hoist region. }
Self.Emit(Format(#9'movq %d(%%rsp), %%rdi', [Self.TopFrameTotal()])); Self.Emit(Format(#9'movq %d(%%rsp), %%rdi', [Self.TopFrameTotal()]));
if MD.VTableSlot >= 0 then if MD.VTableSlot >= 0 then

View file

@ -54,6 +54,15 @@ type
procedure TestRun_Supports_TwoArg_BooleanResult; procedure TestRun_Supports_TwoArg_BooleanResult;
procedure TestRun_Supports_ThreeArg_AssignsAndCalls; procedure TestRun_Supports_ThreeArg_AssignsAndCalls;
procedure TestRun_ConstructorOverload_PicksCorrectArity; procedure TestRun_ConstructorOverload_PicksCorrectArity;
{ Regression: native backend passed float args to methods/constructors via
integer registers instead of the SysV xmm registers — a literal crashed
codegen, a variable silently used the wrong register. }
procedure TestRun_MethodFloatArg_Literal_PassedInXmm;
procedure TestRun_MethodFloatArg_Variable_PassedInXmm;
procedure TestRun_ConstructorFloatArg_Literal_StoredCorrectly;
procedure TestRun_ConstructorFloatArg_Variable_StoredCorrectly;
procedure TestRun_MethodMixedIntFloatString_RegistersInterleave;
procedure TestRun_MethodMultipleFloats_UseXmm0AndXmm1;
procedure TestRun_MethodReadsProgramGlobal; procedure TestRun_MethodReadsProgramGlobal;
procedure TestRun_VarParam_ClassFields_WritebackVisible; procedure TestRun_VarParam_ClassFields_WritebackVisible;
{ Name-resolution priority for unqualified calls inside a class { Name-resolution priority for unqualified calls inside a class
@ -1246,6 +1255,90 @@ begin
AssertEquals('1-arg constructor body ran', '42' + LE, Output); AssertEquals('1-arg constructor body ran', '42' + LE, Output);
end; end;
procedure TE2EClasses2Tests.TestRun_MethodFloatArg_Literal_PassedInXmm;
begin
AssertRunsOnAll(
'''
program c;
type TX = class procedure Take(d: Double); end;
procedure TX.Take(d: Double); begin WriteLn(d); end;
var x: TX;
begin x := TX.Create(); x.Take(1.5); end.
''',
'1.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_MethodFloatArg_Variable_PassedInXmm;
begin
AssertRunsOnAll(
'''
program f;
type TX = class procedure Take(d: Double); end;
procedure TX.Take(d: Double); begin WriteLn(d); end;
var x: TX; y: Double;
begin x := TX.Create(); y := 1.5; x.Take(y); end.
''',
'1.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_ConstructorFloatArg_Literal_StoredCorrectly;
begin
AssertRunsOnAll(
'''
program ctl;
type TN = class V: Double; constructor CreateFloat(d: Double); end;
constructor TN.CreateFloat(d: Double); begin V := d; end;
var n: TN;
begin n := TN.CreateFloat(2.5); WriteLn(n.V); end.
''',
'2.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_ConstructorFloatArg_Variable_StoredCorrectly;
begin
AssertRunsOnAll(
'''
program ct;
type TN = class V: Double; constructor CreateFloat(d: Double); end;
constructor TN.CreateFloat(d: Double); begin V := d; end;
var n: TN; y: Double;
begin y := 2.5; n := TN.CreateFloat(y); WriteLn(n.V); end.
''',
'2.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_MethodMixedIntFloatString_RegistersInterleave;
begin
{ Exercises int + float + string interleaving: the string and Int64 take
integer arg registers, the Double takes an xmm register, independently. }
AssertRunsOnAll(
'''
program mix;
type TX = class procedure M(const s: string; i: Int64; d: Double); end;
procedure TX.M(const s: string; i: Int64; d: Double);
begin WriteLn('s=', s, ' i=', i, ' d=', d); end;
var x: TX;
begin x := TX.Create(); x.M('hi', 7, 3.5); end.
''',
's=hi i=7 d=3.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_MethodMultipleFloats_UseXmm0AndXmm1;
begin
{ Two Double/Single args must land in distinct xmm registers (xmm0, xmm1),
interleaved with an integer arg that consumes an integer register. }
AssertRunsOnAll(
'''
program mf;
type TX = class procedure M(a: Double; i: Int64; b: Double; c: Single); end;
procedure TX.M(a: Double; i: Int64; b: Double; c: Single);
begin WriteLn('a=', a, ' i=', i, ' b=', b, ' c=', c); end;
var x: TX;
begin x := TX.Create(); x.M(1.5, 9, 2.5, 3.5); end.
''',
'a=1.5 i=9 b=2.5 c=3.5' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_MethodReadsProgramGlobal; procedure TE2EClasses2Tests.TestRun_MethodReadsProgramGlobal;
var Output: string; RCode: Integer; var Output: string; RCode: Integer;
begin begin

View file

@ -61,6 +61,10 @@ type
{ Self is typed as the owning class; value record params present the { Self is typed as the owning class; value record params present the
'_data' shadow slot (the inline copy) so field drilldown works. } '_data' shadow slot (the inline copy) so field drilldown works. }
procedure TestOpdf_Facts_SelfAndRecordParamTyping; procedure TestOpdf_Facts_SelfAndRecordParamTyping;
{ Regression: a float argument to an instance method must be marshalled
into an SSE (xmm) argument register per the System V ABI, not an integer
register. Asserts the call site emits movsd into %xmm0. }
procedure TestMethodFloatArg_LoadedIntoXmm;
end; end;
implementation implementation
@ -626,6 +630,29 @@ begin
CG.Free(); CG.Free();
end; end;
procedure TNativeConstArgTests.TestMethodFloatArg_LoadedIntoXmm;
const
Src = '''
program P;
type TX = class procedure Take(d: Double); end;
procedure TX.Take(d: Double); begin end;
var x: TX;
begin
x := TX.Create();
x.Take(1.5)
end.
''';
var
Asm: string;
begin
Asm := GenAsm(Src);
{ The Double argument must reach the SSE arg register, not an integer one. }
AssertTrue('float method arg loaded into %xmm0',
Pos('movsd', Asm) >= 0);
AssertTrue('float method arg targets the SSE arg register',
Pos('%xmm0', Asm) >= 0);
end;
initialization initialization
RegisterTest(TNativeConstArgTests); RegisterTest(TNativeConstArgTests);