Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
[<return: Struct>]
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 <range> do` to `let inputSequence = <range> in <for-of>`,
/// so the range is bound one level above the enumerator loop matched by `ForOf`.
[<return: Struct>]
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
[<return: Struct>]
Expand Down
11 changes: 11 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions tests/Beam/LoopTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ let ``test for loop with zero iterations works`` () =
count <- count + 1
count |> equal 0

[<Fact>]
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"

[<Fact>]
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

[<Fact>]
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

[<Fact>]
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"

[<Fact>]
let ``test for loop single iteration works`` () =
let mutable value = 0
Expand Down
24 changes: 24 additions & 0 deletions tests/Dart/src/MiscTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions tests/Js/Main/MiscTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>()
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<unit -> 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
Expand Down
28 changes: 28 additions & 0 deletions tests/Python/TestLoops.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,31 @@ let ``test For-loop downto works`` () =

result
|> equal 55

[<Fact>]
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"

[<Fact>]
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

[<Fact>]
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

[<Fact>]
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"
28 changes: 28 additions & 0 deletions tests/Rust/tests/src/ControlFlowTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,34 @@ let ``for_downto loop works`` () =
a <- i + a
a |> equal 57

[<Fact>]
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

[<Fact>]
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

[<Fact>]
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

[<Fact>]
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

[<Fact>]
let ``while loop works`` () =
let mutable i = 0
Expand Down
Loading