Skip to content

Commit 7bd5cdc

Browse files
committed
feat(collections): add immutable list builder core
1 parent d9f67e1 commit 7bd5cdc

7 files changed

Lines changed: 248 additions & 27 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
## Non-negotiable rules
1010

1111
1. **Zero errors, zero warnings** before any commit. `cmake --build build --parallel 4` must be clean.
12-
2. **No test-count regression.** `scripts/run_component_tests.sh build` must show no failures. The verified baseline is 12,662 tests across 36 component executables and one integration executable; this floor should be raised as new tests are added and lowered only with an explicit, documented reason.
12+
2. **No test-count regression.** `scripts/run_component_tests.sh build` must show no failures. The verified baseline is 12,666 tests across 36 component executables and one integration executable; this floor should be raised as new tests are added and lowered only with an explicit, documented reason.
1313
3. **Push only to `feature/work`.** Never push to `develop` or `master`, and never create tags, without explicit per-action user approval.
1414
4. **SPDX header on every project source/header**`// SPDX-License-Identifier: MIT` + copyright + .NET attribution. Vendored sources retain their upstream headers; Markdown uses an HTML SPDX comment where one is present.
1515
5. **Property naming:** always `getXxxProperty()` / `setXxxProperty()`. Exception: indexers

NEXT.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# NEXT.md
55

66
*Last verified: 2026-07-25. Branch: `feature/work`. The P0 component-boundary
7-
repair, three P1 parity repairs, P1 portability revalidation, and seventeen bounded
7+
repair, three P1 parity repairs, P1 portability revalidation, and eighteen bounded
88
P2 API slices are complete: 41 physical modules, 90 production dependency
9-
edges, and 12,662 tests across 37 executables.*
9+
edges, and 12,666 tests across 37 executables.*
1010

1111
This is the cold-start handoff for the next working session. Keep it focused
1212
on verified facts, remaining bounded work, and commands needed to resume.
@@ -28,7 +28,7 @@ Historical session detail belongs in git history and `plan.sqlite3`.
2828
- The ten-job selective consumer matrix, including a direct
2929
`Collections.Blocking` consumer, is green. Text.Json retains its target
3030
absence and negative include-leakage assertions.
31-
- The full native baseline is a warning-free build with 12,662 passing tests
31+
- The full native baseline is a warning-free build with 12,666 passing tests
3232
across 36 component executables and one integration executable.
3333
- `TaskT<TResult>::ContinueWith` now supports both action and result-producing
3434
callbacks. It runs inline on completion; `NotOn*` and `OnlyOn*` filter the
@@ -70,6 +70,10 @@ Historical session detail belongs in git history and `plan.sqlite3`.
7070
integer values. It also supports signed left and arithmetic right shifts,
7171
plus minimal byte-vector conversion with signed/unsigned and little/big-endian
7272
options.
73+
- `ImmutableList<T>` provides `CreateBuilder()` and `ToBuilder()` with core
74+
mutable operations and independent `ToImmutable()` snapshots. The current
75+
vector backend copies source/snapshot contents rather than claiming .NET's
76+
tree-backed O(1) conversion characteristics.
7377
- MinGW-w64 GCC 14-win32/CMake 3.31.6 and Emscripten 5.0.7/CMake 3.31.6 both
7478
compile the post-modular `All` graph and selective `Text.Json` libraries.
7579
This is compile-only evidence: cross tests were deliberately disabled.
@@ -78,7 +82,7 @@ Historical session detail belongs in git history and `plan.sqlite3`.
7882
ASan/LSan ownership scenarios, including 100 continuation teardowns, pass.
7983

