← board

SetLength on a var dynamic-array parameter (cross-cutting ABI)

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:

  1. Misclassification (parser). AllocParam (symtab.inc) sets every array parameter's ArrLen := 1000 (the open-array marker), never -1. The SetLength classifier in parser.inc routes to the dynamic-array path (-102) only when Syms[idx].IsArray and (Syms[idx].ArrLen = -1), so a var dynamic-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.

  2. 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 why Length(a) and indexing already work on a var dynamic-array param (verified: a fill-through-var test reads/writes the caller's elements correctly on x86-64).
    • What SetLength needs: 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 -102 IsRef branch is written assuming &caller_slot (it does mov rsi,[rbp+off] then mov rsi,[rsi]), which is simply not what the call site passes → garbage.

    These two cannot both hold for the same single pointer slot. Managed AnsiString var params already resolve this by using the by-ref-handle ABI consistently (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). Dynamic arrays would need the same treatment: mark such params ArrLen = -1, pass &caller_slot at every call site, and thread the extra deref through Length / indexing / read / SetLength on all four targets.

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):

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.)

  1. Parser — classify (the foundational fix). In the named-array-type param branch (parser.inc ~7621), when ArrTypeIsDyn[paramAi], mark the param a true dynamic array: set its symbol ArrLen = -1 (and SymDynDepth/element type) instead of letting AllocParam stamp the open-array ArrLen = 1000. The SetLength classifier (parser.inc ~5584) then routes it to -102 automatically. Open array of T literal params stay ArrLen = 1000.
  2. Call site — pass &caller_slot. For a var/out param 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 managed AnsiString var args are already lowered.
  3. Read paths — one extra deref for the by-ref dynarray param. Thread the IsRef-param deref (slot → caller_slot → handle) through Length, indexing, and element load/store, on all four backends — copy the shape of the existing managed-AnsiString-var IR_LEA read/write gate (ir_codegen.inc:1661-1679 and the three cross equivalents).
  4. SetLength (-102). The x86-64 IsRef branch (ir_codegen.inc:3066) already assumes &caller_slot — once (2) actually passes that, it works. Replace the cross backends' not yet supported guards with the &caller_slot deref + 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).
  5. Open-array SetLength stays a hard error on all four targets (you cannot SetLength an 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