Skip to content

fix(js/ts/python/dart/beam): add FSharp.Core's argument checks to collection functions - #5154

Merged
MangelMaxime merged 5 commits into
fable-compiler:mainfrom
leonitousconforti:fix-missing-collection-checks
Oct 28, 2026
Merged

MangelMaxime merged 5 commits into
fable-compiler:mainfrom
leonitousconforti:fix-missing-collection-checks

Conversation

@leonitousconforti

@leonitousconforti leonitousconforti commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #5153. One commit per target.

Several List, Array and Seq functions returned a value, often a wrong one, where FSharp.Core rejects the argument. This adds FSharp.Core's checks, with its exception types and parameter names:

  • Behaviour change: List.fold2, map2, mapi2, map3, zip, zip3, iter2, iteri2 and forall2 raise ArgumentException for lists of different lengths instead of truncating. As in FSharp.Core, the callback still runs on the common prefix and forall2/exists2 still stop early. Code that relied on the truncation will now throw, so I can split this part out if you'd rather ship it separately.
  • Negative counts: List.init/replicate and Seq.init/take raise ArgumentException, Seq.replicate raises ArgumentOutOfRangeException. The Seq ones, like Seq.chunkBySize/windowed/splitInto, now check when called rather than when enumerated.
  • List.average/averageBy on an empty list (returned NaN).
  • Array.sub, Array.fill, Array.blit and System.Array.Copy out of range (clamped, wrote past the end, or threw a native error).
  • List/Array/Seq.removeManyAt raise exactly when FSharp.Core does. A count of 0 used to remove an item.
  • Seq.transpose accepts jagged input like FSharp.Core (it groups by index) instead of raising.

Notes:

  • JS/TS: System.Array.Copy compiles to the checked Array.copyTo instead of Util.copyToArray. The Int64 overloads convert their arguments, which also makes them work, and a blit within one array now copies overlapping ranges correctly.
  • Python: this changes existing fable-library-py functions in place, like earlier Python bug fixes. AGENTS.md asks for new names when semantics change, so I can do that instead if you prefer. The Rust-backed Array module is unchanged.
  • Dart: Array.sub/fill/blit get the same checks as JS (they threw a native RangeError).
  • Beam: Seq.fs, plus Erlang functions that returned a value, looped or raised where .NET doesn't: chunkBySize 0 looped forever, transpose truncated jagged lists, Array.sub and Array.blit past the end truncated, forall2/exists2 couldn't stop early, and splitInto on an empty list divided by zero. Functions that already fail with a native Erlang error (e.g. List.zip with different lengths) are unchanged.
  • Deliberate differences from .NET: Array.fill checks before writing (.NET fills up to the end, then raises), Array.removeManyAt with a negative count removes nothing, and int32 overflow with huge counts isn't emulated.

Not covered: null checks, BCL collections (ResizeArray.CopyTo stays unchecked), Array.create/zeroCreate sizes, and Rust.

The new tests assert exception types, since Fable code can't read ParamName.

@leonitousconforti

Copy link
Copy Markdown
Contributor Author

I am rebasing this

leonitousconforti and others added 5 commits October 28, 2026 18:27
Collection functions that FSharp.Core rejects arguments for returned a
value (often a wrong one), and a few raised where FSharp.Core does not.
They now behave like FSharp.Core, raising its exception types with its
ParamNames:

- List.fold2, map2, mapi2, map3, zip, zip3, iter2, iteri2 and forall2
  raise ArgumentException for lists of different lengths, after calling
  the function on the common items. forall2 still stops at the first
  false. List.foldBack2 names the lists like FSharp.Core, and
  List.exists2 now gives the same "The lists had different lengths."
  message as the others.
- List.init, List.replicate, Seq.init and Seq.take raise
  ArgumentException for negative counts, Seq.replicate
  ArgumentOutOfRangeException. The Seq functions check when called, like
  Seq.chunkBySize, Seq.windowed and Seq.splitInto now do.
- List.average and List.averageBy raise ArgumentException on empty lists.
- Array.sub and Array.fill check their ranges. Array.blit and
  System.Array.Copy, which now compiles to Array.copyTo instead of
  Util.copyToArray, raise System.Array.Copy's exceptions and leave the
  target untouched. The Int64 overloads of System.Array.Copy convert
  their arguments (they failed before), and a copy within one array
  handles overlapping ranges like System.Array.Copy.
- List, Array and Seq.removeManyAt raise ArgumentException when
  FSharp.Core does, and a count of 0 removes nothing. A negative count
  removes nothing from an array, where FSharp.Core gives odd results.
- Seq.transpose groups jagged input by index like FSharp.Core instead of
  raising. Array.transpose and List.transpose still raise.
