← board

Copy-on-write for managed strings on cross targets (i386 / ARM32 / AArch64)

Problem

The cross backends (i386, ARM32, AArch64) do not implement copy-on-write for managed AnsiString writes. Only the x86-64 backend does. So a write through a shared string handle mutates the shared data in place instead of first making the target unique.

This is already acknowledged in compiler/ir_codegen386.inc (IR_INDEX comment: "Managed strings and dynamic arrays (which need copy-on-write) aren't available on i386 yet, so no COW path here.") and the ARM32/AArch64 equivalents.

Why it blocks self-host

compiler.pas's own LowerCase (parser.inc ~6199) is the trigger:

function LowerCase(const s: ansistring): ansistring;
var i: integer; res: ansistring;
begin
  res := s;                       { shares s's handle, refcount bump }
  for i := 1 to Length(res) do
    if res[i] in ['A'..'Z'] then
      res[i] := Chr(Ord(res[i]) + 32);   { in-place write — needs COW first }
  LowerCase := res;
end;

With no COW, res[i] := ... mutates the buffer still aliased by s. In the compiler this corrupts a proc's call name: HeapMmap is folded to heapmmap in a buffer that is also the case-preserved decl name, so MatchProcCall's exact Procs[i].Name = name misses and you get:

pascal26:119: error: no overload of heapmmap matches these arguments

(heapmmap is reached on the empty-program startup path via the heap RTL.)

Minimal repro

/tmp/lc.pas:

program lc;
function LowerCase(const s: ansistring): ansistring;
var i: integer; res: ansistring;
begin
  res := s;
  for i := 1 to Length(res) do
    if res[i] in ['A'..'Z'] then res[i] := Chr(Ord(res[i]) + 32);
  LowerCase := res;
end;
var x: ansistring;
begin
  x := 'HeapMmap';
  writeln('orig=', x);
  writeln('lower=', LowerCase(x));
  x[1] := 'Z';
  writeln('afterwrite=', x);
end.

Build/run per target (tools/run_target.sh <arch> <bin>; i386 runs natively):

x86_64 : afterwrite=ZeapMmap      <- correct (x stayed 'HeapMmap')
i386   : afterwrite=Zeapmmap      <- BUG: LowerCase mutated x to 'heapmmap'
arm32  : afterwrite=Zeapmmap      <- BUG: same
aarch64: segfaults inside LowerCase itself (additional/earlier string bug)

So: i386 and ARM32 share exactly this COW gap; AArch64 has at least this plus an earlier crash — investigate AArch64 separately once COW lands, it may be a second bug on top.

Scope

Acceptance

Context / where to look

Log