Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JS/TS] Fix #4031: Hoist vars locally in for and while loops #4032

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [Python] - Print root module and module function comments (by @alfonsogarciacaro)

### Fixed

* [JS/TS] Fix #4031: Hoist vars locally in for and while loops (@alfonsogarciacaro)

## 5.0.0-alpha.9 - 2025-01-28

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [Python] - Print root module and module function comments (by @alfonsogarciacaro)

### Fixed

* [JS/TS] Fix #4031: Hoist vars locally in for and while loops (@alfonsogarciacaro)

## 5.0.0-alpha.9 - 2025-01-28

### Fixed
Expand Down
56 changes: 33 additions & 23 deletions src/Fable.Transforms/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,27 @@ module Util =
let transformBlock (com: IBabelCompiler) ctx ret expr : BlockStatement =
com.TransformAsStatements(ctx, ret, expr) |> BlockStatement

let transformBlockWithVarHoisting (com: IBabelCompiler) ctx ret expr : BlockStatement =
let declaredVars = ResizeArray()

let ctx =
{ ctx with
HoistVars =
fun ids ->
declaredVars.AddRange(ids)
true
}

let body = transformBlock com ctx ret expr

if declaredVars.Count = 0 then
body
else
let varDeclStatement =
declaredVars |> Seq.map (fun v -> v, None) |> multiVarDeclaration com ctx Let

BlockStatement(Array.append [| varDeclStatement |] body.Body)

let transformTryCatch com ctx r returnStrategy (body, catch, finalizer) =
// try .. catch statements cannot be tail call optimized
let ctx = { ctx with TailCallOpportunity = None }
Expand Down Expand Up @@ -3053,9 +3074,10 @@ module Util =
transformDecisionTreeSuccessAsStatements com ctx returnStrategy idx boundValues

| Fable.WhileLoop(TransformExpr com ctx guard, body, range) ->
[|
Statement.whileStatement (guard, transformBlock com ctx None body, ?loc = range)
|]
// Hoist vars within the loop so if a closure captures the variable
// it doesn't change in the next iteration, see #4031
let body = transformBlockWithVarHoisting com ctx None body
[| Statement.whileStatement (guard, body, ?loc = range) |]

| Fable.ForLoop(var, TransformExpr com ctx start, TransformExpr com ctx limit, body, isUp, range) ->
let op1, op2 =
Expand All @@ -3066,7 +3088,9 @@ module Util =

[|
Statement.forStatement (
transformBlock com ctx None body,
// Hoist vars within the loop so if a closure captures the variable
// it doesn't change in the next iteration, see #4031
transformBlockWithVarHoisting com ctx None body,
VariableDeclaration.variableDeclaration (
Let,
var.Name,
Expand All @@ -3084,26 +3108,21 @@ module Util =
Option.map (fun name -> NamedTailCallOpportunity(com, ctx, name, args) :> ITailCallOpportunity) name

let args = FSharp2Fable.Util.discardUnitArg args
let declaredVars = ResizeArray()
let mutable isTailCallOptimized = false

let ctx =
{ ctx with
TailCallOpportunity = tailcallChance
HoistVars =
fun ids ->
declaredVars.AddRange(ids)
true
OptimizeTailCall = fun () -> isTailCallOptimized <- true
}

let body =
let returnStrategy =
if body.Type = Fable.Unit then
transformBlock com ctx (Some ReturnUnit) body
elif isJsStatement ctx (Option.isSome tailcallChance) body then
transformBlock com ctx (Some Return) body
ReturnUnit
else
transformAsExpr com ctx body |> wrapExprInBlockWithReturn
Return

let body = transformBlockWithVarHoisting com ctx (Some returnStrategy) body

let args, body =
match isTailCallOptimized, tailcallChance with
Expand Down Expand Up @@ -3143,15 +3162,6 @@ module Util =
),
body

let body =
if declaredVars.Count = 0 then
body
else
let varDeclStatement =
declaredVars |> Seq.map (fun v -> v, None) |> multiVarDeclaration com ctx Let

BlockStatement(Array.append [| varDeclStatement |] body.Body)

args |> List.toArray, body

let declareEntryPoint _com _ctx (funcExpr: Expression) =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

function g(a) {
return a + 1;
return (a + 1) | 0;
}

export function f(b) {
return g(b);
return g(b) | 0;
}

44 changes: 44 additions & 0 deletions tests/Js/Main/MiscTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@

let mutable mutableValue = 0

let rec recursive1 = delay (fun () -> recursive2())

Check warning on line 388 in tests/Js/Main/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-javascript (ubuntu-latest)

This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.

Check warning on line 388 in tests/Js/Main/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-javascript (windows-latest)

This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.
and recursive2 =
mutableValue <- 5
fun () -> mutableValue <- mutableValue * 2
Expand Down Expand Up @@ -495,8 +495,52 @@
let dInvoke (d: MyIntDelegate) =
d.Invoke ()

let (>>*) (f: string -> unit) () (x: string) =
f x

type RecordWithMutableFn = { mutable MutableFn: string -> unit }

let buildRecordWithMutableFnWithWhile (xs: RecordWithMutableFn list) =
let mutable items = []
for x in xs do
items <- (x.MutableFn >>* ()) :: items
items

let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) =
let mutable items = []
for i = 0 to xs.Length - 1 do
let x = xs[i]
items <- (x.MutableFn >>* ()) :: items
items
let tests =
testList "Miscellaneous" [
testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031
let mutable output = ""

let xs = [
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "First" s }
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "Second" s }
]
let ys = buildRecordWithMutableFnWithWhile xs

xs |> List.iter (fun x -> x.MutableFn "output")
ys |> List.iter (fun y -> y "output")
System.Text.RegularExpressions.Regex.Replace(output.Trim(), @"\s+", " ")
|> equal "First output Second output Second output First output"

testCase "Hoisted vars don't conflict with for loop" <| fun () -> // see #4031
let mutable output = ""

let xs = [
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "First" s }
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "Second" s }
]
let ys = buildRecordWithMutableFnWithFor xs

xs |> List.iter (fun x -> x.MutableFn "output")
ys |> List.iter (fun y -> y "output")
System.Text.RegularExpressions.Regex.Replace(output.Trim(), @"\s+", " ")
|> equal "First output Second output Second output First output"

testCase "Passing delegate works" <| fun _ -> // #3862
dtest1 dInvoke |> equal 42
Expand Down Expand Up @@ -1103,7 +1147,7 @@
| 2 -> x <- "2"
| 3 | 4 -> x <- "3" // Multiple cases are allowed
// | 5 | 6 as j -> x <- string j // This prevents the optimization
| 4 -> x <- "4" // Unreachable cases are removed

Check warning on line 1150 in tests/Js/Main/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-javascript (ubuntu-latest)

This rule will never be matched

Check warning on line 1150 in tests/Js/Main/MiscTests.fs

View workflow job for this annotation

GitHub Actions / build-javascript (windows-latest)

This rule will never be matched
| _ -> x <- "?"
equal "3" x

Expand Down
Loading