From 2cca03dbd039db06fb5a77c5c2aceacd903efce1 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Sun, 25 Oct 2026 01:14:52 -0500 Subject: [PATCH 1/6] fix(js/ts): align collection exception types with FSharp.Core Now that invalidArg and invalidOp raise typed exceptions, List, Array, Seq, Map, Set and range functions raise the exception types FSharp.Core raises: - reduce/reduceBack (and min/max/minBy/maxBy, Set.intersectMany) and List.last on empty input raise ArgumentException. Array.reduce and Array.reduceBack no longer map to the native JS reduce, which threw a TypeError. - list.Head and list.Tail raise InvalidOperationException, while List.head and List.tail keep raising ArgumentException. PHP, which shares these replacements, keeps calling List.head and List.tail. - take/splitAt past the end and Seq.skip raise InvalidOperationException; List.skip and Array.skip raise ArgumentOutOfRangeException. Seq.tail keeps raising ArgumentException. - Array length mismatches, windowed, permute and zero range steps raise ArgumentException, and find/pick misses raise KeyNotFoundException. Array.permute now also rejects an index map that is not a bijection: its flags array had empty slots, which `every` skips. - random*By with a randomizer outside [0, 1) raises ArgumentOutOfRangeException. - Map and Set enumerators raise InvalidOperationException, and mutating a Map through IDictionary or ICollection raises NotSupportedException with FSharp.Core's message. SR.inputWasEmpty now reads "The input list was empty." like FSharp.Core. --- src/Fable.Transforms/Replacements.fs | 11 ++++-- src/fable-library-ts/Array.fs | 59 +++++++++++++++------------- src/fable-library-ts/Global.fs | 2 +- src/fable-library-ts/List.fs | 26 +++++++----- src/fable-library-ts/Map.fs | 18 +++++---- src/fable-library-ts/Range.fs | 2 +- src/fable-library-ts/Seq.fs | 24 ++++++++--- src/fable-library-ts/Set.fs | 10 +++-- tests/Js/Main/ArrayTests.fs | 54 +++++++++++++++++++++++++ tests/Js/Main/ListTests.fs | 37 +++++++++++++++++ tests/Js/Main/MapTests.fs | 16 ++++++++ tests/Js/Main/SeqTests.fs | 28 +++++++++++++ tests/Js/Main/SetTests.fs | 16 ++++++++ tests/Js/Main/Util/Util.fs | 14 +++++++ 14 files changed, 258 insertions(+), 59 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 8109a1ae2a..6932952ccd 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2283,8 +2283,6 @@ let nativeArrayFunctions = "Filter", "filter" "ForAll", "every" "Iterate", "forEach" - "Reduce", "reduce" - "ReduceBack", "reduceRight" "SortInPlaceWith", "sort" |] @@ -2403,7 +2401,14 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists + // Use the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: + // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException. + // PHP's FSharpList has no such members, so PHP keeps the head/tail functions below. + | ("get_Head" | "get_Tail"), Some x, _ when com.Options.Language <> Php -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "List", mangledName, t, [], thisArg = x, ?loc = r) |> Some | ReplaceName [ "get_Head", "head" "get_Tail", "tail" "get_Item", "item" diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index 9632b11662..522a405ed6 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -11,9 +11,10 @@ open Native open Native.Helpers let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // Pay attention when benchmarking to append and filter functions below // if implementing via native JS array .concat() and .filter() do not fall behind due to js-native transitions. @@ -77,7 +78,7 @@ let mapIndexed2 : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -88,7 +89,7 @@ let mapIndexed2 let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) ([] cons: Cons<'U>) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -106,7 +107,7 @@ let mapIndexed3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -124,7 +125,7 @@ let map3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -328,7 +329,7 @@ let scanBack<'T, 'State> folder (array: 'T[]) (state: 'State) ([] cons: Cons<'T>) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then allocateArrayFromCons cons 0 @@ -357,7 +358,7 @@ let take count (array: 'T[]) ([] cons: Cons<'T>) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then allocateArrayFromCons cons 0 @@ -571,14 +572,14 @@ let iterateIndexed action (array: 'T[]) = let iterate2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1.[i] array2.[i] let iterateIndexed2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1.[i] array2.[i] @@ -599,14 +600,15 @@ let forAll predicate (array: 'T[]) = let permute f (array: 'T[]) = let size = array.Length let res = copyImpl array - let checkFlags = allocateArray size + // Fill the flags: native `every` skips the empty slots of `new Array(size)` + let checkFlags = fillImpl (allocateArray size) 0 0 size iterateIndexed (fun i x -> let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res.[j] <- x checkFlags.[j] <- 1 @@ -616,7 +618,7 @@ let permute f (array: 'T[]) = let isValid = checkFlags |> forAllImpl ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -719,7 +721,7 @@ let unzip3 (array: _[]) = let zip (array1: 'T[]) (array2: 'U[]) = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let result = allocateArray array1.Length @@ -731,7 +733,7 @@ let zip (array1: 'T[]) (array2: 'U[]) = let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" let result = allocateArray array1.Length @@ -758,9 +760,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = result |> asArray let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + subArrayImpl array 0 index, skipImpl array index // Note that, though it's not consistent with `compare` operator, @@ -921,7 +926,7 @@ let foldIndexed2 folder state (array1: _[]) (array2: _[]) = let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder i acc array1.[i] array2.[i] @@ -935,7 +940,7 @@ let foldBackIndexed2<'T1, 'T2, 'State> folder (array1: 'T1[]) (array2: 'T2[]) (s let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let size = array1.Length @@ -949,7 +954,7 @@ let foldBack2<'T1, 'T2, 'State> f (array1: 'T1[]) (array2: 'T2[]) (state: 'State let reduce reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldIndexed (fun i acc x -> if i = 0 then x else reduction acc x) Unchecked.defaultof<_> array // else @@ -957,7 +962,7 @@ let reduce reduction (array: 'T[]) = let reduceBack reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldBackIndexed (fun i x acc -> if i = 0 then x else reduction acc x) array Unchecked.defaultof<_> // else @@ -983,7 +988,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -1069,7 +1074,7 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let res = FSharp.Core.Operators.max 0 (source.Length - windowSize + 1) |> allocateArray @@ -1120,7 +1125,7 @@ let transpose (arrays: 'T[] seq) ([] cons: Cons<'T>) : let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" let result: 'T[][] = allocateArray lenInner @@ -1178,7 +1183,7 @@ let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = int (r * float (i + 1)) let tmp = xs[i] @@ -1209,7 +1214,7 @@ let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] @@ -1239,7 +1244,7 @@ let randomChoicesBy let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] ) @@ -1274,7 +1279,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = i + int (r * float (len - i)) let tmp = arr[i] diff --git a/src/fable-library-ts/Global.fs b/src/fable-library-ts/Global.fs index 8b132814d3..416e179656 100644 --- a/src/fable-library-ts/Global.fs +++ b/src/fable-library-ts/Global.fs @@ -22,7 +22,7 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." let inputSequenceEmpty = "The input sequence was empty." diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index e41e9724c7..bf29b18fa2 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -48,12 +48,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -186,7 +186,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -194,7 +197,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -210,7 +216,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -605,13 +611,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -747,7 +753,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -767,7 +773,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -833,7 +839,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-ts/Map.fs b/src/fable-library-ts/Map.fs index ee52a22643..97d8f177fc 100644 --- a/src/fable-library-ts/Map.fs +++ b/src/fable-library-ts/Map.fs @@ -592,9 +592,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -905,14 +907,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value @@ -935,11 +937,11 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key, p.Value) @@ -948,7 +950,7 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key) member m.set(k, v) = - failwith "Map cannot be mutated" + raise (System.NotSupportedException("Map values cannot be mutated.")) m :> JS.Map<'Key, 'Value> member m.values() = m |> Seq.map (fun p -> p.Value) diff --git a/src/fable-library-ts/Range.fs b/src/fable-library-ts/Range.fs index d6ce3d8933..eeaa35e429 100644 --- a/src/fable-library-ts/Range.fs +++ b/src/fable-library-ts/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index 20631e4a89..6271cd8cd1 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -991,7 +991,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -999,7 +999,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -1034,7 +1034,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1056,7 +1056,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take count (xs: seq<'T>) = generateIndexed @@ -1066,7 +1080,7 @@ let take count (xs: seq<'T>) = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-ts/Set.fs b/src/fable-library-ts/Set.fs index 4a39504015..ac222fc327 100644 --- a/src/fable-library-ts/Set.fs +++ b/src/fable-library-ts/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index b0b90a14a6..0e97aad4f6 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -1526,4 +1526,58 @@ let tests = coll.CopyTo(ys, 0) ys = xs |> equal true + testCase "Array functions raise ArgumentException on empty arrays" <| fun () -> + let xs: int[] = [||] + let ys: string[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduce (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id ys) |> equal "ArgumentException" + + testCase "Array functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array functions raise ArgumentException for arrays of different lengths" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array functions raise KeyNotFoundException when nothing satisfies the predicate" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 2 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun i -> i + 1) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 2 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + ] diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index d5f9a68272..4df3113faf 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1075,4 +1075,41 @@ let tests = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List functions raise ArgumentException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List Head and Tail properties raise InvalidOperationException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [1; 2]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun i -> i + 1) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 2 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" ] diff --git a/tests/Js/Main/MapTests.fs b/tests/Js/Main/MapTests.fs index eee708d352..caa3d043e1 100644 --- a/tests/Js/Main/MapTests.fs +++ b/tests/Js/Main/MapTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Maps open System.Collections.Generic open Util.Testing +open Fable.Tests.Util type R = { i: int; s: string } type R2 = { kv: KeyValuePair } @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] |> Array.map KeyValuePair coll.CopyTo(ys, 0) ys = (xs |> Array.map KeyValuePair) |> equal true + + testCase "Map enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let m = Map [1, "a"; 2, "b"] + let e = (m :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + + testCase "Map raises NotSupportedException when mutated through IDictionary or ICollection" <| fun () -> + let dic = Map [1, "a"] :> IDictionary + let coll = Map [1, "a"] :> ICollection> + raisedExceptionType (fun () -> dic.Add(2, "b")) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.Remove(1)) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.[2] <- "b") |> equal "NotSupportedException" + raisedExceptionType (fun () -> coll.Clear()) |> equal "NotSupportedException" ] diff --git a/tests/Js/Main/SeqTests.fs b/tests/Js/Main/SeqTests.fs index 620324d219..fc19f494ba 100644 --- a/tests/Js/Main/SeqTests.fs +++ b/tests/Js/Main/SeqTests.fs @@ -568,6 +568,13 @@ let tests = |> Seq.reduce (+) |> equal 19990000000000002000I + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + let step = 0 + raisedExceptionType (fun () -> [0 .. step .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|0L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0. .. 0. .. 1.]) |> equal "ArgumentException" + raisedExceptionType (fun () -> seq {0M .. 0M .. 1M} |> Seq.toList) |> equal "ArgumentException" + testCase "Seq.reduce works" <| fun () -> let xs = [1.; 2.] xs |> Seq.reduce (+) @@ -1162,4 +1169,25 @@ let tests = throwsAnyError (fun () -> Seq.removeManyAt 0 2 [] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt -1 2 [1] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt 2 2 [1] |> Seq.toList |> ignore) + + testCase "Seq functions raise ArgumentException on empty sequences" <| fun () -> + let xs = seq { for x in [1; 2] do if x > 2 then yield x } + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.tail xs |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq functions raise InvalidOperationException when there are not enough elements" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun i -> i + 1) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" ] diff --git a/tests/Js/Main/SetTests.fs b/tests/Js/Main/SetTests.fs index 2f752a51e5..88e36cc8d1 100644 --- a/tests/Js/Main/SetTests.fs +++ b/tests/Js/Main/SetTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Sets open System.Collections.Generic open Util.Testing +open Fable.Tests.Util let tests = testList "Sets" [ @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] coll.CopyTo(ys, 0) ys = xs |> equal true + + testCase "Set functions raise ArgumentException on empty sets" <| fun () -> + let s: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MaximumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let s = set [1; 2] + let e = (s :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" ] diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index da64962240..e7aca8c23d 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -108,6 +108,20 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Names the exception `f` raises, testing the most specific type first, so a test can +/// check the exception type without depending on .NET's detailed messages. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? NotSupportedException -> "NotSupportedException" + | e -> "Unexpected exception: " + e.Message + let rec sumFirstSeq (zs: seq) (n: int): float = match n with | 0 -> 0. From f25c668946c6dd84e71b813881ad87bbb4f046e4 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Sun, 25 Oct 2026 01:11:23 -0500 Subject: [PATCH 2/6] fix(python): align collection exception types with FSharp.Core List, Seq, Set, Map and range functions raised a plain Exception or a different System exception than FSharp.Core. They now match .NET: reduce, min, max and last on empty input raise ArgumentException, Seq.head on empty input and Seq.item past the end raise ArgumentException, take, splitAt and Seq.skip past the end raise InvalidOperationException, List.skip past the end raises ArgumentOutOfRangeException, a zero range step raises ArgumentException, and the Map and Set enumerators raise InvalidOperationException outside the enumeration. Map's ICollection mutators raise NotSupportedException with FSharp.Core's message. list.Head and list.Tail raise InvalidOperationException while List.head and List.tail keep raising ArgumentException, so Replacements now sends the properties to the FSharpList members. Seq.tail gets its own implementation so it keeps raising ArgumentException. The List messages pick up FSharp.Core's "The input list was empty." from the Global.fs shared with fable-library-ts. --- src/Fable.Transforms/Python/Replacements.fs | 16 ++++---- src/fable-library-py/fable_library/List.fs | 26 +++++++----- src/fable-library-py/fable_library/Map.fs | 12 +++--- src/fable-library-py/fable_library/Range.fs | 2 +- src/fable-library-py/fable_library/Seq.fs | 22 ++++++++-- src/fable-library-py/fable_library/Set.fs | 10 +++-- .../fable_library/seq_native.py | 9 +++-- tests/Python/TestList.fs | 40 +++++++++++++++++++ tests/Python/TestMap.fs | 8 ++++ tests/Python/TestSeq.fs | 39 ++++++++++++++++++ tests/Python/TestSet.fs | 20 ++++++++++ tests/Python/Util.fs | 12 ++++++ 12 files changed, 181 insertions(+), 35 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 248e3266ef..edfa4feb86 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2107,14 +2107,14 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // Use the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: + // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException. + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "list", mangledName, t, [], thisArg = x, ?loc = r) |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] diff --git a/src/fable-library-py/fable_library/List.fs b/src/fable-library-py/fable_library/List.fs index 509cf2d126..9859354847 100644 --- a/src/fable-library-py/fable_library/List.fs +++ b/src/fable-library-py/fable_library/List.fs @@ -48,12 +48,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -177,7 +177,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -185,7 +188,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -201,7 +207,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -596,13 +602,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -738,7 +744,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -758,7 +764,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -824,7 +830,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-py/fable_library/Map.fs b/src/fable-library-py/fable_library/Map.fs index 6416366633..c3368deb0a 100644 --- a/src/fable-library-py/fable_library/Map.fs +++ b/src/fable-library-py/fable_library/Map.fs @@ -593,9 +593,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -900,14 +902,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value diff --git a/src/fable-library-py/fable_library/Range.fs b/src/fable-library-py/fable_library/Range.fs index db1175c507..617f2472a8 100644 --- a/src/fable-library-py/fable_library/Range.fs +++ b/src/fable-library-py/fable_library/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 7991b5b6ad..2063add4f1 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -880,7 +880,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -888,7 +888,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -923,7 +923,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -945,7 +945,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) // `take`/`takeWhile`: no F# impl - Replacements.fs redirects both straight to `seq_native`. diff --git a/src/fable-library-py/fable_library/Set.fs b/src/fable-library-py/fable_library/Set.fs index 7398b1d7de..0e326e35b1 100644 --- a/src/fable-library-py/fable_library/Set.fs +++ b/src/fable-library-py/fable_library/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py index 335ba69ebe..f9a9750ce4 100644 --- a/src/fable-library-py/fable_library/seq_native.py +++ b/src/fable-library-py/fable_library/seq_native.py @@ -26,6 +26,7 @@ from .list import try_item as list_try_item from .option import Option, some, value from .protocols import IEnumerable_1, IEnumerator +from .system import ArgumentException__ctor_Z384F8060, InvalidOperationException__ctor_Z721C83C5 from .types import UNIT, Unit from .util import Enumerator, get_enumerator, to_iterator @@ -165,7 +166,9 @@ def gen() -> Iterator[T]: for _ in range(count): v = next(it, _SENTINEL) if v is _SENTINEL: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'source')") + raise InvalidOperationException__ctor_Z721C83C5( + "The input sequence has an insufficient number of elements." + ) yield v return NativeSeq(gen) @@ -187,7 +190,7 @@ def try_head[T](xs: IEnumerable_1[T]) -> Option[T]: def head[T](xs: IEnumerable_1[T]) -> T: result = try_head(xs) if result is None: - raise Exception("The input sequence was empty. (Parameter 'source')") + raise ArgumentException__ctor_Z384F8060("The input sequence was empty.", "source") return value(result) @@ -213,7 +216,7 @@ def try_item[T](index: int, xs: IEnumerable_1[T]) -> Option[T]: def item[T](index: int, xs: IEnumerable_1[T]) -> T: result = try_item(index, xs) if result is None: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'index')") + raise ArgumentException__ctor_Z384F8060("The input sequence has an insufficient number of elements.", "index") return value(result) diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs index 05badb8fa9..c7db8d1859 100644 --- a/tests/Python/TestList.fs +++ b/tests/Python/TestList.fs @@ -1246,3 +1246,43 @@ let ``test List.collect works II`` () = xs |> List.collect (fun a -> [a.Length]) |> equal [1; 5; 3] + +[] +let ``test List.reduce and List.reduceBack raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test List.min, List.max, List.minBy and List.maxBy raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test List.last, List.head and List.tail raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + +[] +let ``test list.Head and list.Tail raise InvalidOperationException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + +[] +let ``test List.take and List.splitAt raise InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + +[] +let ``test List.skip raises ArgumentOutOfRangeException when the list is too short`` () = + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" diff --git a/tests/Python/TestMap.fs b/tests/Python/TestMap.fs index 4c33e1d710..bee7396796 100644 --- a/tests/Python/TestMap.fs +++ b/tests/Python/TestMap.fs @@ -1,6 +1,7 @@ module Fable.Tests.Maps open System.Collections.Generic +open Fable.Tests.Util open Util.Testing type R = { i: int; s: string } @@ -351,3 +352,10 @@ let ``test Map keys and values work correctly`` () = let myMap = Map [ "a", 10; "b", 20 ] :> IDictionary<_,_> myMap.Keys |> Seq.toList |> equal ["a"; "b"] myMap.Values |> Seq.toList |> equal [10; 20] + +[] +let ``test Map enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (Map [1, "a"] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs index 35a4ff6f8f..78314876de 100644 --- a/tests/Python/TestSeq.fs +++ b/tests/Python/TestSeq.fs @@ -1319,3 +1319,42 @@ let ``test Seq.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test Seq.min, Seq.max, Seq.minBy and Seq.maxBy raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.take 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test range with a zero step raises ArgumentException`` () = + raisedExceptionType (fun () -> [0..0..5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0L..0L..5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0.0..0.0..5.0]) |> equal "ArgumentException" + +[] +let ``test Seq.head and Seq.item raise ArgumentException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.head Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([]: int list)) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([||]: int[])) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 (seq { 1 })) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item -1 (seq { 1 })) |> equal "ArgumentException" diff --git a/tests/Python/TestSet.fs b/tests/Python/TestSet.fs index df67710256..fe5e9956d1 100644 --- a/tests/Python/TestSet.fs +++ b/tests/Python/TestSet.fs @@ -1,5 +1,6 @@ module Fable.Tests.Set +open Fable.Tests.Util open Util.Testing [] @@ -333,3 +334,22 @@ let ``test Comparing large sets works`` () = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + +[] +let ``test Set.minElement and Set.maxElement raise ArgumentException on empty input`` () = + let xs = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + +[] +let ``test Set.intersectMany raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + +[] +let ``test Set enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (set [1] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs index e80a7e9561..e4a1bd2a17 100644 --- a/tests/Python/Util.fs +++ b/tests/Python/Util.fs @@ -106,6 +106,18 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Names the exception `f` raises, testing the most specific type first, so a test can +/// check the exception type without depending on .NET's detailed messages. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | e -> "Unexpected exception: " + e.Message + let Int32Array = [|1;2|] module Float64Array = From c0edb6c32b26ff296157d0cf0c6d46bb22eb9828 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Sun, 25 Oct 2026 01:11:39 -0500 Subject: [PATCH 3/6] fix(dart): align collection exception types with FSharp.Core List, Array, Seq, Set, Map and range functions raised untyped errors (failwith, or Dart's own StateError for Array.reduce and Array.find) or a different typed exception than FSharp.Core does on .NET. They now raise the same exception types: - reduce/min/max on empty inputs, List.last, List.head/tail and Set.minElement/maxElement: ArgumentException - list.Head/list.Tail on an empty list: InvalidOperationException - take/splitAt past the end and Seq.skip: InvalidOperationException (Seq.tail keeps ArgumentException) - List.skip/Array.skip past the end and an out-of-range randomizer: ArgumentOutOfRangeException - Array length mismatches, windowed, permute and a zero range step: ArgumentException - Array.find/findBack/findIndex/findIndexBack/pick: KeyNotFoundException - Map/Set enumerator Current outside the enumeration: InvalidOperationException SR.inputWasEmpty now reads "The input list was empty." like FSharp.Core. --- src/Fable.Transforms/Dart/Replacements.fs | 27 +++++++--- src/fable-library-dart/Array.fs | 63 +++++++++++++---------- src/fable-library-dart/Global.fs | 2 +- src/fable-library-dart/List.fs | 29 +++++++---- src/fable-library-dart/Map.fs | 4 +- src/fable-library-dart/Range.fs | 2 +- src/fable-library-dart/Seq.fs | 24 +++++++-- src/fable-library-dart/Set.fs | 8 +-- tests/Dart/src/ArrayTests.fs | 55 ++++++++++++++++++++ tests/Dart/src/ListTests.fs | 43 ++++++++++++++++ tests/Dart/src/MapTests.fs | 8 +++ tests/Dart/src/SeqTests.fs | 33 ++++++++++++ tests/Dart/src/SetTests.fs | 18 +++++++ tests/Dart/src/Tests.Util.fs | 13 +++++ 14 files changed, 271 insertions(+), 58 deletions(-) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 65a3c7311e..e0c3fe5f56 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1874,14 +1874,25 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // Call the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: + // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall( + com, + "List", + mangledName, + t, + [], + i.SignatureArgTypes, + genArgs = i.GenericArgs, + thisArg = x, + ?loc = r + ) + |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index 447fd3cdb6..c9fdd1480f 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -101,9 +101,10 @@ type Native = static member firstWhere(xs: 'T[], predicate: 'T -> bool) : 'T = jsNative let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // https://stackoverflow.com/a/9113136 let reverseInPlace (xs: 'T[]) : unit = @@ -168,25 +169,25 @@ let map (f: 'T -> 'U) (source: 'T[]) : 'U[] = let mapIndexed2 (f: int -> 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f i source1[i] source2[i]) let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f source1[i] source2[i]) let mapIndexed3 (f: int -> 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f i source1[i] source2[i] source3[i]) let map3 (f: 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f source1[i] source2[i] source3[i]) @@ -344,7 +345,7 @@ let scanBack<'T, 'State> (folder: 'T -> 'State -> 'State) (array: 'T[]) (state: let skip count (array: 'T[]) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then Array.empty @@ -367,7 +368,7 @@ let take count (array: 'T[]) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then Array.empty @@ -423,7 +424,10 @@ let partition (f: 'T -> bool) (source: 'T[]) = Native.asFixed res1, Native.asFixed res2 -let find (predicate: 'T -> bool) (array: 'T[]) : 'T = Native.firstWhere (array, predicate) +let find (predicate: 'T -> bool) (array: 'T[]) : 'T = + match Native.indexWhere (array, predicate) with + | -1 -> indexNotFound () + | index -> array[index] let tryFind (predicate: 'T -> bool) (array: 'T[]) : 'T option = try @@ -546,14 +550,14 @@ let iterateIndexed (action: int -> 'T -> unit) (array: 'T[]) : unit = let iterate2 (action: 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1[i] array2[i] let iterateIndexed2 (action: int -> 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1[i] array2[i] @@ -570,7 +574,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res[j] <- x checkFlags[j] <- 1 @@ -580,7 +584,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let isValid = checkFlags |> forAll ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -678,14 +682,14 @@ let unzip3 (array: ('a * 'b * 'c)[]) : 'a[] * 'b[] * 'c[] = let zip (array1: 'T[]) (array2: 'U[]) : ('T * 'U)[] = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" Native.generate array1.Length (fun i -> array1[i], array2[i]) let zip3 (array1: 'T1[]) (array2: 'T2[]) (array3: 'T3[]) : ('T1 * 'T2 * 'T3)[] = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" Native.generate array1.Length (fun i -> array1[i], array2[i], array3[i]) @@ -708,9 +712,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = Native.asFixed result let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + Native.sublist (array, 0, index), Native.sublist (array, index) // Note that, unlike the `compare` operator, Array.compareWith compares elements first, @@ -822,7 +829,7 @@ let fold2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder acc array1[i] array2[i] @@ -839,7 +846,7 @@ let foldBack2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = array1.Length - 1 downto 0 do acc <- folder array1[i] array2[i] acc @@ -848,12 +855,14 @@ let foldBack2<'T1, 'T2, 'State> let reduce (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = // Dart's native reduce will fail if collection is empty - // if Array.isEmpty array then invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + if Array.isEmpty array then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + Native.reduce reduction array let reduceBack (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = if Array.isEmpty array then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString let mutable i = array.Length - 1 let mutable state = array[i] @@ -884,7 +893,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -970,7 +979,7 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let len = Operators.max 0 (source.Length - windowSize + 1) Native.generate len (fun i -> source[i .. i + windowSize - 1]) @@ -1017,7 +1026,7 @@ let transpose (arrays: 'T[] seq) : 'T[][] = let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" Native.generate lenInner (fun i -> Native.generate len (fun j -> arrays[j][i])) @@ -1138,7 +1147,7 @@ let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = int (r * float (i + 1)) let tmp = xs[i] @@ -1169,7 +1178,7 @@ let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] @@ -1193,7 +1202,7 @@ let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] ) @@ -1221,7 +1230,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = i + int (r * float (len - i)) let tmp = arr[i] diff --git a/src/fable-library-dart/Global.fs b/src/fable-library-dart/Global.fs index 0fcd96d1d0..5da4b222b5 100644 --- a/src/fable-library-dart/Global.fs +++ b/src/fable-library-dart/Global.fs @@ -16,7 +16,7 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." let inputSequenceEmpty = "The input sequence was empty." diff --git a/src/fable-library-dart/List.fs b/src/fable-library-dart/List.fs index d0cd36bb3b..50c0948e56 100644 --- a/src/fable-library-dart/List.fs +++ b/src/fable-library-dart/List.fs @@ -27,14 +27,17 @@ and [] LinkedList<'T>(tail_: 'T list member this.Head: 'T = match this with | :? ConsList<'T> as this -> this.GetHead() - | _ -> failwith "Empty list" + | _ -> invalidOp SR.inputWasEmpty member this.TryHead: 'T option = match this with | :? ConsList<'T> as this -> this.GetHead() |> Some | _ -> None - member _.Tail: LinkedList<'T> = Option.get tail + member _.Tail: LinkedList<'T> = + match tail with + | Some t -> t + | None -> invalidOp SR.inputWasEmpty member _.TryTail: LinkedList<'T> option = tail @@ -159,7 +162,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs with + | :? ConsList<'T> as xs -> xs.GetHead() + | _ -> invalidArg "list" SR.inputWasEmpty let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -167,7 +173,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.TryTail with + | Some t -> t + | None -> invalidArg "list" SR.inputWasEmpty let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -183,7 +192,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -572,13 +581,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -710,7 +719,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -730,7 +739,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -796,7 +805,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-dart/Map.fs b/src/fable-library-dart/Map.fs index 050ec50915..dc03ad26b2 100644 --- a/src/fable-library-dart/Map.fs +++ b/src/fable-library-dart/Map.fs @@ -607,9 +607,9 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/src/fable-library-dart/Range.fs b/src/fable-library-dart/Range.fs index 2e921701bd..75477dbe4e 100644 --- a/src/fable-library-dart/Range.fs +++ b/src/fable-library-dart/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T> (step: 'T) (stop: 'T) (zero: 'T) (add: 'T -> 'T -> let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index 36d5fdfb2f..7e8efe8a1a 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -944,13 +944,13 @@ let reduce (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = let arr = toArray xs if Array.isEmpty arr then - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty Array.reduceBack folder arr @@ -987,7 +987,7 @@ let skip (count: int) (source: seq<'T>) : seq<'T> = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1009,7 +1009,21 @@ let skipWhile (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = ) ) -let tail (xs: seq<'T>) : seq<'T> = skip 1 xs +// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +let tail (xs: seq<'T>) : seq<'T> = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take (count: int) (xs: seq<'T>) : seq<'T> = Enumerator.generateIndexed @@ -1019,7 +1033,7 @@ let take (count: int) (xs: seq<'T>) : seq<'T> = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-dart/Set.fs b/src/fable-library-dart/Set.fs index 6d24568e3b..0381ed456f 100644 --- a/src/fable-library-dart/Set.fs +++ b/src/fable-library-dart/Set.fs @@ -480,12 +480,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -515,9 +515,9 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index 54aaa76232..a2a02efb42 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -1328,6 +1328,61 @@ let tests () = equal c2 -1 equal c3 1 + testCase "Array.reduce and Array.min on an empty array raise ArgumentException" <| fun () -> + let xs: int[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id xs) |> equal "ArgumentException" + + testCase "Array.take and Array.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + + testCase "Array functions on arrays of different lengths raise ArgumentException" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array.find and Array.pick raise KeyNotFoundException when nothing matches" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 5 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array.windowed and Array.permute with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 5) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + + testCase "Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + // testCase "System.Array.Resize works" <| fun () -> // let mutable xs = [|1; 2; 3; 4; 5|] // System.Array.Resize(&xs, 3) diff --git a/tests/Dart/src/ListTests.fs b/tests/Dart/src/ListTests.fs index 58cd87c5df..acdde1fa7d 100644 --- a/tests/Dart/src/ListTests.fs +++ b/tests/Dart/src/ListTests.fs @@ -1063,3 +1063,46 @@ let tests() = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List.reduce and List.min on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List.head, List.tail and List.last on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + + testCase "list.Head and list.Tail on an empty list raise InvalidOperationException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List.take and List.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 5) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [2; 3]]) |> equal "ArgumentException" + + testCase "List random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 1 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" diff --git a/tests/Dart/src/MapTests.fs b/tests/Dart/src/MapTests.fs index 68161e9c95..366f6b9dab 100644 --- a/tests/Dart/src/MapTests.fs +++ b/tests/Dart/src/MapTests.fs @@ -295,3 +295,11 @@ let tests() = |> Map.add { Bar = "a"; Foo = 10 } 2 |> Map.count |> equal 1 + + testCase "Map enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Map.ofList [(1, "a")] :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current.Key |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/SeqTests.fs b/tests/Dart/src/SeqTests.fs index ce53aaf7c8..795c9e297e 100644 --- a/tests/Dart/src/SeqTests.fs +++ b/tests/Dart/src/SeqTests.fs @@ -1163,3 +1163,36 @@ let tests() = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + + testCase "Seq.reduce and Seq.min on an empty sequence raise ArgumentException" <| fun () -> + let xs: int seq = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + + testCase "Seq.take and Seq.skip past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq.tail on an empty sequence raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + Seq.tail [1; 2; 3] |> Seq.toList |> equal [2; 3] + + testCase "Seq functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 5) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|1L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1. .. 0. .. 5.]) |> equal "ArgumentException" diff --git a/tests/Dart/src/SetTests.fs b/tests/Dart/src/SetTests.fs index 21d9ea6fb0..bd1b5c7241 100644 --- a/tests/Dart/src/SetTests.fs +++ b/tests/Dart/src/SetTests.fs @@ -289,3 +289,21 @@ let tests() = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + + testCase "Set.minElement and Set.maxElement on an empty set raise ArgumentException" <| fun () -> + let xs: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + + testCase "Set.intersectMany with no sets raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Set.ofList [1] :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index 4e0754c735..c276aa33fb 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -22,6 +22,19 @@ let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) let throwsAnyError (f: unit -> 'a): unit = Test.expect(f, Test.throwsException) +/// Names the exception `f` raises, testing the most specific type first, so a test can +/// check the exception type without depending on .NET's detailed messages. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | :? System.InvalidOperationException -> "InvalidOperationException" + | :? System.Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | e -> "Unexpected exception: " + e.Message + let private run (f: unit -> 'a) = try f() From d6ac90242c550d5c224c38044db5202a06533bb3 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Sun, 25 Oct 2026 01:07:54 -0500 Subject: [PATCH 4/6] fix(beam): align collection exception types with FSharp.Core Collection functions raised untyped errors, or the wrong exception type, where FSharp.Core raises a typed one, so handlers like `:? InvalidOperationException` never caught them: - Seq.reduce, Seq.reduceBack (and Seq.min/max/minBy/maxBy) on an empty sequence raise ArgumentException instead of InvalidOperationException. - Seq.take and Seq.skip past the end raise InvalidOperationException instead of ArgumentException. Seq.tail gets its own implementation so it keeps raising ArgumentException on an empty sequence. - List.take and Array.take past the end raise InvalidOperationException. - List/Array find, findBack, findIndex, findIndexBack and pick raise KeyNotFoundException. - Map.findKey and Map.pick raise KeyNotFoundException when nothing matches. - The random*By functions raise ArgumentOutOfRangeException for a randomizer outside [0, 1), and ArgumentException for an empty source, a negative count or a sample larger than the source. - A range with a zero step raises ArgumentException. --- src/fable-library-beam/Range.fs | 2 +- src/fable-library-beam/Seq.fs | 25 ++++++++--- src/fable-library-beam/fable_list.erl | 60 ++++++++++++++++++++------- src/fable-library-beam/fable_map.erl | 11 ++++- tests/Beam/ArrayTests.fs | 29 +++++++++++++ tests/Beam/ListTests.fs | 28 +++++++++++++ tests/Beam/MapTests.fs | 6 +++ tests/Beam/SeqTests.fs | 34 +++++++++++++++ tests/Beam/Util.fs | 13 ++++++ 9 files changed, 185 insertions(+), 23 deletions(-) diff --git a/src/fable-library-beam/Range.fs b/src/fable-library-beam/Range.fs index e97846a5af..6d729e8cf7 100644 --- a/src/fable-library-beam/Range.fs +++ b/src/fable-library-beam/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs index bdb4b24110..680067c92f 100644 --- a/src/fable-library-beam/Seq.fs +++ b/src/fable-library-beam/Seq.fs @@ -992,7 +992,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -1000,7 +1000,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -1035,7 +1035,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1057,7 +1057,22 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// Not `skip 1`: on an empty source FSharp.Core's Seq.tail raises ArgumentException, +// where Seq.skip raises InvalidOperationException. +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take count (xs: seq<'T>) = generateIndexed @@ -1067,7 +1082,7 @@ let take count (xs: seq<'T>) = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 15c526e5f8..c30efe27bd 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -225,10 +225,17 @@ sort_by_descending(Fn, Comparer, List) -> sort_with(Fn, List) -> lists:sort(fun(A, B) -> (Fn(A))(B) =< 0 end, List). +%% FSharp.Core's find, findIndex, pick and their Back variants raise KeyNotFoundException. +index_not_found() -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => <<"An index satisfying the predicate was not found in the collection.">> + }). + find(Fn, List) -> case lists:dropwhile(fun(E) -> not Fn(E) end, List) of [H | _] -> H; - [] -> erlang:error(<<"key_not_found">>) + [] -> index_not_found() end. try_find(Fn, List) -> @@ -245,7 +252,7 @@ find_index(Fn, [H | T], I) -> false -> find_index(Fn, T, I + 1) end; find_index(_Fn, [], _I) -> - erlang:error(<<"key_not_found">>). + index_not_found(). try_find_index(Fn, List) -> try_find_index(Fn, List, 0). @@ -580,7 +587,7 @@ map_fold_back(Fn, List, State) -> pick(Fn, List) -> case try_pick(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); V -> V end. @@ -594,7 +601,7 @@ try_pick(Fn, [H | T]) -> find_index_back(Fn, List) -> case try_find_index_back(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); Idx -> Idx end. @@ -673,7 +680,11 @@ get_slice(Lower, Upper, List) -> take(Count, List) -> case erlang:length(List) >= Count of true -> lists:sublist(List, Count); - false -> erlang:error(<<"The input sequence has an insufficient number of elements.">>) + false -> + erlang:error(#{ + exn_type => invalid_operation_exception, + message => <<"The input sequence has an insufficient number of elements.">> + }) end. %% ---- Random functions -------------------------------------------------------- @@ -697,13 +708,13 @@ random_shuffle(Xs) -> -spec random_choice_by(fun((ok) -> float()), list()) -> term(). random_choice_by(_Randomizer, []) -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); random_choice_by(Randomizer, Xs) -> Len = erlang:length(Xs), R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> lists:nth(erlang:trunc(R * Len) + 1, Xs) end. @@ -720,9 +731,9 @@ random_choice(Xs) -> random_choices_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); Count > 0 andalso Xs =:= [] -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); true -> Arr = erlang:list_to_tuple(Xs), Len = erlang:tuple_size(Arr), @@ -736,7 +747,7 @@ choices_loop(Randomizer, Arr, Len, N, Acc) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> E = erlang:element(erlang:trunc(R * Len) + 1, Arr), choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]) @@ -754,14 +765,14 @@ random_choices(Count, Xs) -> random_sample_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); true -> Len = erlang:length(Xs), if Len =:= 0 andalso Count > 0 -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); Count > Len -> - erlang:error(<<"The input sequence has an insufficient number of elements.">>); + invalid_arg(<<"count">>, <<"The input sequence has an insufficient number of elements.">>); true -> sample_loop(Randomizer, erlang:list_to_tuple(Xs), Len, Count, 0) end @@ -782,7 +793,7 @@ sample_loop(Randomizer, Arr, Len, Count, I) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> J = I + erlang:trunc(R * (Len - I)), EI = erlang:element(I + 1, Arr), @@ -798,7 +809,7 @@ shuffle_loop(Randomizer, Arr, I) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> J = erlang:trunc(R * (I + 1)), EI = erlang:element(I + 1, Arr), @@ -806,3 +817,22 @@ shuffle_loop(Randomizer, Arr, I) -> Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), shuffle_loop(Randomizer, Arr2, I - 1) end. + +%% FSharp.Core raises ArgumentOutOfRangeException when the randomizer leaves [0.0, 1.0). +randomizer_out_of_range(R) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => iolist_to_binary( + io_lib:format( + "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", + [R] + ) + ) + }). + +%% The ArgumentException that invalidArg raises, with its message formatted the same way. +invalid_arg(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_exception, + message => <> + }). diff --git a/src/fable-library-beam/fable_map.erl b/src/fable-library-beam/fable_map.erl index d5f257ea4c..5801a8e7b4 100644 --- a/src/fable-library-beam/fable_map.erl +++ b/src/fable-library-beam/fable_map.erl @@ -74,7 +74,7 @@ iterate(Fn, Map) -> find_key(Fn, Map) -> case lists:dropwhile(fun({K, V}) -> not (Fn(K))(V) end, maps:to_list(Map)) of [{K, _} | _] -> K; - [] -> erlang:error(<<"key_not_found">>) + [] -> key_not_found(<<"An index satisfying the predicate was not found in the collection.">>) end. try_find_key(Fn, Map) -> @@ -109,7 +109,7 @@ try_get_value(Key, Map, OutRef) -> pick(Fn, Map) -> case try_pick(Fn, Map) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> key_not_found(<<"The given key was not present in the dictionary.">>); V -> V end. @@ -142,3 +142,10 @@ change(Key, Fn, Map) -> NewV -> maps:put(Key, NewV, Map) end end. + +%% FSharp.Core's Map.findKey and Map.pick raise KeyNotFoundException, each with its own message. +key_not_found(Message) -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => Message + }). diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 9b4544b7d5..8fbd898d6d 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -1343,3 +1343,32 @@ let ``test byte array from literal works`` () = data.[1] |> equal 2uy data.[2] |> equal 3uy data.Length |> equal 3 + +[] +let ``test Array.take raises InvalidOperationException when the array is too short`` () = + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + +[] +let ``test Array.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun _ -> None: int option) [|1|]) |> equal "KeyNotFoundException" + +[] +let ``test Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> -0.1) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Array random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> Array.randomChoice [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices 1 [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 3 [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 1 [||]) |> equal "ArgumentException" diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index 2733453992..280819fb6e 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -906,3 +906,31 @@ let ``test List.range works`` () = [0..2..9] |> List.reduce (+) |> equal 20 + +[] +let ``test List.take raises InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + +[] +let ``test List.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> List.find (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndex (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndexBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.pick (fun _ -> None: int option) [1]) |> equal "KeyNotFoundException" + +[] +let ``test List random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test List random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> List.randomChoice []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices 1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 3 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 1 []) |> equal "ArgumentException" diff --git a/tests/Beam/MapTests.fs b/tests/Beam/MapTests.fs index 68c4fcf07a..a99b869105 100644 --- a/tests/Beam/MapTests.fs +++ b/tests/Beam/MapTests.fs @@ -409,3 +409,9 @@ let ``test Map.toSeq works`` () = let zs = Map.toSeq ys (Seq.item 2 xs |> snd) = (Seq.item 2 zs |> snd) |> equal true + +[] +let ``test Map.findKey and Map.pick raise KeyNotFoundException when nothing matches`` () = + let m = Map.ofList [ (1, "a"); (2, "b") ] + raisedExceptionType (fun () -> Map.findKey (fun k _ -> k > 5) m) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Map.pick (fun _ _ -> None: int option) m) |> equal "KeyNotFoundException" diff --git a/tests/Beam/SeqTests.fs b/tests/Beam/SeqTests.fs index 1e43a350b3..ea637949d3 100644 --- a/tests/Beam/SeqTests.fs +++ b/tests/Beam/SeqTests.fs @@ -1161,3 +1161,37 @@ let ``test Seq.toList on string works`` () = let text = "ABC" let chars = text |> Seq.toList chars |> List.length |> equal 3 + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.reduce (+) Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.min and friends raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.min Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException past the end`` () = + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Range expressions raise ArgumentException for a zero step`` () = + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1L .. 0L .. 5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1.0 .. 0.0 .. 5.0]) |> equal "ArgumentException" diff --git a/tests/Beam/Util.fs b/tests/Beam/Util.fs index 279a109b8c..3c024b87fd 100644 --- a/tests/Beam/Util.fs +++ b/tests/Beam/Util.fs @@ -69,3 +69,16 @@ let throwsAnyError (f: unit -> 'a): unit = let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess + +/// Names the exception `f` raises, testing the most specific type first, so a test can +/// check the exception type without depending on .NET's detailed messages. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | e -> "Unexpected exception: " + e.Message From 593db18988e5e853bef3105b41123f749691d647 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 20:56:51 +0100 Subject: [PATCH 5/6] fix(beam): raise ArgumentException from List.exactlyOne --- src/fable-library-beam/fable_list.erl | 8 +++----- tests/Beam/ListTests.fs | 6 ++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index c30efe27bd..580eb7c9e5 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -225,7 +225,6 @@ sort_by_descending(Fn, Comparer, List) -> sort_with(Fn, List) -> lists:sort(fun(A, B) -> (Fn(A))(B) =< 0 end, List). -%% FSharp.Core's find, findIndex, pick and their Back variants raise KeyNotFoundException. index_not_found() -> erlang:error(#{ exn_type => key_not_found_exception, @@ -394,7 +393,8 @@ try_item(_, _) -> undefined. exactly_one([X]) -> X; -exactly_one(_) -> erlang:error(<<"Sequence contains more than one element">>). +exactly_one([]) -> invalid_arg(<<"source">>, <<"The input sequence was empty.">>); +exactly_one(_) -> invalid_arg(<<"source">>, <<"The input sequence contains more than one element.">>). try_exactly_one([X]) -> fable_option:some(X); try_exactly_one(_) -> undefined. @@ -818,7 +818,6 @@ shuffle_loop(Randomizer, Arr, I) -> shuffle_loop(Randomizer, Arr2, I - 1) end. -%% FSharp.Core raises ArgumentOutOfRangeException when the randomizer leaves [0.0, 1.0). randomizer_out_of_range(R) -> erlang:error(#{ exn_type => argument_out_of_range_exception, @@ -830,9 +829,8 @@ randomizer_out_of_range(R) -> ) }). -%% The ArgumentException that invalidArg raises, with its message formatted the same way. invalid_arg(ParamName, Message) -> erlang:error(#{ exn_type => argument_exception, - message => <> + message => fable_utils:argument_exception_message(Message, ParamName) }). diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index 280819fb6e..498beca6e1 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -911,6 +911,12 @@ let ``test List.range works`` () = let ``test List.take raises InvalidOperationException when the list is too short`` () = raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" +[] +let ``test List.exactlyOne raises ArgumentException unless the list has one element`` () = + raisedExceptionType (fun () -> List.exactlyOne []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exactlyOne [1; 2]) |> equal "ArgumentException" + List.exactlyOne [1] |> equal 1 + [] let ``test List.find and friends raise KeyNotFoundException when nothing matches`` () = raisedExceptionType (fun () -> List.find (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" From 5db945bbaf2447182a3e8bcb3a4a936e7ac791f2 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 20:56:52 +0100 Subject: [PATCH 6/6] chore(all): trim exception comments to one line --- src/Fable.Transforms/Dart/Replacements.fs | 3 +-- src/Fable.Transforms/Python/Replacements.fs | 3 +-- src/Fable.Transforms/Replacements.fs | 3 +-- src/fable-library-beam/Seq.fs | 3 +-- src/fable-library-beam/fable_map.erl | 1 - src/fable-library-dart/Seq.fs | 2 +- src/fable-library-py/fable_library/Seq.fs | 2 +- src/fable-library-ts/Array.fs | 2 +- src/fable-library-ts/Seq.fs | 2 +- tests/Beam/Util.fs | 3 +-- tests/Dart/src/Tests.Util.fs | 3 +-- tests/Js/Main/Util/Util.fs | 3 +-- tests/Python/Util.fs | 3 +-- 13 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index e0c3fe5f56..77bed3bf65 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1874,8 +1874,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Call the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: - // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException | ("get_Head" | "get_Tail"), Some x, _ -> let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index edfa4feb86..559fe0176a 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2107,8 +2107,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: - // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException. + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException | ("get_Head" | "get_Tail"), Some x, _ -> let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 6932952ccd..b96b3ef2f1 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2401,8 +2401,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use the Head and Tail members (instead of Get(ListHead) for example) to check for empty lists: - // they raise InvalidOperationException, while List.head and List.tail raise ArgumentException. + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException // PHP's FSharpList has no such members, so PHP keeps the head/tail functions below. | ("get_Head" | "get_Tail"), Some x, _ when com.Options.Language <> Php -> let mangledName = diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs index 680067c92f..71671d5394 100644 --- a/src/fable-library-beam/Seq.fs +++ b/src/fable-library-beam/Seq.fs @@ -1057,8 +1057,7 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -// Not `skip 1`: on an empty source FSharp.Core's Seq.tail raises ArgumentException, -// where Seq.skip raises InvalidOperationException. +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException let tail (xs: seq<'T>) = mkSeq (fun () -> let e = ofSeq xs diff --git a/src/fable-library-beam/fable_map.erl b/src/fable-library-beam/fable_map.erl index 5801a8e7b4..7ab10c6338 100644 --- a/src/fable-library-beam/fable_map.erl +++ b/src/fable-library-beam/fable_map.erl @@ -143,7 +143,6 @@ change(Key, Fn, Map) -> end end. -%% FSharp.Core's Map.findKey and Map.pick raise KeyNotFoundException, each with its own message. key_not_found(Message) -> erlang:error(#{ exn_type => key_not_found_exception, diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index 7e8efe8a1a..dc22feb87f 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -1009,7 +1009,7 @@ let skipWhile (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = ) ) -// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException let tail (xs: seq<'T>) : seq<'T> = mkSeq (fun () -> let e = ofSeq xs diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 2063add4f1..e15b92cd11 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -945,7 +945,7 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException let tail (xs: seq<'T>) = mkSeq (fun () -> let e = ofSeq xs diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index 522a405ed6..066ecf9e29 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -600,7 +600,7 @@ let forAll predicate (array: 'T[]) = let permute f (array: 'T[]) = let size = array.Length let res = copyImpl array - // Fill the flags: native `every` skips the empty slots of `new Array(size)` + // Native `every` skips the empty slots of `new Array(size)` let checkFlags = fillImpl (allocateArray size) 0 0 size iterateIndexed diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index 6271cd8cd1..fe33447e9a 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -1056,7 +1056,7 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -// Not `skip 1`: on an empty input FSharp.Core's `tail` raises ArgumentException, `skip` InvalidOperationException. +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException let tail (xs: seq<'T>) = mkSeq (fun () -> let e = ofSeq xs diff --git a/tests/Beam/Util.fs b/tests/Beam/Util.fs index 3c024b87fd..28fd76e22c 100644 --- a/tests/Beam/Util.fs +++ b/tests/Beam/Util.fs @@ -70,8 +70,7 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess -/// Names the exception `f` raises, testing the most specific type first, so a test can -/// check the exception type without depending on .NET's detailed messages. +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. let raisedExceptionType (f: unit -> 'a): string = try f () |> ignore diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index c276aa33fb..d821dfb7b0 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -22,8 +22,7 @@ let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) let throwsAnyError (f: unit -> 'a): unit = Test.expect(f, Test.throwsException) -/// Names the exception `f` raises, testing the most specific type first, so a test can -/// check the exception type without depending on .NET's detailed messages. +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. let raisedExceptionType (f: unit -> 'a): string = try f () |> ignore diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index e7aca8c23d..e0c24a8220 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -108,8 +108,7 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess -/// Names the exception `f` raises, testing the most specific type first, so a test can -/// check the exception type without depending on .NET's detailed messages. +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. let raisedExceptionType (f: unit -> 'a): string = try f () |> ignore diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs index e4a1bd2a17..72637b7aa4 100644 --- a/tests/Python/Util.fs +++ b/tests/Python/Util.fs @@ -106,8 +106,7 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess -/// Names the exception `f` raises, testing the most specific type first, so a test can -/// check the exception type without depending on .NET's detailed messages. +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. let raisedExceptionType (f: unit -> 'a): string = try f () |> ignore