8084
The local `plan.sqlite3` snapshot contains 16,201 classified `task` rows and
81-
1,759 completed tickets. Ticket #1737 records the completed P0 split, tickets
85+
1,760 completed tickets. Ticket #1737 records the completed P0 split, tickets
8286
#1738/#1739 the MemoryStream and generic-continuation repairs, ticket #1740 the
8387
XML whitespace repair, #1741 the completed cross-build revalidation and
8488
`WebProxy` portability fix, #1742 focused sanitizer evidence, and #1743 the
@@ -91,8 +95,9 @@ database also records #1752 for its `IComparer<T>` sorting overloads, #1753
9195
for its equality-based item mutations, #1754 for its equality-based range
9296
queries, #1755 for its comparer-aware binary search, #1756 for its default
9397
range sort, #1757 for `BigInteger` bitwise operators, #1758 for signed
94-
`BigInteger` shifts, and #1759 for byte-vector conversion; it is git-ignored
95-
and is not part of a fresh clone.
98+
`BigInteger` shifts, #1759 for byte-vector conversion, and #1760 for the
99+
`ImmutableList<T>` Builder core; it is git-ignored and is not part of a fresh
100+
clone.
96101

97102
## P0 completion: restore Collections isolation
98103

@@ -235,11 +240,11 @@ full-list/range order and invalid range validation.
235240
All currently planned P1 work is complete. Choose one consumer-driven P2
236241
slice, create a ticket, and keep the changes isolated:
237242

238-
1. **`ImmutableList<T>` builder support.** Treat `ToBuilder`/`Builder` as a
239-
separately scoped API design, not an opportunistic extension of the
240-
vector-backed immutable-list methods.
241-
2. **Other documented partial surfaces.** Examples include fuller UTF-7
243+
1. **Other documented partial surfaces.** Examples include fuller UTF-7
242244
behavior and wider debugger/process/XML surfaces.
245+
2. **Advanced `ImmutableList<T>::Builder` operations by consumer need.**
246+
Query, sorting, and copy overloads remain explicitly deferred; retain the
247+
vector-backed snapshot semantics if a focused consumer requires one.
243248
3. **Doxygen warning baseline.** Establish a reproducible warning count, then
244249
keep touched public APIs from increasing it; do not do a mass comment-only
245250
rewrite.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ collector, or the complete .NET platform.
77

88
The repository currently builds as 41 independently selectable CMake
99
components. The verified Linux baseline on 2026-07-25 is a warning-free build
10-
with **12,662 passing tests across 37 test executables**.
10+
with **12,666 passing tests across 37 test executables**.
1111

1212
## What is included
1313

@@ -181,7 +181,7 @@ Other platform evidence is narrower:
181181

182182
| Platform/toolchain | Verified scope |
183183
|---|---|
184-
| Linux/GCC | Current full component build and all 12,662 tests. |
184+
| Linux/GCC | Current full component build and all 12,666 tests. |
185185
| Windows/MinGW | MinGW-w64 GCC 14-win32/CMake 3.31.6 compiled the post-component `All` and selective `Text.Json` library graphs under ticket #1741. GoogleTest was not cross-built and repository CI remains Ubuntu-only. |
186186
| Emscripten | Emscripten 5.0.7/CMake 3.31.6 compiled the post-component `All` and selective `Text.Json` library graphs under ticket #1741. Tests were not cross-built or run, and some runtime APIs deliberately throw `PlatformNotSupportedException`. |
187187
| macOS/Apple Clang | Real downstream Xcode 15.4 builds drove portability fixes on 2026-07-20; this repository has no macOS job or recorded full standalone test baseline. |

docs/CMakeComponents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ are named `SharpRuntimeTests_<Component>`; genuinely cross-module scenarios
131131
are in `SharpRuntimeIntegrationTests`. CTest also discovers every individual
132132
GoogleTest case.
133133

134-
The verified 2026-07-25 `All` baseline contains 12,662 tests across 36
134+
The verified 2026-07-25 `All` baseline contains 12,666 tests across 36
135135
component executables and one integration executable.
136136

137137
## Boundary validation and CI

modules/collections/include/System/Collections/Immutable/ImmutableList.hpp

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ using SharpRuntime::intcs;
2626
*
2727
* Covers the core mutation/lookup surface (Add/AddRange/Insert/InsertRange/SetItem/Replace/
2828
* Remove/RemoveAll/RemoveAt/RemoveRange(int,int)/Sort/Reverse/Contains/IndexOf/LastIndexOf/
29-
* BinarySearch).
29+
* BinarySearch) and the core mutable Builder workflow.
3030
* Deliberately deferred relative to real .NET's ImmutableList<T> (a much larger surface backed
31-
* by an AVL tree, not a flat vector): ToBuilder/Builder. This is a real gap, not incorrect
32-
* behavior for the surface that does exist -- left undone here rather than expanded ad hoc in a
33-
* single audit pass; a full port would need an AVL/red-black backing structure to match .NET's
34-
* O(log n) persistent-update complexity (this port's vector-copy approach is O(n) per mutation).
31+
* by an AVL tree, not a flat vector): advanced Builder query, sorting, and copy operations. The
32+
* implemented Builder makes independent vector-backed immutable snapshots, so it does not claim
33+
* the .NET tree implementation's O(1) ToBuilder or near-O(1) ToImmutable characteristics.
3534
*
3635
* @tparam T The type of elements stored in the list.
3736
*/
@@ -63,6 +62,128 @@ class ImmutableList {
6362
}
6463

