fix(codegen): return fixed-size static array by value via sret (#112)

Functions returning a static array >8 bytes now use the sret ABI on
both QBE and native x86-64 backends: the caller passes the destination
buffer as a hidden first argument, the callee writes through it, and
returns void.  Previously only the first 8 bytes survived (QBE
segfaulted, native rejected the function outright).

Key changes:
- QBE: IsRecordCall, signature, prologue, epilogue, and assignment
  dispatch all extended to handle tyStaticArray alongside tyRecord
- Native: allowed return type guard, BuildFrame FSretFunc, sret
  call-site dispatch, and subscript-assign Result indirection all
  extended for tyStaticArray
This commit is contained in:
Graeme Geldenhuys 2026-06-17 15:50:22 +01:00
parent d4a066aa93
commit cdd1d0cbe2
4 changed files with 138 additions and 31 deletions

View file

@ -3547,6 +3547,11 @@ begin
FSretFunc := True;
Self.AddSlot('Result', nil, Offset);
end
else if ADecl.ResolvedReturnType.Kind = tyStaticArray then
begin
FSretFunc := True;
Self.AddSlot('Result', nil, Offset);
end
else if ADecl.ResolvedReturnType.Kind = tyRecord then
begin
FRecRetClass := ClassifyRecordReturn(
@ -9610,11 +9615,13 @@ begin
record/jumbo-set-returning call. Pass the destination buffer address as
the hidden first arg (%rdi). }
if (Asgn.ResolvedLhsType <> nil) and
((Asgn.ResolvedLhsType.Kind = tyRecord) or IsJumboSet(Asgn.ResolvedLhsType)) and
((Asgn.ResolvedLhsType.Kind in [tyRecord, tyStaticArray]) or
IsJumboSet(Asgn.ResolvedLhsType)) and
(Asgn.Expr is TFuncCallExpr) and
(TFuncCallExpr(Asgn.Expr).ResolvedDecl <> nil) and
(TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl).ResolvedReturnType <> nil) and
((TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl).ResolvedReturnType.Kind = tyRecord) or
((TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl).ResolvedReturnType.Kind in
[tyRecord, tyStaticArray]) or
IsJumboSet(TMethodDecl(TFuncCallExpr(Asgn.Expr).ResolvedDecl).ResolvedReturnType)) then
begin
{ For a local jumbo set the dest is leaq'd; EmitSretCall's caller passes
@ -9651,11 +9658,13 @@ begin
{ sret assignment: method call returning a record (or jumbo set).
%rdi = sret ptr, %rsi = Self, %rdx.. = user args. }
if (Asgn.ResolvedLhsType <> nil) and
((Asgn.ResolvedLhsType.Kind = tyRecord) or IsJumboSet(Asgn.ResolvedLhsType)) and
((Asgn.ResolvedLhsType.Kind in [tyRecord, tyStaticArray]) or
IsJumboSet(Asgn.ResolvedLhsType)) and
(Asgn.Expr is TMethodCallExpr) and
(TMethodCallExpr(Asgn.Expr).ResolvedMethod <> nil) and
(TMethodDecl(TMethodCallExpr(Asgn.Expr).ResolvedMethod).ResolvedReturnType <> nil) and
((TMethodDecl(TMethodCallExpr(Asgn.Expr).ResolvedMethod).ResolvedReturnType.Kind = tyRecord) or
((TMethodDecl(TMethodCallExpr(Asgn.Expr).ResolvedMethod).ResolvedReturnType.Kind in
[tyRecord, tyStaticArray]) or
IsJumboSet(TMethodDecl(TMethodCallExpr(Asgn.Expr).ResolvedMethod).ResolvedReturnType)) then
begin
if FSretFunc and SameText(Asgn.Name, 'Result') then
@ -11480,8 +11489,8 @@ begin
if SSA.ImplicitFieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [SSA.ImplicitFieldInfo.Offset]));
end
else if SSA.IsVarParam then
{ var/out param: the slot HOLDS the caller array's address. }
else if SSA.IsVarParam or
(FSretFunc and SameText(SSA.ArrayName, 'Result')) then
Self.Emit(Format(#9'movq %s, %%rcx', [Self.VarOperand(SSA.ArrayName)]))
else if Self.IsLocal(SSA.ArrayName) then
Self.Emit(Format(#9'leaq %s, %%rcx', [Self.VarOperand(SSA.ArrayName)]))
@ -11536,8 +11545,8 @@ begin
if SSA.ImplicitFieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [SSA.ImplicitFieldInfo.Offset]));
end
else if SSA.IsVarParam then
{ var/out param: the slot HOLDS the caller array's address. }
else if SSA.IsVarParam or
(FSretFunc and SameText(SSA.ArrayName, 'Result')) then
Self.Emit(Format(#9'movq %s, %%rcx', [Self.VarOperand(SSA.ArrayName)]))
else if Self.IsLocal(SSA.ArrayName) then
Self.Emit(Format(#9'leaq %s, %%rcx', [Self.VarOperand(SSA.ArrayName)]))
@ -13930,9 +13939,10 @@ begin
if (ADecl.ResolvedReturnType <> nil) and
not IsIntFamily(ADecl.ResolvedReturnType) and
not IsFloatFamily(ADecl.ResolvedReturnType) and
not (ADecl.ResolvedReturnType.Kind in [tyRecord, tyString, tyPChar, tyPointer,
tyClass, tyMetaClass, tyDynArray,
tyInterface, tyProcedural, tySet]) then
not (ADecl.ResolvedReturnType.Kind in [tyRecord, tyStaticArray, tyString,
tyPChar, tyPointer, tyClass, tyMetaClass,
tyDynArray, tyInterface, tyProcedural,
tySet]) then
raise ENativeCodeGenError.Create(
'native backend: unsupported return type (function ' + ADecl.Name + ')');

View file

@ -4249,16 +4249,18 @@ begin
[VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp,
TSetTypeDesc(AAssign.ResolvedLhsType).RawSize()]));
end
else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyStaticArray) and
IsRecordCall(AAssign.Expr) then
begin
EmitRecordCallSret(AAssign.Expr, VarRef(AAssign.Name, AAssign.IsGlobal));
end
else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyRecord) then
begin
{ Record assignment (non-var-param LHS — the var/out case is handled in the
IsVarParam branch above): use sret (directly into dest) or field-by-field
copy. }
ClassRT := TRecordTypeDesc(AAssign.ResolvedLhsType);
if IsRecordCall(AAssign.Expr) then
begin
{ Release old ARC fields, zero the slot, then call function with dest as sret }
EmitRecordReleaseFields(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal));
EmitLine(Format(' call $memset(l %s, w 0, l %d)',
[VarRef(AAssign.Name, AAssign.IsGlobal), ClassRT.TotalSize()]));
@ -4266,7 +4268,6 @@ begin
end
else
begin
{ RHS is a record variable: get its address then ARC-copy field-by-field }
ValTemp := EmitExpr(AAssign.Expr);
EmitRecordCopy(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp);
end;
@ -4857,14 +4858,14 @@ begin
if TFuncCallExpr(AExpr).ResolvedDecl = nil then Exit;
MDecl := TMethodDecl(TFuncCallExpr(AExpr).ResolvedDecl);
Result := (MDecl.ResolvedReturnType <> nil) and
(MDecl.ResolvedReturnType.Kind = tyRecord);
(MDecl.ResolvedReturnType.Kind in [tyRecord, tyStaticArray]);
end
else if AExpr is TMethodCallExpr then
begin
if TMethodCallExpr(AExpr).ResolvedMethod = nil then Exit;
MDecl := TMethodDecl(TMethodCallExpr(AExpr).ResolvedMethod);
Result := (MDecl.ResolvedReturnType <> nil) and
(MDecl.ResolvedReturnType.Kind = tyRecord);
(MDecl.ResolvedReturnType.Kind in [tyRecord, tyStaticArray]);
end
else if AExpr is TFieldAccessExpr then
begin
@ -4873,7 +4874,7 @@ begin
if FldA.ResolvedMethod = nil then Exit;
MDecl := TMethodDecl(FldA.ResolvedMethod);
Result := (MDecl.ResolvedReturnType <> nil) and
(MDecl.ResolvedReturnType.Kind = tyRecord);
(MDecl.ResolvedReturnType.Kind in [tyRecord, tyStaticArray]);
end;
end;
@ -5309,7 +5310,8 @@ begin
{ Jumbo set returns always use the plain sret path (a hidden dest pointer +
void return); they share this call helper with records but must not go
through ClassifyRecordReturn (which reads record-specific size fields). }
if (ARetType <> nil) and (TTypeDesc(ARetType).Kind = tySet) then
if (ARetType <> nil) and
(TTypeDesc(ARetType).Kind in [tySet, tyStaticArray]) then
begin
if AVisibleArgs <> '' then
ArgLine := Format('l %s, %s', [ADestAddr, AVisibleArgs])
@ -7016,7 +7018,7 @@ begin
RC := Self.ClassifyRecordReturn(TRecordTypeDesc(AMethod.ResolvedReturnType));
Sig := 'l %_par_Self';
if IsFunc and ((AMethod.ResolvedReturnType.Kind = tyInterface) or
if IsFunc and ((AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo())) then
Sig := 'l %_par__sret, l %_par_Self'
@ -7046,10 +7048,9 @@ begin
if IsFunc then
begin
RetQType := QbeTypeOf(AMethod.ResolvedReturnType);
if (AMethod.ResolvedReturnType.Kind = tyInterface) or
if (AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo()) then
{ sret aggregate return — void function. }
EmitLine(Format('%sfunction %s(%s) {', [ExportPrefix(), FuncName, Sig]))
else if AMethod.ResolvedReturnType.Kind = tyRecord then
begin
@ -7116,10 +7117,15 @@ begin
begin
if AMethod.ResolvedReturnType.Kind = tyRecord then
EmitRecordReturnPrologue(TRecordTypeDesc(AMethod.ResolvedReturnType), RC)
else if AMethod.ResolvedReturnType.Kind = tyStaticArray then
begin
EmitLine(' %_var_Result =l copy %_par__sret');
EmitLine(Format(' call $memset(l %%_var_Result, w 0, l %d)',
[AMethod.ResolvedReturnType.ByteSize()]));
end
else if (AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo() then
begin
{ sret: jumbo-set Result aliases the caller's buffer; zero-init it. }
EmitLine(' %_var_Result =l copy %_par__sret');
EmitLine(Format(' call $memset(l %%_var_Result, w 0, l %d)',
[TSetTypeDesc(AMethod.ResolvedReturnType).RawSize()]));
@ -7200,10 +7206,9 @@ begin
begin
if AMethod.ResolvedReturnType.Kind = tyRecord then
EmitRecordReturnEpilogue(TRecordTypeDesc(AMethod.ResolvedReturnType), RC)
else if (AMethod.ResolvedReturnType.Kind = tyInterface) or
else if (AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo()) then
{ sret aggregate return — written through %_par__sret; return void. }
EmitLine(' ret')
else
begin
@ -7965,12 +7970,12 @@ begin
if IsFunc then
begin
RetQType := QbeTypeOf(ADecl.ResolvedReturnType);
if (ADecl.ResolvedReturnType.Kind = tyInterface) or
if (ADecl.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((ADecl.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(ADecl.ResolvedReturnType).IsJumbo()) then
begin
{ Interface and jumbo-set returns use a hidden sret pointer (the callee
writes the aggregate through it) and return void. }
{ Interface, static-array, and jumbo-set returns use a hidden sret
pointer (the callee writes the aggregate through it) and return void. }
if Sig <> '' then Sig := 'l %_par__sret, ' + Sig
else Sig := 'l %_par__sret';
EmitLine(Format('%sfunction %s(%s) {', [Prefix, FuncName, Sig]));
@ -8124,6 +8129,12 @@ begin
begin
if ADecl.ResolvedReturnType.Kind = tyRecord then
EmitRecordReturnPrologue(TRecordTypeDesc(ADecl.ResolvedReturnType), RC)
else if ADecl.ResolvedReturnType.Kind = tyStaticArray then
begin
EmitLine(' %_var_Result =l copy %_par__sret');
EmitLine(Format(' call $memset(l %%_var_Result, w 0, l %d)',
[ADecl.ResolvedReturnType.ByteSize()]));
end
else if (ADecl.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(ADecl.ResolvedReturnType).IsJumbo() then
begin
@ -8216,10 +8227,9 @@ begin
begin
if ADecl.ResolvedReturnType.Kind = tyRecord then
EmitRecordReturnEpilogue(TRecordTypeDesc(ADecl.ResolvedReturnType), RC)
else if (ADecl.ResolvedReturnType.Kind = tyInterface) or
else if (ADecl.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((ADecl.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(ADecl.ResolvedReturnType).IsJumbo()) then
{ sret: the aggregate was written through %_par__sret — return void. }
EmitLine(' ret')
else
begin

View file

@ -61,6 +61,10 @@ type
{ Named-type alias array const (GitHub #113) }
procedure TestRun_NamedArrayAlias_IntConst;
{ Static array return by value (GitHub #112) }
procedure TestRun_StaticArrayReturn_12Bytes;
procedure TestRun_StaticArrayReturn_16Bytes;
{ Multi-arg WriteLn }
procedure TestRun_WriteLn_MultipleArgs_MixedTypes;
end;
@ -612,6 +616,52 @@ end;
{ ---- Multi-arg WriteLn ---- }
procedure TE2EGapTests.TestRun_StaticArrayReturn_12Bytes;
const
Src =
'''
program P;
type TVec3 = array[0..2] of Integer;
function MakeVec(A, B, C: Integer): TVec3;
begin
Result[0] := A;
Result[1] := B;
Result[2] := C
end;
var V: TVec3;
begin
V := MakeVec(10, 20, 30);
WriteLn(V[0]);
WriteLn(V[1]);
WriteLn(V[2])
end.
''';
begin
AssertRunsOnAll(Src, '10' + Chr(10) + '20' + Chr(10) + '30' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_StaticArrayReturn_16Bytes;
const
Src =
'''
program P;
type TPair = array[0..1] of Int64;
function MakePair(A, B: Int64): TPair;
begin
Result[0] := A;
Result[1] := B
end;
var P2: TPair;
begin
P2 := MakePair(111, 222);
WriteLn(P2[0]);
WriteLn(P2[1])
end.
''';
begin
AssertRunsOnAll(Src, '111' + Chr(10) + '222' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_WriteLn_MultipleArgs_MixedTypes;
const Src = '''
program T;

View file

@ -95,6 +95,12 @@ type
procedure TestCodegen_MultiDim_CommaWrite_EmitsRowOffset;
procedure TestCodegen_MultiDim_ChainedWrite_SameAsComma;
procedure TestCodegen_MultiDim_CommaRead_NestedSubscript;
{ ------------------------------------------------------------------ }
{ Function returning static array by value (issue #112) }
{ ------------------------------------------------------------------ }
procedure TestCodegen_ReturnStaticArray_SretParam;
procedure TestCodegen_ReturnStaticArray_VoidReturn;
end;
implementation
@ -847,6 +853,37 @@ begin
AssertTrue('emits a word load for the element', Pos('loadw', IR) >= 0);
end;
const
SrcArrayReturn =
'''
program P;
type TVec3 = array[0..2] of Integer;
function MakeVec(A, B, C: Integer): TVec3;
begin
Result[0] := A;
Result[1] := B;
Result[2] := C
end;
var V: TVec3;
begin
V := MakeVec(1, 2, 3)
end.
''';
procedure TStaticArrayTests.TestCodegen_ReturnStaticArray_SretParam;
var IR: string;
begin
IR := GenIR(SrcArrayReturn);
AssertTrue('sret hidden param', Pos('_par__sret', IR) >= 0);
end;
procedure TStaticArrayTests.TestCodegen_ReturnStaticArray_VoidReturn;
var IR: string;
begin
IR := GenIR(SrcArrayReturn);
AssertTrue('void return', Pos('ret', IR) >= 0);
end;
initialization
RegisterTest(TStaticArrayTests);