refactor(codegen): hoist record-return ABI classifier into shared blaise.codegen

The SysV/Win64 record-return classifier — ClassifyRecordReturn plus its five
leaf predicates (IsRecordManagedClean, IsRecordAllIntegerLeaves,
IsRecordAllFloatLeaves, IsRecordAllIntOrFloatLeaves, EightbyteIsSSE) — existed
as byte-identical twins in TCodeGenQBE and TNativeBackend (~145 lines each).
This is a drift-prone ABI decision table that both backends must keep in
lockstep; a divergence would silently misclassify a record return on one
backend only.

Move the logic to free functions in blaise.codegen (Recret* — renamed to avoid
colliding with the backend methods, since Blaise resolves an unqualified call
inside a method to the method, not the unit function).  Only the top-level
classifier consults the target OS (the Win64 aggregate rule), so it takes a
TTargetDesc parameter; the leaf predicates are target-independent pure type
walks.  Each backend keeps its method as a one-line delegator, so every
existing Self.X call site is unchanged.

Validated behaviour-preserving: native .s AND QBE IR are byte-identical
pre/post for a record-return program exercising every return class
(rcInt1/rcInt2/rcSSE1/rcSSE2/rcIntSSE/rcSSEInt/rcSret/rcWin64-clean).  All four
self-host fixpoints (QBE, native, internal-assembler, warm-cache) pass; full
suite OK (3853 tests) on both QBE-built and native-built test runners.

Implements item F4 of Andrew's native-codegen de-duplication plan — the
highest-value, lowest-risk cross-backend hoist.
This commit is contained in:
Graeme Geldenhuys 2026-06-27 02:29:19 +01:00
parent 6b99e4db79
commit c8faf1ed24
3 changed files with 210 additions and 271 deletions

View file

