docs+test: collection ownership conventions — generic retention pinned

Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):

- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
  this already works (generic stores lower to ARC-aware pointer writes);
  TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
  pins it on both backends (object survives its creating scope).

- TStringList.Objects[] stays NON-OWNING by design: the API type is
  Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
  places in the compiler itself) makes blind retention impossible.
  Convention documented at the declaration and in the rationale.

- Known limitation recorded: generic Clear/Destroy do not yet release
  remaining managed elements (needs a Default(T)-style zero-store);
  tracked in the leak backlog.

Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
This commit is contained in:
Graeme Geldenhuys 2026-06-11 11:55:34 +01:00
parent d6b706ffd3
commit b9e0ea7a9e
3 changed files with 93 additions and 0 deletions

View file

@ -29,6 +29,11 @@ type
procedure TestRun_TListInteger_AddGetCount;
procedure TestRun_TListInteger_IndexOf;
procedure TestRun_TListString_IndexOf_NotFound;
{ Class elements are RETAINED on store: an object whose only other
reference was a local in an exited routine must survive in the list
(decision recorded in language-rationale: collection ownership). }
procedure TestRun_TList_ClassElements_RetainedAcrossScope;
end;
implementation
@ -158,6 +163,50 @@ begin
AssertTrue('IndexOf(gamma)=-1', Pos('-1', Output) >= 0);
end;
const
SrcTListClassRetain = '''
program P;
uses generics.collections;
type
TC = class
N: Integer;
end;
procedure Fill(L: TList<TC>; S: TStack<TC>);
var C: TC;
begin
C := TC.Create();
C.N := 77;
L.Add(C);
C := TC.Create();
C.N := 88;
S.Push(C);
end;
var
L: TList<TC>;
S: TStack<TC>;
begin
L := TList<TC>.Create();
S := TStack<TC>.Create();
Fill(L, S);
writeln(L.Get(0).N);
writeln(S.Peek().N);
end.
''';
procedure TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+link+run (qbe)',
CompileAndRunWithRTLDebugOn(beQBE, SrcTListClassRetain, Output, RCode, False));
AssertEquals('exit code (qbe)', 0, RCode);
AssertEquals('retained elements readable (qbe)', '77' + #10 + '88' + #10, Output);
AssertTrue('compile+link+run (native)',
CompileAndRunWithRTLDebugOn(beNative, SrcTListClassRetain, Output, RCode, False));
AssertEquals('exit code (native)', 0, RCode);
AssertEquals('retained elements readable (native)', '77' + #10 + '88' + #10, Output);
end;
initialization
RegisterTest(TE2ETListTests);

View file

@ -3566,3 +3566,40 @@ publishing the interface as the API surface.
through chained expressions is not yet wired.
* Indexed and default array properties in interfaces are not yet
supported.
== Collection Ownership — ARC Conventions
=== Decision
The collection types follow three explicit ownership conventions:
* `TObjectList` — ownership is opt-in: `Create(True)` retains on add and
releases (destroying at refcount zero) on remove/clear/destroy;
`Create(False)` stores borrowed references.
* `TList<T>`, `TStack<T>`, `TQueue<T>` and the other generic collections —
managed elements (`string`, class, dynamic array) ARE retained on store
and released when an element slot is overwritten (stores lower to
ARC-aware pointer writes). An object whose only other reference was a
local in an exited routine therefore survives in the collection.
* `TStringList.Objects[]` — NON-OWNING raw pointer slots, by design.
The API type is `Pointer`, and storing integer payloads cast to
pointers (`TObject(PtrUInt(N))`) is an accepted idiom (the compiler
itself uses it); blind retention would corrupt such entries. Callers
storing real objects must hold a strong reference elsewhere for the
entry's lifetime.
=== Rationale
Retention for typed generic elements is safe because `T` is statically
known — only genuinely managed types get ARC operations. `Objects[]`
cannot distinguish a pointer-encoded integer from an object reference at
runtime, so it keeps FPC/Delphi's borrowed-pointer contract.
=== Known limitation (leak backlog)
`Clear`/`Destroy` on the generic collections do not yet release the
retained references of remaining managed elements (releasing requires a
`Default(T)`-style generic zero-store that the language does not provide
yet). Until then, collections of managed elements leak their contents
when destroyed non-empty; element overwrite and `Delete` shifting are
ARC-balanced.

View file

@ -187,6 +187,13 @@ type
constructor Create;
procedure Destroy;
function Add(S: string): Integer;
{ Objects[] is NON-OWNING by design: slots hold raw, untyped pointers
with no ARC retain/release. Callers commonly store integer payloads
cast to pointers (TObject(PtrUInt(N))), which makes blind retention
impossible. When storing real objects, the caller must keep a strong
reference alive for the lifetime of the entry (e.g. an owning
TObjectList keep-alive) see docs/language-rationale.adoc,
"Collection Ownership". }
procedure AddObject(S: string; AObject: Pointer);
function Find(const S: string; var Index: Integer): Boolean;
function IndexOf(const S: string): Integer;