馃捇
How are you using Babel?
babel-loader (webpack)
Input code
REPL reproduction
for (let i = 0, f = () => i; i < 1;) {
i = 42;
console.log(f());
}
Configuration file name
No response
Configuration
{
"plugins": ["@babel/plugin-transform-block-scoping"]
}
Current and expected behavior
Babel changes the observable runtime behavior of this program.
The original JavaScript prints 0.
However, @babel/plugin-transform-block-scoping transforms it into:
for (var i = 0, f = () => i; i < 1;) {
i = 42;
console.log(f());
}
The transformed program prints 42.
The closure f is created while evaluating the lexical for initializer and captures the initial i binding.
Before executing the first iteration, the ECMAScript for semantics create a new per-iteration binding for i. Therefore, the assignment i = 42 in the loop body updates the per-iteration binding, while f must continue to observe the initial binding whose value is 0.
Lowering both bindings to the same var i loses this distinction. As a result, the transform silently changes valid JavaScript semantics.
Environment
- Babel version(s): 8.0.4; also reproduces with 7.29.1
- Plugin: @babel/plugin-transform-block-scoping
- Node: 24.14.0
- npm: 11.11.0
- OS: macOS
- Monorepo: no
Possible solution
The loop-binding capture analysis should also account for closures created in a ForStatement initializer.
Currently, references outside the loop body appear to be discarded even when they occur inside a closure. Consequently, the reference to i in f = () => i is not treated as requiring preservation of the lexical environment.
The transform needs to preserve the initializer environment separately from the per-iteration environment when lowering the lexical declaration to var.
Additional context
No response
馃捇
How are you using Babel?
babel-loader (webpack)
Input code
REPL reproduction
Configuration file name
No response
Configuration
{ "plugins": ["@babel/plugin-transform-block-scoping"] }Current and expected behavior
Babel changes the observable runtime behavior of this program.
The original JavaScript prints
0.However, @babel/plugin-transform-block-scoping transforms it into:
The transformed program prints
42.The closure
fis created while evaluating the lexical for initializer and captures the initialibinding.Before executing the first iteration, the ECMAScript for semantics create a new per-iteration binding for
i. Therefore, the assignmenti = 42in the loop body updates the per-iteration binding, whilefmust continue to observe the initial binding whose value is0.Lowering both bindings to the same var
iloses this distinction. As a result, the transform silently changes valid JavaScript semantics.Environment
Possible solution
The loop-binding capture analysis should also account for closures created in a
ForStatementinitializer.Currently, references outside the loop body appear to be discarded even when they occur inside a closure. Consequently, the reference to
iinf = () => iis not treated as requiring preservation of the lexical environment.The transform needs to preserve the initializer environment separately from the per-iteration environment when lowering the lexical declaration to
var.Additional context
No response