Skip to content

Commit 1ef2a08

Browse files
committed
feat(collections): add immutable list copy operations
1 parent e49aee6 commit 1ef2a08

7 files changed

Lines changed: 155 additions & 21 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,625 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,630 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: 19 additions & 8 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 six bounded
7+
repair, three P1 parity repairs, P1 portability revalidation, and seven bounded
88
P2 API slices are complete: 41 physical modules, 90 production dependency
9-
edges, and 12,625 tests across 37 executables.*
9+
edges, and 12,630 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,625 passing tests
31+
- The full native baseline is a warning-free build with 12,630 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
@@ -40,6 +40,9 @@ Historical session detail belongs in git history and `plan.sqlite3`.
4040
work on non-seekable streams, return partial data at clean EOF, and reject
4141
truncated or malformed UTF-8. Seekable `PeekChar` restores both stream and
4242
decoder state; it deliberately throws on non-seekable streams.
43+
- `ImmutableList<T>` supports all three `CopyTo` overloads using a fixed-size
44+
`std::vector` destination. Its bounds checks distinguish invalid source
45+
ranges from an undersized destination and avoid signed-overflow-prone sums.
4346
- MinGW-w64 GCC 14-win32/CMake 3.31.6 and Emscripten 5.0.7/CMake 3.31.6 both
4447
compile the post-modular `All` graph and selective `Text.Json` libraries.
4548
This is compile-only evidence: cross tests were deliberately disabled.
@@ -48,15 +51,15 @@ Historical session detail belongs in git history and `plan.sqlite3`.
4851
ASan/LSan ownership scenarios, including 100 continuation teardowns, pass.
4952

5053
The local `plan.sqlite3` snapshot contains 16,201 classified `task` rows and
51-
1,748 completed tickets. Ticket #1737 records the completed P0 split, tickets
54+
1,749 completed tickets. Ticket #1737 records the completed P0 split, tickets
5255
#1738/#1739 the MemoryStream and generic-continuation repairs, ticket #1740 the
5356
XML whitespace repair, #1741 the completed cross-build revalidation and
5457
`WebProxy` portability fix, #1742 focused sanitizer evidence, and #1743 the
5558
`ImmutableList<T>` predicate-query slice. Ticket #1744 records seekable
5659
`BinaryReader::PeekChar`, #1745 `ImmutableList<T>::Sort`/`Reverse`, and #1746
57-
`ImmutableList<T>::GetRange`, #1747 `ImmutableList<T>::ConvertAll`, and #1748
58-
the UTF-8 `BinaryReader` batch-character APIs. The database is git-ignored and
59-
is not part of a fresh clone.
60+
`ImmutableList<T>::GetRange`, #1747 `ImmutableList<T>::ConvertAll`, #1748 the
61+
UTF-8 `BinaryReader` batch-character APIs, and #1749 `ImmutableList<T>`
62+
copying. The database is git-ignored and is not part of a fresh clone.
6063

6164
## P0 completion: restore Collections isolation
6265

@@ -165,13 +168,21 @@ cover slice content, source immutability, boundary empties, and invalid ranges.
165168
backed immutable list. It preserves an empty source and rejects an empty
166169
converter with `ArgumentNullException`; three regressions cover those cases.
167170

171+
## P2 completion: `ImmutableList<T>::CopyTo`
172+
173+
All three `CopyTo` overloads now copy to a fixed-size `std::vector<T>`
174+
destination: full list, destination offset, and source/destination range.
175+
They preserve order and source immutability, allow valid empty end ranges,
176+
and distinguish invalid indices/ranges from an undersized destination. Five
177+
regressions cover each overload, boundary behavior, and validation.
178+
168179
## Recommended next bounded tasks
169180

170181
All currently planned P1 work is complete. Choose one consumer-driven P2
171182
slice, create a ticket, and keep the changes isolated:
172183

173184
1. **`ImmutableList<T>` breadth.** Select one real consumer-needed group from
174-
copying, builder support, or comparer overloads.
185+
builder support or comparer overloads.
175186
2. **Other documented partial surfaces.** Examples include `BigInteger`
176187
bitwise operations, fuller UTF-7 behavior, and wider debugger/process/XML
177188
surfaces.

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,625 passing tests across 37 test executables**.
10+
with **12,630 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,625 tests. |
184+
| Linux/GCC | Current full component build and all 12,630 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,625 tests across 36
134+
The verified 2026-07-25 `All` baseline contains 12,630 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: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ using SharpRuntime::intcs;
2626
* Remove/RemoveAll/RemoveAt/RemoveRange(int,int)/Sort/Reverse/Contains/IndexOf/LastIndexOf/
2727
* BinarySearch).
2828
* Deliberately deferred relative to real .NET's ImmutableList<T> (a much larger surface backed
29-
* by an AVL tree, not a flat vector): range and custom-comparer Sort/Reverse overloads, the 3
30-
* CopyTo overloads, ToBuilder/Builder,
29+
* by an AVL tree, not a flat vector): range and custom-comparer Sort/Reverse overloads,
30+
* ToBuilder/Builder,
3131
* RemoveRange(IEnumerable<T>), and every
3232
* IEqualityComparer<T>/IComparer<T>-taking overload of Remove/RemoveRange/Replace/IndexOf/
3333
* LastIndexOf/BinarySearch (this port always uses T::operator== / operator< instead). These are
@@ -314,6 +314,62 @@ class ImmutableList {
314314
return ImmutableList<TOutput>::Create(values);
315315
}
316316

