← board

Nested routines: capture of fixed-size array locals not supported

Symptom

A nested routine that reads or writes a fixed-size array local of its enclosing routine fails to compile:

pascal26:144: error: nested routine: capture of fixed-size array 'rightG' not yet supported ()

Repro shape (rejected):

function Outer: Boolean;
var
  leftG, rightG: array[0..7] of Integer;
  leftN, rightN: Integer;
  onRight: Boolean;

  function AddGroup(v: Integer): Boolean;
  begin
    AddGroup := False;
    if leftN + rightN >= 8 then Exit;
    if onRight then begin rightG[rightN] := v; rightN := rightN + 1; end
    else begin leftG[leftN] := v; leftN := leftN + 1; end;
    AddGroup := True;
  end;

begin
  ...
end;

Scalar captures (leftN, onRight) work; the fixed-size array locals are the missing case. FPC accepts this.

Workaround used

Flattened the helper into the enclosing routine (single g[0..7] array + index bookkeeping inline) — see DnsParseIpv6 in lib/rtl/dns_config.pas.

Acceptance

Implemented 2026-08-31 (frankA) — 1-D fixed arrays; multi-dim still refused

50fcbddef. Nested routines lift: the routine becomes top-level and each captured local becomes a by-reference parameter. A dynamic array is self-describing at runtime; a fixed one carries its extent only in its type, and the enclosing Syms slot is recycled before the lifted body is parsed — so the shape has to be snapshotted at capture rather than read later.

Measured first, and that is what made it plumbing rather than a feature. A named-fixed-array var parameter already works end to end, and ParseSubroutine already replays a shape onto one via pFixedLen/pFixedLo. Nothing new was taught to the backend; the capture path simply refused and dropped the two numbers.

capture (pasparser_decl.inc) snapshot ArrLen and the low bound
carry (defs.inc) LiftCapFixedLen / LiftCapFixedLo
lift (pasparser_proc.inc) replay onto pFixedLen / pFixedLo

The low bound is the half that fails silently — without it IR_INDEX subtracts 0 and g[1] of an array[1..3] writes the next element. It is row 1 of the test for that reason.

Acceptance, item by item

NOT done, and named in the diagnostic rather than left to be rediscovered

Multi-dimensional arrays still refuse, now with capture of multi-dimensional array 'm' not yet supported (1-D fixed arrays are). They need the per-dim lo/span vectors carried as well, which is wider than the use case this was filed from. Array parameters of an enclosing routine were in the acceptance text and are not separately tested — the capture arm keys off Syms[].IsArray for skLocal and skParam alike, so it should follow, but "should" is not a measurement and this row is honestly open. CLOSED by frankS's 416cbc997 — see the addendum below; ParamCapture is now a row, so the reasoning above stands and is no longer a gap. Struck rather than deleted because the addendum is what answers it, and a reader who lands here first should be sent there rather than left believing the row is open.

Controls

The test fails on the pre-change compiler with the original error — checked, because a test written after the fix that passes on the old binary is testing nothing. And the change is provably additive: same sources through both compilers give a byte-identical ELF for 8 programs, including test_nested_dynarray, test_nested_dynarray_managed, test_nested_dynarray_setlen and test_nested_alias — which exercise the very capture path this edits — plus a NilPy nested-capture canary, since defs.inc is shared across frontends.

gate.sh quick GREEN; fixedpoint converged, 1 round, ee45a08cbc7f.

Track B follow-up available: DnsParseIpv6 in lib/rtl/dns_config.pas was flattened by hand to work around this and can now be written the intended way. Not done here — that is B's file and B's gate.

Log


Addendum (frankS, 2026-08-31): the i386 leg, and three acceptance rows

I implemented this concurrently and dropped my duplicate — same design, same three files, and 50fcbddef was first. Two things from that work are additive and landed on top:

1. 29704fd69 — i386 refused the feature, for a reason that predates it. TwoArrays calls a nested FUNCTION as a STATEMENT. The capture is a var open-array param, so its copy-OUT runs after the call and would clobber the result register; the caller spills the result to a compiler-minted temp typed from the call STATEMENT's AST node, which has no type. i386 is the only backend that asserts on an unresolved temp.

Measured rather than argued: a compiler with 29704fd69 reverted rejects test/test_nested_fixed_array_capture.pas at TwoArrays for --target=i386 and compiles it fine for x86-64. The native-only Makefile row could not have seen it, which is why there is now an i386 row.

Independently confirmed (frankA), and it sharpens the boundary: the discriminator is statement vs expression, not nesting. Against pinned, on --target=i386, with no nested routine anywhere in the source:

if F(a) then writeln(...)   ->  ok: ... compiles
F(a);                       ->  pascal26:8: error: target i386: a compiler-minted
                                temporary reached codegen with an UNRESOLVED type
                                (tyUnknown)

So an open-array var param whose call is a statement was already broken on i386; the capture feature only supplied the first program in the tree that took that shape. The current compiler compiles and runs both.

Not this feature's bug — the two-line repro in that commit has no nested routine in it and pinned refuses it today:

var a: array[0..3] of Integer;
function F(var o: array of Integer): Boolean; begin o[0] := 9; F := True; end;
begin F(a); end.        { --target=i386 }

2. 416cbc997 — three acceptance clauses had no row. Folded into the same test file rather than a second one: ElemKinds (record / Double / Char / AnsiString elements — different byte counts, and the AnsiString rides the copy-in/copy-out as raw handles), Depth3, and ParamCapture (the acceptance says "local and array parameter"; every existing row captured a local). 6 rows -> 9, byte-identical to FPC, matching on i386/aarch64/arm32/riscv32.