From 227090afcf61d02900976b81339eb32d0703714f Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 23 Jul 2026 13:34:38 +0200 Subject: [PATCH] =?UTF-8?q?perf(all):=20lower=20constant=20=C2=B11=20step?= =?UTF-8?q?=20ranges=20to=20counted=20for-loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Fable.Transforms/FSharp2Fable.Util.fs | 33 +++++++++++++++ src/Fable.Transforms/FSharp2Fable.fs | 11 +++++ tests/Beam/LoopTests.fs | 28 +++++++++++++ tests/Dart/src/MiscTests.fs | 24 +++++++++++ tests/Js/Main/MiscTests.fs | 51 +++++++++++++++++++++++ tests/Python/TestLoops.fs | 28 +++++++++++++ tests/Rust/tests/src/ControlFlowTests.fs | 28 +++++++++++++ 7 files changed, 203 insertions(+) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index ed89a7689b..b030c9cd89 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1251,6 +1251,39 @@ module Patterns = ValueSome(ident, value, body) | _ -> ValueNone + /// Matches an `int` range operator with a compile-time-constant step of +1 or -1, + /// i.e. `start .. 1 .. stop` or `start .. -1 .. stop`. Any coercion to `seq` is stripped. + /// Only ±1 int32 steps are matched, so the range lowers to a plain counted `for` + /// (`Fable.ForLoop`), producing the same output as the `to`/`downto` forms. + [] + let (|ConstStepIntRange|_|) (expr: FSharpExpr) = + let rec stripCoerce = + function + | Coerce(_, inner) -> stripCoerce inner + | e -> e + + match stripCoerce expr with + | Call(None, meth, _, _, [ start; Const((:? int as step), _); stop ]) when + meth.CompiledName = "op_RangeStep" && (step = 1 || step = -1) + -> + ValueSome(start, step, stop) + | _ -> ValueNone + + /// A `for x in start .. ±1 .. stop do` loop, which F# does not lower to a fast + /// counted loop (unlike `to`/`downto`) and would otherwise allocate a range + /// sequence + enumerator. Yields the loop variable, bounds, step and body. + /// + /// F# desugars `for i in do` to `let inputSequence = in `, + /// so the range is bound one level above the enumerator loop matched by `ForOf`. + [] + let (|ForOfConstStepRange|_|) = + function + | Let((seqVar, ConstStepIntRange(start, step, stop), _), ForOf(ident, Value enumeratedVar, body)) when + seqVar.Equals(enumeratedVar) + -> + ValueSome(ident, start, step, stop, body) + | _ -> ValueNone + /// This matches the boilerplate generated for TryGetValue/TryParse/DivRem (see #154, or #1744) /// where the F# compiler automatically passes a byref arg and returns it as a tuple [] diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 309da446ac..fa16aa7ff1 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -841,6 +841,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | Dart -> return Fable.Value(Fable.Null typ, r) | _ -> return Replacements.Api.defaultof com ctx r typ + // `for i in start .. ±1 .. stop do` does not get F#'s fast counted-loop + // lowering (only `to`/`downto` do), so it would allocate a range sequence + + // enumerator. Emit a plain `Fable.ForLoop` instead, as `to`/`downto` produce. + | ForOfConstStepRange(var, startExpr, step, stopExpr, bodyExpr) -> + let r = makeRangeFrom fsExpr + let! start = transformExpr com ctx [] startExpr + let! limit = transformExpr com ctx [] stopExpr + let ctx, ident = putIdentInScope com ctx var None + let! body = transformExpr com ctx [] bodyExpr + return makeForLoop r (step = 1) ident start limit body + | FSharpExprPatterns.Let((var, value, _), body) -> match value with diff --git a/tests/Beam/LoopTests.fs b/tests/Beam/LoopTests.fs index 46123f5e6e..24b19fea84 100644 --- a/tests/Beam/LoopTests.fs +++ b/tests/Beam/LoopTests.fs @@ -24,6 +24,34 @@ let ``test for loop with zero iterations works`` () = count <- count + 1 count |> equal 0 +[] +let ``test for-in descending range with -1 step works`` () = + let mutable result = "" + for i in 3 .. -1 .. 1 do + result <- result + string i + result |> equal "321" + +[] +let ``test for-in ascending range with 1 step works`` () = + let mutable sum = 0 + for i in 1 .. 1 .. 5 do + sum <- sum + i + sum |> equal 15 + +[] +let ``test for-in const-step range with zero iterations works`` () = + let mutable count = 0 + for _i in 0 .. -1 .. 5 do + count <- count + 1 + count |> equal 0 + +[] +let ``test for-in step range other than one still works`` () = + let mutable result = "" + for i in 9 .. -2 .. 0 do + result <- result + string i + result |> equal "97531" + [] let ``test for loop single iteration works`` () = let mutable value = 0 diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index 92e77e7f20..c6278ff2ec 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -69,6 +69,30 @@ let tests() = increase x x.Value |> equal 6 + testCase "for-in descending range with -1 step works" <| fun () -> + let mutable result = "" + for i in 5 .. -1 .. 1 do + result <- result + string i + result |> equal "54321" + + testCase "for-in ascending range with 1 step works" <| fun () -> + let mutable sum = 0 + for i in 1 .. 1 .. 5 do + sum <- sum + i + sum |> equal 15 + + testCase "for-in const-step range with zero iterations works" <| fun () -> + let mutable count = 0 + for _i in 0 .. -1 .. 5 do + count <- count + 1 + count |> equal 0 + + testCase "for-in step range other than one still works" <| fun () -> + let mutable result = "" + for i in 9 .. -2 .. 0 do + result <- result + string i + result |> equal "97531" + testCase "Custom computation expressions work" <| fun () -> execMaybe 5 |> equal (Some 23) execMaybe 99 |> equal None diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 1ec3220d45..41a54fcd46 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -688,6 +688,57 @@ let tests = y <- y + (string i) equal "54321" y + testCase "for in descending range with -1 step works" <| fun () -> + let mutable y = "" + for i in 5 .. -1 .. 1 do + y <- y + (string i) + equal "54321" y + + testCase "for in ascending range with 1 step works" <| fun () -> + let mutable x = "" + for i in 1 .. 1 .. 5 do + x <- x + (string i) + equal "12345" x + + testCase "for in const-step range handles empty and single-element ranges" <| fun () -> + // descending range that never runs (start below stop) + let mutable a = 0 + for i in 0 .. -1 .. 5 do + a <- a + i + equal 0 a + // ascending range that never runs (start above stop) + let mutable b = 0 + for i in 5 .. 1 .. 3 do + b <- b + i + equal 0 b + // single-element ranges + let mutable c = 0 + for i in 3 .. -1 .. 3 do + c <- c + i + equal 3 c + + testCase "for in const-step range with negative bounds works" <| fun () -> + let acc = ResizeArray() + for i in -2 .. -1 .. -6 do + acc.Add i + equal [| -2; -3; -4; -5; -6 |] (acc.ToArray()) + + testCase "for in step range other than 1 still works" <| fun () -> + let mutable a = "" + for i in 9 .. -2 .. 0 do + a <- a + (string i) + equal "97531" a + let mutable b = "" + for i in 0 .. 2 .. 9 do + b <- b + (string i) + equal "02468" b + + testCase "Captured loop variable in const-step range keeps its value" <| fun () -> + let fns = ResizeArray int>() + for i in 3 .. -1 .. 1 do + fns.Add(fun () -> i) + fns |> Seq.map (fun f -> f ()) |> Seq.toArray |> equal [| 3; 2; 1 |] + testCase "Self references in constructors work" <| fun () -> // See #124 let t = Test(5) equal 12 t.Value diff --git a/tests/Python/TestLoops.fs b/tests/Python/TestLoops.fs index 2e2d153e25..4305d08977 100644 --- a/tests/Python/TestLoops.fs +++ b/tests/Python/TestLoops.fs @@ -33,3 +33,31 @@ let ``test For-loop downto works`` () = result |> equal 55 + +[] +let ``test for-in descending range with -1 step works`` () = + let mutable result = "" + for i in 5 .. -1 .. 1 do + result <- result + string i + result |> equal "54321" + +[] +let ``test for-in ascending range with 1 step works`` () = + let mutable result = 0 + for i in 1 .. 1 .. 5 do + result <- result + i + result |> equal 15 + +[] +let ``test for-in const-step range with zero iterations works`` () = + let mutable count = 0 + for _i in 0 .. -1 .. 5 do + count <- count + 1 + count |> equal 0 + +[] +let ``test for-in step range other than one still works`` () = + let mutable result = "" + for i in 9 .. -2 .. 0 do + result <- result + string i + result |> equal "97531" diff --git a/tests/Rust/tests/src/ControlFlowTests.fs b/tests/Rust/tests/src/ControlFlowTests.fs index f906be25d4..a66a6bce17 100644 --- a/tests/Rust/tests/src/ControlFlowTests.fs +++ b/tests/Rust/tests/src/ControlFlowTests.fs @@ -58,6 +58,34 @@ let ``for_downto loop works`` () = a <- i + a a |> equal 57 +[] +let ``for_in descending range with -1 step works`` () = + let mutable a = 42 + for i in 5 .. -1 .. 0 do + a <- i + a + a |> equal 57 + +[] +let ``for_in ascending range with 1 step works`` () = + let mutable a = 42 + for i in 0 .. 1 .. 5 do + a <- i + a + a |> equal 57 + +[] +let ``for_in const-step range with zero iterations works`` () = + let mutable count = 0 + for _i in 0 .. -1 .. 5 do + count <- count + 1 + count |> equal 0 + +[] +let ``for_in step range other than one still works`` () = + let mutable a = 0 + for i in 9 .. -2 .. 0 do + a <- a + i + a |> equal 25 + [] let ``while loop works`` () = let mutable i = 0