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;