Summary
In the interpreter, an async function* (or async *m() method) whose body contains a C-style for loop with a condition that reads a parameter (or other enclosing-scope variable) produces no values — the loop body never runs. The equivalent while loop works, the equivalent sync generator works, and compiled mode is correct in all cases. The failure is silent (no error, exit 0), which makes it especially dangerous.
Repro
async function* g(n: number) { for (let i = 0; i < n; i++) yield i; }
async function main() { for await (const x of g(3)) console.log(x); }
main();
// interpreter: (no output)
// compiled: 0 1 2 (correct)
Minimal diff matrix (interpreter)
| Variant |
Result |
async gen, for (let i=0;i<n;i++) reading param n |
❌ no output |
async gen, for (let i=0;i<3;i++) literal bound |
✅ works |
async gen, while (i<n) reading param n |
✅ works |
async gen, reads param into a local then yield (no loop) |
✅ works |
sync gen, for (let i=0;i<n;i++) reading param n |
✅ works |
async gen, bare yield a; yield a*2 reading param a |
✅ works |
Compiled mode is correct for every row.
Likely cause
The parser desugars a C-style for into a block-scoped while ({ let i = 0; while (i < n) { … i++; } }). In the interpreter's lazy async-generator coroutine, that extra block scope around the desugared while apparently does not chain back to the function's parameter/closure environment, so the condition reads n as undefined → i < undefined is false → zero iterations. A hand-written while (no wrapping block) works, and the sync generator for works, which points at the async-generator coroutine's environment handling of the for-desugaring block specifically (cf. the lazy-coroutine rewrite around #690/#717/#752).
Scope
Interpreter-only; compiled mode (IL) is unaffected and correct. Discovered while implementing compiled static/async generator methods (#778).
Summary
In the interpreter, an
async function*(orasync *m()method) whose body contains a C-styleforloop with a condition that reads a parameter (or other enclosing-scope variable) produces no values — the loop body never runs. The equivalentwhileloop works, the equivalent sync generator works, and compiled mode is correct in all cases. The failure is silent (no error, exit 0), which makes it especially dangerous.Repro
Minimal diff matrix (interpreter)
for (let i=0;i<n;i++)reading paramnfor (let i=0;i<3;i++)literal boundwhile (i<n)reading paramnyield(no loop)for (let i=0;i<n;i++)reading paramnyield a; yield a*2reading paramaCompiled mode is correct for every row.
Likely cause
The parser desugars a C-style
forinto a block-scopedwhile({ let i = 0; while (i < n) { … i++; } }). In the interpreter's lazy async-generator coroutine, that extra block scope around the desugaredwhileapparently does not chain back to the function's parameter/closure environment, so the condition readsnasundefined→i < undefinedisfalse→ zero iterations. A hand-writtenwhile(no wrapping block) works, and the sync generatorforworks, which points at the async-generator coroutine's environment handling of the for-desugaring block specifically (cf. the lazy-coroutine rewrite around #690/#717/#752).Scope
Interpreter-only; compiled mode (IL) is unaffected and correct. Discovered while implementing compiled static/async generator methods (#778).