6564
public:
65+
/**
66+
* @brief Mutable staging collection for building an ImmutableList snapshot.
67+
*
68+
* C++ counterpart of .NET ImmutableList<T>.Builder. It uses the current
69+
* vector-backed representation, so ToImmutable copies the current contents
70+
* and later Builder changes cannot mutate an already returned list.
71+
*/
72+
class Builder {
73+
std::vector<T> data_;
74+
75+
void requireIndexInRange(intcs index) const {
76+
if (index < 0 || index >= static_cast<intcs>(data_.size())) {
77+
throw System::ArgumentOutOfRangeException(
78+
"index", "Index was out of range. Must be non-negative and less than the size of the collection.");
79+
}
80+
}
81+
82+
void requireValidRange(intcs index, intcs count) const {
83+
if (index < 0 || index > static_cast<intcs>(data_.size())) {
84+
throw System::ArgumentOutOfRangeException(
85+
"index", "Index was out of range. Must be non-negative and less than or equal to the size of the collection.");
86+
}
87+
if (count < 0 || index > static_cast<intcs>(data_.size()) - count) {
88+
throw System::ArgumentOutOfRangeException(
89+
"count", "Count must refer to a location within the collection.");
90+
}
91+
}
92+
93+
public:
94+
/** @brief Creates an empty Builder. Prefer ImmutableList<T>::CreateBuilder(). */
95+
Builder() = default;
96+
97+
/** @brief Creates a Builder initialized from the supplied vector. */
98+
explicit Builder(const std::vector<T>& items) : data_(items) {}
99+
100+
/** @brief Gets the number of elements in this Builder. */
101+
[[nodiscard]] intcs getCountProperty() const { return static_cast<intcs>(data_.size()); }
102+
103+
/** @brief Gets a mutable reference to an element after range validation. */
104+
T& operator[](intcs index) {
105+
requireIndexInRange(index);
106+
return data_[static_cast<size_t>(index)];
107+
}
108+
109+
/** @brief Gets a const reference to an element after range validation. */
110+
const T& operator[](intcs index) const {
111+
requireIndexInRange(index);
112+
return data_[static_cast<size_t>(index)];
113+
}
114+
115+
/** @brief Appends an item to this Builder. */
116+
void Add(const T& item) { data_.push_back(item); }
117+
118+
/** @brief Appends all items in @p items to this Builder. */
119+
void AddRange(const std::vector<T>& items) {
120+
data_.insert(data_.end(), items.begin(), items.end());
121+
}
122+
123+
/** @brief Inserts an item at @p index. */
124+
void Insert(intcs index, const T& item) {
125+
if (index < 0 || index > static_cast<intcs>(data_.size())) {
126+
throw System::ArgumentOutOfRangeException("index");
127+
}
128+
data_.insert(data_.begin() + index, item);
129+
}
130+
131+
/** @brief Inserts all @p items at @p index. */
132+
void InsertRange(intcs index, const std::vector<T>& items) {
133+
if (index < 0 || index > static_cast<intcs>(data_.size())) {
134+
throw System::ArgumentOutOfRangeException("index");
135+
}
136+
data_.insert(data_.begin() + index, items.begin(), items.end());
137+
}
138+
139+
/** @brief Replaces the element at @p index. */
140+
void SetItem(intcs index, const T& item) {
141+
requireIndexInRange(index);
142+
data_[static_cast<size_t>(index)] = item;
143+
}
144+
145+
/** @brief Removes the first matching item and reports whether one was removed. */
146+
bool Remove(const T& item) {
147+
const auto found = std::find(data_.begin(), data_.end(), item);
148+
if (found == data_.end()) {
149+
return false;
150+
}
151+
data_.erase(found);
152+
return true;
153+
}
154+
155+
/** @brief Removes the element at @p index. */
156+
void RemoveAt(intcs index) {
157+
requireIndexInRange(index);
158+
data_.erase(data_.begin() + index);
159+
}
160+
161+
/** @brief Removes @p count elements starting at @p index. */
162+
void RemoveRange(intcs index, intcs count) {
163+
requireValidRange(index, count);
164+
data_.erase(data_.begin() + index, data_.begin() + index + count);
165+
}
166+
167+
/** @brief Removes all elements from this Builder. */
168+
void Clear() { data_.clear(); }
169+
170+
/** @brief Determines whether this Builder contains @p item. */
171+
[[nodiscard]] bool Contains(const T& item) const {
172+
return std::find(data_.begin(), data_.end(), item) != data_.end();
173+
}
174+
175+
/** @brief Returns the first index of @p item, or -1 if it is absent. */
176+
[[nodiscard]] intcs IndexOf(const T& item) const {
177+
const auto found = std::find(data_.begin(), data_.end(), item);
178+
return found == data_.end() ? -1 : static_cast<intcs>(found - data_.begin());
179+
}
180+
181+
/** @brief Returns an immutable snapshot unaffected by later Builder mutations. */
182+
[[nodiscard]] ImmutableList<T> ToImmutable() const {
183+
return ImmutableList<T>(std::make_shared<std::vector<T>>(data_));
184+
}
185+
};
186+
66187
/** @brief Default-constructs an empty ImmutableList. */
67188
ImmutableList() : data_(std::make_shared<std::vector<T>>()) {}
68189

@@ -74,6 +195,13 @@ class ImmutableList {
74195
*/
75196
static ImmutableList<T> Empty() { return ImmutableList<T>(); }
76197

198+
/**
199+
* @brief Creates an empty mutable Builder.
200+
*
201+
* C++ counterpart of .NET ImmutableList.CreateBuilder<T>().
202+
*/
203+
[[nodiscard]] static Builder CreateBuilder() { return Builder(); }
204+
77205
/**
78206
* @brief Creates an ImmutableList from an initializer list.
79207
*
@@ -96,6 +224,14 @@ class ImmutableList {
96224
return ImmutableList<T>(std::make_shared<std::vector<T>>(items));
97225
}
98226

227+
/**
228+
* @brief Creates a mutable Builder with a copy of this list's contents.
229+
*
230+
* C++ counterpart of .NET ImmutableList<T>.ToBuilder(). The vector-backed
231+
* representation copies contents so the source remains immutable.
232+
*/
233+
[[nodiscard]] Builder ToBuilder() const { return Builder(*data_); }
234+
99235
/**
100236
* @brief Gets the number of elements in the list.
101237
*
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) Robert Vokac and contributors
3+
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
4+
#include <gtest/gtest.h>
5+
6+
#include <vector>
7+
8+
#include "System/ArgumentOutOfRangeException.hpp"
9+
#include "System/Collections/Immutable/ImmutableList.hpp"
10+
11+
using System::Collections::Immutable::ImmutableList;
12+
13+
TEST(ImmutableListBuilderTest, ToBuilderMutatesWorkingCopyWithoutChangingSource) {
14+
const auto source = ImmutableList<int>::Create({1, 2, 4});
15+
auto builder = source.ToBuilder();
16+
17+
builder.Insert(2, 3);
18+
builder[0] = 10;
19+
builder.Add(5);
20+
21+
const auto snapshot = builder.ToImmutable();
22+
EXPECT_EQ(source.getCountProperty(), 3);
23+
EXPECT_EQ(source[0], 1);
24+
EXPECT_EQ(source[2], 4);
25+
EXPECT_EQ(snapshot.getCountProperty(), 5);
26+
EXPECT_EQ(snapshot[0], 10);
27+
EXPECT_EQ(snapshot[1], 2);
28+
EXPECT_EQ(snapshot[2], 3);
29+
EXPECT_EQ(snapshot[4], 5);
30+
}
31+
32+
TEST(ImmutableListBuilderTest, SnapshotsRemainImmutableAfterFurtherBuilderMutations) {
33+
auto builder = ImmutableList<int>::CreateBuilder();
34+
builder.AddRange({1, 2});
35+
36+
const auto first = builder.ToImmutable();
37+
builder.SetItem(0, 10);
38+
builder.Add(3);
39+
const auto second = builder.ToImmutable();
40+
41+
EXPECT_EQ(first.getCountProperty(), 2);
42+
EXPECT_EQ(first[0], 1);
43+
EXPECT_EQ(first[1], 2);
44+
EXPECT_EQ(second.getCountProperty(), 3);
45+
EXPECT_EQ(second[0], 10);
46+
EXPECT_EQ(second[2], 3);
47+
}
48+
49+
TEST(ImmutableListBuilderTest, RangeMutationsAndQueriesFollowMutableListSemantics) {
50+
auto builder = ImmutableList<int>::CreateBuilder();
51+
builder.AddRange({1, 4});
52+
builder.InsertRange(1, {2, 3});
53+
EXPECT_TRUE(builder.Contains(3));
54+
EXPECT_EQ(builder.IndexOf(3), 2);
55+
EXPECT_TRUE(builder.Remove(2));
56+
EXPECT_FALSE(builder.Remove(99));
57+
builder.RemoveRange(1, 2);
58+
59+
const auto result = builder.ToImmutable();
60+
ASSERT_EQ(result.getCountProperty(), 1);
61+
EXPECT_EQ(result[0], 1);
62+
}
63+
64+
TEST(ImmutableListBuilderTest, ValidatesIndexesAndRangesAndClearCreatesEmptySnapshot) {
65+
auto builder = ImmutableList<int>::CreateBuilder();
66+
builder.Add(1);
67+
68+
EXPECT_THROW(builder[-1], System::ArgumentOutOfRangeException);
69+
EXPECT_THROW(builder.Insert(2, 3), System::ArgumentOutOfRangeException);
70+
EXPECT_THROW(builder.RemoveAt(1), System::ArgumentOutOfRangeException);
71+
EXPECT_THROW(builder.RemoveRange(1, 1), System::ArgumentOutOfRangeException);
72+
EXPECT_THROW(builder.RemoveRange(0, -1), System::ArgumentOutOfRangeException);
73+
74+
builder.Clear();
75+
EXPECT_TRUE(builder.ToImmutable().getIsEmptyProperty());
76+
}

0 commit comments

Comments
 (0)