← board

riscv32: an element write through a NESTED dynamic array segfaults

Repro

program nest;
var a, b: array of array of Integer;
    i: Integer;
begin
  SetLength(a, 2);
  for i := 0 to 1 do begin SetLength(a[i], 2); a[i][0] := i; a[i][1] := i * 10; end;
  b := a;
  b[0][0] := 77;
  writeln('nested write thru b: a[0][0]=', a[0][0], ' b[0][0]=', b[0][0]);
end.
$ ./compiler/pascal26 -dPXX_MANAGED_STRING --target=riscv32 nest.pas n_rv
$ tools/run_target.sh riscv32 n_rv
Segmentation fault (core dumped)     # exit 139, no output at all

Nothing prints, so it dies before the first writeln — i.e. in the SetLength(a[i], ...) / nested-element-write sequence, not in output.

Boundary, measured

case riscv32
writeln(42) — riscv32 runs at all
FLAT dynamic array: b := a; b[0] := 77 ✅ correct, and matches FPC
NESTED: array of array of Integer, element write SIGSEGV
the same nested program on i386 / arm32 / aarch64 / x86-64 ✅ all four correct

So this is riscv32-specific and depth-specific. The flat case working rules out the store arm and the runner; the other four targets working on the identical program rules out the shared IR.

Where to look first

IR_DYNUNIQUE in compiler/ir_codegen_riscv32.inc is the nested-level data-pointer load, and its comment records that it "was an 'unsupported node' error on riscv32" before being added — i.e. riscv32 got this arm late, mirroring arm32. IR_SETLEN_DYN on a nested slot address is the other candidate. Note the riscv32 arm was simplified along with every other backend when the nested copy-on-write was removed, and the crash predates and survives that change identically, so it is not the deref itself.

Worth checking whether PXXDynSetLen's ESP-lean body is what riscv32 links (see compiler/builtin/builtinheap.pas — riscv32 is also the C3 target and takes some ESP-shaped paths), because a lean SetLen that skips the managed/sub-array retain would explain a nested handle being wrong while flat is fine.

Gate

The repro above running clean under tools/run_target.sh riscv32 and matching an FPC build's output, plus test/test_dynarray_whole_assign.pas — whose AliasesNested case is exactly this shape — extended to the riscv32 differential job once it passes. (It is deliberately NOT wired into the riscv32 job today, because it would land red on this bug rather than on anything the aliasing fix did.)

2026-08-07 — FIXED

The crash was not where the ticket looked, and the first symptom was earlier

IR_DYNUNIQUE and PXXDynSetLen's ESP-lean body were both innocent. Bisecting the repro moved the failure up, before any element write:

var a: array of array of Integer;
SetLength(a, 2);
writeln(Length(a));    { riscv32: 0 — x86-64/i386/arm32/aarch64: 2 }

The outer SetLength silently did nothing. Everything after it — SetLength(a[i], …), the element writes, Length(a[0]) — was then operating through a nil handle, which is where the SIGSEGV came from. array of AnsiString and array of record (managed elements, depth 1) are fine, so it is depth, not element management.

Cause

SetLength on a depth >= 2 array lowers to IR_SETLEN_DYN, whose riscv32 arm emits its slot-address operand as an ordinary node under InLValueWrite. That operand is an IR_LEA of the array symbol — and riscv32's IR_LEA always dereferences a dynamic array's slot, in read and write position alike. So PXXDynSetLen was handed the array's CURRENT handle instead of &slot; for a fresh array that is nil, and its first line is

  if (arrSlot = nil) or (desc = nil) then Exit;

— a silent no-op. The FLAT SetLength(a, n) path never comes through IR_LEA (the -102 intrinsic computes the slot address with EmitSlotAddrRISCV32 directly), which is exactly why flat worked and nested did not.

arm32 carried the identical bug and fixed it in bug-nested-dynarray-cross-segfault by gating its IR_LEA dynamic-array deref on InLValueWrite. That fix does not transplant. Tried it first and measured the result: on riscv32 an indexed WRITE (a[3] := 42) also arrives at IR_LEA with InLValueWrite set and needs the data pointer, so gating it broke every element store on a flat array — d2.pas printed len=8 and then segfaulted on a[3] := 42. Reverted.

Fix

IR_SETLEN_DYN computes the slot address itself when its target is a plain dyn-array symbol — EmitSlotAddrRISCV32, plus the one deref a by-ref var param needs — and falls back to emitting the node otherwise. Same shape the flat -102 path uses two thousand lines above, and it leaves IR_LEA's read/write behaviour untouched.

Measured

case before after
ticket's nest.pas repro SIGSEGV, no output a[0][0]=77 b[0][0]=77 — matches an FPC build of the same file
SetLength(a, 2) on array of array of Integer Length(a) = 0 2
test/test_dynarray_whole_assign.pas on riscv32 SIGSEGV byte-identical to the x86-64 build
test/test_nested_dynarray_setlen.pas on riscv32 (skipped) byte-identical to the x86-64 build
flat dynamic arrays, managed-element arrays ok ok

Gate + coverage

make test-riscv32 GREEN in full, tools/gate.sh quick GREEN, self-host byte-identical. Two tests un-skipped / newly wired into the riscv32 differential job, which is what the ticket's Gate line asked for:

Log