← board

A function returning a dynamic array is refused on every cross target

Measured

$ ./compiler/pascal26 --target=i386 test/test_dynarray_torture.pas /tmp/x
pascal26:129: error: target i386: only ordinal/pointer/string function results
supported yet

Identical refusal, same cause, on all four:

test native i386 arm32 aarch64 riscv32
test_dynarray_torture ok BUILDFAIL BUILDFAIL BUILDFAIL BUILDFAIL
test_length_dynarray_call ok BUILDFAIL BUILDFAIL BUILDFAIL BUILDFAIL
test_setlength_dynarray_result ok BUILDFAIL BUILDFAIL BUILDFAIL BUILDFAIL

The shape is just:

function MakeArr(n: Integer): array of Integer;
var i: Integer;
begin
  SetLength(MakeArr, n);
  for i := 0 to n - 1 do MakeArr[i] := i + 1;
end;

Cause

Each cross epilogue guards its result load with

if Syms[retSymIdx].IsArray or (Syms[retSymIdx].Kind <> skLocal) or
   not (TypeIsOrdinal(...) or TypeIsFloat(...) or
        (Syms[retSymIdx].TypeKind in [tyAnsiString, tyPointer, tyClass])) then
  Error('target <t>: only ordinal/pointer/string function results supported yet');

IsArray is refused outright. But a DYNAMIC array result is not an aggregate — it is a single pointer-sized handle, exactly like the tyAnsiString case already allowed two lines down, and x86-64 returns it that way. A STATIC array result is the genuinely unsupported case and must keep erroring.

So the fix is to let IsArray and (ArrLen = -1) through and load it at pointer width — not through EmitLoadVar*, which sizes by Syms[].TypeKind and for an array that is the ELEMENT kind (the truncation EmitLoadVarA64 had until 2026-08-21: array of Char came back through a 4-byte ldr w0 with the handle's top half gone).

Ownership: the callee's Result handle transfers to the caller, which is why every epilogue's managed-local cleanup already skips retSymIdx. That carve-out is in place on all four, so this is the load, not the lifetime.

Four copies of one epilogue, again

Same finding as [[bug-a-local-static-array-of-string-never-released-at-scope-exit]] and [[bug-a-no-dyn-array-scope-exit-release-on-four-backends]]: five hand-written epilogues, each missing a different arm. [[refactor-a-the-missing-layer-between-frontends-and-backends]] is where that stops recurring.

Gate

The three tests above pass under tools/run_target.sh on i386 / arm32 / aarch64 / riscv32 with the native output; a static-array result still errors; self-host fixedpoint + tools/gate.sh quick.

RESOLVED 2026-08-21

One arm per cross epilogue, placed before the IsArray refusal:

else if Syms[retSymIdx].IsArray and (Syms[retSymIdx].ArrLen = -1) and
        (Syms[retSymIdx].Kind = skLocal) then

loading the handle at POINTER width by hand rather than through EmitLoadVar* — which sizes by TypeSize(Syms[].TypeKind), and an array's TypeKind is its ELEMENT kind, so array of Char would have come back through a byte load with most of the handle gone. (aarch64 uses EmitLoadVarA64, whose ArrLen = -1 arm was taught pointer width earlier the same day.)

Mirrors x86-64's arm exactly, comment and all — it already documented the same reasoning: "a narrow EmitLoadVar keyed on the element type would truncate (and sign-extend) the pointer."

Static arrays were never the problem

Checked rather than assumed: a function F: array[0..3] of Integer builds and runs correctly on all four cross targets both before and after, because it goes through ABIRetViaHiddenDestProc long before the branch this ticket touched. The IsArray refusal was catching only the dynamic case, which is the one that did not need catching.

Measured

test i386 arm32 aarch64 riscv32
test_dynarray_torture BUILDFAIL -> ok same same same
test_length_dynarray_call BUILDFAIL -> ok same same same
test_setlength_dynarray_result BUILDFAIL -> ok same same same
test_dynarray_result BUILDFAIL -> ok same same same

test_dynarray_result was not in the ticket's list — it turned up in the differential, which is the argument for running one rather than checking the three files the ticket names.

Cross differential

53-test dyn-array + interface family, each target against the native answer, versus the previous commit's baseline: broke 0, fixed 16 (four tests x four targets). The 18 disagreements that remain are all pre-existing interface-ARC and ESP-platform issues untouched by this change.

Gate

tools/gate.sh quick GREEN (self-host fixedpoint byte-identical).

Log

2026-09-02 (frankC) — "every cross-target" meant four of five; xtensa was still refusing

Not a re-opening: the arm this ticket added is correct and stands. Recording that its title and summary over-claimed, because they are what the next reader believes.

Four backends got the epilogue arm — i386, arm32, aarch64, riscv32. Xtensa did not, and xtensa is a cross target. It still answered target xtensa: only ordinal/float/pointer/string function results supported yet for a function returning array of T. Ported at 7cc404961.

What the gap cost, which is larger than a refused test: lib/rtl/sysutils.pas ITSELF would not build for xtensa, because TStringHelper.Split (sysutils.pas:5785) returns a TStringArray. So every Pascal program that touches sysutils was unbuildable for the ESP target. Measured by compiling examples/ across five backends: 5 of 6 programs that build for the other four died on xtensa, all with that one message, none of it mentioning arrays.

Why nothing said so: test_dynarray_result.pas was wired on x86-64 only. The fix for four cross backends was never gated on a single one of them, so a missing fifth could not show up. Now wired on all five (7cc404961).

This is the second time xtensa has been the backend missed by a "cross-target" fix — ir_codegen_xtensa.inc's concat-ownership note records the first, in the same words: "which fixed 'the four cross backends' and never listed xtensa — the seventh backend that a grep for the common spelling does not return." "Every cross-target" in a summary should be read as a claim to check, not a count.