fix(native): ARC for managed-type dynamic array element assignment

When assigning to a dynamic array element where the element type is
managed (string or class), the native backend now emits AddRef on the
new value and Release on the old value before storing, matching the
QBE backend's behaviour.

Previously A[I] := 'new' would store without releasing the old string
at A[I], leaking one reference per overwrite.

E2E test TestRun_Native_DynArrayElemArc_String verifies string element
assignment with replacement on both backends.
This commit is contained in:
Graeme Geldenhuys 2026-06-10 08:29:12 +01:00
parent 02b03dbbd0
commit 0317a3b454
2 changed files with 48 additions and 7 deletions

View file

@ -7151,6 +7151,7 @@ var
FAE: TFieldAccessExpr;
SSA: TStaticSubscriptAssign;
MD: TMethodDecl;
DAElemType: TTypeDesc;
I: Integer;
LThen, LElse, LEnd: string;
LCond, LBody: string;
@ -8602,23 +8603,39 @@ begin
if (SSA.ResolvedArrayType <> nil) and
(SSA.ResolvedArrayType.Kind = tyDynArray) then
begin
{ Dynamic array element write: A[I] := V.
Data pointer lives directly in the variable slot. }
DAElemType := TDynArrayTypeDesc(SSA.ResolvedArrayType).ElementType;
Self.EmitExprToEax(SSA.ValueExpr);
Self.Emit(#9'pushq %rax');
{ Index * elem_size. }
Self.EmitExprToEax(SSA.IndexExpr);
Self.Emit(Format(#9'imulq $%d, %%rax',
[TDynArrayTypeDesc(SSA.ResolvedArrayType).ElementType.RawSize()]));
{ Base pointer into %rcx. }
Self.Emit(Format(#9'imulq $%d, %%rax', [DAElemType.RawSize()]));
if Self.IsLocal(SSA.ArrayName) then
Self.Emit(Format(#9'movq %s, %%rcx', [Self.VarOperand(SSA.ArrayName)]))
else
Self.Emit(Format(#9'movq %s(%%rip), %%rcx', [SSA.ArrayName]));
Self.Emit(#9'addq %rcx, %rax');
Self.Emit(#9'movq %rax, %rcx');
if DAElemType.Kind = tyString then
begin
Self.Emit(#9'pushq %rcx');
Self.Emit(#9'movq 8(%rsp), %rdi');
Self.Emit(#9'callq _StringAddRef');
Self.Emit(#9'movq (%rsp), %rcx');
Self.Emit(#9'movq (%rcx), %rdi');
Self.Emit(#9'callq _StringRelease');
Self.Emit(#9'popq %rcx');
end
else if DAElemType.Kind = tyClass then
begin
Self.Emit(#9'pushq %rcx');
Self.Emit(#9'movq 8(%rsp), %rdi');
Self.Emit(#9'callq _ClassAddRef');
Self.Emit(#9'movq (%rsp), %rcx');
Self.Emit(#9'movq (%rcx), %rdi');
Self.Emit(#9'callq _ClassRelease');
Self.Emit(#9'popq %rcx');
end;
Self.Emit(#9'popq %rax');
Self.EmitStoreVar('(%rcx)', TDynArrayTypeDesc(SSA.ResolvedArrayType).ElementType);
Self.EmitStoreVar('(%rcx)', DAElemType);
Exit;
end;
{ Static array element write. }

View file

@ -325,6 +325,8 @@ type
procedure TestRun_Native_IntfFieldAsArg;
{ M8b — nil assignment to interface-typed fields (implicit-Self and non-Self). }
procedure TestRun_Native_IntfFieldNilAssign;
{ M8b — dynarray element ARC: A[I] := 'new' releases old string at A[I]. }
procedure TestRun_Native_DynArrayElemArc_String;
end;
implementation
@ -2861,6 +2863,21 @@ const
end.
''';
SrcDynArrayElemArcString = '''
program P;
var A: array of String;
begin
SetLength(A, 2);
A[0] := 'first';
A[1] := 'second';
WriteLn(A[0]);
WriteLn(A[1]);
A[0] := 'replaced';
WriteLn(A[0]);
WriteLn(A[1])
end.
''';
{ Dyn-array field inside a class: the field must be ARC-refcounted on store
and released when the holder is destroyed (f74e5cc). Observed by reading an
element back after the field assignment — a dropped/garbled buffer would
@ -4792,6 +4809,13 @@ begin
AssertRunsOnBoth(SrcIntfFieldNilAssign, '99' + LE + 'cleared' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_DynArrayElemArc_String;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnBoth(SrcDynArrayElemArcString,
'first' + LE + 'second' + LE + 'replaced' + LE + 'second' + LE, 0);
end;
initialization
RegisterTest(TE2ENativeTests);