Skip to content

A class declared in a loop body captures stale loop variables: methods see the LAST iteration's values (c3,c3,c3 vs node c0,c1,c2) #9528

Description

@proggeramlug
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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions