fix(native): record return/assign through out params and const record params

Three related bugs in the native x86-64 backend where record data was
written to the pointer slot (leaq = address-of-slot) rather than through
the pointer (movq = load-pointer-then-deref):

1. EmitSretCall / EmitMethodSretCall: when the LHS of a record-returning
   function call is a var/out parameter, pass ASretIsIndirect=True so the
   memset and reg-return capture target the caller's buffer, not the
   local stack slot holding the pointer.

2. EmitRecordRegReturnCapture: honour a new AIsIndirect flag — emit
   movq (load pointer) instead of leaq (address-of-slot) for all
   classification shapes (rcInt1..rcSSEInt).

3. EmitExprToEax for TIdentExpr with tyRecord/tyStaticArray: when the
   identifier is a const/value record parameter (ParamMode = pmRecordValue
   or pmStaticArrayValue), the frame slot holds a pointer to the data
   copy, not inline data — emit movq instead of leaq.

Bug 1+2 caused out-param record assignments to overwrite the pointer
with the return value.  Bug 3 caused const-record field assignments
(e.g. FTarget := ATarget in SetTarget) to copy a stack address into the
field instead of the record data.  Both crashed the native-compiled
compiler when using --backend native.

Added E2E test: TestRun_Native_RecordSret_OutParam.
This commit is contained in:
Graeme Geldenhuys 2026-06-10 13:06:10 +01:00
parent 7aca461a16
commit f50f206cfa
2 changed files with 90 additions and 13 deletions

View file

@ -338,7 +338,8 @@ type
methods with Self in %rdi). Only handles the return-capture part. }
procedure EmitRecordRegReturnCapture(const ADestAddr: string;
ARec: TRecordTypeDesc;
AClass: TRecReturnClass);
AClass: TRecReturnClass;
AIsIndirect: Boolean);
{ Emit a call to a function that returns an interface via sret. The 16-byte
buffer (obj+itab) is left on the stack; caller is responsible for loading
the slots and cleaning up (addq $16, %rsp). }
@ -3577,6 +3578,9 @@ begin
begin
if FSretFunc and (TIdentExpr(AExpr).Name = 'Result') then
Self.Emit(Format(#9'movq %s, %%rax', [Self.VarOperand('Result')]))
else if Self.IsLocal(TIdentExpr(AExpr).Name) and
(TIdentExpr(AExpr).ParamMode in [pmRecordValue, pmStaticArrayValue]) then
Self.Emit(Format(#9'movq %s, %%rax', [Self.VarOperand(TIdentExpr(AExpr).Name)]))
else if Self.IsLocal(TIdentExpr(AExpr).Name) then
Self.Emit(Format(#9'leaq %s, %%rax', [Self.VarOperand(TIdentExpr(AExpr).Name)]))
else if TIdentExpr(AExpr).ConstArraySymbol <> '' then
@ -7659,6 +7663,12 @@ begin
TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl),
TFuncCallExpr(Asgn.Expr).Args,
Self.VarOperand('Result'), True)
else if Asgn.IsVarParam then
Self.EmitSretCall(
FuncSymbolOf(TFuncCallExpr(Asgn.Expr)),
TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl),
TFuncCallExpr(Asgn.Expr).Args,
Self.VarOperand(Asgn.Name), True)
else if Self.IsLocal(Asgn.Name) then
Self.EmitSretCall(
FuncSymbolOf(TFuncCallExpr(Asgn.Expr)),
@ -7685,6 +7695,9 @@ begin
if FSretFunc and SameText(Asgn.Name, 'Result') then
Self.EmitMethodSretCall(TMethodCallExpr(Asgn.Expr),
Self.VarOperand('Result'), True)
else if Asgn.IsVarParam then
Self.EmitMethodSretCall(TMethodCallExpr(Asgn.Expr),
Self.VarOperand(Asgn.Name), True)
else if Self.IsLocal(Asgn.Name) then
Self.EmitMethodSretCall(TMethodCallExpr(Asgn.Expr),
Self.VarOperand(Asgn.Name), False)
@ -9853,15 +9866,20 @@ begin
end;
procedure TX86_64Backend.EmitRecordRegReturnCapture(const ADestAddr: string;
ARec: TRecordTypeDesc; AClass: TRecReturnClass);
ARec: TRecordTypeDesc; AClass: TRecReturnClass; AIsIndirect: Boolean);
var
Sz: Integer;
Sz: Integer;
LoadOp: string;
begin
Sz := ARec.TotalSize();
if AIsIndirect then
LoadOp := 'movq'
else
LoadOp := 'leaq';
case AClass of
rcInt1:
begin
Self.Emit(Format(#9'leaq %s, %%rcx', [ADestAddr]));
Self.Emit(Format(#9'%s %s, %%rcx', [LoadOp, ADestAddr]));
case Sz of
1: Self.Emit(#9'movb %al, (%rcx)');
2: Self.Emit(#9'movw %ax, (%rcx)');
@ -9871,32 +9889,43 @@ begin
end;
rcInt2:
begin
Self.Emit(Format(#9'leaq %s, %%rcx', [ADestAddr]));
Self.Emit(Format(#9'%s %s, %%rcx', [LoadOp, ADestAddr]));
Self.Emit(#9'movq %rax, (%rcx)');
Self.Emit(#9'movq %rdx, 8(%rcx)');
end;
rcSSE1:
begin
if Sz = 4 then
Self.Emit(Format(#9'movss %%xmm0, %s', [ADestAddr]))
if AIsIndirect then
begin
Self.Emit(Format(#9'movq %s, %%rcx', [ADestAddr]));
if Sz = 4 then
Self.Emit(#9'movss %xmm0, (%rcx)')
else
Self.Emit(#9'movsd %xmm0, (%rcx)');
end
else
Self.Emit(Format(#9'movsd %%xmm0, %s', [ADestAddr]));
begin
if Sz = 4 then
Self.Emit(Format(#9'movss %%xmm0, %s', [ADestAddr]))
else
Self.Emit(Format(#9'movsd %%xmm0, %s', [ADestAddr]));
end;
end;
rcSSE2:
begin
Self.Emit(Format(#9'leaq %s, %%rcx', [ADestAddr]));
Self.Emit(Format(#9'%s %s, %%rcx', [LoadOp, ADestAddr]));
Self.Emit(#9'movsd %xmm0, (%rcx)');
Self.Emit(#9'movsd %xmm1, 8(%rcx)');
end;
rcIntSSE:
begin
Self.Emit(Format(#9'leaq %s, %%rcx', [ADestAddr]));
Self.Emit(Format(#9'%s %s, %%rcx', [LoadOp, ADestAddr]));
Self.Emit(#9'movq %rax, (%rcx)');
Self.Emit(#9'movsd %xmm0, 8(%rcx)');
end;
rcSSEInt:
begin
Self.Emit(Format(#9'leaq %s, %%rcx', [ADestAddr]));
Self.Emit(Format(#9'%s %s, %%rcx', [LoadOp, ADestAddr]));
Self.Emit(#9'movsd %xmm0, (%rcx)');
Self.Emit(#9'movq %rax, 8(%rcx)');
end;
@ -9933,7 +9962,7 @@ begin
Self.Emit(#9'callq memset');
Self.EmitCall(AFuncSym, ADecl, AArgs);
Self.EmitRecordRegReturnCapture(ASretAddr,
TRecordTypeDesc(ADecl.ResolvedReturnType), RC);
TRecordTypeDesc(ADecl.ResolvedReturnType), RC, ASretIsIndirect);
Exit;
end;
{ Save the destination address in %r10 (caller-saved scratch that survives
@ -10091,7 +10120,7 @@ begin
if FMethodArgExtraStack > 0 then
Self.Emit(Format(#9'addq $%d, %%rsp', [FMethodArgExtraStack]));
Self.EmitRecordRegReturnCapture(ASretAddr,
TRecordTypeDesc(MD.ResolvedReturnType), RC);
TRecordTypeDesc(MD.ResolvedReturnType), RC, ASretIsIndirect);
Exit;
end;

View file

@ -347,6 +347,7 @@ type
procedure TestRun_Native_RecReturn_Nested_RcInt2;
procedure TestRun_Native_RecReturn_Method_RcInt1;
procedure TestRun_Native_RecReturn_ManagedStaysSret;
procedure TestRun_Native_RecordSret_OutParam;
end;
implementation
@ -5176,6 +5177,53 @@ begin
'hello' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_RecordSret_OutParam;
begin
AssertRunsOnAll('''
program P;
type
TOS = (osLinux, osFreeBSD, osWindows);
TCPU = (cpuX86_64, cpuI386, cpuArm64);
TTarget = record
OS: TOS;
CPU: TCPU;
end;
procedure MakeTarget(AOS: TOS; ACPU: TCPU; out ATarget: TTarget);
begin
ATarget.OS := AOS;
ATarget.CPU := ACPU
end;
function HostTarget: TTarget;
begin
MakeTarget(osLinux, cpuX86_64, Result)
end;
function ParseArgs(out S1: string; out S2: string; out B1: Boolean;
out B2: Boolean; out B3: Boolean;
out Target: TTarget): Boolean;
begin
Result := True;
S1 := '';
S2 := '';
B1 := False;
B2 := False;
B3 := False;
Target := HostTarget()
end;
var
S1, S2: string;
B1, B2, B3: Boolean;
T: TTarget;
begin
ParseArgs(S1, S2, B1, B2, B3, T);
if (T.OS = osLinux) and (T.CPU = cpuX86_64) then
WriteLn('linux-x86_64')
else
WriteLn('unknown-unknown')
end.
''',
'linux-x86_64' + LE, 0);
end;
initialization
RegisterTest(TE2ENativeTests);