Skip to content

Engine: closures captured in generator's for-loop/for-of body see wrong value after first yield resume #634

@frostney

Description

@frostney

Summary

Closures created inside the body of a for(;;) or for-of loop that is itself inside a generator lose their per-iteration binding once any yield in the body is resumed. All closures end up reading the same iteration's value (typically iter 0's). Interpreter only — bytecode mode is spec-conformant.

Why

ES2026 §14.7.4.4 requires let/const for-init bindings to pin per-iteration: the textbook fns.push(() => i) case yields [0, 1, 2], not [3, 3, 3]. That works correctly in Goccia without yield in the body. Adding yield does not change the closure-pinning semantics per spec, but in the interpreter it does — the closures end up collapsed onto the same scope.

Reproducer

const obj = {
  *gen() {
    const fns = [];
    for (let i = 0; i < 3; i++) {
      fns.push(() => i);
      yield i;
    }
    return fns.map(f => f());
  },
};
const g = obj.gen();
g.next(); g.next(); g.next();
console.log(g.next().value); // expected [0, 1, 2], actual [0, 0, 0]

Run with --compat-traditional-for-loop. The same pattern with for (const i of [0, 1, 2]) (no compat flag needed) also returns [0, 0, 0] in the interpreter.

Current behavior

All closures captured across the generator's iterations return the same value.

Expected behavior

Each closure returns the iteration's value at capture time, matching what the same code produces without a yield in the body and what the bytecode VM produces.

Scope notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengineTGocciaEngine: language semantics, ECMAScript built-ins, parser, interpreter, bytecode VMspec complianceMismatch against official JavaScript/TypeScript specification

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions