Skip to content

Commit 1e91221

Browse files
[JS/TS] Fix #4031: Hoist vars locally in for and while loops (#4032)
* Fix #4031: Hoist vars locally in for/while loops * update changlog --------- Co-authored-by: Maxime Mangel <[email protected]>
1 parent 38fdf45 commit 1e91221

File tree

5 files changed

+81
-26
lines changed

5 files changed

+81
-26
lines changed

src/Fable.Cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
* [JS/TS] - Fix anonymous record printing (#4029) (by @alfonsogarciacaro)
1919
* [Python] - Fix #3998: PhysicalEquality (by @alfonsogarciacaro)
20+
* [JS/TS] Fix #4031: Hoist vars locally in for and while loops (@alfonsogarciacaro)
2021

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

src/Fable.Compiler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
* [JS/TS] - Fix anonymous record printing (#4029) (by @alfonsogarciacaro)
1919
* [Python] - Fix #3998: PhysicalEquality (by @alfonsogarciacaro)
20+
* [JS/TS] Fix #4031: Hoist vars locally in for and while loops (@alfonsogarciacaro)
2021

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

src/Fable.Transforms/Fable2Babel.fs

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,6 +2208,27 @@ module Util =
22082208
let transformBlock (com: IBabelCompiler) ctx ret expr : BlockStatement =
22092209
com.TransformAsStatements(ctx, ret, expr) |> BlockStatement
22102210

2211+
let transformBlockWithVarHoisting (com: IBabelCompiler) ctx ret expr : BlockStatement =
2212+
let declaredVars = ResizeArray()
2213+
2214+
let ctx =
2215+
{ ctx with
2216+
HoistVars =
2217+
fun ids ->
2218+
declaredVars.AddRange(ids)
2219+
true
2220+
}
2221+
2222+
let body = transformBlock com ctx ret expr
2223+
2224+
if declaredVars.Count = 0 then
2225+
body
2226+
else
2227+
let varDeclStatement =
2228+
declaredVars |> Seq.map (fun v -> v, None) |> multiVarDeclaration com ctx Let
2229+
2230+
BlockStatement(Array.append [| varDeclStatement |] body.Body)
2231+
22112232
let transformTryCatch com ctx r returnStrategy (body, catch, finalizer) =
22122233
// try .. catch statements cannot be tail call optimized
22132234
let ctx = { ctx with TailCallOpportunity = None }
@@ -3053,9 +3074,10 @@ module Util =
30533074
transformDecisionTreeSuccessAsStatements com ctx returnStrategy idx boundValues
30543075

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

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

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

30863110
let args = FSharp2Fable.Util.discardUnitArg args
3087-
let declaredVars = ResizeArray()
30883111
let mutable isTailCallOptimized = false
30893112

30903113
let ctx =
30913114
{ ctx with
30923115
TailCallOpportunity = tailcallChance
3093-
HoistVars =
3094-
fun ids ->
3095-
declaredVars.AddRange(ids)
3096-
true
30973116
OptimizeTailCall = fun () -> isTailCallOptimized <- true
30983117
}
30993118

3100-
let body =
3119+
let returnStrategy =
31013120
if body.Type = Fable.Unit then
3102-
transformBlock com ctx (Some ReturnUnit) body
3103-
elif isJsStatement ctx (Option.isSome tailcallChance) body then
3104-
transformBlock com ctx (Some Return) body
3121+
ReturnUnit
31053122
else
3106-
transformAsExpr com ctx body |> wrapExprInBlockWithReturn
3123+
Return
3124+
3125+
let body = transformBlockWithVarHoisting com ctx (Some returnStrategy) body
31073126

31083127
let args, body =
31093128
match isTailCallOptimized, tailcallChance with
@@ -3143,15 +3162,6 @@ module Util =
31433162
),
31443163
body
31453164

3146-
let body =
3147-
if declaredVars.Count = 0 then
3148-
body
3149-
else
3150-
let varDeclStatement =
3151-
declaredVars |> Seq.map (fun v -> v, None) |> multiVarDeclaration com ctx Let
3152-
3153-
BlockStatement(Array.append [| varDeclStatement |] body.Body)
3154-
31553165
args |> List.toArray, body
31563166

31573167
let declareEntryPoint _com _ctx (funcExpr: Expression) =
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11

22
function g(a) {
3-
return a + 1;
3+
return (a + 1) | 0;
44
}
55

66
export function f(b) {
7-
return g(b);
7+
return g(b) | 0;
88
}
9-

tests/Js/Main/MiscTests.fs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,52 @@ let dtest2 (f: MyIntDelegate -> int) =
495495
let dInvoke (d: MyIntDelegate) =
496496
d.Invoke ()
497497

498+
let (>>*) (f: string -> unit) () (x: string) =
499+
f x
500+
501+
type RecordWithMutableFn = { mutable MutableFn: string -> unit }
502+
503+
let buildRecordWithMutableFnWithWhile (xs: RecordWithMutableFn list) =
504+
let mutable items = []
505+
for x in xs do
506+
items <- (x.MutableFn >>* ()) :: items
507+
items
508+
509+
let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) =
510+
let mutable items = []
511+
for i = 0 to xs.Length - 1 do
512+
let x = xs[i]
513+
items <- (x.MutableFn >>* ()) :: items
514+
items
498515
let tests =
499516
testList "Miscellaneous" [
517+
testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031
518+
let mutable output = ""
519+
520+
let xs = [
521+
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "First" s }
522+
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "Second" s }
523+
]
524+
let ys = buildRecordWithMutableFnWithWhile xs
525+
526+
xs |> List.iter (fun x -> x.MutableFn "output")
527+
ys |> List.iter (fun y -> y "output")
528+
System.Text.RegularExpressions.Regex.Replace(output.Trim(), @"\s+", " ")
529+
|> equal "First output Second output Second output First output"
530+
531+
testCase "Hoisted vars don't conflict with for loop" <| fun () -> // see #4031
532+
let mutable output = ""
533+
534+
let xs = [
535+
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "First" s }
536+
{ MutableFn = fun s -> output <- sprintf "%s\n%s %s" output "Second" s }
537+
]
538+
let ys = buildRecordWithMutableFnWithFor xs
539+
540+
xs |> List.iter (fun x -> x.MutableFn "output")
541+
ys |> List.iter (fun y -> y "output")
542+
System.Text.RegularExpressions.Regex.Replace(output.Trim(), @"\s+", " ")
543+
|> equal "First output Second output Second output First output"
500544

501545
testCase "Passing delegate works" <| fun _ -> // #3862
502546
dtest1 dInvoke |> equal 42

0 commit comments

Comments
 (0)