Skip to content

feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module - #31

Open
xperiandri wants to merge 3 commits into
mainfrom
feat/indexed-list
Open

xperiandri wants to merge 3 commits into
mainfrom
feat/indexed-list

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites the legacy ImmutableList module as IndexedList (ImmutableList<'T>) and adds the interface module IIndexedList (IImmutableList<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged into main.

  • Layout (D2): IndexedList, IIndexedList, their Option and Tuple modules and IndexedListBuilder are the root modules of their files, such as module FSharp.Collections.Immutable.IndexedList. The IndexedList<'T> and IIndexedList<'T> abbreviations live in IndexedList/IndexedListTypes.fs, which needs no signature file.
    • Every other file with public bindings has a signature file that carries its XML documentation, compiled right before it: IndexedListBuilder.fsi and IndexedListCE.fsi included.
    • indexedList has its val in ComputationExpressions.fsi, after flatList under the Creation banner.
  • IndexedList/IndexedList.fsi and IndexedList.fs: 162 functions, in the canonical category order. They are the IndexedList column of ADR Appendix A, with exactly the parameter names of the FSharp.Core List module, so the exceptions name the same parameters as List.
    • The members of ImmutableList come first (D5): ConvertAll, FindAll, FindIndex, FindLastIndex, TrueForAll, Exists, GetRange, Insert, InsertRange, RemoveAt, RemoveRange, SetItem, IndexOf, LastIndexOf, BinarySearch, and now Remove, RemoveRange (items), Replace and Reverse (index, count).
    • Everything else copies to an array, calls the Array function and builds a balanced tree with ImmutableList.CreateRange. The section on cost below lists what takes another way and why: exists2, forall2 and compareWith walk the lists and stop at the pair that decides, max, maxBy, min and minBy are inline, collect and concat join long indexed lists to the result instead of copying them, and the range functions copy a range with CopyTo.
    • rev takes the faster path that the maintainer chose over ImmutableList.Reverse (D5). It copies the elements to an array, reverses it with Array.Reverse and builds the result with ImmutableList.CreateRange. At 100 000 integers it takes about 3 ms where ImmutableList.Reverse, which the 1.0.0 reverse called, takes about 23 ms (about 8 times faster; 40 ms against 265 ms at 1 000 000), and it allocates 5.2 MB where Reverse allocates 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 from CreateRange and for one built with Add, run on the 1.0.0 assembly and on this branch.
    • Every sort is stable, as List.sort* is. ImmutableList.Sort is unstable, so it is not used.
      • sort and sortWith sort with the FSharp.Core Seq sorts, which share the stable sort of List: they make the same comparisons, in the same Array.Sort.
      • sortDescending and sortByDescending call Seq.sortWith through the internal DescendingSorts module (Helper/DescendingSorts.fs), with the cached comparer reversed. Seq.sortDescending and Seq.sortByDescending are 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 of List; only the boxes go.
      • sortBy and sortWithComparer sort with LINQ OrderBy/Order, which compute the keys first, as List.sortBy does.
      • sortRange, sortRangeWith and sortRangeWithComparer sort a copy of the range with LINQ Order and put it back with RemoveRange and InsertRange, so they are stable too. ImmutableList.Sort (index, count, comparer) is unstable and copies the whole list.
    • The sorts and binarySearch compare with the cached DefaultComparer<'T>.Instance of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26, so a call does not create a comparer.
    • The extras are add, cons, uncons, tryUncons, tryTail, skipLast, takeLast, build, update, blit, binarySearch, indexOf/indexOfWith/lastIndexOf/lastIndexOfWith, sortWithComparer, the span constructors and the Cons/Nil patterns.
    • Functions that wrap more members of ImmutableList, from the API gap analysis of the modules (20 functions):
      • Removal and replacement by value, in EqualityComparer<'T>.Default or a given equality comparer: remove, removeWith, removeMany, removeManyWith, replace, replaceWith. Nothing to remove returns the input itself, and replace throws ArgumentException for a value that no element equals, as the BCL does.
      • The range: sub (it throws what Array.sub throws, as FlatList.sub does), revRange, indexOfRange, indexOfRangeWith, lastIndexOfRange, lastIndexOfRangeWith, tryFindIndexRange and tryFindIndexBackRange. The searches return indices of the whole indexed list.
      • Binary searches with a comparer, binarySearchWithComparer, binarySearchRange and binarySearchRangeWithComparer, and the stable sortRange*.
  • IIndexedList.fsi and IIndexedList.fs: 136 functions over IImmutableList<'T>.
    • Results of the same element type keep the implementation of the input: Clear () plus AddRange, or the member of the interface for an edit of one element. rev reverses its copy in place, as IndexedList.rev does, before it rebuilds.
    • Results of another element type are an IndexedList. So are concat and transpose, because their inputs can have different implementations.
    • The sorts are the same as in IndexedList.
    • A function that has a faster path for an indexed list takes it when the input is one, and goes the general way for another implementation: exists2, forall2 and compareWith hand two indexed lists to IndexedList, the searches from the end search an indexed list from its end, and skip, take and the other range functions copy the range of an indexed list. The sequence-like contract runs over an implementation that is not an indexed list as well, to cover the general way.
    • remove, removeWith, removeMany, removeManyWith, replace and replaceWith call 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.
    • A test with a custom IImmutableList implementation proves that the module keeps it.
  • Exceptions (D6, D14): every function throws what the FSharp.Core List function throws: the type, the message and the parameter name. This includes the quirks of List:
    • The functions of two and three lists call their callback on the common prefix before they throw.
    • exists2 and forall2 can return before they check the lengths.
    • removeManyAt removes nothing for a count that is not positive, and nothing when index + count overflows, because List walks the first index + count elements. The Copilot review found the overflow: such a range used to reach RemoveRange, which threw.
    • A comparison or a projection that throws in sort, sortDescending, sortWith or sortByDescending throws an InvalidOperationException with the original exception inside, as Array.Sort does in List. List.sortByDescending calls the projection in the comparison, and so does this module now. sortBy lets an exception of the projection escape, as List.sortBy does.
    • List reports some unusual parameter names, and the module reports them too: "source" for exactlyOne, average and averageBy; "count" for init; "list.[j]" for transpose.
    • A null input throws ArgumentNullException.
    • A parameter of type seq<'T> rejects null and a default flat list given as a sequence, a boxed struct that fails with InvalidOperationException as soon as the BCL enumerates it, with ArgumentNullException that names the parameter of the function. checkNotNullOrDefaultSeq does it in IndexedList (ofSeq, concat, insertManyAt, removeMany, removeManyWith, transpose, except), in IIndexedList (the same functions but ofSeq; concat and transpose reject a default flat list as an element too, because it implements IImmutableList<'T>), in IndexedListBuilder (addRange, insertManyAt, removeMany, removeManyWith) and in indexedList { } (yield! of a sequence and for). The Option and Tuple modules have no sequence parameter.
    • The functions that wrap a member of ImmutableList that List lacks 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 module ListRanges (Helper/ListRanges.fs) checks the ranges first, comparing the count with the difference between the length and the index:
      • The members add the index and the count, which wraps around Int32.MaxValue: IndexOf (item, Int32.MaxValue, 1) returns -1, and Reverse (Int32.MaxValue, 1) returns the list, for a list of five elements. The module throws ArgumentOutOfRangeException.
      • BinarySearch (index, count, item, comparer) does not check that the range lies in the list, although its documentation promises ArgumentException. binarySearchRange* throws ArgumentException.
      • LastIndexOf and FindLastIndex accept 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. FindLastIndex also takes a negative count for "all". lastIndexOfRange* and tryFindIndexBackRange throw ArgumentOutOfRangeException, also where LastIndexOf throws a plain ArgumentException.
  • Option and Tuple namespaces: the IndexedList and IIndexedList modules repeat the functions with voption or struct tuples (D3). Option.IndexedList has tryFindIndexRange and tryFindIndexBackRange too.
  • Builder and computation expression (D7): IndexedListBuilder works on ImmutableList<'T>.Builder (37 functions), and its sorts are stable too. It gets insertManyAt, removeAll, removeMany, removeManyAt, removeManyWith, removeWith, replace, replaceWith, revRange, sortRange*, the four searches in a range, binarySearch, binarySearchWithComparer, binarySearchRangeWithComparer and blit. replace throws ArgumentException for a value that no element equals, as the builder of the BCL does, and addRange names values in its ArgumentNullException, where the BCL names items. indexedList { } builds an indexed list. yield! of a null sequence, array or indexed list throws ArgumentNullException for items.
  • Helper/ gets a shared List layer that Queue and Stack will reuse, one root module per file:
    • ListChecks holds the checks of the List functions, with their exact exceptions.
    • ListArrays holds the two- and three-collection algorithms with the behaviour of List.
    • Snapshot copies a sequence to an array.
    • Collectors holds the accumulator of collect and concat: it copies the lists of fewer than 16 elements into a buffer and joins the longer indexed lists to a builder with Builder.AddRange, which shares their nodes (see the cost of the whole-list functions below).
    • DescendingSorts holds sortDescending and sortByDescending: the stable sorts of List without the boxes of the inline Seq sorts, for the descending sorts of Queue, Stack and their interface modules too.
    • Lengths counts a queue or a stack only up to a limit, for the checks that tell an empty input, one element and more apart. queueUpTo reads IsEmpty and calls Dequeue at most limit − 1 times; stackUpTo pops without an allocation. No function of this pull request calls it, so the tests of Queue and Stack will cover it.
    • ListRanges holds the checks of the functions that take a range of an indexed list or of its builder: forward, backward, search and blit. IndexedList.blit uses it too.
    • Raise.fs gets invalidArg2ListsDifferent, invalidArgRandomizerOutOfRange and invalidOpListEmpty, written with interpolated strings like the rest of the module. The message that the index functions of List (insertAt, insertManyAt, removeAt, removeManyAt and updateAt) write as a literal is in no resource, so it is a private literal of ListChecks and not a constant of ErrorStrings.
  • Changelog: the new functions are listed, and the Added, Changed and Removed entries compare with the published 1.0.0 assembly, where the list module is ImmutableListModule with 75 functions and IndexedSeq has 3. They were checked by reflection, by compiling against the assembly and by running its functions next to the new ones, not against main.
    • Removed names the 23 functions that 1.0.0 had and IndexedList lacks, 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, reverseRange and withItem. The other ten are index, indexWith, indexFrom and indexFromWith (they returned a function that took the list a second time), lastIndexFrom and lastIndexFromWith (with the formulas), clear, skipUntil, takeUntil and filterFold (no replacement). The IndexedSeq module (check, item, length) and the IIndexedSeq<'T> abbreviation are listed with IReadOnlyList<'T> as the replacement.
    • Added no longer names what 1.0.0 had: add, cons, remove, removeWith, replace, replaceWith and tryTail.
    • Changed names what running both showed. The module is compiled as IndexedList with PascalCase names, where ImmutableListModule had the F# names. The functions take ImmutableList<'T>, where 1.0.0 took any IImmutableList<'T>; ofList, collect, concat and append take indexed lists or an F# list, where they took any sequences. tryFind, tryFindIndex, tryHead, tryItem, tryLast and tryTail return voption. head, last, tail, item, removeAt, take, skip, truncate, splitAt, forall2, contains and ofBuilder throw other exceptions or return other results. skipWhile and takeWhile do what their names say, except follows List.except, and the equality of contains, remove, replace and the indexOf family differs. rev is faster.
    • The Removed entry of the IndexedSeq module does not list the ReadOnlyList alias of 1.0.0: a module abbreviation cannot be used from another assembly. A project that references the 1.0.0 package compiles IndexedSeq.length and IIndexedSeq<int>, and does not compile ReadOnlyList.length.
  • Removed files: the legacy ImmutableList.fs and IndexedSeq.fs. docsSrc/Tutorials/Getting_Started.md no longer calls the removed ImmutableList.ofSeq.

Tests: 8315 per target framework, 5555 of them new (main has 2760).

  • The tests compare arrays and take their cases as DataRow and DynamicData rows, so every case runs and reports as a test of its own.
    • The case tables are maps whose names are the rows.
    • Lists remain only as the oracles of the List functions where Array behaves differently (the stable sorts, the walk of the common prefix, exists2/forall2, removeManyAt, the exceptions) and as the list case of yield!.
  • IndexedList derives the sequence-like, creation, index-based, stable-sort and cost contracts.
  • IIndexedList derives the sequence-like, index-based, stable-sort and cost contracts, over indexed lists upcast to the interface. Its sequence-like contract runs again over TaggedList, an implementation of IImmutableList<'T> that is not an indexed list, which covers the general way of the functions that take a shortcut for indexed lists.
  • Contracts/CostContract.fs is new. It bounds what max, maxBy, min and minBy allocate for 10 000 integers (they must not box), what exists2, forall2 and compareWith allocate for two lists of 100 000 integers when the first pair decides (they must not copy the lists), and what concat and collect allocate for 100 indexed lists of 1000 elements (they must join the lists, not copy their elements), for IndexedList and IIndexedList. With the sources of the previous head in place of the new ones, exactly these 18 tests fail and the other 8 297 pass. The four tests of the boxes are inconclusive in a Debug build: with the optimizer of the compiler off, the comparison of two integers in a function that is inline at the call site goes through the generic operator, which boxes them, as it does for the inline functions of FSharp.Core.
  • Contracts/AllocationAssert.fs measures what an action allocates, and the stable sorting contract uses it too. Contracts/OptionAssert.fs has WantSome and WantValueSome, which return the value of an option or a value option and fail the test with a message when there is none; they replace the matches that failed on nothing, ValueOption.get and Value in the tests of IndexedList, IIndexedList and the oracles of FlatList.
  • SeqLikeTransformationContract has a test that collect and concat keep the order of the elements across collections shorter and longer than 16 elements, also with the same long list twice.
  • Contracts/StableSortContract.fs is new.
    • It checks that every sort* keeps the order of equal keys, on the 20 pairs of the new Inputs.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 six Inputs.Pairs therefore could not tell a stable sort from an unstable one.
    • With every sort switched to the unstable algorithm of ImmutableList.Sort for a moment, the 12 stable-sort contract tests and the stability test of the builder sorts fail.
    • It checks that a comparison that throws (sort, sortDescending, sortWith) and a projection that throws (sortBy, sortByDescending) throw what List throws, as the review of feat: Queue and IQueue modules with the vocabulary of the FSharp.Core List module #33 asked, and that sortByDescending calls the projection in the comparisons, in the same order, as List.sortByDescending does.
    • It checks that sortDescending and sortByDescending allocate less than 3 MB for 10 000 integers: 1.2 MB with DescendingSorts, 8 MB with the inline Seq sorts, in Debug and in Release. With the inline sorts back, exactly these four tests fail.
  • IndexedList/ListMembersContract.fs is new. IndexedList and IIndexedList derive it, and no other module has these functions, which is why it is not part of the shared contracts that FlatList and the queues run.
    • It checks the removal and the replacement by value, in the default equality and with a comparer that ignores the case, on strings; the input itself when nothing is removed; the exception and the parameter of 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 of ImmutableList accept (D14).
    • IndexedList/ListMemberCases.fs is new too. It holds the strings and the rows that the contract, IndexedListRangeTests and IndexedListBuilderTests share, and the oracles that feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 does not have: the change of a range, the stable sort, the removal of several values and the search from the back.
    • Both files come after FlatList/FlatListOracles.fs of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 and use its removeFirst, replaceFirst, firstInRange, equalIgnoringCase, descending and byAbsolute. The tests of null inputs call FlatListAssert.RejectsNull of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 too. The oracle of the search from the back answers -1 for a range of no elements before it calls 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.
  • IndexedListRangeTests covers sub, revRange, sortRange*, the binary searches and tryFindIndexRange/tryFindIndexBackRange: every range of the inputs against the arrays, the stability of the range sorts on Inputs.ManyPairs, and the parameter that each function names for a range that it rejects. Its rows of the ranges are the ones of FlatListTests.
  • The sequence inputs are in the shared contracts wherever every implementer has the function, so FlatList runs them too and the queues and the stacks will:
    • SeqCreationContract: ofSeq rejects a default flat list given as a sequence with what it throws for a null sequence.
    • SeqLikeTransformationContract: concat and transpose, one data-row test each, reject a null sequence of collections with ArgumentNullException that names the parameter of FSharp.Core (arrays or lists, mapped to the name of the module), and reject a default flat list with what they throw for a null one. List.concat throws NullReferenceException for null, so the test asserts the exception of the module.
    • SeqLikePartitioningContract: except rejects a default flat list given as the items to exclude like null items, also for an empty input.
    • IndexedContract: insertManyAt rejects null values and a default flat list with ArgumentNullException for values, and checks the index before the values.
    • ListMembersContract (IndexedList and IIndexedList): removeMany and removeManyWith reject 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, and yield! and for of the expression. Inputs.DefaultFlatListAsSeq is the one small addition to the contracts of chore: lay the foundation for the per-collection modules #25.
  • Shared contracts, without weakening them for FlatList:
    • The null array case of ofArray uses Array.toList as its List reference, because List.ofArray throws NullReferenceException.
    • removeManyAt with a count that is not positive moved to its own test, and a new test covers ranges whose end overflows Int32.MaxValue. Both expect exactly what the mirrored function does: its result or its exception.
  • Categories are on the test classes only (D8): the collection and the area that a class examines. The classes with the tests of the Cons and Nil patterns also carry PatternMatchingTestCategory.
  • IndexedListOps and IIndexedListOps are the root modules of their own files.
  • The per-collection tests cover null inputs of every function, shared trees, cons/uncons and the patterns, binarySearch, blit, the builder, the computation expression, yield! of null and both namespaces.
  • Breaking each new behaviour once, in a copy of the sources (81 changes: 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), fails the tests in every case that changes a result: 78 of the 81 changes. The other three change no result:
    • an additional check in 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.search without the check of a negative count, which BinarySearch makes again and answers with the same exception.
  • Breaking each fix of the sequence inputs once (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.

Types of changes

What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

These changes break existing code:

  • ImmutableList is renamed to IndexedList.
  • The try* functions return voption, and tuples are struct tuples.
  • skipWhile and takeWhile return what their names say. In 1.0.0 each of them returned the result of the other one.
  • The IndexedSeq module and the legacy functions in the Removed section of the changelog are gone.

Checklist

Put an x in 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.

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

  • Checked against 1.0.0: running the published assembly next to this branch showed, besides the differences in the changelog, that the 1.0.0 skipWhile, takeWhile, skipUntil and takeUntil returned each other's results; that index, indexWith, indexFrom and indexFromWith returned a function that took the list a second time; that lastIndexRange accepted a start index at the end of the list and answered with an index that the list does not have, and lastIndex of an empty list threw; and that forall2 returned true for lists of different lengths when the common pairs satisfied the predicate.
  • LINQ and the exceptions of a sort: LINQ Order already wraps the exception of a comparison in InvalidOperationException, as Array.Sort does. With the old LINQ sorts, only the sortByDescending test fails, because OrderByDescending computes the keys before it sorts. The four sorts moved to Seq anyway: they now make exactly the comparisons that List makes.
  • Cost of the Seq sorts: the inline Seq.sortDescending and Seq.sortByDescending boxed every integer in every comparison; the work on Queue found 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.sortDescending took 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.sortByDescending by 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.sortDescending and sortByDescending took 7.20 ms and 7.96 ms with the same allocation, and take 1.68 ms and 2.04 ms.
    • The other sorts do not change: sortWith takes 1.76 ms (0.82 ms) and allocates 680 312 bytes, sort 0.80 ms and 560 208 bytes, sortBy 1.40 ms and 640 416 bytes, sortWithComparer 0.73 ms and 520 104 bytes. Most of the allocation is the tree of 10 000 nodes and the arrays of the sort.
  • Boxes that remain: sort and sortDescending of a value type without a primitive comparer, such as struct (int * int), allocate 17 MB for 10 000 elements. F# comparison boxes such a value in every comparison, and FSharp.Core does so in List.sort (17.2 MB), Seq.sort and Array.sort (15.3 MB) too; the cached comparer is that comparison. Integers and strings do not box.
  • Cost of the range functions: the numbers are those of the library, in Release on .NET 10, as the median of 9 to 31 runs after a warm-up, for integers.
    • The cost of a range depends on its length, not on the length of the list. sub list (n / 2) 1000 takes 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.skip followed by take takes 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. Since the range is copied with CopyTo instead of read with GetRange, sub list (n / 2) 1000 of a list of 1 000 000 takes 0.05 to 0.07 ms instead of 0.07 to 0.09 ms, and skip followed by take 17 ms instead of 36 to 40 ms (measured on the same machine, one after the other).
    • sortRange (n / 2) 1000 takes 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 BCL Sort (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; sortRange took 2.3 ms and allocated 1.6 MB when it read the range with GetRange, and takes about 2 ms and allocates 1.1 MB since it copies the range with CopyTo. Writing the sorted elements back through a builder is not faster (the same 2.9 ms for 10 000 elements, 132 ms against 104 ms for 500 000), so RemoveRange and InsertRange stay.
    • revRange (n / 2) 1000 takes 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.
    • indexOfRange at 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.
  • Cost of the whole-list functions (the review of 2026-10-19 asked whether arrays and ResizeArray are the best way to build the results): Release on .NET 10 with FSharp.Core 10.1.301, 100 000 integers unless said otherwise, the median of 7 to 31 runs after a warm-up of 400 ms and a pause that lets the tiered compiler install the optimized code. Each range holds two runs of the previous head and of this one, made alternately, because the machine drifts by up to 30 % between runs.
    • max, maxBy, min and minBy of both modules are inline, as the ones of List are. Compiled into a generic function, Seq.max and the others compare through the generic operator and box every integer twice: 4.1 to 9.4 ms and 4.8 MB before, 2.1 to 2.9 ms and 40 bytes after (112 for IIndexedList, which allocates the enumerator).
    • exists2, forall2 and compareWith of IndexedList walk the two lists with their enumerators and stop at the pair that decides. When the first pair decides, exists2 takes 0.006 to 0.007 ms and 40 bytes where it took 3.3 to 4.9 ms and 800 KB. A walk to the end takes 2.5 to 2.8 ms and 40 bytes where forall2 and compareWith took 4.7 to 6.3 ms and 4.8 to 4.9 ms and 800 KB. An enumerator reached through an interface takes more than twice as long as the snapshots on a walk to the end (8.6 to 9.1 ms against 3.6 to 4.0 ms), so IIndexedList hands two indexed lists to IndexedList and copies the other implementations.
    • findBack, tryFindBack, findIndexBack and tryFindIndexBack of IIndexedList search an indexed list from its end with FindLastIndex: 0.005 ms and 120 bytes where the copy took 2.4 to 2.5 ms and 400 KB when the last element matches, and 0.6 to 0.8 ms where it took 2.4 ms when none does. Another implementation is still copied, because its indexer can take O(n) for an element.
    • collect and concat (Helper/Collectors.fs): Builder.AddRange of an indexed list shares its nodes, which takes O(log n) whatever its length, where a copy takes O(length). A builder alone is 1.2 to 1.8 times slower than the buffer for lists of 1, 3 and 10 elements and 1.4 times faster for 30, 2.4 times for 100 and 1000 (fresh lists, 100 000 elements in all), so the collector copies the lists of fewer than 16 elements and joins the others. concat of 100 indexed lists of 1000 elements takes 0.09 to 0.13 ms and 76 KB where it took 2.8 to 3.5 ms and 5.8 MB, one indexed list of 100 000 elements 0.004 to 0.005 ms where it took 3.2 to 3.4 ms, and collect of 100 results that are one list of 1000 elements 0.07 to 0.08 ms and 76 KB where it took 2.2 ms and 5.8 MB. collect of 10 000 fresh lists of 3 elements does not change (1.3 to 1.7 ms against 1.4 to 1.5 ms).
    • skip, take, sub, splitAt, takeLast, truncate and the range sorts copy the range with CopyTo, O(log n + count), where GetRange read the elements one by one, O(count log n). Half a million elements of a list of a million take 16 to 19 ms and 26 MB where they took 35 to 36 ms and 24 MB, and IIndexedList.skip of the same range takes 17 ms where it took 37 to 39 ms. sortRange of 10 000 and of 500 000 elements takes 1.9 to 2.2 ms and 113 to 120 ms where it took 2.6 to 2.8 ms and 164 to 171 ms, and allocates 1.1 MB and 56 MB where it allocated 1.6 MB and 78 MB.
    • chunkBySize and windowed of IndexedList copy the list once and build a tree for each chunk or window, as IIndexedList does. Chunks of 2, 100 and 10 000 elements of a list of a million take 44 to 48, 32 to 34 and 31 to 49 ms where they took 76 to 87, 71 to 73 and 75 to 76 ms; windows of 3 and 100 elements of 100 000 take 9 to 9.5 and 94 to 102 ms where they took 18 to 19 and 430 to 514 ms. They allocate 9 to 27 % more, for the copy and the arrays of the chunks. The outer list of chunkBySize, windowed, splitInto and transpose is built from Seq.map and ofSeq in both modules, not from an array of the inner lists: the BCL copies a sequence into a buffer anyway, and the time and the allocation are the same.
    • Compared and kept:
      • allPairs: two snapshots take 1.4 to 2.5 times less time than an enumerator of the second list for each element of the first (300 x 300, 1000 x 1000, 10 x 100 000 and 100 000 x 10 elements). A snapshot of the second list alone is between 2.3 times slower and 17 % faster, so the Arrays.allPairs that FlatList, Queue and Stack share stays.
      • choose and unfold: a ResizeArray and CreateRange take 3.4 ms where Builder.Add takes 7.7 ms (2.3 times as long).
      • IIndexedList.chunkBySize: spans cut from one snapshot, instead of the arrays of Array.chunkBySize, are between 1.2 times faster and as fast for chunks of 2, 100 and 10 000 elements of a list of a million, so the array function stays.
      • contains: a version that is inline takes as long as the current one (2.7 ms), and ImmutableList.Contains takes 1.6 ms but compares with EqualityComparer<'T>.Default, where List.contains compares in F# equality.
      • iter: the loop takes 2.1 ms where ForEach took 2.2 to 2.6 ms; it does not allocate the delegate.
  • Temporary arrays and ArrayPool<T>.Shared (asked in the review): the functions that copy a list into an array, work on it and build the tree from the result allocate an array that nobody sees again, because the tree copies the elements. rev was measured, ToArray + Array.Reverse + CreateRange against Rent + CopyTo + Array.Reverse + ImmutableList.Create (ReadOnlySpan) + Return; the figures are the mean of a loop of 2 s with the allocation and the collections counted. Renting the array pays when it is large, and only then:
    • Up to 16 KB nothing changes: 0.24 against 0.25 µs for 16 elements, 3.2 against 3.3 µs for 256 and 51 against 49 µs for 4 096, and no collection of generation 1 or 2.
    • From 85 000 bytes the array is allocated in the large object heap, whose collections are those of generation 2. 21 000 integers (84 000 bytes) cause none, 22 000 cause 3.7 collections of generation 2 for every 1000 calls, 100 000 integers 13.8 and 1 000 000 integers 70. Renting the array makes them 0, and takes 11 % less time at 22 000 integers, 13 % at 100 000, 4 % at 1 000 000 and 19 % for 100 000 strings (the pool clears an array of references when it takes it back, which costs less than the collections). The heap of the measurement is small; a collection of generation 2 in an application with a large heap costs more.
    • The tree dominates what a call allocates (about 48 bytes for an element, the array 4), so the allocation drops by 8 %, not more.
    • It cannot be applied where the array leaves the function or must have the exact length. The functions of Array take the length of the array they get, and the pool returns a longer one, so foldBack, scanBack, windowed, chunkBySize, splitInto, permute, partition and the other functions that call an Array function on a snapshot, and the algorithms of Arrays and ListArrays that FlatList, Queue and Stack share and that return arrays, would first need versions that take a span. rev, mapi, mapFold, scan and the random functions work on the snapshot or on their result in their own loop, and could rent it today.
    • The ResizeArray of choose, unfold and concat allocates a new array at every doubling, the large ones in the large object heap; a buffer from the pool would avoid that for long results.
    • It is not done in this pull request, because it needs the span versions of the shared algorithms and pays only for lists from about 21 000 elements.
  • Decisions for the maintainers:
    • The module throws ArgumentOutOfRangeException where LastIndexOf throws a plain ArgumentException, because the first is an ArgumentException too and it names the parameter. binarySearchRange* throws a plain ArgumentException for a range beyond the end, as the documentation of BinarySearch says.
    • The throwing pair findIndexRange and findIndexBackRange is not added; the analysis calls it optional.
    • IndexedListBuilder has binarySearchRangeWithComparer only, as the analysis proposes; binarySearchRange of IndexedList is the same call with the default comparer.
    • Whether to rent the temporary arrays of lists from about 21 000 elements from ArrayPool (see above). It needs a rented snapshot (a struct that returns the array when it is disposed) and span versions of Arrays and ListArrays, which FlatList, Queue and Stack share, so it is a change of its own.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 11:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
Comment thread tests/FSharp.Collections.Immutable.Tests/Contracts/StableSortContract.fs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad breaking API migration and exact List-compatibility requirements need human validation across both target frameworks.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

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
xperiandri requested a balanced review from Copilot October 18, 2026 18:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad breaking API migration and exact FSharp.Core compatibility guarantees require final human validation.

No open issues 🎉

Review effort: Balanced

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
xperiandri requested a balanced review from Copilot October 18, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad breaking API migration and exact FSharp.Core compatibility requirements warrant final human validation.

No open issues 🎉

Review effort: Balanced

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 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>
Base automatically changed from feat/flat-list to main October 18, 2026 22:34
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 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 added a commit that referenced this pull request Oct 19, 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 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>
@xperiandri
xperiandri changed the base branch from main to build/assembly-info-props October 19, 2026 14:49
Base automatically changed from build/assembly-info-props to main October 19, 2026 15:14
xperiandri added a commit that referenced this pull request Oct 19, 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 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>
xperiandri added a commit that referenced this pull request Oct 19, 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 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>
xperiandri added a commit that referenced this pull request Oct 19, 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 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>
Comment thread src/FSharp.Collections.Immutable/Helper/ListChecks.fs Outdated
Comment thread src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
Comment thread src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
Comment thread src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs Outdated
Comment thread src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs Outdated
Comment thread tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListOps.fs Outdated
Comment thread src/FSharp.Collections.Immutable/IndexedList/IndexedListTypes.fs
Comment thread tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListOptionTests.fs Outdated
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 and others added 3 commits October 20, 2026 18:13
… 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants