Repository navigation
Generator never suspends on a yield in a loop condition/update when the loop is nested in if/try/switch/label #10419
Copy link
Copy link
Closed
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. #5933 made a yield in a loop's condition or update work when the loop is a
direct statement of the generator body. The same loop nested inside if, try, switch or a labeled statement is
not split into resume states, so the yield never suspends and the generator yields nothing.
Reproduction
main.ts:
function* noIf() {
for (let t = 2; t >= 0 && ((yield t), t !== 0); ) t = t - 1;
}
function* forInIf(n: number) {
if (n) for (let t = 2; t >= 0 && ((yield t), t !== 0); ) t = t - 1;
}
function* whileInIf(n: number) {
let t = 2;
if (n) while (((yield t), t > 0)) t = t - 1;
}
function* forInTry() {
try { for (let t = 2; ((yield t), t > 0); ) t = t - 1; } finally {}
}
console.log([...noIf()]);
console.log([...forInIf(1)]);
console.log([...whileInIf(1)]);
console.log([...forInTry()]);node main.ts
perry compile main.ts -o main && ./mainExpected (Node 26.5.1)
[ 2, 1, 0 ]
[ 2, 1, 0 ]
[ 2, 1, 0 ]
[ 2, 1, 0 ]
Actual (Perry)
Same result with and without PERRY_NO_AUTO_OPTIMIZE=1:
[ 2, 1, 0 ]
[]
[]
[]
Impact
- lru-cache 11.5.2: Perry resolves
import+nodetodist/esm/node/index.min.js. The esbuild-minified private
generators*#A(indexes) and*#z(rindexes) have exactly this shape:
*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#a);)t=this.#u[t]}.
All iteration APIs are empty:keys,values,entries,rkeys,forEach,for...of,dump.clear()
also skips disposing the remaining entries. The non-minifieddist/esm/node/index.jsmatches Node. - Minifiers routinely fold a loop body's first statement into the loop test and drop braces around
if (x) for (...),
so any minified generator may hit this. - Sent values are lost the same way:
if (n) { while ((v = yield count) !== "stop" && count < 5) count++; } return count;
finishes with{"done":true}(valueundefined). Node gives{"value":2,"done":true}. With nocountbound, the
same loop never ends in Perry and allocates without limit.
Notes
Results (.js and .ts; [...g(1)] unless noted):
| shape | Perry |
|---|---|
for (...; <yield in test>; ) / while (<yield in test>) / for (...; ...; t = (yield t, t - 1)) directly in the generator body |
OK |
same loop inside a nested for body, or inside a bare block { ... } |
OK |
same loop inside if (n) (braced or not), or the else branch |
[] |
if (n) containing do { } while ((yield t), t > 0) |
[] |
loop inside try { } finally { } |
[] |
loop inside switch case |
[] |
labeled loop outer: for (...; (yield t), t > 0; ), with or without an enclosing if |
[] |
if (n) for (let t = 0; t < 3; t++) yield t (yield in the body) |
OK |
async function* with if (n) for (...; ((yield t), t > 0); ) |
[] (for-await) |
async function with if (n) while (await p) |
OK |
Cause (verified by reading the code; the fix direction is inferred):
- The per-iteration rewrite of a yielding loop header exists only as arms of
linearize_body:Whileatcrates/perry-transform/src/generator/linearize.rs:686DoWhileatlinearize.rs:729For(condition/update) atlinearize.rs:769
- A compound statement is only descended into for linearization when
body_contains_yieldsays it contains a
suspend point:Ifatlinearize.rs:1351-1356Tryatlinearize.rs:1126-1132Labeledatlinearize.rs:1594Switchatlinearize.rs:1660-1665
body_contains_yield(crates/perry-transform/src/generator/break_continue.rs:276-371) inspects only loop bodies
(While:326,DoWhile:332,For:338), never a loop's condition or update expression. So anifwhose
onlyyieldis in a nested loop header is treated as yield-free and emitted inline. The residualExpr::Yielddoes
not suspend: the loop runs to completion and yields nothing.- Possible fix: make
body_contains_yieldalso checkWhile/DoWhileconditions andForcondition/update with
hoist_yields::expr_contains_yield.
Related: closed #5933 (await/yield in loop condition or update position evaluated once). Its fix covered only the
top-level loop arms.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Activity
proggeramlug commentedon Sep 17, 2026
Fixed on
mainby PR #10537, landed in merge train #10559 (v0.5.1592) — the train closes its source PRs instead of merging them, so theFixes #10419keyword in #10537 never evaluated. Changelog fragmentchangelog.d/10537-*.mdis onmain.