317+
/**
318+
* @brief Copies every element to a compatible destination vector.
319+
*
320+
* C++ counterpart of .NET ImmutableList<T>.CopyTo(T[]). The vector is not
321+
* resized: callers retain the same fixed-destination-array contract as .NET.
322+
* @throws System::ArgumentException if @p destination is too small.
323+
*/
324+
void CopyTo(std::vector<T>& destination) const {
325+
CopyTo(0, destination, 0, getCountProperty());
326+
}
327+
328+
/**
329+
* @brief Copies every element to @p destination starting at @p arrayIndex.
330+
*
331+
* C++ counterpart of .NET ImmutableList<T>.CopyTo(T[], int).
332+
* @throws System::ArgumentOutOfRangeException if @p arrayIndex is negative.
333+
* @throws System::ArgumentException if @p destination cannot hold all elements.
334+
*/
335+
void CopyTo(std::vector<T>& destination, intcs arrayIndex) const {
336+
CopyTo(0, destination, arrayIndex, getCountProperty());
337+
}
338+
339+
/**
340+
* @brief Copies a source range to a compatible destination vector.
341+
*
342+
* C++ counterpart of .NET ImmutableList<T>.CopyTo(int, T[], int, int).
343+
* @param index The zero-based source index.
344+
* @param destination The fixed-size destination vector.
345+
* @param arrayIndex The zero-based destination index.
346+
* @param count The number of elements to copy.
347+
* @throws System::ArgumentOutOfRangeException if a source index, destination index, or
348+
* count is negative, or the source range is invalid.
349+
* @throws System::ArgumentException if @p destination is too small.
350+
*/
351+
void CopyTo(intcs index, std::vector<T>& destination, intcs arrayIndex, intcs count) const {
352+
const intcs sourceCount = getCountProperty();
353+
if (index < 0 || index > sourceCount) {
354+
throw System::ArgumentOutOfRangeException("index", "Index was out of range. Must be non-negative and within the collection.");
355+
}
356+
if (count < 0 || count > sourceCount - index) {
357+
throw System::ArgumentOutOfRangeException("count", "Count must refer to a location within the collection.");
358+
}
359+
if (arrayIndex < 0) {
360+
throw System::ArgumentOutOfRangeException("arrayIndex", "Non-negative number required.");
361+
}
362+
// Use subtraction after validating the non-negative destination index. This avoids the
363+
// signed-overflow hazard in an `arrayIndex + count > destination.size()` check.
364+
if (static_cast<intcs>(destination.size()) - arrayIndex < count) {
365+
throw System::ArgumentException(
366+
"Destination array was not long enough. Check the destination index, length, and the array's lower bounds.");
367+
}
368+
369+
std::copy(data_->begin() + index, data_->begin() + index + count,
370+
destination.begin() + arrayIndex);
371+
}
372+
317373
/**
318374
* @brief Returns a new list with all elements sorted using T::operator<.
319375
*
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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/ArgumentException.hpp"
9+
#include "System/ArgumentOutOfRangeException.hpp"
10+
#include "System/Collections/Immutable/ImmutableList.hpp"
11+
12+
using System::Collections::Immutable::ImmutableList;
13+
14+
TEST(ImmutableListCopyTests, CopyToCopiesEntireListWithoutResizingDestination) {
15+
const auto source = ImmutableList<int>::Create({10, 20, 30});
16+
std::vector<int> destination{0, 0, 0};
17+
18+
source.CopyTo(destination);
19+
20+
EXPECT_EQ(destination, (std::vector<int>{10, 20, 30}));
21+
EXPECT_EQ(source.getCountProperty(), 3);
22+
EXPECT_EQ(source[0], 10);
23+
}
24+
25+
TEST(ImmutableListCopyTests, CopyToWithDestinationOffsetPreservesSurroundingElements) {
26+
const auto source = ImmutableList<int>::Create({10, 20, 30});
27+
std::vector<int> destination{-1, -1, -1, -1, -1};
28+
29+
source.CopyTo(destination, 1);
30+
31+
EXPECT_EQ(destination, (std::vector<int>{-1, 10, 20, 30, -1}));
32+
}
33+
34+
TEST(ImmutableListCopyTests, CopyToRangeCopiesOnlyRequestedSourceRange) {
35+
const auto source = ImmutableList<int>::Create({10, 20, 30, 40, 50});
36+
std::vector<int> destination{-1, -1, -1, -1, -1, -1};
37+
38+
source.CopyTo(1, destination, 2, 3);
39+
40+
EXPECT_EQ(destination, (std::vector<int>{-1, -1, 20, 30, 40, -1}));
41+
}
42+
43+
TEST(ImmutableListCopyTests, CopyToRangeAllowsEmptyRangeAtEndOfSourceAndDestination) {
44+
const auto source = ImmutableList<int>::Create({10, 20});
45+
std::vector<int> destination{7, 8};
46+
47+
source.CopyTo(2, destination, 2, 0);
48+
49+
EXPECT_EQ(destination, (std::vector<int>{7, 8}));
50+
}
51+
52+
TEST(ImmutableListCopyTests, CopyToValidatesSourceRangeAndDestinationCapacity) {
53+
const auto source = ImmutableList<int>::Create({10, 20, 30});
54+
std::vector<int> destination(3);
55+
std::vector<int> tooSmall(2);
56+
57+
EXPECT_THROW(source.CopyTo(destination, -1), System::ArgumentOutOfRangeException);
58+
EXPECT_THROW(source.CopyTo(-1, destination, 0, 1), System::ArgumentOutOfRangeException);
59+
EXPECT_THROW(source.CopyTo(1, destination, 0, -1), System::ArgumentOutOfRangeException);
60+
EXPECT_THROW(source.CopyTo(2, destination, 0, 2), System::ArgumentOutOfRangeException);
61+
EXPECT_THROW(source.CopyTo(0, destination, -1, 1), System::ArgumentOutOfRangeException);
62+
EXPECT_THROW(source.CopyTo(0, tooSmall, 0, 3), System::ArgumentException);
63+
EXPECT_THROW(source.CopyTo(destination, 1), System::ArgumentException);
64+
}

plan.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Sharp Runtime plan
22

33
*Last verified: 2026-07-25 — 41 physical components, 90 direct production
4-
dependency edges, a clean native build, 12,625 passing tests across 37
4+
dependency edges, a clean native build, 12,630 passing tests across 37
55
executables, and a green ten-job selective matrix.*
66

77
Sharp Runtime is in a consumer-driven expansion phase. The original type
@@ -32,7 +32,7 @@ was never created. Neither file should be linked as current documentation.
3232
### Code and validation
3333

3434
- Native Linux/GCC build: zero errors and zero warnings.
35-
- Tests: 12,625 passing across 36 component binaries plus one integration
35+
- Tests: 12,630 passing across 36 component binaries plus one integration
3636
binary.
3737
- Component graph: 41 physical modules and 90 direct production edges.
3838
- Boundary validator: no cycles, duplicate public include paths, orphan
@@ -52,7 +52,7 @@ The 2026-07-25 local snapshot contains:
5252
| Table | State |
5353
|---|---|
5454
| `task` | 16,201 rows: 1,082 `ported`, 140 `ignore`, 14,979 legacy `ignored`; no unclassified or `tobedecided` rows |
55-
| `ticket` | 1,748 rows, all `done`; no `todo`, `doing`, `blocked`, or `needs_user` rows |
55+
| `ticket` | 1,749 rows, all `done`; no `todo`, `doing`, `blocked`, or `needs_user` rows |
5656

5757
Because `plan.sqlite3` is git-ignored, these counts describe the maintainer
5858
snapshot, not data shipped in a fresh clone.
@@ -104,6 +104,9 @@ assertion without an explicit architecture decision.
104104
preserve UTF-8 decoder output across calls, return a partial result at clean
105105
EOF, propagate truncated input, and expose supplementary scalars as UTF-16
106106
surrogate pairs.
107+
- Added all three `ImmutableList<T>::CopyTo` overloads under ticket #1749,
108+
using checked fixed-size `std::vector` destinations, including source range
109+
and destination offset handling without signed-overflow-prone bounds checks.
107110
- Added consumer-driven coverage across core, collections, IO, networking,
108111
threading/tasks, text/JSON, XML, numerics, globalization, and cryptographic
109112
hashing/random APIs.
@@ -164,7 +167,7 @@ The first consumer-driven ports after modularization added:
164167
- XML schema exception types.
165168

166169
The verified test baseline grew from 12,494 at the modularization checkpoint
167-
to 12,625.
170+
to 12,630.
168171

169172
## Candidate roadmap
170173

@@ -174,9 +177,9 @@ acceptance criteria and a validation command before changing code.
174177
### P2 — Consumer-driven API breadth
175178

176179
1. **Continue a bounded `ImmutableList<T>` slice.**
177-
Its documented omissions include copying, builder support, and comparer
178-
overloads. Do not attempt the entire surface in one change; select methods
179-
required by a real consumer and port them against the .NET reference.
180+
Its documented omissions include builder support and comparer overloads.
181+
Do not attempt the entire surface in one change; select methods required by
182+
a real consumer and port them against the .NET reference.
180183

181184
2. **Extend `BinaryReader` only from a concrete consumer need.**
182185
`ReadChar`, `ReadChars`, `Read(char[])`, `ReadDecimal`, and seekable

0 commit comments

Comments
 (0)