- Array.chunkBySize and Array.splitInto (and so List's) use FSharp.Core's
  ParamNames.
Several List and Seq functions returned a value, often a wrong one, where
FSharp.Core rejects the argument, and a few raised where FSharp.Core does
not. They now behave like FSharp.Core:

- List.fold2, map2, mapi2, map3, zip, zip3, iter2, iteri2 and forall2
  raise ArgumentException when the lists have different lengths, after
  calling back on the common prefix. forall2 still stops at the first
  false. List.foldBack2, which raised the Rust core's ValueError, checks
  the lengths before calling back. Every List length check, exists2's
  included, now uses FSharp.Core's message.
- List.init, List.replicate, Seq.init and Seq.take raise
  ArgumentException for a negative count, Seq.replicate
  ArgumentOutOfRangeException. The Seq checks run when the function is
  called.
- List.average and List.averageBy raise ArgumentException on an empty
  list.
- List.removeManyAt raises exactly when FSharp.Core does and removes
  nothing for a count <= 0. Seq.removeManyAt raises after enumeration
  only when the source is too short for index, and no longer when the
  count runs past the end. Both use FSharp.Core's messages.
- Seq.chunkBySize, Seq.windowed and Seq.splitInto check the size when
  called, with FSharp.Core's parameter names.
- Seq.transpose groups jagged input by index like FSharp.Core instead of
  raising through Array.transpose.
Collection functions returned a value, often a wrong one, where
FSharp.Core rejects the arguments, and a few raised where FSharp.Core
does not. They now follow FSharp.Core, with its exception types and
ParamNames:

- List.fold2, map2, mapi2, map3, zip, zip3, iter2, iteri2 and forall2
  raise ArgumentException when the lists have different lengths, after
  calling the function on the common prefix. forall2 still returns
  false before reaching the end. List.foldBack2 names the lists like
  FSharp.Core, and every List length check says "The lists had
  different lengths.".
- List.init and List.replicate with a negative count, and List.average
  and List.averageBy on an empty list, raise ArgumentException.
- Seq.init, Seq.take, Seq.chunkBySize, Seq.windowed and Seq.splitInto
  check their argument when called, and Seq.replicate raises
  ArgumentOutOfRangeException, instead of returning an empty sequence or
  failing on enumeration. Array.chunkBySize and Array.splitInto use
  FSharp.Core's ParamNames.
- Array.sub, Array.fill, Array.blit and System.Array.Copy check their
  indexes and counts up front, and Array.fill allows a count of 0 past
  the end. Array.sub gets its own library function so
  ResizeArray.GetRange keeps using getSubArray.
- List, Array and Seq.removeManyAt raise ArgumentException with
  FSharp.Core's messages when FSharp.Core does (Seq also when the
  sequence is not longer than index), and a count of 0 or less removes
  nothing (on arrays FSharp.Core copies odd ranges or fails in
  System.Array.Copy). Seq.removeManyAt allows a count past the end.
- Seq.transpose groups the elements by index like FSharp.Core instead
  of raising for sequences of different lengths.
Some collection functions returned a value (often a wrong one), looped
forever or failed with an untyped Erlang error where FSharp.Core rejects
the argument, and a few raised where FSharp.Core does not:

- Array.sub raises ArgumentException for a negative startIndex or count
  and ArgumentOutOfRangeException for a range past the end, instead of
  truncating through lists:sublist/3. It still walks only the items it
  needs.
- Array.blit checks its arguments like System.Array.Copy, in the same
  order, before it copies anything. A source range past the end
  shortened the target instead of raising ArgumentException.
- Seq.init and Seq.take raise ArgumentException, and Seq.replicate
  ArgumentOutOfRangeException (it is Enumerable.Repeat on .NET), for a
  negative count when they are called. They returned an empty sequence.
- List, Array and Seq chunkBySize, windowed and splitInto reject a size
  below 1 with FSharp.Core's parameter names, the Seq ones when they are
  called. chunkBySize 0 looped forever, and so did splitInto with some
  negative counts.
- splitInto returns an empty result for an empty input instead of
  dividing by zero.
- List.transpose and Array.transpose raise ArgumentException for jagged
  input instead of truncating it or failing in hd/1. Seq.transpose
  groups jagged input by index like FSharp.Core instead of going through
  Array.transpose.
- List.forall2 and List.exists2 walk both lists like FSharp.Core instead
  of zipping them first, so they return as soon as the predicate decides
  and raise ArgumentException only when one list runs out first.
  Array.forall2 and Array.exists2 check the lengths up front.
- Seq.removeManyAt follows FSharp.Core: a count past the end or below 1
  is not an error, and a source without an element at index raises
  ArgumentException once it has been enumerated.

The 3-argument System.Array.Copy, which ignores its destination, is a
different bug and is left alone.
@MangelMaxime
MangelMaxime force-pushed the fix-missing-collection-checks branch from b5a7838 to ce0ad52 Compare October 28, 2026 19:40
@MangelMaxime
MangelMaxime merged commit 5185e65 into fable-compiler:main Oct 28, 2026
36 checks passed
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