From 42ca9d75353ce673aae7a9285d950741eb2a2f8f Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 16 Jun 2026 14:57:50 +0100 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20TList=20default=20array=20pr?= =?UTF-8?q?operty=20=E2=80=94=20List[i]=20subscript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TList now exposes a `default` array property, so elements can be read and written with the familiar subscript syntax instead of .Get(i): L[0] := 100; // L.SetItem(0, 100) WriteLn(L[0] + L[1]); // L.Get(i) Adds TList.SetItem (a pointer-target store, so the compiler's ARC discipline releases the old element and retains the new for managed T) and the property Items[AIndex]: T read Get write SetItem; default. Also fixes the generic-instance clone path in the semantic analyser: it copied every property-decl field except IsDefault, so a default property declared on a generic template (TList) lost its default flag on instantiation and List[i] did not resolve. The clone now carries IsDefault. Verified List[i] read, write, and polymorphic element dispatch (List[i].Area through a base-typed element) on both backends. Adds TE2ETListTests.TestRun_TList_DefaultProperty_{ReadWrite,Polymorphic}. Note: this makes the stdlib depend on the `default` directive (added in the previous commit), so the pre-release bootstrap binary is refreshed to a default-aware stage-2 in a follow-up. --- compiler/src/main/pascal/uSemantic.pas | 1 + .../src/test/pascal/cp.test.e2e.tlist.pas | 67 +++++++++++++++++++ .../src/main/pascal/generics.collections.pas | 13 ++++ 3 files changed, 81 insertions(+) diff --git a/compiler/src/main/pascal/uSemantic.pas b/compiler/src/main/pascal/uSemantic.pas index 0194305..5a4fc55 100644 --- a/compiler/src/main/pascal/uSemantic.pas +++ b/compiler/src/main/pascal/uSemantic.pas @@ -2386,6 +2386,7 @@ begin NewPDecl.WriteName := PDecl.WriteName; NewPDecl.IndexParamName := PDecl.IndexParamName; NewPDecl.IndexTypeName := SubstTypeParam(PDecl.IndexTypeName, Templ.ParamNames, Args); + NewPDecl.IsDefault := PDecl.IsDefault; ClonedCD.Properties.Add(NewPDecl); end; diff --git a/compiler/src/test/pascal/cp.test.e2e.tlist.pas b/compiler/src/test/pascal/cp.test.e2e.tlist.pas index b049b49..e380d40 100644 --- a/compiler/src/test/pascal/cp.test.e2e.tlist.pas +++ b/compiler/src/test/pascal/cp.test.e2e.tlist.pas @@ -34,6 +34,10 @@ type 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; + + { Default array property: List[i] subscript for read and write. } + procedure TestRun_TList_DefaultProperty_ReadWrite; + procedure TestRun_TList_DefaultProperty_Polymorphic; end; implementation @@ -207,6 +211,69 @@ begin AssertEquals('retained elements readable (native)', '77' + #10 + '88' + #10, Output); end; +const + SrcTListDefaultRW = ''' + program P; + uses Generics.Collections; + var lst: TList; + begin + lst := TList.Create; + lst.Add(1); lst.Add(2); + lst[0] := 100; lst[1] := 200; + WriteLn(lst[0] + lst[1]); + lst.Free() + end. + '''; + + SrcTListDefaultPoly = ''' + program P; + uses Generics.Collections; + type + TShape = class function Area: Integer; virtual; begin Result := 0 end; end; + TBox = class(TShape) + FS: Integer; + constructor Create(s: Integer); begin FS := s end; + function Area: Integer; override; begin Result := FS * FS end; + end; + var lst: TList; i, total: Integer; + begin + lst := TList.Create; + lst.Add(TBox.Create(3)); lst.Add(TBox.Create(4)); + total := 0; + for i := 0 to lst.Count - 1 do total := total + lst[i].Area(); + WriteLn(total); + lst.Free() + end. + '''; + +procedure TE2ETListTests.TestRun_TList_DefaultProperty_ReadWrite; +var Output: string; RCode: Integer; +begin + if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end; + AssertTrue('compile+run (qbe)', + CompileAndRunWithRTLDebugOn(beQBE, SrcTListDefaultRW, Output, RCode, False)); + AssertEquals('exit code (qbe)', 0, RCode); + AssertEquals('List[i] read/write (qbe)', '300' + #10, Output); + AssertTrue('compile+run (native)', + CompileAndRunWithRTLDebugOn(beNative, SrcTListDefaultRW, Output, RCode, False)); + AssertEquals('exit code (native)', 0, RCode); + AssertEquals('List[i] read/write (native)', '300' + #10, Output); +end; + +procedure TE2ETListTests.TestRun_TList_DefaultProperty_Polymorphic; +var Output: string; RCode: Integer; +begin + if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end; + AssertTrue('compile+run (qbe)', + CompileAndRunWithRTLDebugOn(beQBE, SrcTListDefaultPoly, Output, RCode, False)); + AssertEquals('exit code (qbe)', 0, RCode); + AssertEquals('List[i].Area (qbe)', '25' + #10, Output); + AssertTrue('compile+run (native)', + CompileAndRunWithRTLDebugOn(beNative, SrcTListDefaultPoly, Output, RCode, False)); + AssertEquals('exit code (native)', 0, RCode); + AssertEquals('List[i].Area (native)', '25' + #10, Output); +end; + initialization RegisterTest(TE2ETListTests); diff --git a/stdlib/src/main/pascal/generics.collections.pas b/stdlib/src/main/pascal/generics.collections.pas index d25dc3f..60d4a18 100644 --- a/stdlib/src/main/pascal/generics.collections.pas +++ b/stdlib/src/main/pascal/generics.collections.pas @@ -36,12 +36,15 @@ type procedure Grow; procedure Add(Value: T); function Get(AIndex: Integer): T; + procedure SetItem(AIndex: Integer; Value: T); function IndexOf(Value: T): Integer; procedure Delete(AIndex: Integer); procedure Clear; procedure Destroy; function GetEnumerator: TListEnumerator; property Count: Integer read FCount; + { Default array property — enables List[i] for read and write. } + property Items[AIndex: Integer]: T read Get write SetItem; default; end; { Generic LIFO stack backed by a dynamic array. Push/Pop/Peek operate on @@ -262,6 +265,16 @@ begin Result := Src^ end; +procedure TList.SetItem(AIndex: Integer; Value: T); +var + Dest: ^T; +begin + { The ^T := Value store carries the compiler's ARC discipline for a managed + T — the previous element is released and the new one retained. } + Dest := Self.FData + AIndex * SizeOf(T); + Dest^ := Value +end; + function TList.IndexOf(Value: T): Integer; var I: Integer;