Bug
Found while verifying the fix for #128. A closure created inside a loop body that captures a let/const/class declared inside that same loop body sees the same, final shared value for every closure, instead of the value from its own iteration. This is the classic "let in a for-loop" JS pattern that block-scoping is specifically designed to support, and it's broken for both plain variables and classes.
Confirmed present on main (39ad7e4), independent of #128 (i.e. this isn't something introduced by the #128 fix — it reproduces identically before and after).
Repro 1: plain let
function f() {
let out = [];
for (let i = 0; i < 3; i++) {
let x = i * 10;
out.push(() => x);
}
return out.map(g => g());
}
console.log(f());
Expected: [0, 10, 20] (each iteration's let x is a fresh binding closed over independently).
Actual: [2, 2, 2] (all three closures read the same final slot).
Repro 2: class declared inside the loop body
function f() {
let out = [];
for (let i = 0; i < 3; i++) {
class C { static id() { return i; } }
out.push(() => C.id());
}
return out.map(g => g());
}
console.log(f());
Expected: [0, 1, 2].
Actual: [2, 2, 2].
(Before #128 landed, this repro didn't even get this far — it crashed with TypeError: undefined is not a function because the class name wasn't visible to the nested closure at all. After #128, it runs but returns the wrong per-iteration values, which is what this issue is about.)
Notes
- Sibling (non-loop) blocks reusing the same name are unaffected — e.g.
if (flag) { let x = 1; ... } else { let x = 2; ... } resolves correctly. The bug is specifically about iteration of the same block, not block scoping in general.
- This is likely a significant real-world blocker: "register an event handler / push a closure per loop iteration that captures the loop-scoped variable" is an extremely common JS pattern (was the textbook reason
let was added to the language over var).
- Given the shared-slot behavior, the loop's per-iteration environment (the "CreatePerIterationEnvironment" step in the
for statement spec) doesn't appear to be implemented — each iteration's block-scoped declarations should get a fresh copy in a for (let ...) loop, seeded from the previous iteration's values.
Not filed against noderati; this is a paserati compiler/VM gap.
Bug
Found while verifying the fix for #128. A closure created inside a loop body that captures a
let/const/classdeclared inside that same loop body sees the same, final shared value for every closure, instead of the value from its own iteration. This is the classic "let in a for-loop" JS pattern that block-scoping is specifically designed to support, and it's broken for both plain variables and classes.Confirmed present on
main(39ad7e4), independent of #128 (i.e. this isn't something introduced by the #128 fix — it reproduces identically before and after).Repro 1: plain
letExpected:
[0, 10, 20](each iteration'slet xis a fresh binding closed over independently).Actual:
[2, 2, 2](all three closures read the same final slot).Repro 2: class declared inside the loop body
Expected:
[0, 1, 2].Actual:
[2, 2, 2].(Before #128 landed, this repro didn't even get this far — it crashed with
TypeError: undefined is not a functionbecause the class name wasn't visible to the nested closure at all. After #128, it runs but returns the wrong per-iteration values, which is what this issue is about.)Notes
if (flag) { let x = 1; ... } else { let x = 2; ... }resolves correctly. The bug is specifically about iteration of the same block, not block scoping in general.letwas added to the language overvar).forstatement spec) doesn't appear to be implemented — each iteration's block-scoped declarations should get a fresh copy in afor (let ...)loop, seeded from the previous iteration's values.Not filed against noderati; this is a paserati compiler/VM gap.