@ -163,164 +163,38 @@ begin
FAsm.AppendLine;
end;
{ The record-return ABI classifier and its leaf predicates now live as shared
free functions in blaise.codegen (byte-identical to the QBE backend's former
twin). These methods delegate so existing Self.X call sites are unchanged. }
function TNativeBackend.IsRecordManagedClean(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyString, tyClass, tyInterface, tyDynArray:
Exit;
tyRecord:
if not Self.IsRecordManagedClean(TRecordTypeDesc(F.TypeDesc)) then Exit;
end;
end;
Result := True;
Result := RecretManagedClean(ARec);
end;
function TNativeBackend.IsRecordAllIntegerLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass:
;
tyRecord:
if not Self.IsRecordAllIntegerLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllIntegerLeaves(ARec);
end;
function TNativeBackend.IsRecordAllFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyDouble, tySingle: ;
tyRecord:
if not Self.IsRecordAllFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllFloatLeaves(ARec);
end;
function TNativeBackend.IsRecordAllIntOrFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass,
tyDouble, tySingle: ;
tyRecord:
if not Self.IsRecordAllIntOrFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllIntOrFloatLeaves(ARec);
end;
function TNativeBackend.EightbyteIsSSE(ARec: TRecordTypeDesc;
AStartByte: Integer): Boolean;
var
I, Off: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
Off := F.Offset;
if (Off < AStartByte) or (Off >= AStartByte + 8) then Continue;
case F.TypeDesc.Kind of
tyDouble, tySingle:
Exit(True);
tyRecord:
if Self.EightbyteIsSSE(TRecordTypeDesc(F.TypeDesc),
AStartByte - Off) then Exit(True);
end;
end;
Result := RecretEightbyteIsSSE(ARec, AStartByte);
end;
function TNativeBackend.ClassifyRecordReturn(ARec: TRecordTypeDesc): TRecReturnClass;
var
Sz: Integer;
Eb0SSE, Eb1SSE: Boolean;
begin
Result := rcSret;
if (ARec = nil) or (ARec.Kind <> tyRecord) then Exit;
if ARec.Fields.Count = 0 then Exit;
if not Self.IsRecordManagedClean(ARec) then Exit;
Sz := ARec.TotalSize();
if FTarget.OS = osWindows then
begin
if Self.IsRecordAllIntOrFloatLeaves(ARec) then
Result := rcWin64Agg;
Exit;
end;
if Self.IsRecordAllIntegerLeaves(ARec) then
begin
case Sz of
1, 2, 4, 8: Result := rcInt1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcInt2;
end;
Exit;
end;
if Self.IsRecordAllFloatLeaves(ARec) then
begin
case Sz of
4, 8: Result := rcSSE1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcSSE2;
end;
Exit;
end;
if not Self.IsRecordAllIntOrFloatLeaves(ARec) then Exit;
case Sz of
9, 10, 11, 12, 13, 14, 15, 16:
begin
Eb0SSE := Self.EightbyteIsSSE(ARec, 0);
Eb1SSE := Self.EightbyteIsSSE(ARec, 8);
if (not Eb0SSE) and Eb1SSE then Result := rcIntSSE
else if Eb0SSE and (not Eb1SSE) then Result := rcSSEInt;
end;
end;
Result := RecretClassify(ARec, FTarget);
end;
function TNativeBackend.GenerateProgram(AProg: TProgram): string;

View file

@ -27,7 +27,7 @@ unit blaise.codegen;
interface
uses
uAST, uSymbolTable, uDebugFacts;
uAST, uSymbolTable, uDebugFacts, blaise.codegen.target;
type
TRecReturnClass = (
@ -91,6 +91,196 @@ type
function GetOutput: string;
end;
{ ----------------------------------------------------------------------
Shared System V / Win64 record-return ABI classifier.
Both the QBE backend and the native x86-64 backend must agree, byte for
byte, on how a record-typed return value is passed (sret vs register, and
which register class). The decision is a pure walk over the record's field
layout plus the target OS — no backend state — so it lives here as free
functions that both backends call, instead of being carried as two
drift-prone twins. Only RecretClassify consults the target (the Win64
aggregate rule); the leaf predicates are target-independent type walks. }
{ True when ARec (and every nested record) contains no managed fields
(string / class / interface / dynamic array) — a precondition for any
register return. }
function RecretManagedClean(ARec: TRecordTypeDesc): Boolean;
{ True when every leaf field is an integer-class scalar (or a nested record
thereof). }
function RecretAllIntegerLeaves(ARec: TRecordTypeDesc): Boolean;
{ True when every leaf field is a float (Double/Single) or a nested record
thereof. }
function RecretAllFloatLeaves(ARec: TRecordTypeDesc): Boolean;
{ True when every leaf field is an integer- or float-class scalar. }
function RecretAllIntOrFloatLeaves(ARec: TRecordTypeDesc): Boolean;
{ True when the eightbyte starting at AStartByte holds any float leaf — i.e.
the eightbyte is SSE-classified under the SysV mixed-class rules. }
function RecretEightbyteIsSSE(ARec: TRecordTypeDesc; AStartByte: Integer): Boolean;
{ Classify ARec's return-passing for ATarget's OS. }
function RecretClassify(ARec: TRecordTypeDesc;
const ATarget: TTargetDesc): TRecReturnClass;
implementation
function RecretManagedClean(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyString, tyClass, tyInterface, tyDynArray:
Exit;
tyRecord:
if not RecretManagedClean(TRecordTypeDesc(F.TypeDesc)) then Exit;
end;
end;
Result := True;
end;
function RecretAllIntegerLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass:
;
tyRecord:
if not RecretAllIntegerLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
end;
function RecretAllFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyDouble, tySingle: ;
tyRecord:
if not RecretAllFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
end;
function RecretAllIntOrFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass,
tyDouble, tySingle: ;
tyRecord:
if not RecretAllIntOrFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
end;
function RecretEightbyteIsSSE(ARec: TRecordTypeDesc; AStartByte: Integer): Boolean;
var
I, Off: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
Off := F.Offset;
if (Off < AStartByte) or (Off >= AStartByte + 8) then Continue;
case F.TypeDesc.Kind of
tyDouble, tySingle:
Exit(True);
tyRecord:
if RecretEightbyteIsSSE(TRecordTypeDesc(F.TypeDesc),
AStartByte - Off) then Exit(True);
end;
end;
end;
function RecretClassify(ARec: TRecordTypeDesc;
const ATarget: TTargetDesc): TRecReturnClass;
var
Sz: Integer;
Eb0SSE, Eb1SSE: Boolean;
begin
Result := rcSret;
if (ARec = nil) or (ARec.Kind <> tyRecord) then Exit;
if ARec.Fields.Count = 0 then Exit;
if not RecretManagedClean(ARec) then Exit;
Sz := ARec.TotalSize();
if ATarget.OS = osWindows then
begin
if RecretAllIntOrFloatLeaves(ARec) then
Result := rcWin64Agg;
Exit;
end;
if RecretAllIntegerLeaves(ARec) then
begin
case Sz of
1, 2, 4, 8: Result := rcInt1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcInt2;
end;
Exit;
end;
if RecretAllFloatLeaves(ARec) then
begin
case Sz of
4, 8: Result := rcSSE1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcSSE2;
end;
Exit;
end;
if not RecretAllIntOrFloatLeaves(ARec) then Exit;
case Sz of
9, 10, 11, 12, 13, 14, 15, 16:
begin
Eb0SSE := RecretEightbyteIsSSE(ARec, 0);
Eb1SSE := RecretEightbyteIsSSE(ARec, 8);
if (not Eb0SSE) and Eb1SSE then Result := rcIntSSE
else if Eb0SSE and (not Eb1SSE) then Result := rcSSEInt;
end;
end;
end;
end.

View file

@ -5275,164 +5275,39 @@ begin
end;
end;
{ The record-return ABI classifier and its leaf predicates now live as shared
free functions in blaise.codegen (byte-identical to the native backend's
former twin). These methods delegate so existing Self.X call sites are
unchanged. }
function TCodeGenQBE.IsRecordManagedClean(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyString, tyClass, tyInterface, tyDynArray:
Exit;
tyRecord:
if not Self.IsRecordManagedClean(TRecordTypeDesc(F.TypeDesc)) then Exit;
end;
end;
Result := True;
Result := RecretManagedClean(ARec);
end;
function TCodeGenQBE.IsRecordAllIntegerLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass:
;
tyRecord:
if not Self.IsRecordAllIntegerLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllIntegerLeaves(ARec);
end;
function TCodeGenQBE.IsRecordAllFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyDouble, tySingle: ;
tyRecord:
if not Self.IsRecordAllFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllFloatLeaves(ARec);
end;
function TCodeGenQBE.IsRecordAllIntOrFloatLeaves(ARec: TRecordTypeDesc): Boolean;
var
I: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
case F.TypeDesc.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean,
tyEnum, tyPointer, tyProcedural, tyMetaClass,
tyDouble, tySingle: ;
tyRecord:
if not Self.IsRecordAllIntOrFloatLeaves(TRecordTypeDesc(F.TypeDesc)) then Exit;
else
Exit;
end;
end;
Result := True;
Result := RecretAllIntOrFloatLeaves(ARec);
end;
function TCodeGenQBE.EightbyteIsSSE(ARec: TRecordTypeDesc;
AStartByte: Integer): Boolean;
var
I, Off: Integer;
F: TFieldInfo;
begin
Result := False;
if ARec = nil then Exit;
for I := 0 to ARec.Fields.Count - 1 do
begin
F := TFieldInfo(ARec.Fields.Items[I]);
Off := F.Offset;
if (Off < AStartByte) or (Off >= AStartByte + 8) then Continue;
case F.TypeDesc.Kind of
tyDouble, tySingle:
Exit(True);
tyRecord:
if Self.EightbyteIsSSE(TRecordTypeDesc(F.TypeDesc),
AStartByte - Off) then Exit(True);
end;
end;
Result := RecretEightbyteIsSSE(ARec, AStartByte);
end;
function TCodeGenQBE.ClassifyRecordReturn(ARec: TRecordTypeDesc): TRecReturnClass;
var
Sz: Integer;
Eb0SSE, Eb1SSE: Boolean;
begin
Result := rcSret;
if (ARec = nil) or (ARec.Kind <> tyRecord) then Exit;
if ARec.Fields.Count = 0 then Exit;
if not Self.IsRecordManagedClean(ARec) then Exit;
Sz := ARec.TotalSize();
if GTarget.OS = osWindows then
begin
if Self.IsRecordAllIntOrFloatLeaves(ARec) then
Result := rcWin64Agg;
Exit;
end;
if Self.IsRecordAllIntegerLeaves(ARec) then
begin
case Sz of
1, 2, 4, 8: Result := rcInt1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcInt2;
end;
Exit;
end;
if Self.IsRecordAllFloatLeaves(ARec) then
begin
case Sz of
4, 8: Result := rcSSE1;
9, 10, 11, 12, 13, 14, 15, 16: Result := rcSSE2;
end;
Exit;
end;
if not Self.IsRecordAllIntOrFloatLeaves(ARec) then Exit;
case Sz of
9, 10, 11, 12, 13, 14, 15, 16:
begin
Eb0SSE := Self.EightbyteIsSSE(ARec, 0);
Eb1SSE := Self.EightbyteIsSSE(ARec, 8);
if (not Eb0SSE) and Eb1SSE then Result := rcIntSSE
else if Eb0SSE and (not Eb1SSE) then Result := rcSSEInt;
end;
end;
Result := RecretClassify(ARec, GTarget);
end;
procedure TCodeGenQBE.EmitRecordReturnSignature(var ASig: string;