From b9e0ea7a9e3a851b0d342b8760284652ecceb23f Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Thu, 11 Jun 2026 11:55:34 +0100 Subject: [PATCH] =?UTF-8?q?docs+test:=20collection=20ownership=20conventio?= =?UTF-8?q?ns=20=E2=80=94=20generic=20retention=20pinned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the TStringList.Objects / TList retention question from the bug backlog as an explicit design decision (language-rationale, 'Collection Ownership'): - TList/TStack/TQueue 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. --- .../src/test/pascal/cp.test.e2e.tlist.pas | 49 +++++++++++++++++++ docs/language-rationale.adoc | 37 ++++++++++++++ stdlib/src/main/pascal/classes.pas | 7 +++ 3 files changed, 93 insertions(+) diff --git a/compiler/src/test/pascal/cp.test.e2e.tlist.pas b/compiler/src/test/pascal/cp.test.e2e.tlist.pas index f480d95..b049b49 100644 --- a/compiler/src/test/pascal/cp.test.e2e.tlist.pas +++ b/compiler/src/test/pascal/cp.test.e2e.tlist.pas @@ -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; S: TStack); + 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; + S: TStack; + begin + L := TList.Create(); + S := TStack.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); diff --git a/docs/language-rationale.adoc b/docs/language-rationale.adoc index 4dd387f..84bf604 100644 --- a/docs/language-rationale.adoc +++ b/docs/language-rationale.adoc @@ -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`, `TStack`, `TQueue` 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. diff --git a/stdlib/src/main/pascal/classes.pas b/stdlib/src/main/pascal/classes.pas index f1fc43b..a7a8a9c 100644 --- a/stdlib/src/main/pascal/classes.pas +++ b/stdlib/src/main/pascal/classes.pas @@ -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;