diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index b83489bc1f..66d15d3596 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2676,10 +2676,26 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "GetArray", _, [ ar; idx ] -> getExpr r t ar idx |> Some | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // Python slicing counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.LibCall(com, "util", "slice_index", Int32.Number, [ index ], ?loc = r) + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> Helper.GlobalCall("len", t, [ ar ], [ t ], ?loc = r) - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index b80a88f6e4..9c640fc58c 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3096,10 +3096,26 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "GetArray", _, [ ar; idx ] -> arrayGetItem com r t [ idx; ar ] (i.SignatureArgTypes |> List.rev) | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // JS slice counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.GlobalCall("Math", Int32.Number, [ index; makeIntConst 0 ], memb = "max") + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> getExpr None (Int32.Number) ar (makeStrConst "length") - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index 7bb0f065eb..eda490d628 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -762,6 +762,11 @@ def random_shared() -> random.Random: return _shared_random +def slice_index(index: int) -> int: + """Clamp a slice bound to 0, as .NET does where Python would count from the end.""" + return index if index > 0 else 0 + + def random_int(rand: random.Random, a: int, b: int) -> int: if a == b: return int32(a) @@ -990,6 +995,7 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "random_shared", "range", "round", + "slice_index", "to_iterable", "unescape_data_string", ] diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 533950180c..2fd87be533 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -230,6 +230,21 @@ let tests = a.[3..3] <- [|999|] a |> equal [|999; 2; 3; 999; 5; 6|] + testCase "Array slices clamp the bounds like .NET" <| fun () -> // See #5164 + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + testCase "Array literals work" <| fun () -> let x = [| 1; 2; 3; 4; 5 |] equal 5 x.Length diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index b32397991e..774afb888d 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -694,6 +694,17 @@ let tests = testList "Strings" [ sprintf "%s" s.[2..] |> equal "t and dog" sprintf "%s" s.[..8] |> equal "cat and d" + testCase "String slices clamp the bounds like .NET" <| fun () -> // See #5164 + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab" + testCase "String.Format works" <| fun () -> let arg1, arg2, arg3 = "F#", "Fable", "Babel" String.Format(CultureInfo.InvariantCulture, "{2} is to {1} what {1} is to {0}", arg1, arg2, arg3) diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index 7333a46076..1d85ae5af3 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -248,6 +248,22 @@ let ``test Array slice with non-numeric arrays work`` () = equal xs.[2] "X" equal xs.[3] "D" +[] +let ``test Array slices clamp the bounds like .NET`` () = + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + [] let ``test Array literals work`` () = let x = [| 1; 2; 3; 4; 5 |] diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index 6bc2e2c975..95ea6e9ed3 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -1215,3 +1215,15 @@ let ``test unicode categories`` () = Char.IsLetterOrDigit c |> ignore ) #endif + +[] +let ``test String slices clamp the bounds like .NET`` () = + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab"