diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 840846a3..316a54bf 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -23,7 +23,13 @@ module's `.fsi` and the `.fs` on its `.fs`, so that Visual Studio shows them as
│ └── CheckOrder.fs – checks the category order of the signature files with the Fantomas parser
├── docsSrc/ – FSharp.Formatting documentation source
├── src/FSharp.Collections.Immutable/ – main library
-│ ├── Helper.fs – argument checks and exceptions shared by the modules
+│ ├── Helper/ – internal helpers shared by the modules, one root module per file
+│ │ ├── ImmutableCollectionUtil.fs – argument checks and the wrapping of arrays
+│ │ ├── Comparers.fs – the default comparer of F# generic comparison
+│ │ ├── ErrorStrings.fs – the exception messages of FSharp.Core and the .NET libraries
+│ │ ├── Raise.fs – the exceptions of the FSharp.Core DetailedExceptions module
+│ │ ├── ArrayChecks.fs – the checks that the functions of several arrays share
+│ │ └── Arrays.fs – the algorithms over arrays that the default and Tuple modules share
│ ├── FlatList/ – ImmutableArray API surface and builders
│ │ ├── FlatListTypes.fs – the FlatList abbreviation
│ │ ├── FlatList.fs / FlatList.fsi – includes the nested Parallel module
diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
index 8e3efc2d..e7284ad8 100644
--- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
+++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
@@ -18,9 +18,20 @@
true
+
+
+
+
+
-
+
+
+
+
+
+
diff --git a/src/FSharp.Collections.Immutable/Helper.fs b/src/FSharp.Collections.Immutable/Helper.fs
deleted file mode 100644
index efb31d80..00000000
--- a/src/FSharp.Collections.Immutable/Helper.fs
+++ /dev/null
@@ -1,454 +0,0 @@
-#if INTERACTIVE
-namespace global
-#else
-namespace FSharp.Collections.Immutable
-#endif
-
-open System
-open System.Collections.Generic
-open System.Collections.Immutable
-open System.Linq
-open System.Runtime.InteropServices
-
-/// Argument checks shared by the collection modules.
-/// The module keeps its old name, so that the modules written before ADR 0001 compile unchanged until they are replaced.
-[]
-module internal ImmutableCollectionUtil =
-
- /// Throws `ArgumentNullException` for a `null` argument, as FSharp.Core does for a `null` array or list.
- let inline checkNotNull name (arg : _ | null) =
- match arg with
- | null -> nullArg name
- | _ -> ()
-
- ///
- /// Throws for a default .
- ///
- ///
- /// A default immutable array has no underlying array. The collection modules treat it as FSharp.Core treats a
- /// array (ADR 0001, D6).
- ///
- let inline checkNotDefault name (array : ImmutableArray<'T>) =
- if array.IsDefault then
- nullArg name
-
- ///
- /// Tells whether an object is a default , of any element type.
- ///
- ///
- ///
- /// is covariant, so a sequence of a reference type is also a
- /// sequence of its base types: a default ImmutableArray<string> can be a seq<obj>. The type
- /// test :? ImmutableArray<obj> does not recognize it, because the type is another instantiation.
- ///
- ///
- /// The function finds the instantiation from the type of the object and reads the property IsDefault through
- /// reflection. It allocates nothing for an object of another type. It is inline, because the public inline members of
- /// the computation expression call the check, and an inline member cannot call an internal function.
- ///
- ///
- let inline isDefaultImmutableArray (source : obj) : bool =
- let sourceType = source.GetType ()
-
- sourceType.IsGenericType
- && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>)
- && unbox((nonNull (sourceType.GetProperty "IsDefault")).GetValue source)
-
- ///
- /// Throws for a sequence and for a default
- /// that is given as a sequence.
- ///
- ///
- ///
- /// A default immutable array converts to a sequence without a check, and the sequence fails with
- /// when a function enumerates it. A function that takes a sequence
- /// calls this check, which treats a default immutable array as FSharp.Core treats a sequence
- /// (ADR 0001, D6). Any other sequence passes.
- ///
- ///
- /// An immutable array of the element type is recognized by its type, without reflection. A default immutable array of
- /// a derived reference type, which arrives as a sequence of a base type by covariance, is recognized by its generic
- /// type and by reading IsDefault through reflection.
- ///
- ///
- let inline checkNotNullOrDefaultSeq name (source : seq<'T>) =
- checkNotNull name source
-
- match source with
- | :? ImmutableArray<'T> as array -> checkNotDefault name array
- | _ ->
- if isDefaultImmutableArray source then
- nullArg name
-
- ///
- /// The array under an immutable array, without a copy (ADR 0001, D5).
- ///
- ///
- /// The caller reads the array and never writes to it: the immutable array and every value that shares it see the same
- /// memory. A default immutable array throws for .
- ///
- let inline unwrapArray name (array : ImmutableArray<'T>) : 'T array =
- checkNotDefault name array
- // The check above guarantees an underlying array
- ImmutableCollectionsMarshal.AsArray array
- |> Unchecked.nonNull
-
- ///
- /// An immutable array over an array, without a copy (ADR 0001, D5).
- ///
- ///
- /// Only an array that nobody can write to any more may be wrapped: a fresh result of a computation.
- ///
- let inline wrapArray (array : 'T array) : ImmutableArray<'T> = ImmutableCollectionsMarshal.AsImmutableArray array
-
- /// The constructor of a struct pair, for the algorithms shared by the default and the `Tuple` modules.
- let inline structPair (first : 'T1) (second : 'T2) = struct (first, second)
-
- /// The constructor of a reference pair, for the algorithms shared by the default and the `Tuple` modules.
- let inline referencePair (first : 'T1) (second : 'T2) = (first, second)
-
- /// The constructor of a struct triple, for the algorithms shared by the default and the `Tuple` modules.
- let inline structTriple (first : 'T1) (second : 'T2) (third : 'T3) = struct (first, second, third)
-
- /// The constructor of a reference triple, for the algorithms shared by the default and the `Tuple` modules.
- let inline referenceTriple (first : 'T1) (second : 'T2) (third : 'T3) = (first, second, third)
-
-/// The default comparers of the collections (ADR 0001, D5).
-[]
-module internal Comparers =
-
- ///
- /// The comparer of F# generic comparison for a type: the comparer of the sorts, of binarySearch and of the
- /// sorted collections that are created without a comparer.
- ///
- ///
- ///
- /// The class resolves once per type, in
- /// generic code, where FSharp.Core returns the comparer that it keeps for the type.
- ///
- ///
- /// A call of FastGenericComparer at a concrete type that is not primitive creates a new comparer each time, and
- /// the F# optimizer creates such calls when it inlines a small function of the library into its caller. Collections of
- /// one type would then have different comparers, and the BCL could not take its shortcuts for two collections with the
- /// same comparer, such as the one of ImmutableSortedSet.Union. A static field cannot be inlined.
- ///
- ///
- []
- type DefaultComparer<'T when 'T : comparison> private () =
-
- static let comparer = LanguagePrimitives.FastGenericComparer<'T>
-
- /// The comparer of F# generic comparison for the type.
- static member Instance : IComparer<'T> = comparer
-
-///
-/// The exception messages of FSharp.Core (FSCore.resx) and of the .NET libraries, in English.
-///
-///
-/// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources
-/// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception
-/// under the name of their own parameter.
-///
-module internal ErrorStrings =
-
- []
- let ArraysHadDifferentLengths = "The arrays have different lengths."
-
- []
- let IndexOutOfBounds = "The index was outside the range of elements in the list."
-
- []
- let InputArrayEmpty = "The input array was empty."
-
- []
- let InputListWasEmpty = "The input list was empty."
-
- []
- let InputMustBeNonNegative = "The input must be non-negative."
-
- []
- let InputMustBePositive = "The input must be positive."
-
- []
- let InputSequenceEmpty = "The input sequence was empty."
-
- []
- let InputSequenceTooLong = "The input sequence contains more than one element."
-
- []
- let KeyNotFound = "The item, key, or index was not found in the collection."
-
- []
- let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection."
-
- []
- let ListsHadDifferentLengths = "The lists had different lengths."
-
- /// The message of the modules written before ADR 0001.
- []
- let ListsHaveDifferentLengths = "The lists have different lengths."
-
- /// The message of the .NET libraries for a negative argument, as `Array.BinarySearch` reports it.
- []
- let NeedNonNegativeNumber = "Non-negative number required."
-
- []
- let NotAPermutation = "The function did not compute a permutation."
-
- []
- let NotEnoughElements = "The input sequence has an insufficient number of elements."
-
- []
- let OutOfRange = "The index is outside the legal range."
-
-///
-/// The exceptions of the DetailedExceptions module of FSharp.Core, with the same types, messages and parameter names.
-///
-///
-/// The callers pass the names of their own parameters (ADR 0001, D6).
-///
-module internal Raise =
-
- let inline private invalidArgFormat (arg : string) (format : string) (paramArray : obj array) =
- raise (ArgumentException (String.Format (format, paramArray), arg))
-
- /// The arrays of a function of two arrays have different lengths.
- let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T =
- invalidArgFormat arg1 "{0}\n{1}.Length = {2}, {3}.Length = {4}" [|
- ErrorStrings.ArraysHadDifferentLengths
- arg1
- len1
- arg2
- len2
- |]
-
- /// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is.
- let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T =
- invalidArgFormat arg1 "{0}\n{1} is {2} {3} shorter than {4}" [|
- ErrorStrings.ListsHadDifferentLengths
- arg1
- diff
- (if diff = 1 then "element" else "elements")
- arg2
- |]
-
- /// The arrays of a function of three arrays have different lengths.
- let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T =
- invalidArgFormat
- (String.Concat [| arg1; ", "; arg2; ", "; arg3 |])
- "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}"
- [| ErrorStrings.ArraysHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |]
-
- /// The lists of a function of three lists have different lengths.
- let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T =
- invalidArgFormat
- (String.Concat [| arg1; ", "; arg2; ", "; arg3 |])
- "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}"
- [| ErrorStrings.ListsHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |]
-
- /// A count or an index is negative.
- let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T =
- invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBeNonNegative; arg; count |]
-
- /// A size is not positive.
- let invalidArgInputMustBePositive (arg : string) (count : int) : 'T =
- invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBePositive; arg; count |]
-
- /// An index or a count is beyond a bound.
- let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T =
- let message =
- String.Format ("{0}\n{1} = {2}, {3} = {4}", [| box ErrorStrings.OutOfRange; arg; index; text; bound |])
-
- raise (ArgumentOutOfRangeException (arg, message))
-
- /// A list is shorter than an index.
- let invalidOpListNotEnoughElements (index : int) : 'T =
- let message =
- String.Format (
- "{0}\nThe list was {1} {2} shorter than the index",
- [| box ErrorStrings.NotEnoughElements; index; (if index = 1 then "element" else "elements") |]
- )
-
- raise (InvalidOperationException message)
-
- /// No element satisfies the predicate of a `find` or `pick` function.
- let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt)
-
- /// A collection has fewer elements than a function needs.
- let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements)
-
-/// The checks of the FSharp.Core Array module that the functions of several arrays share.
-module internal ArrayChecks =
-
- /// The arrays of a function of two arrays must have the same length.
- let inline sameLengths (name1 : string) (array1 : 'T1 array) (name2 : string) (array2 : 'T2 array) =
- if array1.Length <> array2.Length then
- Raise.invalidArgDifferentArrayLength name1 array1.Length name2 array2.Length
-
- /// The arrays of a function of three arrays must have the same length.
- let inline sameLengths3
- (name1 : string)
- (array1 : 'T1 array)
- (name2 : string)
- (array2 : 'T2 array)
- (name3 : string)
- (array3 : 'T3 array)
- =
- if
- array1.Length <> array2.Length
- || array1.Length <> array3.Length
- then
- Raise.invalidArg3ArraysDifferent name1 name2 name3 array1.Length array2.Length array3.Length
-
-///
-/// Algorithms over arrays that the default modules and the Tuple modules share (ADR 0001, D3).
-///
-///
-/// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass.
-/// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror.
-///
-module internal Arrays =
-
- /// The pairs of every element of `array1` with every element of `array2`.
- let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array =
- let length2 = array2.Length
- let result = Array.zeroCreate (array1.Length * length2)
-
- for i = 0 to array1.Length - 1 do
- for j = 0 to length2 - 1 do
- result[i * length2 + j] <- tuple array1[i] array2[j]
-
- result
-
- /// A key that may be null, for a dictionary, which rejects null keys.
- []
- type NullableKey<'Key> = { Key : 'Key }
-
- /// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys.
- let nullableKeyComparer<'Key when 'Key : equality> () =
- let structural = HashIdentity.Structural<'Key>
-
- {
- new IEqualityComparer> with
- member _.Equals (left, right) = structural.Equals (left.Key, right.Key)
- member _.GetHashCode key = structural.GetHashCode key.Key
- }
-
- ///
- /// The keys of the projection in the order of their first occurrence, each with the number of its elements.
- ///
- ///
- /// The counting keeps one counter per key, not the elements, so it needs memory for the keys only.
- ///
- let inline countBy
- ([] tuple : 'Key -> int -> 'R)
- (projection : 'T -> 'Key)
- (array : 'T array)
- : 'R array when 'Key : equality =
- let indices = Dictionary, int>(nullableKeyComparer<'Key>())
- let keys = ResizeArray<'Key>()
- let counts = ResizeArray()
-
- for item in array do
- let key = projection item
- let mutable index = 0
-
- if indices.TryGetValue ({ Key = key }, &index) then
- counts[index] <- counts[index] + 1
- else
- indices.Add ({ Key = key }, keys.Count)
- keys.Add key
- counts.Add 1
-
- let result = Array.zeroCreate keys.Count
-
- for index = 0 to result.Length - 1 do
- result[index] <- tuple keys[index] counts[index]
-
- result
-
- /// The keys of the projection in the order of their first occurrence, each with its elements in their order.
- let inline groupBy
- ([] tuple : 'Key -> 'T array -> 'R)
- (projection : 'T -> 'Key)
- (array : 'T array)
- : 'R array when 'Key : equality =
- let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>)
- let result = Array.zeroCreate lookup.Count
- let mutable index = 0
-
- for grouping in lookup do
- result[index] <- tuple grouping.Key (grouping.ToArray ())
- index <- index + 1
-
- result
-
- /// Every element with its index.
- let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array =
- let result = Array.zeroCreate array.Length
-
- for i = 0 to result.Length - 1 do
- result[i] <- tuple i array[i]
-
- result
-
- /// Every element with its successor.
- let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array =
- if array.Length < 2 then
- Array.Empty ()
- else
- let result = Array.zeroCreate (array.Length - 1)
-
- for i = 0 to result.Length - 1 do
- result[i] <- tuple array[i] array[i + 1]
-
- result
-
- /// Splits the pairs of an array with `split` into two arrays.
- let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) =
- let result1 = Array.zeroCreate array.Length
- let result2 = Array.zeroCreate array.Length
-
- for i = 0 to array.Length - 1 do
- let struct (first, second) = split array[i]
- result1[i] <- first
- result2[i] <- second
-
- struct (result1, result2)
-
- /// Splits the triples of an array with `split` into three arrays.
- let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) =
- let result1 = Array.zeroCreate array.Length
- let result2 = Array.zeroCreate array.Length
- let result3 = Array.zeroCreate array.Length
-
- for i = 0 to array.Length - 1 do
- let struct (first, second, third) = split array[i]
- result1[i] <- first
- result2[i] <- second
- result3[i] <- third
-
- struct (result1, result2, result3)
-
- /// Pairs the elements of two arrays of the same length.
- let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array =
- let result = Array.zeroCreate array1.Length
-
- for i = 0 to result.Length - 1 do
- result[i] <- tuple array1[i] array2[i]
-
- result
-
- /// Combines the elements of three arrays of the same length.
- let inline zip3
- ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R)
- (array1 : 'T1 array)
- (array2 : 'T2 array)
- (array3 : 'T3 array)
- : 'R array =
- let result = Array.zeroCreate array1.Length
-
- for i = 0 to result.Length - 1 do
- result[i] <- tuple array1[i] array2[i] array3[i]
-
- result
diff --git a/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs b/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs
new file mode 100644
index 00000000..4b97f77c
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs
@@ -0,0 +1,22 @@
+/// The checks of the FSharp.Core Array module that the functions of several arrays share.
+module internal FSharp.Collections.Immutable.ArrayChecks
+
+/// The arrays of a function of two arrays must have the same length.
+let inline sameLengths (name1 : string) (array1 : 'T1 array) (name2 : string) (array2 : 'T2 array) =
+ if array1.Length <> array2.Length then
+ Raise.invalidArgDifferentArrayLength name1 array1.Length name2 array2.Length
+
+/// The arrays of a function of three arrays must have the same length.
+let inline sameLengths3
+ (name1 : string)
+ (array1 : 'T1 array)
+ (name2 : string)
+ (array2 : 'T2 array)
+ (name3 : string)
+ (array3 : 'T3 array)
+ =
+ if
+ array1.Length <> array2.Length
+ || array1.Length <> array3.Length
+ then
+ Raise.invalidArg3ArraysDifferent name1 name2 name3 array1.Length array2.Length array3.Length
diff --git a/src/FSharp.Collections.Immutable/Helper/Arrays.fs b/src/FSharp.Collections.Immutable/Helper/Arrays.fs
new file mode 100644
index 00000000..b09b927a
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/Arrays.fs
@@ -0,0 +1,187 @@
+/// Algorithms over arrays that the default modules and the `Tuple` modules share (ADR 0001, D3).
+/// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass.
+/// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror.
+/// The algorithms exist because the functions of the FSharp.Core `Array` module that build or take pairs and triples work
+/// with reference tuples. The default functions of `FlatList` return struct tuples, and the functions of the `Tuple`
+/// namespace return reference tuples. Calling the `Array` function and converting its result would allocate every
+/// reference tuple and a second array. Each algorithm has exactly two callers, the function of `FlatList` with
+/// `structPair` or `structTriple` and the function of `Tuple.FlatList` with `referencePair` or `referenceTriple`, so
+/// that it exists once. For 10 000 elements, `zip` of struct tuples allocates 80 KB where `Array.zip` followed by a
+/// conversion allocates 400 KB.
+/// The first sentences of the comment of each function are the summary of the function of the FSharp.Core `Array` module
+/// word for word (FSharp.Core 10.1), and the sentences after them say what differs and why the function exists.
+module internal FSharp.Collections.Immutable.Arrays
+
+open System
+open System.Collections.Generic
+open System.Linq
+
+/// Returns a new array that contains all pairings of elements from the first and second arrays.
+/// The pairs are built with `tuple`. It exists because `Array.allPairs` returns reference tuples. `FlatList.allPairs`
+/// builds struct tuples with it directly (720 KB for 300 by 300 elements, where `Array.allPairs` followed by a conversion
+/// allocates 3.6 MB).
+let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array =
+ let length2 = array2.Length
+ let result = Array.zeroCreate (array1.Length * length2)
+
+ for i = 0 to array1.Length - 1 do
+ for j = 0 to length2 - 1 do
+ result[i * length2 + j] <- tuple array1[i] array2[j]
+
+ result
+
+/// A key that may be null, for a dictionary, which rejects null keys. FSharp.Core has no such type in its public API.
+/// It exists because `Array.countBy` and `Array.groupBy` accept a null key (`[| null; "a"; null |]` has two keys), and
+/// `Dictionary` throws `ArgumentNullException` for one.
+[]
+type NullableKey<'Key> = { Key : 'Key }
+
+/// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys.
+/// It exists because the default comparer of a `Dictionary` compares array keys by reference, where `Array.countBy`
+/// finds `[| 1 |]` and `[| 1 |]` equal.
+let nullableKeyComparer<'Key when 'Key : equality> () =
+ let structural = HashIdentity.Structural<'Key>
+
+ {
+ new IEqualityComparer> with
+ member _.Equals (left, right) = structural.Equals (left.Key, right.Key)
+ member _.GetHashCode key = structural.GetHashCode key.Key
+ }
+
+/// Applies a key-generating function to each element of an array and returns an array yielding unique keys and their
+/// number of occurrences in the original array.
+/// The unique keys come in the order of their first occurrence, and the counting keeps one counter per key, not the
+/// elements, so it needs memory for the keys only. The tuples are built with `tuple`. It exists because `Array.countBy`
+/// returns reference tuples; null keys and keys that are arrays work as in FSharp.Core.
+let inline countBy
+ ([] tuple : 'Key -> int -> 'R)
+ (projection : 'T -> 'Key)
+ (array : 'T array)
+ : 'R array when 'Key : equality =
+ let indices = Dictionary, int>(nullableKeyComparer<'Key>())
+ let keys = ResizeArray<'Key>()
+ let counts = ResizeArray()
+
+ for item in array do
+ let key = projection item
+ let mutable index = 0
+
+ if indices.TryGetValue ({ Key = key }, &index) then
+ counts[index] <- counts[index] + 1
+ else
+ indices.Add ({ Key = key }, keys.Count)
+ keys.Add key
+ counts.Add 1
+
+ let result = Array.zeroCreate keys.Count
+
+ for index = 0 to result.Length - 1 do
+ result[index] <- tuple keys[index] counts[index]
+
+ result
+
+/// Applies a key-generating function to each element of an array and yields an array of unique keys. Each unique key
+/// contains an array of all elements that match to this key.
+/// The keys come in the order of their first occurrence, and the elements of a key in their order. The tuples are built
+/// with `tuple`. It exists because `Array.groupBy` returns reference tuples. It groups once with a lookup, as the project
+/// rules ask.
+let inline groupBy
+ ([] tuple : 'Key -> 'T array -> 'R)
+ (projection : 'T -> 'Key)
+ (array : 'T array)
+ : 'R array when 'Key : equality =
+ let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>)
+ let result = Array.zeroCreate lookup.Count
+ let mutable index = 0
+
+ for grouping in lookup do
+ result[index] <- tuple grouping.Key (grouping.ToArray ())
+ index <- index + 1
+
+ result
+
+/// Builds a new array whose elements are the corresponding elements of the input array paired with the integer index
+/// (from 0) of each element.
+/// The pairs are built with `tuple`. It exists because `Array.indexed` returns reference tuples (320 KB for 10 000
+/// elements, where struct tuples take 80 KB).
+let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array =
+ let result = Array.zeroCreate array.Length
+
+ for i = 0 to result.Length - 1 do
+ result[i] <- tuple i array[i]
+
+ result
+
+/// Returns an array of each element in the input array paired with its predecessor, with the exception of the first
+/// element which is only returned as the predecessor of the second element. The predecessor comes first in the returned
+/// pairs.
+/// The pairs are built with `tuple`. It exists because `Array.pairwise` returns reference tuples.
+let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array =
+ if array.Length < 2 then
+ Array.Empty ()
+ else
+ let result = Array.zeroCreate (array.Length - 1)
+
+ for i = 0 to result.Length - 1 do
+ result[i] <- tuple array[i] array[i + 1]
+
+ result
+
+/// Splits an array of pairs into two arrays.
+/// `split` reads a pair as a struct tuple. It exists because `Array.unzip` takes reference tuples. `FlatList.unzip` has
+/// struct tuples and passes `id` as `split`, `Tuple.FlatList.unzip` has reference tuples and converts one pair at a time,
+/// so that no array of converted pairs (400 KB for 10 000 pairs) is built.
+let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) =
+ let result1 = Array.zeroCreate array.Length
+ let result2 = Array.zeroCreate array.Length
+
+ for i = 0 to array.Length - 1 do
+ let struct (first, second) = split array[i]
+ result1[i] <- first
+ result2[i] <- second
+
+ struct (result1, result2)
+
+/// Splits an array of triples into three arrays.
+/// `split` reads a triple as a struct tuple. It exists for the reason of `unzip`: `Array.unzip3` takes reference tuples.
+let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) =
+ let result1 = Array.zeroCreate array.Length
+ let result2 = Array.zeroCreate array.Length
+ let result3 = Array.zeroCreate array.Length
+
+ for i = 0 to array.Length - 1 do
+ let struct (first, second, third) = split array[i]
+ result1[i] <- first
+ result2[i] <- second
+ result3[i] <- third
+
+ struct (result1, result2, result3)
+
+/// Combines the two arrays into an array of pairs. The two arrays must have equal lengths, otherwise an ArgumentException
+/// is raised.
+/// The pairs are built with `tuple`, and the callers check the lengths first and raise the exception of `Array.zip`. It
+/// exists because `Array.zip` returns reference tuples (400 KB for 10 000 elements with the conversion, 80 KB here).
+let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array =
+ let result = Array.zeroCreate array1.Length
+
+ for i = 0 to result.Length - 1 do
+ result[i] <- tuple array1[i] array2[i]
+
+ result
+
+/// Combines three arrays into an array of triples. The three arrays must have equal lengths, otherwise an
+/// ArgumentException is raised. (The summary of `Array.zip3` says "an array of pairs", which is a slip.)
+/// The triples are built with `tuple`, and the callers check the lengths first. It exists for the reason of `zip`:
+/// `Array.zip3` returns reference tuples.
+let inline zip3
+ ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R)
+ (array1 : 'T1 array)
+ (array2 : 'T2 array)
+ (array3 : 'T3 array)
+ : 'R array =
+ let result = Array.zeroCreate array1.Length
+
+ for i = 0 to result.Length - 1 do
+ result[i] <- tuple array1[i] array2[i] array3[i]
+
+ result
diff --git a/src/FSharp.Collections.Immutable/Helper/Comparers.fs b/src/FSharp.Collections.Immutable/Helper/Comparers.fs
new file mode 100644
index 00000000..68d08d17
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/Comparers.fs
@@ -0,0 +1,29 @@
+/// The default comparers of the collections (ADR 0001, D5).
+[]
+module internal FSharp.Collections.Immutable.Comparers
+
+open System.Collections.Generic
+
+///
+/// The comparer of F# generic comparison for a type: the comparer of the sorts, of binarySearch and of the
+/// sorted collections that are created without a comparer.
+///
+///
+///
+/// The class resolves once per type, in
+/// generic code, where FSharp.Core returns the comparer that it keeps for the type.
+///
+///
+/// A call of FastGenericComparer at a concrete type that is not primitive creates a new comparer each time, and
+/// the F# optimizer creates such calls when it inlines a small function of the library into its caller. Collections of
+/// one type would then have different comparers, and the BCL could not take its shortcuts for two collections with the
+/// same comparer, such as the one of ImmutableSortedSet.Union. A static field cannot be inlined.
+///
+///
+[]
+type DefaultComparer<'T when 'T : comparison> private () =
+
+ static let comparer = LanguagePrimitives.FastGenericComparer<'T>
+
+ /// The comparer of F# generic comparison for the type.
+ static member Instance : IComparer<'T> = comparer
diff --git a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs
new file mode 100644
index 00000000..1e42ecec
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs
@@ -0,0 +1,71 @@
+///
+/// The exception messages of FSharp.Core (FSCore.resx) and of the .NET libraries, in English.
+///
+///
+/// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources
+/// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception
+/// under the name of their own parameter. Each constant is the text of one resource and its comment names the resource,
+/// so a text that no resource of FSharp.Core or of the .NET libraries holds does not belong here.
+///
+module internal FSharp.Collections.Immutable.ErrorStrings
+
+/// FSCore.resx, arraysHadDifferentLengths.
+[]
+let ArraysHadDifferentLengths = "The arrays have different lengths."
+
+/// FSCore.resx, indexOutOfBounds.
+[]
+let IndexOutOfBounds = "The index was outside the range of elements in the list."
+
+/// FSCore.resx, arrayWasEmpty.
+[]
+let InputArrayEmpty = "The input array was empty."
+
+/// FSCore.resx, inputListWasEmpty.
+[]
+let InputListWasEmpty = "The input list was empty."
+
+/// FSCore.resx, inputMustBeNonNegative.
+[]
+let InputMustBeNonNegative = "The input must be non-negative."
+
+/// FSCore.resx, inputMustBePositive.
+[]
+let InputMustBePositive = "The input must be positive."
+
+/// FSCore.resx, inputSequenceEmpty.
+[]
+let InputSequenceEmpty = "The input sequence was empty."
+
+/// FSCore.resx, inputSequenceTooLong.
+[]
+let InputSequenceTooLong = "The input sequence contains more than one element."
+
+/// FSCore.resx, keyNotFound.
+[]
+let KeyNotFound = "The item, key, or index was not found in the collection."
+
+/// FSCore.resx, keyNotFoundAlt.
+[]
+let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection."
+
+/// FSCore.resx, listsHadDifferentLengths.
+[]
+let ListsHadDifferentLengths = "The lists had different lengths."
+
+/// System.Private.CoreLib, ArgumentOutOfRange_NeedNonNegNum: the message of the .NET libraries for a negative argument, as
+/// `Array.BinarySearch` reports it.
+[]
+let NeedNonNegativeNumber = "Non-negative number required."
+
+/// FSCore.resx, notAPermutation.
+[]
+let NotAPermutation = "The function did not compute a permutation."
+
+/// FSCore.resx, notEnoughElements.
+[]
+let NotEnoughElements = "The input sequence has an insufficient number of elements."
+
+/// FSCore.resx, outOfRange.
+[]
+let OutOfRange = "The index is outside the legal range."
diff --git a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs
new file mode 100644
index 00000000..5a7e4eae
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs
@@ -0,0 +1,116 @@
+/// Argument checks shared by the collection modules.
+/// The module keeps its old name, so that the modules written before ADR 0001 compile unchanged until they are replaced.
+[]
+module internal FSharp.Collections.Immutable.ImmutableCollectionUtil
+
+open System
+open System.Collections.Immutable
+open System.Runtime.InteropServices
+
+/// Throws `ArgumentNullException` for a `null` argument, as FSharp.Core does for a `null` array or list.
+let inline checkNotNull name (arg : _ | null) =
+ match arg with
+ | null -> nullArg name
+ | _ -> ()
+
+///
+/// Throws for a default .
+///
+///
+/// A default immutable array has no underlying array. The collection modules treat it as FSharp.Core treats a
+/// array (ADR 0001, D6).
+///
+let inline checkNotDefault name (array : ImmutableArray<'T>) =
+ if array.IsDefault then
+ nullArg name
+
+/// Reads the property IsDefault of a boxed immutable array through reflection, from the type of the array.
+let inline readIsDefault (arrayType : Type) (array : obj) : bool =
+ arrayType.GetProperty "IsDefault"
+ |> nonNull
+ |> _.GetValue(array)
+ |> unbox
+
+///
+/// Tells whether an object is a default , of any element type.
+///
+///
+///
+/// is covariant, so a sequence of a reference type is also a
+/// sequence of its base types: a default ImmutableArray<string> can be a seq<obj>. The type
+/// test :? ImmutableArray<obj> does not recognize it, because the type is another instantiation.
+///
+///
+/// The function finds the instantiation from the type of the object and reads the property IsDefault through
+/// reflection. A boxed immutable array is a value type, so every class is rejected first by one flag of its type, before
+/// the generic definition is read. The function allocates nothing for an object of another type. It is inline, because
+/// the public inline members of the computation expression call the check, and an inline member cannot call an internal
+/// function.
+///
+///
+let inline isDefaultImmutableArray (source : obj) : bool =
+ let sourceType = source.GetType ()
+
+ sourceType.IsValueType
+ && sourceType.IsGenericType
+ && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>)
+ && readIsDefault sourceType source
+
+///
+/// Throws for a sequence and for a default
+/// that is given as a sequence.
+///
+///
+///
+/// A default immutable array converts to a sequence without a check, and the sequence fails with
+/// when a function enumerates it. A function that takes a sequence
+/// calls this check, which treats a default immutable array as FSharp.Core treats a sequence
+/// (ADR 0001, D6). Any other sequence passes.
+///
+///
+/// An immutable array of the element type is recognized by its type, without reflection. A default immutable array of
+/// a derived reference type, which arrives as a sequence of a base type by covariance, is recognized by its generic
+/// type and by reading IsDefault through reflection.
+///
+///
+let inline checkNotNullOrDefaultSeq name (source : seq<'T>) =
+ checkNotNull name source
+
+ match source with
+ | :? ImmutableArray<'T> as array -> checkNotDefault name array
+ | _ ->
+ if isDefaultImmutableArray source then
+ nullArg name
+
+///
+/// The array under an immutable array, without a copy (ADR 0001, D5).
+///
+///
+/// The caller reads the array and never writes to it: the immutable array and every value that shares it see the same
+/// memory. A default immutable array throws for .
+///
+let inline unwrapArray name (array : ImmutableArray<'T>) : 'T array =
+ checkNotDefault name array
+ // The check above guarantees an underlying array
+ ImmutableCollectionsMarshal.AsArray array
+ |> Unchecked.nonNull
+
+///
+/// An immutable array over an array, without a copy (ADR 0001, D5).
+///
+///
+/// Only an array that nobody can write to any more may be wrapped: a fresh result of a computation.
+///
+let inline wrapArray (array : 'T array) : ImmutableArray<'T> = ImmutableCollectionsMarshal.AsImmutableArray array
+
+/// The constructor of a struct pair, for the algorithms shared by the default and the `Tuple` modules.
+let inline structPair (first : 'T1) (second : 'T2) = struct (first, second)
+
+/// The constructor of a reference pair, for the algorithms shared by the default and the `Tuple` modules.
+let inline referencePair (first : 'T1) (second : 'T2) = (first, second)
+
+/// The constructor of a struct triple, for the algorithms shared by the default and the `Tuple` modules.
+let inline structTriple (first : 'T1) (second : 'T2) (third : 'T3) = struct (first, second, third)
+
+/// The constructor of a reference triple, for the algorithms shared by the default and the `Tuple` modules.
+let inline referenceTriple (first : 'T1) (second : 'T2) (third : 'T3) = (first, second, third)
diff --git a/src/FSharp.Collections.Immutable/Helper/Raise.fs b/src/FSharp.Collections.Immutable/Helper/Raise.fs
new file mode 100644
index 00000000..b36a1141
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs
@@ -0,0 +1,69 @@
+///
+/// The exceptions of the DetailedExceptions module of FSharp.Core, with the same types, messages and parameter names.
+///
+///
+/// The callers pass the names of their own parameters (ADR 0001, D6).
+///
+module internal FSharp.Collections.Immutable.Raise
+
+open System
+open System.Collections.Generic
+
+/// The arrays of a function of two arrays have different lengths.
+let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T =
+ let message =
+ $"{ErrorStrings.ArraysHadDifferentLengths}\n{arg1}.Length = {len1}, {arg2}.Length = {len2}"
+
+ raise (ArgumentException (message, arg1))
+
+/// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is.
+let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T =
+ let elements = if diff = 1 then "element" else "elements"
+
+ let message =
+ $"{ErrorStrings.ListsHadDifferentLengths}\n{arg1} is {diff} {elements} shorter than {arg2}"
+
+ raise (ArgumentException (message, arg1))
+
+/// The arrays of a function of three arrays have different lengths.
+let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T =
+ let message =
+ $"{ErrorStrings.ArraysHadDifferentLengths}\n {arg1}.Length = {len1}, {arg2}.Length = {len2}, {arg3}.Length = {len3}"
+
+ raise (ArgumentException (message, $"{arg1}, {arg2}, {arg3}"))
+
+/// The lists of a function of three lists have different lengths.
+let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T =
+ let message =
+ $"{ErrorStrings.ListsHadDifferentLengths}\n {arg1}.Length = {len1}, {arg2}.Length = {len2}, {arg3}.Length = {len3}"
+
+ raise (ArgumentException (message, $"{arg1}, {arg2}, {arg3}"))
+
+/// A count or an index is negative.
+let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T =
+ raise (ArgumentException ($"{ErrorStrings.InputMustBeNonNegative}\n{arg} = {count}", arg))
+
+/// A size is not positive.
+let invalidArgInputMustBePositive (arg : string) (count : int) : 'T =
+ raise (ArgumentException ($"{ErrorStrings.InputMustBePositive}\n{arg} = {count}", arg))
+
+/// An index or a count is beyond a bound.
+let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T =
+ let message = $"{ErrorStrings.OutOfRange}\n{arg} = {index}, {text} = {bound}"
+
+ raise (ArgumentOutOfRangeException (arg, message))
+
+/// A list is shorter than an index.
+let invalidOpListNotEnoughElements (index : int) : 'T =
+ let elements = if index = 1 then "element" else "elements"
+
+ let message =
+ $"{ErrorStrings.NotEnoughElements}\nThe list was {index} {elements} shorter than the index"
+
+ raise (InvalidOperationException message)
+
+/// No element satisfies the predicate of a `find` or `pick` function.
+let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt)
+
+/// A collection has fewer elements than a function needs.
+let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements)