Skip to content

feat: Stack and IStack modules with the vocabulary of the FSharp.Core List module - #34

Open
xperiandri wants to merge 3 commits into
feat/indexed-listfrom
feat/stack
Open

xperiandri wants to merge 3 commits into
feat/indexed-listfrom
feat/stack

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites the legacy Stack module for ImmutableStack<'T> and adds the interface module IStack (IImmutableStack<'T>) after ADR 0001 (#23). Both have the vocabulary of the FSharp.Core List module. This pull request is stacked on #31 (feat/indexed-list).

  • Layout (D2): Stack, IStack and their Option and Tuple modules are root modules, such as module FSharp.Collections.Immutable.Stack. The Stack<'T> and IStack<'T> abbreviations live in Stack/StackTypes.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: StackCE.fsi included.
    • stack has its val in ComputationExpressions.fsi, after indexedList under the Creation banner.
  • Stack.fsi and Stack.fs: 124 functions, in the canonical category order. They are the Stack column of ADR Appendix A.
    • cons, head, tail and uncons call Push, Peek and Pop. They take O(1), and a result shares the nodes of its input.
    • The functions that drop the first elements return the stack below them without a copy: tail, skip, skipWhile, takeLast and the second part of splitAt. A function that keeps every element returns its input.
    • The other transformations copy the elements to an array, call the Array function and push the result onto a new stack.
    • Every sort is stable, as List.sort* is, and compares in F# generic comparison, so strings sort ordinally.
      • 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 DescendingSorts of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31, which runs Seq.sortWith with the comparer that DefaultComparer of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 caches, reversed. The inline Seq.sortDescending and Seq.sortByDescending box every element, or key, in every comparison once they are compiled into a generic function. sortByDescending calls the projection in the comparison, as List.sortByDescending does.
      • sortBy and sortWithComparer sort with LINQ OrderBy/Order, which compute the keys first, as List.sortBy does. sortBy compares the keys with the cached DefaultComparer<'Key>.Instance.
    • length is O(n), because a stack does not store its length. The checks count a stack with Lengths.stackUpTo of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31, only as far as they need.
    • The extras are cons, consMany, uncons, tryUncons, tryTail, skipLast, takeLast, sortWithComparer, the span constructors and the struct partial active patterns Cons and Nil.
  • The order of a stack (D5): every function treats a stack as a list of its elements from the top to the bottom.
    • ofSeq, ofList, ofArray, ofSpan, ofReadOnlySpan, init, unfold and stack { } put the first element on top, so ofSeq is the inverse of toList. ImmutableStack.CreateRange and Create put the last element on top, so the module does not wrap them.
    • cons pushes a value, which becomes the first element. consMany pushes its values so that the first one ends on top.
    • ofSeq of an ImmutableStack returns it as it is.
  • IStack.fsi and IStack.fs: 115 functions over IImmutableStack<'T>: the functions of Stack without the creation functions, plus clear.
    • Results of the same element type keep the implementation of the input: Pop, or Clear () plus Push from the last element to the first one.
    • Results of another element type are a Stack. So are collect, concat and transpose, because their stacks can have different implementations.
    • The sorts are the same as in Stack.
    • append keeps the implementation and the nodes of the second stack (D14). The collection that receives the elements keeps its implementation. IStack.append pushes the first stack onto the second one, as List.append shares its second list, so the second stack receives the elements. IIndexedList.append adds the second list to the first one, which keeps its implementation.
  • Exceptions (D6): every function throws what the FSharp.Core List function throws: the type, the message and the parameter name. The names stack, stack1, stack2, stack3 and stacks replace the list names of List.
    • head, tail and uncons of an empty stack throw the ArgumentException of List.head and List.tail, not the InvalidOperationException of ImmutableStack.
    • The quirks of List stay. The functions of two and three stacks call their callback for the same elements as List before they throw. exists2 and forall2 can return before they check the lengths. except does not read the excluded items for an empty stack.
    • 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 do these modules now. sortBy lets an exception of the projection escape, as List.sortBy does.
    • A null input throws ArgumentNullException.
    • Sequence inputs (D6): a function that takes a sequence (ofSeq, concat, consMany, transpose and except of Stack, the same without ofSeq of IStack, and yield! and for of stack { }) throws ArgumentNullException for its own parameter when the sequence is null and when it is a default FlatList, which would fail with InvalidOperationException when the function enumerates it.
  • Option and Tuple namespaces: the Stack and IStack modules of FSharp.Collections.Immutable.Option and FSharp.Collections.Immutable.Tuple repeat the functions with voption or struct tuples (D3).
  • Compiled names (D2): cons, consMany, head, tail and uncons compile to Push, PushMany, Peek, Pop and PeekPop. tryHead, tryTail and tryUncons compile to TryPeek, TryPop and TryPeekPop. The IStack, Option and Tuple modules compile the same way.
  • Computation expression (D7): stack { } collects the elements in a List<'T> and pushes them from the last one, so the first element of the expression ends on top. yield! takes a sequence, an array, a stack or another expression, and a null sequence, array or stack, or a default FlatList given as a sequence, throws ArgumentNullException for items; for throws it for source. The expression checks when it calls the member, as flatList { } does.
  • Helper/StackConstruction.fs holds the internal StackConstruction module. Its pushArray pushes an array onto a stack from the last element. One inline function serves ImmutableStack<'T> and IImmutableStack<'T>, so a result keeps the implementation of its input. Stack, IStack, their Tuple modules and the computation expression build their stacks with it.
  • Removed files: the legacy Stack.fs.
  • Documentation: the changelog, and the collection tables of README.md, docsSrc/index.md and Background.md, list Stack with stack { } and the new IStack.

Tests: 9302 per target framework, 1470 of them new.

  • 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. The names tell apart the cases that repeat a function, such as the two parts of partition and splitAt.
    • StackTests takes 169 cases as rows, IStackTests 156 and StackComputationExpressionTests 15.
    • Lists remain only as the oracles of the List functions where Array behaves differently (the walk of the common prefix, exists2/forall2, contains of NaN, the exceptions), as the function under test (ofList, toList) and as the list case of yield!.
  • Stack derives the sequence-like, creation and stable-sort contracts. IStack derives the sequence-like and stable-sort contracts, over stacks upcast to the interface. Their expectations come from List over the elements from the top to the bottom.
  • The stable-sort contract sorts the 20 pairs of Inputs.ManyPairs, so an unstable sort fails it. It checks that a comparison or a projection that throws makes the sorts throw what List throws, and that sortByDescending calls the projection in the comparisons, in the order of List.sortByDescending.
    • It also checks that sortDescending and sortByDescending allocate less than 3 MB for 10 000 integers. The pipeline of the test (create the stack, sort it, read it) allocates 1.0 MB with DescendingSorts and 7.8 MB with the inline Seq sorts. With the inline sorts put back, exactly the four allocation tests fail, two in Stack and two in IStack.
  • The sequence inputs (D6) are checked by the shared contracts of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31 (ofSeq, concat, transpose and except, in Stack and IStack) and by a table in StackTests, IStackTests and StackComputationExpressionTests: consMany, a default flat list of strings given as a sequence of objects, the order of the checks of except, and the members of the builder called on their own. Breaking each fix once in a copy of the sources (27 changes) fails the tests in every case.
  • A custom IImmutableStack implementation proves that 35 functions of IStack keep the implementation of their input. Data rows check that IStack.append keeps the implementation and the nodes of the second stack, also when either stack is empty.
  • Reflection proves the compiled names of all six modules: 26 rows. A changed CompiledName fails the row of its function.
  • Categories are on the test classes only (D8). The classes with the tests of the Cons and Nil patterns also carry PatternMatchingTestCategory.
  • StackOps and IStackOps are the root modules of their own files.
  • The per-collection tests cover the order of the elements, the shared nodes, null inputs of every function, the complete messages of the List exceptions with the names of the stacks, the ordinal order of strings in the four sorts, the patterns, the computation expression, yield! and for of null and of a default FlatList, and both namespaces.

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:
The list compares with the published 1.0.0, whose Stack module has cons, empty, head, ofArray, ofSeq, peek, pop, push, tail and the complete active pattern Cons | Nil.

  • Stack.cons, head and tail took and returned IImmutableStack<'T>. They take and return ImmutableStack<'T>, and the IStack module serves the interface. They compile to Push, Peek and Pop, where 1.0.0 compiled them under their F# names.
  • Stack.ofSeq and Stack.ofArray put the first element on top. Before, they wrapped ImmutableStack.CreateRange, which puts the last element on top.
  • Stack.ofSeq of a default FlatList given as a sequence throws ArgumentNullException, as it does for null, instead of InvalidOperationException.
  • Stack.push, peek and pop are removed: use cons, head and uncons. pop returned a reference tuple, and uncons returns a struct tuple.
  • The complete active pattern Cons | Nil, which matched a reference tuple, is replaced by the struct partial active patterns Cons, which matches a struct tuple, and Nil.
  • head and tail of an empty stack throw ArgumentException instead of InvalidOperationException.

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

  • 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 of the stable-sort contract fails, in Stack and in IStack, because OrderByDescending computes the keys before it sorts. The four sorts moved to Seq anyway, the two descending ones through DescendingSorts: they now make exactly the comparisons that List makes.
  • Cost of the Seq sorts: measured in Release on .NET 10 with a stack of 10 000 integers, as the median of 75 calls in alternating rounds with the old build, in two runs.
    • The inline Seq.sortDescending and Seq.sortByDescending allocated 7.5 MB per call. With DescendingSorts, both allocate 0.65 MB, as sortWith does. sortDescending takes 1.0 ms instead of 2.1 to 2.5 ms, and sortByDescending 1.1 to 2.2 ms instead of 2.2 to 4.6 ms. The two runs differ most for sortByDescending.
    • sort and sortWith stay on Seq and allocate 0.53 and 0.65 MB. Against LINQ Order, Stack.sortWith takes 0.83 ms instead of 1.16 ms and allocates 651 704 bytes 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, which allocates 783 072 bytes.
  • length is O(n): an immutable stack does not store its length. The argument checks count only as far as they need, so head, exactlyOne and the sorts do not count the whole stack.
  • Parameter names of List: List reports some unusual names, and the module reports them too: "source" for contains, exactlyOne, average, averageBy and the random* functions; "count" for init; "stack.[j]" for transpose, where List reports "list.[j]".

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 15:38

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 extensive breaking API rewrite needs final human verification of compatibility and List-equivalent behavior across target frameworks.

No open issues 🎉

What changed in this PR

Replaces the legacy stack bindings with List-style Stack and IStack APIs under ADR 0001, continuing the collection-module rewrite from #31.

Changes:

  • Establishes top-to-bottom ordering, shared-node operations, stable sorting, and List-compatible exceptions.
  • Adds Option/Tuple variants and the stack { } computation expression.
  • Adds contract and collection-specific tests and documents breaking changes.
File Description
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackTupleTests.fs Tests reference-tuple overrides.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackTests.fs Tests ordering, sharing, and exceptions.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackOptionTests.fs Tests option overrides.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackOps.fs Connects Stack to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackContractTests.fs Instantiates both modules’ contracts.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​StackComputationExpressionTests.fs Tests expression ordering and control flow.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​IStackTupleTests.fs Tests interface tuple overrides.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​IStackTests.fs Tests implementation preservation and interface behavior.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​IStackOptionTests.fs Tests interface option overrides.
tests/​FSharp.Collections.Immutable.Tests/​Stack/​IStackOps.fs Connects IStack to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Includes new test files.
src/​FSharp.Collections.Immutable/​Stack/​StackTypes.fs Defines stack type abbreviations.
src/​FSharp.Collections.Immutable/​Stack/​StackTuple.fsi Declares reference-tuple APIs.
src/​FSharp.Collections.Immutable/​Stack/​StackTuple.fs Implements reference-tuple overrides.
src/​FSharp.Collections.Immutable/​Stack/​StackOption.fsi Declares option APIs.
src/​FSharp.Collections.Immutable/​Stack/​StackOption.fs Implements option overrides.
src/​FSharp.Collections.Immutable/​Stack/​StackCE.fs Implements the stack expression builder.
src/​FSharp.Collections.Immutable/​Stack/​IStackTuple.fsi Declares interface tuple APIs.
src/​FSharp.Collections.Immutable/​Stack/​IStackTuple.fs Implements interface tuple overrides.
src/​FSharp.Collections.Immutable/​Stack/​IStackOption.fsi Declares interface option APIs.
src/​FSharp.Collections.Immutable/​Stack/​IStackOption.fs Implements interface option overrides.
src/​FSharp.Collections.Immutable/​Stack/​IStack.fs Implements List-style interface operations.
src/​FSharp.Collections.Immutable/​Stack.fs Removes legacy stack bindings.
src/​FSharp.Collections.Immutable/​Helper.fs Shares order-preserving stack construction.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers replacement stack files.
src/​FSharp.Collections.Immutable/​ComputationExpressions.fs Exposes stack { }.
README.md Lists IStack and stack expressions.
docsSrc/​index.md Updates the collection overview.
docsSrc/​Explanations/​Background.md Updates the collection reference table.
CHANGELOG.md Records additions and breaking changes.

Review effort: Balanced

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 rewrite needs human confirmation of compatibility and runtime contracts across both target frameworks.

No open issues 🎉

Review effort: Balanced

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 extensive breaking API replacement needs human confirmation of compatibility and test results across both target frameworks.

No open issues 🎉

Review effort: Balanced

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

Broad breaking API and ordering changes require human validation of multi-target test results, alongside correction of the retained null-validation issue.

1 open issue

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/Stack/StackCE.fs
…Core `List` module

`Stack` (`ImmutableStack<'T>`) replaces the module of the same name. It has every function of `List` that applies to an
immutable stack, with the checks and the exceptions of `List` and with stable sorts, plus the extras of ADR 0001:
`consMany`, `ofReadOnlySpan`, `ofSpan`, `skipLast`, `sortWithComparer`, `takeLast`, `tryTail`, `tryUncons`, `uncons` and
the struct partial patterns `Cons` and `Nil`. Its functions name their stacks `stack`, `stack1` and `stacks` where `List`
names its lists, and the exceptions report these names. `cons`, `consMany`, `head`, `tail` and `uncons` compile to `Push`,
`PushMany`, `Peek`, `Pop` and `PeekPop`, and their `try` variants to `TryPeek`, `TryPop` and `TryPeekPop`.

A stack enumerates its elements from the top, and every function treats it as a list in that order. The creation
functions push the elements from the last one, so that `ofSeq` puts the first element on top and is the inverse of
`toList`; `ImmutableStack.CreateRange` would leave the last element on top. The transformations copy the elements to an
array, compute the result and push it back from its last element, and `rev` reverses the order. `tail`, `skip`,
`skipWhile`, `takeLast` and the second part of `splitAt` are the stack below the removed elements, without a copy, and
`cons`, `consMany` and `append` push onto the stack that the result shares.

`IStack` has the same functions for `IImmutableStack<'T>` without the creation functions, plus `clear`; a result with the
element type of the input keeps the implementation of the input, through `Pop`, or `Clear` followed by `Push`. The
`Option` and `Tuple` namespaces repeat the functions that use value options or struct tuples, and `stack { }` builds a
stack whose first element is on top. One internal loop, `StackConstruction.pushArray`, does the pushing for both modules
and the expression.

The legacy `Stack.fs` and its `ImmutableStack` module alias are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri and others added 2 commits October 20, 2026 18:13
The sequence-like, creation and stable sorting contracts run for `Stack`, and the sequence-like and stable sorting
contracts for `IStack`, whose stacks are created by `Stack.ofSeq` and seen through the interface. The contracts compare
with `List` over the order in which a stack enumerates its elements, and their `ParamName` maps the names of the lists
that `List` reports, such as `list1` and `list.[1]`, to the names of the stacks.

Further tests cover the order of the elements: `ofSeq` and `toList` undo each other, every creation function and
`stack { }` put the first element on top, `cons` and `consMany` push on top, `rev` reverses, and `append` puts the first
stack on top of the second one. They also cover the nodes that `tail`, `skip`, `skipWhile`, `takeLast`, `splitAt`,
`cons`, `consMany` and `append` share, the inputs that the functions return themselves, null inputs, `uncons` and the
patterns, the complete messages of the `List` exceptions with the names of the stacks, the implementation that `IStack`
keeps for a custom `IImmutableStack<'T>`, `stack { }` and the `Option` and `Tuple` namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog lists the new functions and modules, the order of the elements, which puts the first element on top and
which `rev` reverses, the breaking changes of the types, the exceptions and the patterns, and the removed functions and
module alias. The collection tables name `stack { }` for `Stack` and add `IStack`.

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