← board

x86-64: dynamic-array assignment copies instead of aliasing

Repro

var a, b: array of Integer;
begin
  SetLength(a, 3); a[0] := 1; a[1] := 2; a[2] := 3;
  b := a;
  writeln('after b:=a      a[0]=', a[0], ' b[0]=', b[0]);
  b[0] := 77;                      { write through B }
  writeln('after b[0]:=77  a[0]=', a[0], ' b[0]=', b[0]);
  a[1] := 88;                      { write through A }
  writeln('after a[1]:=88  a[1]=', a[1], ' b[1]=', b[1]);
end.
after b:=a after b[0]:=77 after a[1]:=88
FPC a=1 b=1 a[0]=77 b[0]=77 a[1]=88 b[1]=88
arm32 / riscv32 / i386 / aarch64 a=1 b=1 a[0]=77 b[0]=77 a[1]=88 b[1]=88
pxx x86-64 a=1 b=1 a[0]=1 b[0]=77 a[1]=88 b[1]=2

Writes through either name are invisible to the other on x86-64 — so it is a copy-on-write in both directions, not a one-off deep copy at assignment. Reproduces with both plain (Integer) and managed (string) element types, so it is not the managed-element machinery.

Why this matters

Dynamic arrays are reference types in FPC/Delphi. b := a aliases; the two names are the same array until one is SetLength'd. Code that hands an array to a helper, mutates it through the second name, and expects the caller to see the change is ordinary and correct — and silently does nothing on x86-64. No error, no crash, just a stale read much later. That is the expensive shape described in the debugging playbook.

It is also the WRONG WAY ROUND for the usual cross-target story: here the flagship target is the outlier and every cross target is right.

The open question — direction is a real design call

Two defensible directions, and this ticket does not presume one:

  1. FPC parity (recommended): drop COW on whole dynamic arrays so x86-64 aliases like everyone else. Matches the oracle, matches the other five targets, matches what compat means for Track P. Blast radius is the worry: COW has been x86-64's behaviour for a long time, so existing code and tests may lean on the copy without anyone noticing.
  2. Keep COW deliberately and document it as a dialect divergence — then the FIVE other targets are the ones that need changing, which is a much larger job and puts pxx permanently out of parity on a core type.

Recommend (1). Filed as a bug- per CLAUDE.md's escape rule — a compat finding that produces silent wrong behaviour is a bug in the owning lane, not a parity nicety.

Blocked on decide-dynamic-array-value-vs-reference-semantics (the pre-existing Track U ticket for this fork; a duplicate I filed was merged into it). Since filing this, the COW turned out to be DELIBERATE: IR_DYNUNIQUE + PXXDynArrayUnique exist for it and compiler/ir.inc states the invariant outright — "writing through one alias never mutates another at any depth." So fixing this to match FPC would overrule a design decision rather than repair a slip, and that is Track U's call, not an agent's. The observation here stands either way; only the direction is blocked.

2026-08-06 — unblocked in direction, re-blocked on the escape hatch

[[decide-dynamic-array-value-vs-reference-semantics]] is decided: match FPC, so x86-64 should alias like the other four targets already do. The semantics question is closed; this ticket is now plain work.

But it must land after [[bug-p-copy-single-argument-form-missing-for-dynamic-arrays]]. Copy is the only way to ask for a duplicate once assignment aliases, and Copy(a) does not parse today. Flipping this first would take away the copy and the way to request one in a single change.

Self-compile risk: measured as zero for the compiler

compiler/** declares no named dynamic-array types and contains no assignment to a dynamic-array variable at all (over-approximating scan across 115 candidate names returning zero, so it cannot be hiding a case). The compiler uses SetLength + element writes throughout.

Prediction to check when implementing: the self-host binary should come out byte-identical. If it does not, the linked RTL uses whole-array assignment somewhere — lib/rtl + compiler/builtin have 63 dynamic-array variables and an upper bound of 79 candidate assignments, most of which are name collisions and need per-scope resolution. Also check by-value dynamic-array parameters and dynamic-array function results, which the scan does not cover.

Log