Repository navigation
feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module - #31
Open
xperiandri wants to merge 23 commits into
Open
xperiandri wants to merge 23 commits into
xperiandri wants to merge 23 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Overflowing removal ranges violate the promised List behavior, and stability tests need stronger regression inputs.
2 open issues
What changed in this PR
Replaces the legacy immutable-list API with IndexedList and IIndexedList, continuing ADR 0001’s per-collection rewrite.
Changes:
- Adds List-compatible operations, stable sorting, and implementation-preserving interface operations.
- Adds Option/Tuple variants, builder helpers, and
indexedList { }. - Expands contract tests and updates migration documentation.
| File | Description |
|---|---|
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTupleTests.fs | Tests reference-tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs | Tests concrete-list semantics and extras. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListOptionTests.fs | Tests option overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListOps.fs | Connects both modules to contracts. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListContractTests.fs | Instantiates shared contracts. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListComputationExpressionTests.fs | Tests computation expressions. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListBuilderTests.fs | Tests builder operations and stability. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTupleTests.fs | Tests interface tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs | Tests implementation preservation and validation. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListOptionTests.fs | Tests interface option overrides. |
| tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj | Includes new test files. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/StableSortContract.fs | Adds shared stability checks. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs | Adjusts null-array expectations. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs | Covers nonpositive removal counts. |
| src/FSharp.Collections.Immutable/IndexedSeq.fs | Removes legacy indexed-sequence bindings. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fsi | Declares tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs | Implements tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListOption.fsi | Declares option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListOption.fs | Implements option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListCE.fs | Implements the computation-expression builder. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListBuilder.fs | Adds mutable builder helpers. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fsi | Declares interface tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs | Implements interface tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListOption.fsi | Declares interface option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListOption.fs | Implements interface option overrides. |
| src/FSharp.Collections.Immutable/ImmutableList.fs | Removes the legacy module. |
| src/FSharp.Collections.Immutable/Helper.fs | Adds shared List checks and algorithms. |
| src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj | Registers replacement modules. |
| src/FSharp.Collections.Immutable/ComputationExpressions.fs | Exposes indexedList. |
| README.md | Updates the collection catalog. |
| docsSrc/Tutorials/Getting_Started.md | Migrates the list-construction example. |
| docsSrc/index.md | Updates the collection catalog. |
| docsSrc/Explanations/Background.md | Updates supported collection bindings. |
| CHANGELOG.md | Records additions and breaking changes. |
Review effort: Balanced
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 13:03
3a6e19b to
f5818ff
Compare
xperiandri
force-pushed
the
feat/flat-list
branch
2 times, most recently
from
October 18, 2026 15:35
2b5179a to
9d2526e
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 15:35
f5818ff to
56e9ddf
Compare
This was referenced Oct 18, 2026
xperiandri
force-pushed
the
feat/flat-list
branch
from
October 18, 2026 18:44
9d2526e to
047ee9b
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 18:49
56e9ddf to
e7a6257
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 20:12
430f3fb to
53a3555
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:31
aa81bb5 to
6ce859c
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the FSharp.Core `List` module
`IndexedList` (`ImmutableList<'T>`) replaces the `ImmutableList` module. It has every function of `List` that applies to
an immutable list, with the parameter names, the checks and the exceptions of `List` and with stable sorts, plus the
extras of ADR 0001: `add`, `binarySearch`, `blit`, `cons`, `uncons`, `tryUncons`, the struct partial patterns `Cons` and
`Nil`, `indexOf`, `lastIndexOf`, `skipLast`, `takeLast`, `tryTail` and `sortWithComparer`. `IIndexedList` has the same
functions for `IImmutableList<'T>` without creation, builder, `binarySearch` and `blit`, plus `clear`; a result with the
element type of the input keeps the implementation of the input. The `Option` and `Tuple` namespaces repeat the
functions that use value options or struct tuples, `IndexedListBuilder` works on the builder of an indexed list, and
`indexedList { }` builds one.
`Helper.fs` gains three internal modules for the modules that mirror `List`: `ListChecks` throws the exceptions of `List`
for lengths and indices with the parameter names that the caller passes, `ListArrays` runs the functions of two and three
lists over arrays and checks the lengths after the common prefix, as `List` does, and `Snapshot` copies a collection into
an array.
The `ImmutableList` and `IndexedSeq` modules, the `ReadOnlyList` alias and the `IIndexedSeq<'T>` abbreviation are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orting contract
`StableSortContract` checks that `sort`, `sortBy`, `sortByDescending`, `sortDescending`, `sortWith` and
`sortWithComparer` keep the order of equal elements, as the `List` functions do. The pairs of `Inputs.pairs` show it for
the sorts by key; `sort` and `sortDescending` run on `KeyedItem`, whose comparison looks at a key only.
The sequence-like, creation, index-based and stable sorting contracts run for `IndexedList`, and all of them but the
creation contract for `IIndexedList`. Further tests cover null inputs, shared trees, `cons`, `uncons` and the patterns,
`binarySearch`, `blit`, the order in which the functions of two lists call their callbacks before they throw, the complete
messages of the `List` exceptions, the implementation that `IIndexedList` keeps, the builder functions,
`indexedList { }` and the `Option` and `Tuple` namespaces.
Two shared contracts now serve the modules that mirror `List` as well. `ofArray` of a null array is compared with
`Array.toList`, because `List.ofArray` reads a null array without a check. `removeManyAt` of a count that is not positive
is compared with what the mirrored function returns or throws, because `List.removeManyAt` removes nothing where
`Array.removeManyAt` copies by the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ction table
The changelog lists the new functions, the renamed `ImmutableList` module and functions, the breaking changes of the
`List` semantics and the removed functions and modules. The collection tables name `IndexedList` with `indexedList { }`
and `IIndexedList` instead of `ImmutableList` and `IIndexedSeq`, and the tutorial creates an `IndexedList`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e collection table The changelog says that `IndexedList.rev` is about 2.5 times faster than `ImmutableList.Reverse`, which the old `ImmutableList.reverse` called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract The tests compare arrays and take their cases as data rows, as the converted contracts of `FlatList` do (2292 tests per target framework instead of 771): - `IndexedListOps` and `IIndexedListOps` observe a collection with its `toArray`, and results of another element type are arrays; `ToList` is the `toList` under test. `StableSortOps` observes with `ToArray`, and its `Sort` and `SortDescending` take arrays. - `Inputs.ManyPairs` is a property of the `Inputs` class that builds new pairs on each read. - The case tables of `IndexedListTests` and `IIndexedListTests` are maps whose names are the rows. The names tell apart the cases that repeated a function or a parameter: `blit`, `foldBack2` and the two parts of `partition` and `splitAt`. - `sortWith` of the stable sorting contract takes the direction of its comparison as a `SortDirection` row, and `tryItem` of the `Option` tests takes its index as a `DataRow`. - The `removeManyAt` tests of a count that is not positive and of a range whose end overflows take their ranges as rows of the index-based contract. - Expectations come from the `Array` functions, except where `List` behaves differently: the stable sorts, the walk of the common prefix before a length check, `exists2` and `forall2`, `contains` of `NaN`, `removeManyAt` and the exceptions keep their `List` oracles. - Tuples that hold arrays are compared by their parts. - Every case of the old loops is a row: 190 in `IndexedListTests`, 175 in `IIndexedListTests`, 19 and 17 in the `Option` tests, 7 in each stable sorting class. - The doc comments of the stable sorting contract and of `TaggedList` follow the layout rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. The documentation of `Lengths` and `ListChecks` refers to `IImmutableQueue<'T>`, `IImmutableStack<'T>` and `IndexedList` with crefs, instead of naming the `Queue` and `Stack` modules, which this branch does not have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module - `IndexedListBuilder.fsi` and `IndexedListCE.fsi` carry the signatures and the XML documentation of their files, compiled right before them. `indexedList` has its `val` in `ComputationExpressions.fsi`, after `flatList` under the `Creation` banner. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `IndexedList` and `IIndexedList` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. - LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `sortWith` takes about the same time as before and allocates 680 312 bytes per call instead of 600 328. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract
- The stable sorting contract checks that `sort`, `sortDescending` and `sortWith` throw what
`List` throws when a comparison throws, and that `sortBy` and `sortByDescending` do when the
projection throws. A `KeyedItem` with the key `UncomparableKey` throws when it is compared.
- `indexedList { yield! ... }` rejects a null sequence, array or indexed list with
`ArgumentNullException` for the parameter `items`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract The assertion helpers of `main` are static classes: the tests call `ExceptionAssert.Same` and `OverrideAssert.OptionMatches` with tupled arguments, in place of the curried functions of the former modules (31 calls in 7 files). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module `sortDescending` and `sortByDescending` of `IndexedList` and `IIndexedList` call the new internal module `DescendingSorts` of `Helper.fs`. `Seq.sortDescending` and `Seq.sortByDescending` are inline: in a generic function they compare with the generic operator, which boxes the elements, or the keys, in every comparison. At 10 000 integers, a call allocated 7.5 MB and took 7.2 ms; it allocates 0.68 MB and takes 1.7 ms, as `sortWith` does. `DescendingSorts.sortDescending` and `sortByDescending` run `Seq.sortWith` with the comparer that `DefaultComparer` caches, reversed. The comparisons, their order and the exceptions are those of `List.sortDescending` and `List.sortByDescending`, so `Queue` and `Stack` can call the same helpers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract - The stable sorting contract checks that `sortDescending` and `sortByDescending` allocate less than 3 MB for 10 000 integers: 1.2 MB with the cached comparer, 8 MB with the inline `Seq` sorts. The second run of a call is measured, after the first has compiled the code. `StableSortOps` gets the functions `SortIntegersDescending` and `SortIntegersByDescending` for it. - It checks that `sortByDescending` calls the projection in the comparisons, in the order of `List.sortByDescending`; a sort that computes the keys first fails it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module `IndexedList` gets the functions of `ImmutableList<T>` that the module lacked, with the names and signatures of the API gap analysis: - `remove`, `removeWith`, `removeMany`, `removeManyWith`, `replace`, `replaceWith`: removal and replacement by value, in `EqualityComparer<'T>.Default` or a given equality comparer. Nothing to remove returns the input itself, and `replace` throws `ArgumentException` naming `oldValue` when no element equals it, as the BCL does. - `indexOfRange`, `indexOfRangeWith`, `lastIndexOfRange`, `lastIndexOfRangeWith`, `tryFindIndexRange`, `tryFindIndexBackRange`: searches in a range, which return indices of the whole indexed list. The two `tryFind` functions have their `Option.IndexedList` counterparts. - `sub`, `revRange`: a range of the elements, and the reversal of a range. `sub` throws what `Array.sub` throws, as `FlatList.sub` does. - `sortRange`, `sortRangeWith`, `sortRangeWithComparer`: stable, like every sort of the module. A copy of the range is sorted with LINQ `Order`, and put back with `RemoveRange` and `InsertRange`; `ImmutableList.Sort (index, count, comparer)` is unstable. The cost depends on the range: 10 000 elements of 1 000 000 take 2.3 ms and allocate 1.6 MB, where the BCL member takes 68 ms and allocates 52 MB, because it copies the whole list. - `binarySearchWithComparer`, `binarySearchRange`, `binarySearchRangeWithComparer`. `IIndexedList` gets `remove`, `removeWith`, `removeMany`, `removeManyWith`, `replace`, `replaceWith` and the four searches in a range. They call the members of the interface, so the results keep the implementation of the input. `IndexedListBuilder` gets `insertManyAt`, `removeAll`, `removeMany`, `removeManyAt`, `removeManyWith`, `removeWith`, `replace`, `replaceWith`, `revRange`, `sortRange` (stable, like the sorts of the module), `sortRangeWith`, `sortRangeWithComparer`, the four searches in a range, `binarySearch`, `binarySearchWithComparer`, `binarySearchRangeWithComparer` and `blit`. `replace` of the builder throws when no element equals the old value, as the builder of the BCL does. `addRange` names `values` in its `ArgumentNullException`: the BCL names it `items`. The functions that take a range check it first (ADR 0001, D14). The internal module `ListRanges` of `Helper.fs` holds the checks, which compare the count with the difference between the length and the index. The members of `ImmutableList` add the index and the count and accept a sum that wraps around `Int32.MaxValue`. `BinarySearch` accepts a range beyond the end, although its documentation promises `ArgumentException`, which the module throws. `LastIndexOf` and `FindLastIndex` accept a last element at or after the end and answer with the index of another element, and `FindLastIndex` takes a negative count for "all": the module throws `ArgumentOutOfRangeException` for them. A range of no elements searches nothing, for any start index. `blit` of `IndexedList` uses the same checks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract Tests of the new functions of `IndexedList`, `IIndexedList` and `IndexedListBuilder`, in rows: - `Contracts/ListMemberCases.fs` is new. It holds the strings that tell the default equality from an equality that ignores the case, the oracles of the removal, the replacement and the searches in a range, and the rows: every range of eight strings, the ranges that are rejected with the fault of each (`RangeFault`), and the sorted inputs of the binary searches. `IndexedList`, `IIndexedList` and the builder share them. - `Contracts/ListMembersContract.fs` is new. `IndexedList` and `IIndexedList` derive it: the removal and the replacement by value, the input itself when nothing is removed, the exception and the parameter of `replace`, the four searches in every range, and the exceptions of every range that is rejected, with the ranges that the members of `ImmutableList` accept (ADR 0001, D14). No other module has these functions, which is why it is not part of the contracts that `FlatList` and the queues run. - `IndexedListRangeTests` covers `sub`, `revRange`, `sortRange`, `sortRangeWith`, `sortRangeWithComparer`, the three binary searches, `tryFindIndexRange` and `tryFindIndexBackRange` of `IndexedList`: every range of the inputs against the arrays, the stability on `Inputs.ManyPairs`, the parameter that each function names for a range that it rejects, the input itself for a range of at most one element, and a comparison that does not run for such a range. - `IIndexedListTests` proves that the six functions of removal and replacement keep the implementation of their input, that the searches in a range work on an implementation that is not an indexed list, and that `removeMany` and `removeManyWith` reject null values even for a list that would accept them (`TaggedList` gets that mode). `IndexedListOptionTests` covers the two `tryFind` functions. - `IndexedListBuilderTests` covers the new functions of the builder against the same oracles and rows, including the result of the functions that change a builder, the `bool` of `removeWith`, the parameter names of `addRange` and `insertManyAt`, and `blit`. - `KeyedItem` gets `OfManyPairs` and `KeysAndTags`, in place of the copies in the stable sorting contract and the builder tests. The rows of the rejected copies of `blit` are shared with the builder. Each new behaviour was broken once in a copy of the sources (a call dropped, a comparer ignored, the arguments swapped, a check removed, the stable sort replaced by the BCL sort, the range check replaced by the sum of the index and the count): the tests fail for every change that alters a result. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table The changelog lists the new functions of `IndexedList`, `IIndexedList` and `IndexedListBuilder` and corrects the `Removed` entries of the 1.0.0 `ImmutableList` functions: - `indexFrom`, `indexRange` and their `lastIndex` counterparts are replaced by `indexOfRange`, `indexOfRangeWith`, `lastIndexOfRange` and `lastIndexOfRangeWith`, with the formulas for `indexFrom` and `lastIndexFrom`. They no longer send their users to `indexOf`, which searches the whole list. - `remove`, `removeWith`, `replace` and `replaceWith` are no longer removed: `IndexedList` has them again. `reverseRange` is `revRange`, with two arguments instead of a tuple, and `exceptWith` is `removeManyWith`. The entry no longer sends users to "the members of `ImmutableList<'T>`". - `skipUntil` and `takeUntil` are `skipWhile` and `takeWhile` with the negated predicate. - `filterFold` has no replacement. - The 1.0.0 `append list items` took a sequence: `insertManyAt (length list) items list`, or `append list (ofSeq items)`. The 1.0.0 `except` removed one element for each item, which `removeMany` does. - The 1.0.0 functions compared in F# structural equality (`HashIdentity.Structural`), and the new ones compare in `EqualityComparer<'T>.Default`. - The entry of the `IndexedSeq` module no longer lists the `ReadOnlyList` alias: a module abbreviation is not in the IL of the assembly, and a project that references the 1.0.0 package cannot compile `ReadOnlyList.length`, where `IndexedSeq.length` and the `IIndexedSeq<'T>` abbreviation compile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract The tests of the new functions use what the tests of `FlatList` share, in place of their own copies: - `ListMemberCases.fs` and `ListMembersContract.fs` move from `Contracts/` to `IndexedList/` and come after `FlatList/FlatListOracles.fs` in the compile order, because they use it. - The oracles of the removal of the first equal element, the replacement, the search in a range from the front, the equality that ignores the case and the descending and absolute orders are `FlatListOracles.removeFirst`, `replaceFirst`, `firstInRange`, `equalIgnoringCase`, `descending` and `byAbsolute`. `ListOracles` keeps the oracles that only the indexed list has: the change of a range, the stable sorts, the removal of several values and the search from the back. That one answers -1 for a range of no elements before it calls `FlatListOracles.lastInRange`, because the bounds of a range that starts at `Int32.MaxValue` overflow, and the members of `ImmutableList` accept such a start index for an empty range. - The assertions that a function rejects a `null` input and names its parameter call `FlatListAssert.RejectsNull` in the tests of `IndexedList`, `IIndexedList`, the builder and the computation expression. The tests are the same: 7421 per target framework. The 81 changes of the sources of the package fail the same tests as before. 78 are caught, and the three that remain change no result: an additional check in `IndexedList.blit` that is made anyway (a control), `sortRange*` of the builder for a range of one element, and `ListRanges.search` without the check of a negative count, which `BinarySearch` repeats. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table The `Added`, `Changed` and `Removed` entries of `IndexedList` compare with the published 1.0.0, not with `main`. They were checked in the 1.0.0 assembly by reflection (its list module is the class `ImmutableListModule` with 75 functions, and `IndexedSeq` has 3), by compiling against it and by running its functions next to the new ones. - `Removed` names the 23 functions that 1.0.0 had and `IndexedList` lacks, each with its replacement. Thirteen renames that keep their arguments are one entry. `index`, `indexWith`, `indexFrom` and `indexFromWith` returned a function that took the list a second time, so their entry says that the replacements return the index. `skipUntil` and `takeUntil` returned each other's result, so their entry says which call returns what. `IndexedSeq` and `IIndexedSeq<'T>` point to `IReadOnlyList<'T>`, and `IndexedSeq.check` needs no replacement. - `Changed` lists what running both showed: the compiled names (PascalCase, in the class `IndexedList`), the `ImmutableList<'T>` parameters where 1.0.0 took any `IImmutableList<'T>`, the arguments of `ofList`, `collect`, `concat` and `append`, the `voption` results, the exceptions and results of `head`, `last`, `tail`, `item`, `removeAt`, `take`, `skip`, `truncate`, `splitAt`, `forall2`, `contains` and `ofBuilder`, the swapped `skipWhile` and `takeWhile` of 1.0.0, `except`, the equality of `contains`, `remove`, `replace` and the `indexOf` family, the differences of the functions that replace renamed ones, and the speed of `rev`. It no longer says that a `null` list throws `ArgumentNullException` (it did in 1.0.0, except in `contains`, `reverse` and `reverseRange`), and it no longer lists the sorts and `tryPick`, which 1.0.0 did not have. - `Added` no longer names `add`, `cons`, `remove`, `removeWith`, `replace`, `replaceWith` and `tryTail`, which 1.0.0 had, and its sentence about the wrapped members no longer describes the equality of `remove` and `replace`, which moved to `Changed`. - The speed of `rev` is the measurement against the 1.0.0 `reverse` at 100 000 integers: about 3 ms against 23 ms, and 5.2 MB against 4.8 MB. The entry said about 2.5 times faster with the same allocation. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module The remark of `IndexedList.rev` and the comment in its body said that the function is about 2.5 times faster than `ImmutableList.Reverse` at 100 000 elements, with the same allocation. Measured against the 1.0.0 `reverse`, which calls `Reverse`, at 100 000 integers (Release, .NET 10, the median of 15 rounds), `rev` takes about 3 ms and `Reverse` about 23 ms, and `rev` allocates the copy of the elements in addition (5.2 MB against 4.8 MB). Only the remark and the comment change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module
A parameter of type `seq<'T>` rejects a default flat list that is given as a sequence, as it rejects
`null` (ADR 0001, D6). The default flat list is a boxed struct, so `checkNotNull` lets it through, and the
call fails with `InvalidOperationException` when it enumerates the sequence. A call of
`checkNotNullOrDefaultSeq` does both checks and names the parameter of the function, before anything else
runs:
- `IndexedList`: `ofSeq`, `concat`, `insertManyAt`, `removeMany`, `removeManyWith`, `transpose` and
`except`. `removeMany` and `removeManyWith` did not check the items at all and left it to the member,
which names a null sequence `items` but fails while it enumerates a default flat list.
- `IIndexedList`: the same functions except `ofSeq`. `concat` and `transpose` reject a default flat list
as an element too, because a default flat list implements `IImmutableList<'T>` and can be one.
- `IndexedListBuilder`: `addRange`, `insertManyAt`, `removeMany` and `removeManyWith`. `insertManyAt`
checks the index before the values, as the member and `FlatListBuilder.insertManyAt` do, so an index
out of range wins over a missing sequence.
- `indexedList { }`: `yield!` of a sequence and `for`. The check runs when the expression calls the
member, as in `flatList { }`; `for` did not check the sequence at all and failed with
`NullReferenceException` for `null`.
The documentation of the affected parameters says "`null` or a default flat list", as `FlatList.fsi` does,
and the remarks of the modules say that a function that takes a sequence rejects a default flat list given
as a sequence. The audit covered every `seq<_>` and `IEnumerable<_>` parameter of the branch; the `Option`
and `Tuple` modules have none.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract Tests of the sequence inputs, shared where every implementer has the function. The shared tests run for `FlatList`, `IndexedList` and `IIndexedList`, and for `Queue`, `IQueue`, `Stack` and `IStack` once they derive the contracts: - `SeqCreationContract`: `ofSeq` rejects a default flat list given as a sequence with what it throws for a null sequence (`FlatList`, `IndexedList`, `Queue`, `Stack`). - `SeqLikeTransformationContract`: `concat` and `transpose`, in one data-row test each, reject a null sequence of collections with `ArgumentNullException` that names the parameter of FSharp.Core (`arrays` for `Array`, `lists` for `List`, mapped to the name of the module), and reject a default flat list given as the sequence with what they throw for a null one. `List.concat` fails with `NullReferenceException` for `null`, so the test asserts the exception of the module and not the one of the oracle. - `SeqLikePartitioningContract`: `except` rejects a default flat list given as the items to exclude with what it throws for null items, on an empty and on a non-empty input. - `IndexedContract`: `insertManyAt` rejects null values and a default flat list with `ArgumentNullException` for `values`, and checks the index before the values as `Array.insertManyAt` and `List.insertManyAt` do. `Inputs.DefaultFlatListAsSeq` is the input of these tests. Only the indexed list and its interface module have the next ones: `ListMembersContract` checks that `removeMany` and `removeManyWith` reject null items and a default flat list, on an empty and on a non-empty collection, and that they check the collection before the items. The rest is per module, for what a contract over integers cannot say: the tables of `IndexedListTests`, `IIndexedListTests` and `IndexedListBuilderTests` have the rows of a default flat list of strings given as a sequence of objects, the order of the checks, and, for `IIndexedList`, a default flat list as an element of the sequence of lists of `concat` and `transpose`. `IndexedListBuilderTests` replaces the table of null values by one with a row for each function and each input, and checks that `insertManyAt` checks the index before the values. `IndexedListComputationExpressionTests` has a table for `yield!` and `for`. The tests that a flat list that is not default still passes as a sequence of objects are in the same classes. `IIndexedListTests.insertManyAt rejects null values with the name of the parameter` is gone: the contract of `insertManyAt` covers it for every module. Breaking each fix once in a copy of the sources (28 changes: the check back to a null check, the check removed, the checks in the old order, the check of an element removed) fails the tests in every case. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table
The changelog says that a function which takes a sequence rejects a default flat list given as a sequence
with `ArgumentNullException` that names the parameter (`IndexedList`, `IIndexedList`, `IndexedListBuilder`
and the `indexedList { }` expression), and that the 1.0.0 `ofSeq`, `concat` and `except` threw
`InvalidOperationException` for it, and returned for an empty list in `except`. Both statements were checked
by running the functions of the published 1.0.0 assembly and of this branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:40
6ce859c to
d416a34
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
commented
Oct 19, 2026
|
|
||
| [<CompiledName "AllPairs">] | ||
| let allPairs (list1 : IImmutableList<'T1>) (list2 : IImmutableList<'T2>) = | ||
| Arrays.allPairs structPair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) |
Collaborator
Author
There was a problem hiding this comment.
Have you compared to non-snapshot implementations?
| [<CompiledName "Choose">] | ||
| let choose (chooser : 'T -> 'U voption) (list : IImmutableList<'T>) = | ||
| checkNotNull (nameof list) list | ||
| let chosen = ResizeArray<'U>() |
Collaborator
Author
There was a problem hiding this comment.
why not builder?
Comment on lines
+359
to
+361
| Array.chunkBySize chunkSize (Snapshot.ofSeq (nameof list) list) | ||
| |> Array.map IndexedList.ofArray | ||
| |> IndexedList.ofArray |
Collaborator
Author
There was a problem hiding this comment.
is it the most optimal implementation?
| [<CompiledName "Collect">] | ||
| let collect (mapping : 'T -> IImmutableList<'U>) (list : IImmutableList<'T>) = | ||
| checkNotNull (nameof list) list | ||
| let collected = ResizeArray<'U>() |
Collaborator
Author
There was a problem hiding this comment.
why not builder?
Comment on lines
+13
to
+14
| |> IndexedList.toArray | ||
| |> Array.map IndexedList.toArray |
| /// <remarks> | ||
| /// The list is a balanced binary tree: reading, inserting, replacing or removing the element at an index takes O(log n). | ||
| /// </remarks> | ||
| type IndexedList<'T> = ImmutableList<'T> |
Collaborator
Author
There was a problem hiding this comment.
what about IIndexedList interface alias?
| let found : (int * IndexedList<int>) option = IndexedList.tryUncons list | ||
|
|
||
| // A tuple compares an array by reference, so the first element and the other elements are compared apart | ||
| match found with |
Collaborator
Author
There was a problem hiding this comment.
use WantSome for all such cases in all tests
…lary of the FSharp.Core `List` module The helpers of the branch get the documentation of `Arrays` and `ParallelArrays`: an explicit `<summary>`, `<param>`, `<returns>`, `<exception>` and `<remarks>` for every function, and `<see cref>` instead of `<c>` for every function and type of FSharp.Core. The remarks say why the function exists. - `ListChecks`: every check names the functions of `List` that it serves, the exception that it throws (the type, the message and the parameter name) and, where a sibling check exists, why the two are separate (`item` and `removeAt` differ in the message, `replicate` and `nonNegative` in the value, `exists2` and `sameLengths` in the names, `zip3` and `map3` in the lengths, `notEmpty` and `notEmptySequence` in the word). `foldBack2` says why it has two branches, and `transpose` which pair of lists it reports, checked against `local.fs` of FSharp.Core. The private literal `IndexOutsideListBounds` says which five functions of `List` write it and why it is not in `ErrorStrings`. - `ListArrays`: the summaries, the descriptions of the parameters and of the results are those of the functions of `List`. Each remark says why the function is not `Array.iter2` and its siblings: they compare the lengths before they call the callback and report arrays, where the functions of `List` call the callback for the common prefix first and report lists. - `Snapshot`, `Lengths` (with its private `countUpTo`), `DescendingSorts` and `ListRanges`: parameters, results and exceptions, the reason for each function, and the functions that call it. The private literal of `ListRanges` said that its message is the one of System.Collections.Immutable; no resource of that assembly holds it. The runtime throws the message from ``List`1.BinarySearch`` with "array" and "source collection", and the literal says "list", so the comment says that. - The three functions that the branch adds to `Raise` (`invalidArg2ListsDifferent`, `invalidArgRandomizerOutOfRange` and `invalidOpListEmpty`) replace one-line comments with backticks. No code changed: without the doc comments, every file is identical to the file before. The build with `--warnon:3390`, `dotnet fantomas --check` and a check of every `cref` against the generated XML documentation, FSharp.Core and the reference assemblies are clean. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Proposed Changes
Rewrites the legacy
ImmutableListmodule asIndexedList(ImmutableList<'T>) and adds the interface moduleIIndexedList(IImmutableList<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged intomain.IndexedList,IIndexedList, theirOptionandTuplemodules andIndexedListBuilderare the root modules of their files, such asmodule FSharp.Collections.Immutable.IndexedList. TheIndexedList<'T>abbreviation lives inIndexedList/IndexedListTypes.fs, which needs no signature file.IndexedListBuilder.fsiandIndexedListCE.fsiincluded.indexedListhas itsvalinComputationExpressions.fsi, afterflatListunder theCreationbanner.IndexedList/IndexedList.fsiandIndexedList.fs: 162 functions, in the canonical category order. They are theIndexedListcolumn of ADR Appendix A, with exactly the parameter names of the FSharp.CoreListmodule, so the exceptions name the same parameters asList.ImmutableListcome first (D5):ConvertAll,FindAll,FindIndex,FindLastIndex,TrueForAll,Exists,GetRange,Insert,InsertRange,RemoveAt,RemoveRange,SetItem,IndexOf,LastIndexOf,BinarySearch, and nowRemove,RemoveRange (items),ReplaceandReverse (index, count).Arrayfunction and builds a balanced tree withImmutableList.CreateRange.revtakes the faster path that the maintainer chose overImmutableList.Reverse(D5). It copies the elements to an array, reverses it withArray.Reverseand builds the result withImmutableList.CreateRange. At 100 000 integers it takes about 3 ms whereImmutableList.Reverse, which the 1.0.0reversecalled, takes about 23 ms (about 8 times faster; 40 ms against 265 ms at 1 000 000), and it allocates 5.2 MB whereReverseallocates 4.8 MB, because of the copy of the elements. The numbers are the median of 15 rounds in Release on .NET 10, for a list fromCreateRangeand for one built withAdd, run on the 1.0.0 assembly and on this branch.List.sort*is.ImmutableList.Sortis unstable, so it is not used.sortandsortWithsort with the FSharp.CoreSeqsorts, which share the stable sort ofList: they make the same comparisons, in the sameArray.Sort.sortDescendingandsortByDescendingcallSeq.sortWiththrough the internalDescendingSortsmodule (Helper/DescendingSorts.fs), with the cached comparer reversed.Seq.sortDescendingandSeq.sortByDescendingare inline: inside a generic function they compare with the generic operator, which boxes the elements, or the keys, in every comparison and allocates 7.5 MB at 10 000 integers. The comparisons, their order and the exceptions stay those ofList; only the boxes go.sortByandsortWithComparersort with LINQOrderBy/Order, which compute the keys first, asList.sortBydoes.sortRange,sortRangeWithandsortRangeWithComparersort a copy of the range with LINQOrderand put it back withRemoveRangeandInsertRange, so they are stable too.ImmutableList.Sort (index, count, comparer)is unstable and copies the whole list.binarySearchcompare with the cachedDefaultComparer<'T>.Instanceof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26, so a call does not create a comparer.add,cons,uncons,tryUncons,tryTail,skipLast,takeLast,build,update,blit,binarySearch,indexOf/indexOfWith/lastIndexOf/lastIndexOfWith,sortWithComparer, the span constructors and theCons/Nilpatterns.ImmutableList, from the API gap analysis of the modules (20 functions):EqualityComparer<'T>.Defaultor a given equality comparer:remove,removeWith,removeMany,removeManyWith,replace,replaceWith. Nothing to remove returns the input itself, andreplacethrowsArgumentExceptionfor a value that no element equals, as the BCL does.sub(it throws whatArray.subthrows, asFlatList.subdoes),revRange,indexOfRange,indexOfRangeWith,lastIndexOfRange,lastIndexOfRangeWith,tryFindIndexRangeandtryFindIndexBackRange. The searches return indices of the whole indexed list.binarySearchWithComparer,binarySearchRangeandbinarySearchRangeWithComparer, and the stablesortRange*.IIndexedList.fsiandIIndexedList.fs: 136 functions overIImmutableList<'T>.Clear ()plusAddRange, or the member of the interface for an edit of one element.revreverses its copy in place, asIndexedList.revdoes, before it rebuilds.IndexedList. So areconcatandtranspose, because their inputs can have different implementations.IndexedList.remove,removeWith,removeMany,removeManyWith,replaceandreplaceWithcall the members of the interface, so they keep the implementation too. The four searches in a range call the members of the interface after the module has checked the range.IImmutableListimplementation proves that the module keeps it.Listfunction throws: the type, the message and the parameter name. This includes the quirks ofList:exists2andforall2can return before they check the lengths.removeManyAtremoves nothing for a count that is not positive, and nothing whenindex + countoverflows, becauseListwalks the firstindex + countelements. The Copilot review found the overflow: such a range used to reachRemoveRange, which threw.sort,sortDescending,sortWithorsortByDescendingthrows anInvalidOperationExceptionwith the original exception inside, asArray.Sortdoes inList.List.sortByDescendingcalls the projection in the comparison, and so does this module now.sortBylets an exception of the projection escape, asList.sortBydoes.Listreports some unusual parameter names, and the module reports them too:"source"forexactlyOne,averageandaverageBy;"count"forinit;"list.[j]"fortranspose.nullinput throwsArgumentNullException.seq<'T>rejectsnulland a default flat list given as a sequence, a boxed struct that fails withInvalidOperationExceptionas soon as the BCL enumerates it, withArgumentNullExceptionthat names the parameter of the function.checkNotNullOrDefaultSeqdoes it inIndexedList(ofSeq,concat,insertManyAt,removeMany,removeManyWith,transpose,except), inIIndexedList(the same functions butofSeq;concatandtransposereject a default flat list as an element too, because it implementsIImmutableList<'T>), inIndexedListBuilder(addRange,insertManyAt,removeMany,removeManyWith) and inindexedList { }(yield!of a sequence andfor). TheOptionandTuplemodules have no sequence parameter.ImmutableListthatListlacks throw what the member throws, with the names of their own parameters (D6). Where the member contradicts its own documentation or answers with a wrong index, the module follows the documentation (D14). The internal moduleListRanges(Helper/ListRanges.fs) checks the ranges first, comparing the count with the difference between the length and the index:Int32.MaxValue:IndexOf (item, Int32.MaxValue, 1)returns -1, andReverse (Int32.MaxValue, 1)returns the list, for a list of five elements. The module throwsArgumentOutOfRangeException.BinarySearch (index, count, item, comparer)does not check that the range lies in the list, although its documentation promisesArgumentException.binarySearchRange*throwsArgumentException.LastIndexOfandFindLastIndexaccept a last element at or after the end of the list and answer with the index of another element:LastIndexOf (20, 5, 2)on[10; 20; 30; 20; 10]returns 4, and element 4 is 10.FindLastIndexalso takes a negative count for "all".lastIndexOfRange*andtryFindIndexBackRangethrowArgumentOutOfRangeException, also whereLastIndexOfthrows a plainArgumentException.IndexedListandIIndexedListmodules repeat the functions withvoptionor struct tuples (D3).Option.IndexedListhastryFindIndexRangeandtryFindIndexBackRangetoo.IndexedListBuilderworks onImmutableList<'T>.Builder(37 functions), and its sorts are stable too. It getsinsertManyAt,removeAll,removeMany,removeManyAt,removeManyWith,removeWith,replace,replaceWith,revRange,sortRange*, the four searches in a range,binarySearch,binarySearchWithComparer,binarySearchRangeWithComparerandblit.replacethrowsArgumentExceptionfor a value that no element equals, as the builder of the BCL does, andaddRangenamesvaluesin itsArgumentNullException, where the BCL namesitems.indexedList { }builds an indexed list.yield!of anullsequence, array or indexed list throwsArgumentNullExceptionforitems.Helper/gets a sharedListlayer thatQueueandStackwill reuse, one root module per file:ListChecksholds the checks of theListfunctions, with their exact exceptions.ListArraysholds the two- and three-collection algorithms with the behaviour ofList.Snapshotcopies a sequence to an array.DescendingSortsholdssortDescendingandsortByDescending: the stable sorts ofListwithout the boxes of the inlineSeqsorts, for the descending sorts ofQueue,Stackand their interface modules too.Lengthscounts a queue or a stack only up to a limit, for the checks that tell an empty input, one element and more apart.queueUpToreadsIsEmptyand callsDequeueat most limit − 1 times;stackUpTopops without an allocation. No function of this pull request calls it, so the tests ofQueueandStackwill cover it.ListRangesholds the checks of the functions that take a range of an indexed list or of its builder:forward,backward,searchandblit.IndexedList.blituses it too.Raise.fsgetsinvalidArg2ListsDifferent,invalidArgRandomizerOutOfRangeandinvalidOpListEmpty, written with interpolated strings like the rest of the module. The message that the index functions ofList(insertAt,insertManyAt,removeAt,removeManyAtandupdateAt) write as a literal is in no resource, so it is a private literal ofListChecksand not a constant ofErrorStrings.Added,ChangedandRemovedentries compare with the published 1.0.0 assembly, where the list module isImmutableListModulewith 75 functions andIndexedSeqhas 3. They were checked by reflection, by compiling against the assembly and by running its functions next to the new ones, not againstmain.Removednames the 23 functions that 1.0.0 had andIndexedListlacks, each with its replacement. Thirteen are renames with the same arguments, in one entry:lastIndex,lastIndexWith,indexRange,indexRangeWith,lastIndexRange,lastIndexRangeWith,insert,insertRange,removeRange,exceptWith,reverse,reverseRangeandwithItem. The other ten areindex,indexWith,indexFromandindexFromWith(they returned a function that took the list a second time),lastIndexFromandlastIndexFromWith(with the formulas),clear,skipUntil,takeUntilandfilterFold(no replacement). TheIndexedSeqmodule (check,item,length) and theIIndexedSeq<'T>abbreviation are listed withIReadOnlyList<'T>as the replacement.Addedno longer names what 1.0.0 had:add,cons,remove,removeWith,replace,replaceWithandtryTail.Changednames what running both showed. The module is compiled asIndexedListwith PascalCase names, whereImmutableListModulehad the F# names. The functions takeImmutableList<'T>, where 1.0.0 took anyIImmutableList<'T>;ofList,collect,concatandappendtake indexed lists or an F# list, where they took any sequences.tryFind,tryFindIndex,tryHead,tryItem,tryLastandtryTailreturnvoption.head,last,tail,item,removeAt,take,skip,truncate,splitAt,forall2,containsandofBuilderthrow other exceptions or return other results.skipWhileandtakeWhiledo what their names say,exceptfollowsList.except, and the equality ofcontains,remove,replaceand theindexOffamily differs.revis faster.Removedentry of theIndexedSeqmodule does not list theReadOnlyListalias of 1.0.0: a module abbreviation cannot be used from another assembly. A project that references the 1.0.0 package compilesIndexedSeq.lengthandIIndexedSeq<int>, and does not compileReadOnlyList.length.ImmutableList.fsandIndexedSeq.fs.docsSrc/Tutorials/Getting_Started.mdno longer calls the removedImmutableList.ofSeq.Tests: 7832 per target framework, 5072 of them new (
mainhas 2760).DataRowandDynamicDatarows, so every case runs and reports as a test of its own.Listfunctions whereArraybehaves differently (the stable sorts, the walk of the common prefix,exists2/forall2,removeManyAt, the exceptions) and as the list case ofyield!.IndexedListderives the sequence-like, creation, index-based and stable-sort contracts.IIndexedListderives the sequence-like, index-based and stable-sort contracts, over indexed lists upcast to the interface.Contracts/StableSortContract.fsis new.sort*keeps the order of equal keys, on the 20 pairs of the newInputs.ManyPairs, whose keys repeat. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order. As the Copilot review noted, the sixInputs.Pairstherefore could not tell a stable sort from an unstable one.ImmutableList.Sortfor a moment, the 12 stable-sort contract tests and the stability test of the builder sorts fail.sort,sortDescending,sortWith) and a projection that throws (sortBy,sortByDescending) throw whatListthrows, as the review of feat:QueueandIQueuemodules with the vocabulary of the FSharp.CoreListmodule #33 asked, and thatsortByDescendingcalls the projection in the comparisons, in the same order, asList.sortByDescendingdoes.sortDescendingandsortByDescendingallocate less than 3 MB for 10 000 integers: 1.2 MB withDescendingSorts, 8 MB with the inlineSeqsorts, in Debug and in Release. With the inline sorts back, exactly these four tests fail.IndexedList/ListMembersContract.fsis new.IndexedListandIIndexedListderive it, and no other module has these functions, which is why it is not part of the shared contracts thatFlatListand the queues run.replace; the four searches in every range of eight strings; and the exception of every range that is rejected, with the ranges that the members ofImmutableListaccept (D14).IndexedList/ListMemberCases.fsis new too. It holds the strings and the rows that the contract,IndexedListRangeTestsandIndexedListBuilderTestsshare, and the oracles that feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 does not have: the change of a range, the stable sort, the removal of several values and the search from the back.FlatList/FlatListOracles.fsof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 and use itsremoveFirst,replaceFirst,firstInRange,equalIgnoringCase,descendingandbyAbsolute. The tests ofnullinputs callFlatListAssert.RejectsNullof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 too. The oracle of the search from the back answers -1 for a range of no elements before it callslastInRange, because the bounds of a range that starts atInt32.MaxValueoverflow, and the members ofImmutableListaccept such a start index for an empty range.IndexedListRangeTestscoverssub,revRange,sortRange*, the binary searches andtryFindIndexRange/tryFindIndexBackRange: every range of the inputs against the arrays, the stability of the range sorts onInputs.ManyPairs, and the parameter that each function names for a range that it rejects. Its rows of the ranges are the ones ofFlatListTests.FlatListruns them too and the queues and the stacks will:SeqCreationContract:ofSeqrejects a default flat list given as a sequence with what it throws for a null sequence.SeqLikeTransformationContract:concatandtranspose, one data-row test each, reject a null sequence of collections withArgumentNullExceptionthat names the parameter of FSharp.Core (arraysorlists, mapped to the name of the module), and reject a default flat list with what they throw for a null one.List.concatthrowsNullReferenceExceptionfornull, so the test asserts the exception of the module.SeqLikePartitioningContract:exceptrejects a default flat list given as the items to exclude like null items, also for an empty input.IndexedContract:insertManyAtrejects null values and a default flat list withArgumentNullExceptionforvalues, and checks the index before the values.ListMembersContract(IndexedListandIIndexedList):removeManyandremoveManyWithreject null items and a default flat list, on an empty and on a non-empty collection, and check the collection before the items. The tables of each module add what a contract over integers cannot say: a default flat list of strings given as a sequence of objects, the order of the checks, a default flat list as an element, andyield!andforof the expression.Inputs.DefaultFlatListAsSeqis the one small addition to the contracts of chore: lay the foundation for the per-collection modules #25.FlatList:nullarray case ofofArrayusesArray.toListas itsListreference, becauseList.ofArraythrowsNullReferenceException.removeManyAtwith a count that is not positive moved to its own test, and a new test covers ranges whose end overflowsInt32.MaxValue. Both expect exactly what the mirrored function does: its result or its exception.ConsandNilpatterns also carryPatternMatchingTestCategory.IndexedListOpsandIIndexedListOpsare the root modules of their own files.nullinputs of every function, shared trees,cons/unconsand the patterns,binarySearch,blit, the builder, the computation expression,yield!ofnulland both namespaces.IndexedList.blit, which the function makes anyway (a control that has to stay green);IndexedListBuilder.sortRange*that sorts a range of one element instead of skipping it, which leaves the builder as it was;ListRanges.searchwithout the check of a negative count, whichBinarySearchmakes again and answers with the same exception.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThese changes break existing code:
ImmutableListis renamed toIndexedList.try*functions returnvoption, and tuples are struct tuples.skipWhileandtakeWhilereturn what their names say. In 1.0.0 each of them returned the result of the other one.IndexedSeqmodule and the legacy functions in the Removed section of the changelog are gone.Checklist
Put an
xin the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
skipWhile,takeWhile,skipUntilandtakeUntilreturned each other's results; thatindex,indexWith,indexFromandindexFromWithreturned a function that took the list a second time; thatlastIndexRangeaccepted a start index at the end of the list and answered with an index that the list does not have, andlastIndexof an empty list threw; and thatforall2returnedtruefor lists of different lengths when the common pairs satisfied the predicate.Orderalready wraps the exception of a comparison inInvalidOperationException, asArray.Sortdoes. With the old LINQ sorts, only thesortByDescendingtest fails, becauseOrderByDescendingcomputes the keys before it sorts. The four sorts moved toSeqanyway: they now make exactly the comparisons thatListmakes.Seqsorts: the inlineSeq.sortDescendingandSeq.sortByDescendingboxed every integer in every comparison; the work onQueuefound it. The numbers are those of the library itself, in Release on .NET 10 with FSharp.Core 10.1.301, for 10 000 integers with 1000 distinct values: the median of eight rounds of 100 calls, run alternately before and after the fix, and the best single call in brackets.IndexedList.sortDescendingtook 7.22 ms (3.46 ms) and allocated 7 508 744 bytes per call. It takes 1.67 ms (0.90 ms) and allocates 680 336 bytes.IndexedList.sortByDescendingby the integer itself took 7.64 ms (3.74 ms) and allocated 7 508 768 bytes. It takes 2.20 ms (1.11 ms) and allocates 680 344 bytes.IIndexedList.sortDescendingandsortByDescendingtook 7.20 ms and 7.96 ms with the same allocation, and take 1.68 ms and 2.04 ms.sortWithtakes 1.76 ms (0.82 ms) and allocates 680 312 bytes,sort0.80 ms and 560 208 bytes,sortBy1.40 ms and 640 416 bytes,sortWithComparer0.73 ms and 520 104 bytes. Most of the allocation is the tree of 10 000 nodes and the arrays of the sort.sortandsortDescendingof a value type without a primitive comparer, such asstruct (int * int), allocate 17 MB for 10 000 elements. F# comparison boxes such a value in every comparison, and FSharp.Core does so inList.sort(17.2 MB),Seq.sortandArray.sort(15.3 MB) too; the cached comparer is that comparison. Integers and strings do not box.sub list (n / 2) 1000takes 80 µs, 93 µs and 69 µs for n = 10 000, 100 000 and 1 000 000, and allocates 48 024 bytes each time.IndexedList.skipfollowed bytaketakes 0.59 ms, 5.5 ms and 56 ms and allocates 0.29 MB, 2.4 MB and 24 MB, because each of them builds a tree.sortRange (n / 2) 1000takes 0.23 ms, 0.27 ms and 0.27 ms for the same three lengths, and sorting 1 000, 10 000 and 100 000 elements of a list of 1 000 000 takes 0.27 ms, 4.0 ms and 35 ms. The BCLSort (400 000, 10 000, comparer)takes 68 ms and allocates 52 MB, because it copies the whole list into an array and builds a new tree;sortRangetakes 2.3 ms and allocates 1.6 MB. The alternatives that copy the range into an array (2.3 ms, 1.0 MB) or write the sorted elements through a builder (3.5 ms, 1.0 MB) are not faster, so the plan ofGetRange,Order,RemoveRangeandInsertRangestays.revRange (n / 2) 1000takes 0.13 ms, 0.25 ms and 0.25 ms; reversing 1 000, 10 000 and 100 000 elements of a list of 1 000 000 takes 0.26 ms, 2.9 ms and 27 ms.indexOfRangeat the end of a list: 1 000 elements take 25 µs, 27 µs and 15 µs for the same three lengths, and 100 000 elements of a list of 1 000 000 take 1.5 ms (lastIndexOfRange: 1.1 ms). The work depends on the length of the range, as the O(log n + count) of the documentation says.ArgumentOutOfRangeExceptionwhereLastIndexOfthrows a plainArgumentException, because the first is anArgumentExceptiontoo and it names the parameter.binarySearchRange*throws a plainArgumentExceptionfor a range beyond the end, as the documentation ofBinarySearchsays.findIndexRangeandfindIndexBackRangeis not added; the analysis calls it optional.IndexedListBuilderhasbinarySearchRangeWithCompareronly, as the analysis proposes;binarySearchRangeofIndexedListis the same call with the default comparer.🤖 Generated with Claude Code