const out: string[] = [];
for (let i = 0; i < 3; i++) {
class C { v() { return "c" + i } }
out.push(new C().v());
}
console.log(out.join(",")); // node: c0,c1,c2 perry: c3,c3,c3
Each iteration's class C must close over that iteration's i (a let loop head creates a per-iteration binding). Perry's methods all see the final value.
Mechanism (located during PR #9527, not fixed there)
One RegisterClassCaptures snapshot is taken per class declaration, and it is refreshed at assignments and returns — a loop body has neither, so every iteration's instances share the single stale snapshot.
Proven orthogonal to the ClassId aliasing fix
A non-shadowed control (class Uniq alone in a loop, nothing else by that name) behaves identically before and after PR #9527 — this is capture-snapshot staleness, not identity aliasing. The per-iteration identity half is already correct after #9527 (each iteration's C is a distinct class for instanceof at block scope).
Fix shape
The capture snapshot needs a refresh point at loop-body entry (or per-iteration binding materialization), the same trigger the existing assignment/return refresh uses. Check how per-iteration let closures already handle this for plain arrow functions in a loop — that machinery exists and works; the class-capture path simply never asks it (the recurring sibling pattern).
Verification bar
A fixture demonstrated failing on unfixed main, byte-compared to node: the shape above; a for…of head; methods and field initializers reading the loop variable; an instance stored and called after the loop ends (must keep its own iteration's value); and a var-head control (where all iterations legitimately share one binding — node prints c3,c3,c3 there, pinning that the fix doesn't over-split).
Found by the #9466 investigation (PR #9527).
Each iteration's
class Cmust close over that iteration'si(aletloop head creates a per-iteration binding). Perry's methods all see the final value.Mechanism (located during PR #9527, not fixed there)
One
RegisterClassCapturessnapshot is taken per class declaration, and it is refreshed at assignments and returns — a loop body has neither, so every iteration's instances share the single stale snapshot.Proven orthogonal to the ClassId aliasing fix
A non-shadowed control (
class Uniqalone in a loop, nothing else by that name) behaves identically before and after PR #9527 — this is capture-snapshot staleness, not identity aliasing. The per-iteration identity half is already correct after #9527 (each iteration'sCis a distinct class forinstanceofat block scope).Fix shape
The capture snapshot needs a refresh point at loop-body entry (or per-iteration binding materialization), the same trigger the existing assignment/return refresh uses. Check how per-iteration
letclosures already handle this for plain arrow functions in a loop — that machinery exists and works; the class-capture path simply never asks it (the recurring sibling pattern).Verification bar
A fixture demonstrated failing on unfixed main, byte-compared to node: the shape above; a
for…ofhead; methods and field initializers reading the loop variable; an instance stored and called after the loop ends (must keep its own iteration's value); and avar-head control (where all iterations legitimately share one binding — node printsc3,c3,c3there, pinning that the fix doesn't over-split).Found by the #9466 investigation (PR #9527).