SetLength on a var dynamic-array parameter (cross-cutting ABI)
- Type: feature
- Status: done
- Owner: —
- Opened: 2026-06-19 (spun out of feature-cross-target-feature-parity — the matrix had this mislabelled as "x86-64 ✓ / cross ✗"; investigation showed it is broken on all four targets, so it is a general feature gap, not a cross port)
Summary
SetLength on a dynamic array passed by reference (procedure P(var a: TArr)
where TArr = array of T) does not resize the caller's array on any target,
including x86-64. The earlier parity matrix marked x86-64 ✓ purely because the
x86-64 backend has code for the case (the -102 IsRef branch in
ir_codegen.inc) while the cross backends carry an explicit
Error('… SetLength on a var-array parameter not yet supported'). Neither path
was ever behaviourally tested. A minimal probe:
type TIntArr = array of Integer;
procedure GrowI(var a: TIntArr; n: Integer); begin SetLength(a, n); end;
var a: TIntArr;
begin GrowI(a, 4); writeln(Length(a)); end. { prints 0, then SIGSEGV on x86-64 }
Root cause (the real, cross-cutting issue)
Two independent problems, the second structural:
-
Misclassification (parser).
AllocParam(symtab.inc) sets every array parameter'sArrLen := 1000(the open-array marker), never-1. TheSetLengthclassifier inparser.incroutes to the dynamic-array path (-102) only whenSyms[idx].IsArray and (Syms[idx].ArrLen = -1), so avardynamic-array param falls through to the string path (-101) and is miscompiled. The named-array-type param branch (parser.inc~7621) knows the type is dynamic (ArrTypeIsDyn[paramAi]) but discards that fact. -
Contradictory ABI (the hard part). Even if classified correctly, the two relevant conventions disagree:
- How the argument is passed:
ParseCallArg/ the IR call-arg lowering pass a by-ref or array param as the open-array data pointer (the borrowed heap block). This is whyLength(a)and indexing already work on avardynamic-array param (verified: a fill-through-var test reads/writes the caller's elements correctly on x86-64). - What
SetLengthneeds: to resize and publish a new handle back to the caller, the callee needs the address of the caller's array slot (&caller_slot), not the data pointer. The x86-64-102IsRefbranch is written assuming&caller_slot(it doesmov rsi,[rbp+off]thenmov rsi,[rsi]), which is simply not what the call site passes → garbage.
These two cannot both hold for the same single pointer slot. Managed
AnsiStringvar params already resolve this by using the by-ref-handle ABI consistently (the slot holds&caller_slot; the read path derefs once — see theIR_LEAwrite-mode special case inir_codegen.incandtest_managed_setlength_var). Dynamic arrays would need the same treatment: mark such paramsArrLen = -1, pass&caller_slotat every call site, and thread the extra deref throughLength/ indexing / read /SetLengthon all four targets. - How the argument is passed:
Decision (LOCKED 2026-06-19) — mirror FPC: split the two param forms by declaration
The two array-param "kinds" are not polymorphic over each other and must be distinguished at the declaration, exactly as FPC does. No monomorphization, no static→dynamic jacket, no up/down-typing, no copy.
| Declaration form | Kind | ABI (the slot holds) | Resizable (SetLength) |
Accepts |
|---|---|---|---|---|
array of T (literal in the param list) |
open array | borrowed data pointer (+ high, when wired) | no — hard error (matches FPC) | static array, dynamic array, or single element |
named TDynArr = array of T |
dynamic-array param | by value: the handle; by var/out: &caller_slot |
yes | only a dynamic array of TDynArr |
Why this dodges every trap (recorded so we don't relitigate):
- No jacket / no overhead — open arrays keep borrowing the data pointer; fixed-memory apps unaffected. The open-array path is left untouched.
- No double-compile / no coroutine sabotage — each form has exactly one ABI; nothing monomorphizes, so the stackless/spawn transform still sees one body.
- No up/down-type + copy — the resizable form accepts only a matching
dynamic-array handle; a static array passed to a
var TDynArrparam is a type mismatch rejected at the call (as in FPC), so no conversion ever runs. - No "chicken error on mixed types" — splitting by declaration removes the ambiguity that would have forced it.
The resizable form reuses the managed-AnsiString-var-param machinery as its
template (the slot holds &caller_slot; the read path derefs once — see the
IR_LEA write-mode special case in ir_codegen.inc and
test_managed_setlength_var).
Implementation plan (4 targets; mechanical, but real — do in a clean session)
Pre-flight: confirm compiler.pas passes no named dynamic-array type by
var/out (grep the param decls). If true, the self-host fixedpoint cannot
regress from this change; if false, those call sites convert to the new ABI and
must be re-validated. (Open-array array of T params in compiler.pas are
unaffected — their path does not change.)
- Parser — classify (the foundational fix). In the named-array-type param
branch (
parser.inc~7621), whenArrTypeIsDyn[paramAi], mark the param a true dynamic array: set its symbolArrLen = -1(andSymDynDepth/element type) instead of lettingAllocParamstamp the open-arrayArrLen = 1000. TheSetLengthclassifier (parser.inc~5584) then routes it to-102automatically. Openarray of Tliteral params stayArrLen = 1000. - Call site — pass
&caller_slot. For avar/outparam whose type is a named dynamic array, the IR call-arg lowering must pass the address of the caller's slot (not the borrowed data pointer). For a by-value dynamic-array param, pass the handle (current behaviour is fine). Mirror how managedAnsiStringvar args are already lowered. - Read paths — one extra deref for the by-ref dynarray param. Thread the
IsRef-param deref (slot → caller_slot → handle) throughLength, indexing, and element load/store, on all four backends — copy the shape of the existing managed-AnsiString-varIR_LEAread/write gate (ir_codegen.inc:1661-1679and the three cross equivalents). SetLength(-102). The x86-64IsRefbranch (ir_codegen.inc:3066) already assumes&caller_slot— once (2) actually passes that, it works. Replace the cross backends'not yet supportedguards with the&caller_slotderef +PXXDynSetLen(slotAddr, n, desc)call (the speculative cross edits written + reverted on 2026-06-19 are the right shape; re-derive them against the corrected ABI).- Open-array
SetLengthstays a hard error on all four targets (you cannotSetLengthan open array — FPC errors too). Make the message say "declare the param as a named dynamic-array type to resize it."
Acceptance
SetLength(a, n) inside procedure P(var a: TDynArr) resizes the caller's array
and preserves min(old,new) elements (grow / shrink / zero), with Length and
indexing consistent, on all four hosted targets — output-equal to a reference
(test_cross_setlen_varparam, int + AnsiString element types) and byte-identical
self-host + cross-bootstrap preserved. SetLength on an array of T open-array
param errors cleanly on all four.
Log
- 2026-06-19 — opened; root-caused both the misclassification and the ABI contradiction. No code changed (a speculative cross port was written, then reverted once the x86-64 path was found equally broken).
- 2026-06-19 — design LOCKED (with the user): mirror FPC by splitting the two param forms by declaration (open array = non-resizable fat ptr; named dynamic-array type = resizable by-ref-handle ABI). No monomorphization / jacket / copy. Wrote the 5-step implementation plan above. Ready to implement in a clean session; status stays backlog until then.
- 2026-06-19 — DONE, all 5 steps, byte-identical on all four hosted targets.
Pre-flight confirmed
compiler.pasdeclares no named dynamic-array param (no self-host regression risk). Implementation:- Parser (
parser.inc~7621): named-array param branch now readsArrTypeIsDyn/ArrTypeDynDepthinto a new per-parampDynDepth; the alloc loop stampsArrLen = -1+SymDynDepth(and persists the depth in a newProcParamDynDepth[pi*16+j]array, since param syms are reused across procs). Openarray of Tliteral params stayArrLen = 1000.SetLengthclassifier (parser.inc~5584) now hard-errors on a non-dynamic array target ("declare the parameter as a named dynamic-array type to resize it"). - Call site (
ir.incIRLowerCallArg): a by-ref arg whose target param hasProcParamDynDepth>0is passed viaIR_SLOTADDR(= &caller_slot), not theIRLowerAddress→IR_LEA handle value. A forwarded by-ref param takes the normal address path. - Read paths — one extra deref for the by-ref dynarray param, per backend:
- x86-64 (
ir_codegen.incIR_LEA): write mode →&caller_slot(one load, for COW/SetLength); read mode → data ptr (second load). - i386 / arm32: no COW; IR_LEA always loads to the data ptr (two loads for
by-ref). SetLength reads the frame slot directly, then one extra load to
reach
&caller_slot. - aarch64:
EmitLoadVarAddrA64already bakes the by-ref deref (yields&caller_slot), so the existing single ldr already reaches the data ptr — no extra deref, and SetLength uses&caller_slotas-is. (Initial port over-derefed here; fixed.)
- x86-64 (
SetLength(-102): x86-64 IsRef branch already published to&caller_slot; i386/aarch64/arm32not yet supportedguards replaced with the&caller_slotderef + existingPXXDynSetLen(slotAddr, n, desc)call.- Open-array
SetLengthis a clean hard error on all four. Acceptance met:test_cross_setlen_varparam(int + AnsiString element types, grow/shrink/zero) output-equal to FPC on all four hosted targets; wired into test-core + the i386/aarch64/arm32 cross suites.make testbyte-identical fixedpoint +--threadsafe;make cross-bootstrapbyte-identical on all 3. Landed in commit 15a70de.
- Parser (