diff --git a/changelog.d/10537-generator-loop-header-yield.md b/changelog.d/10537-generator-loop-header-yield.md new file mode 100644 index 0000000000..cde65a7144 --- /dev/null +++ b/changelog.d/10537-generator-loop-header-yield.md @@ -0,0 +1,37 @@ +### Fixed + +- Generators now suspend on a `yield` in a loop header at any nesting depth + (#10419). A `yield` in a `while`/`do…while` condition or a `for` + condition/update was split into resume states only when the loop was a direct + statement of the generator body (#5933). Inside `if`/`else`, `try`/`catch`/ + `finally`, a `switch` case, a label, or another loop, the residual yield never + suspended: `[...g()]` was empty, a sent-value loop such as + `while ((v = yield n) !== "stop")` never terminated, and `.return()`/`.throw()` + found a finished generator. Minifiers emit exactly this shape — lru-cache + 11.5.2's `dist/esm/node/index.min.js` iterators (`keys`, `values`, `entries`, + `rkeys`, `forEach`, `for…of`, `dump`) returned nothing and `clear()` skipped + disposing entries. + + Root cause: the linearizer only descends into a compound statement when + `body_contains_yield` reports a suspend point, and that check inspected loop + bodies but never loop headers, so the `if`/`try`/`switch`/label/loop around a + header-yield loop was emitted inline and the loop's per-iteration header arms + never ran. `body_contains_yield` now checks while/do-while conditions and for + conditions/updates. Three header positions the #5933 arms never covered, even + at top level, are fixed alongside: a for-init's own top-level yield + (`for (let t = yield x; …)`, `for (yield x; …)`, and an async function's + `for (let x = await p; …)` after the await→yield rewrite) is hoisted ahead of + the loop; a yielding update the header arm keeps in place (a `continue` inside + `try`/`finally`) is linearized in the loop's update state; and in an + `async function*` a header yield's operand is awaited like every other yield + operand (a `yield promise` in a loop condition delivered the promise itself). + + Validation: `test_gap_10419_generator_loop_header_yield` (5 header positions × + 12 containers, sent values, `return()`/`throw()` mid-loop, labeled continue, + async generators with `for await`, plain-async for-init await) matches Node + byte-for-byte and differs on the pre-fix compiler; `perry-transform` unit tests + cover detection, the init hoist, residual-yield-free output and the async + operand await. lru-cache 11.5.2's default entry now matches Node on the audit + script (0 diff lines, was 14). Emitted LLVM IR for generators without header + yields (and for top-level header yields) is byte-identical to before, so their + instruction counts are unchanged. diff --git a/crates/perry-transform/src/generator/break_continue.rs b/crates/perry-transform/src/generator/break_continue.rs index 0536466a39..9f8418700b 100644 --- a/crates/perry-transform/src/generator/break_continue.rs +++ b/crates/perry-transform/src/generator/break_continue.rs @@ -323,19 +323,45 @@ pub fn body_contains_yield(stmts: &[Stmt]) -> bool { } } } - Stmt::While { body, .. } if body_contains_yield(body) => { + // A yield in a loop HEADER (while / do-while condition, for + // condition / update) suspends too (#10419). The linearizer's + // header arms split such a loop into per-iteration states, but + // they only run if every enclosing `if` / `try` / `switch` / + // label / loop is linearized as well — which is decided here. + // Checking only loop bodies left `if (n) while ((yield t), t > 0)` + // emitted inline: its residual `Expr::Yield` never suspended and + // the generator yielded nothing (minified lru-cache iterators). + Stmt::While { condition, body } + if super::hoist_yields::expr_contains_yield(condition) + || body_contains_yield(body) => + { return true; } // A yield buried in a do-while or labeled loop must still be seen // by the enclosing construct's linearization (#1824), otherwise it // is never split into resume states. - Stmt::DoWhile { body, .. } if body_contains_yield(body) => { + Stmt::DoWhile { body, condition } + if super::hoist_yields::expr_contains_yield(condition) + || body_contains_yield(body) => + { return true; } Stmt::Labeled { body, .. } if body_contains_yield(std::slice::from_ref(&**body)) => { return true; } - Stmt::For { body, .. } if body_contains_yield(body) => { + Stmt::For { + condition, + update, + body, + .. + } if condition + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) + || update + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) + || body_contains_yield(body) => + { return true; } Stmt::Try { diff --git a/crates/perry-transform/src/generator/hoist_yields.rs b/crates/perry-transform/src/generator/hoist_yields.rs index c895fb835c..442b6ca660 100644 --- a/crates/perry-transform/src/generator/hoist_yields.rs +++ b/crates/perry-transform/src/generator/hoist_yields.rs @@ -101,11 +101,24 @@ fn hoist_yields_in_stmt(mut stmt: Stmt, next_id: &mut LocalId, hoisted: &mut Vec // of the loop. Condition/update are per-iteration — left in place // for the linearizer's For arm (matching the await pass's // single-hoist approximation for loop condition/update). + // + // The init's OWN top-level yield must be hoisted too (#10419): + // `for (let t = yield x; …)` / `for (yield x; …)` sit in a slot no + // linearizer arm splits, so the residual yield never suspended. + // The statement-level walk keeps a top-level yield in place, so + // hoist the init expression fully instead. if let Some(i) = init { - let mut inner = Vec::new(); - let replaced = hoist_yields_in_stmt((**i).clone(), next_id, &mut inner); - hoisted.extend(inner); - **i = replaced; + match i.as_mut() { + Stmt::Let { init: Some(e), .. } | Stmt::Expr(e) => { + hoist_yields_in_expr_full(e, next_id, hoisted); + } + _ => { + let mut inner = Vec::new(); + let replaced = hoist_yields_in_stmt((**i).clone(), next_id, &mut inner); + hoisted.extend(inner); + **i = replaced; + } + } } hoist_yields_in_stmts(body, next_id); } diff --git a/crates/perry-transform/src/generator/linearize.rs b/crates/perry-transform/src/generator/linearize.rs index f4105ebd41..2cbc1ee092 100644 --- a/crates/perry-transform/src/generator/linearize.rs +++ b/crates/perry-transform/src/generator/linearize.rs @@ -412,6 +412,21 @@ pub struct FinallyRoute { pub completion_check_state: Option, } +/// Normalize statements built from a loop HEADER (a condition or update the +/// header arms move into the loop body / update state) into the statement +/// shapes this linearizer splits. `hoist_yields_in_stmts` and — for an async +/// generator — `await_async_generator_yield_operands` ran over the function +/// body before linearization, but neither descends into loop headers, so a +/// header yield reaches this point un-hoisted and, in an `async function*`, +/// with its operand never awaited (spec `AsyncGeneratorYield(? Await(v))`: +/// `yield promise` in a loop condition delivered the promise itself, #10419). +fn normalize_loop_header_stmts(stmts: &mut Vec, next_local_id: &mut u32) { + hoist_yields_in_stmts(stmts, next_local_id); + if linearize_async_generator() { + super::lower::await_async_generator_yield_operands(stmts, next_local_id); + } +} + /// Linearize the generator body into a sequence of states. /// Splits at yield points and handles for-loops with yields. pub fn linearize_body( @@ -694,7 +709,7 @@ pub fn linearize_body( mutable: true, init: Some(condition.clone()), }]; - hoist_yields_in_stmts(&mut prefix, next_local_id); + normalize_loop_header_stmts(&mut prefix, next_local_id); prefix.push(Stmt::If { condition: Expr::Unary { op: UnaryOp::Not, @@ -799,7 +814,7 @@ pub fn linearize_body( mutable: true, init: condition.clone(), }]; - hoist_yields_in_stmts(&mut prefix, next_local_id); + normalize_loop_header_stmts(&mut prefix, next_local_id); new_body.append(&mut prefix); new_body.push(Stmt::If { condition: Expr::Unary { @@ -813,7 +828,7 @@ pub fn linearize_body( let mut taken_body = body.clone(); if upd_yields { let mut upd_stmts = vec![Stmt::Expr(update.clone().unwrap())]; - hoist_yields_in_stmts(&mut upd_stmts, next_local_id); + normalize_loop_header_stmts(&mut upd_stmts, next_local_id); prefix_loop_continues(&mut taken_body, &upd_stmts); new_body.append(&mut taken_body); new_body.extend(upd_stmts); @@ -839,13 +854,21 @@ pub fn linearize_body( ); } - // For-loop containing yield(s) + // For-loop containing yield(s) — in the body, or in an update the + // header arm above left in place (a `continue` inside + // try/finally must run the finally BEFORE the update, which the + // move-to-body-end rewrite cannot express). The update is then + // linearized in its own `continue`-target state below (#10419). Stmt::For { init, condition, update, body, - } if body_contains_yield(body) => { + } if body_contains_yield(body) + || update + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) => + { // State N: pre-loop code + init, goto condition check let init_state = *state_num; *state_num += 1; @@ -954,11 +977,6 @@ pub fn linearize_body( // residual, and (depending on guard placement) loop forever // on the same iteration. let update_state = *state_num; - *state_num += 1; - let mut update_body: Vec = Vec::new(); - if let Some(upd) = update { - update_body.push(Stmt::Expr(upd.clone())); - } // Push tail_state pointing at update_state. states.push(State { @@ -966,10 +984,35 @@ pub fn linearize_body( body: tail_body, exit: StateExit::Goto(update_state), }); - // Push update_state pointing at cond_state. + // Update statements, then the state that jumps back to + // cond_state. A yield in the update is split into its own + // states starting AT `update_state` (the first state pushed + // takes that number); without one, the final push below IS + // `update_state`, exactly as before. + if let Some(upd) = update { + let mut upd_stmts = vec![Stmt::Expr(upd.clone())]; + if super::hoist_yields::expr_contains_yield(upd) { + normalize_loop_header_stmts(&mut upd_stmts, next_local_id); + linearize_body( + &upd_stmts, + states, + current, + state_num, + state_id, + next_local_id, + sent_id, + catches, + finallys, + ); + } else { + current.append(&mut upd_stmts); + } + } + let update_tail_state = *state_num; + *state_num += 1; states.push(State { - num: update_state, - body: update_body, + num: update_tail_state, + body: std::mem::take(current), exit: StateExit::Goto(cond_state), }); diff --git a/crates/perry-transform/src/generator/loop_header_yield_tests.rs b/crates/perry-transform/src/generator/loop_header_yield_tests.rs new file mode 100644 index 0000000000..9acac26b0b --- /dev/null +++ b/crates/perry-transform/src/generator/loop_header_yield_tests.rs @@ -0,0 +1,286 @@ +//! #10419: a `yield` in a loop HEADER (while / do-while condition, for +//! condition / update / init) must be split into resume states at any nesting +//! depth, not only when the loop is a direct statement of the generator body. + +use super::*; + +fn yield_num(v: f64) -> Expr { + Expr::Yield { + value: Some(Box::new(Expr::Number(v))), + delegate: false, + } +} + +/// `((yield v), )` — the comma form minifiers emit in loop tests. +fn comma_yield(v: f64) -> Expr { + Expr::Sequence(vec![yield_num(v), Expr::GlobalGet(0)]) +} + +fn generator(body: Vec, is_async: bool) -> Function { + Function { + id: 1, + name: "header_yield".to_string(), + type_params: Vec::new(), + params: Vec::new(), + return_type: Type::Any, + body, + is_async, + is_generator: true, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } +} + +/// Residual `Expr::Yield` nodes. After the state-machine transform every +/// suspend point is a state exit; one left in the HIR is lowered by codegen +/// without suspending — the #10419 symptom (the generator yields nothing). +fn residual_yields(stmts: &[Stmt]) -> usize { + format!("{stmts:?}").matches("Yield {").count() +} + +fn transformed(body: Vec, is_async: bool) -> Vec { + let mut module = Module::new("loop_header_yield"); + module.functions.push(generator(body, is_async)); + transform_generators(&mut module); + std::mem::take(&mut module.functions[0].body) +} + +fn if_global(then_branch: Vec) -> Stmt { + Stmt::If { + condition: Expr::GlobalGet(0), + then_branch, + else_branch: None, + } +} + +/// One loop per header position, each wrapped in a different container. +fn nested_header_yield_bodies() -> Vec<(&'static str, Vec)> { + vec![ + ( + "if > while condition", + vec![if_global(vec![Stmt::While { + condition: comma_yield(1.0), + body: vec![], + }])], + ), + ( + "try/finally > do-while condition", + vec![Stmt::Try { + body: vec![Stmt::DoWhile { + body: vec![], + condition: comma_yield(1.0), + }], + catch: None, + finally: Some(vec![]), + }], + ), + ( + "catch > for condition", + vec![Stmt::Try { + body: vec![], + catch: Some(CatchClause { + param: None, + body: vec![Stmt::For { + init: None, + condition: Some(comma_yield(1.0)), + update: None, + body: vec![], + }], + }), + finally: None, + }], + ), + ( + "switch case > for update", + vec![Stmt::Switch { + discriminant: Expr::GlobalGet(0), + cases: vec![SwitchCase { + test: Some(Expr::Number(1.0)), + body: vec![Stmt::For { + init: None, + condition: Some(Expr::GlobalGet(0)), + update: Some(comma_yield(1.0)), + body: vec![], + }], + }], + }], + ), + ( + "label > while condition", + vec![Stmt::Labeled { + label: "outer".to_string(), + body: Box::new(Stmt::While { + condition: comma_yield(1.0), + body: vec![Stmt::LabeledContinue("outer".to_string())], + }), + }], + ), + ( + "if > for init", + vec![if_global(vec![Stmt::For { + init: Some(Box::new(Stmt::Let { + id: 1, + name: "t".to_string(), + ty: Type::Any, + mutable: true, + init: Some(yield_num(1.0)), + })), + condition: Some(Expr::GlobalGet(0)), + update: None, + body: vec![], + }])], + ), + ( + "if > for update with continue inside try/finally", + vec![if_global(vec![Stmt::For { + init: None, + condition: Some(Expr::GlobalGet(0)), + update: Some(comma_yield(1.0)), + body: vec![Stmt::Try { + body: vec![Stmt::Continue], + catch: None, + finally: Some(vec![]), + }], + }])], + ), + ] +} + +#[test] +fn body_contains_yield_sees_nested_loop_headers() { + for (name, body) in nested_header_yield_bodies() { + // The init case is normalized by `hoist_yields_in_stmts` (the pass + // that runs before any linearizer decision), not by header detection. + let mut body = body; + let mut next_id = 100; + hoist_yields_in_stmts(&mut body, &mut next_id); + assert!( + body_contains_yield(&body), + "{name}: a loop-header yield must make the enclosing statement linearize" + ); + } +} + +#[test] +fn body_contains_yield_ignores_header_yield_in_nested_closure() { + // A `yield` inside a closure in the loop condition belongs to that closure. + let closure = Expr::Closure { + func_id: 9, + params: Vec::new(), + return_type: Type::Any, + body: vec![Stmt::Expr(yield_num(1.0))], + captures: Vec::new(), + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: true, + is_strict: false, + }; + let body = vec![if_global(vec![Stmt::While { + condition: Expr::Sequence(vec![closure, Expr::Bool(false)]), + body: vec![], + }])]; + assert!(!body_contains_yield(&body)); +} + +#[test] +fn nested_loop_header_yields_become_state_exits() { + for is_async in [false, true] { + for (name, body) in nested_header_yield_bodies() { + assert!( + residual_yields(&body) > 0, + "{name}: fixture must start with a yield, or the check below is vacuous" + ); + let out = transformed(body, is_async); + assert_eq!( + residual_yields(&out), + 0, + "{name} (async={is_async}): header yield left unsplit: {out:?}" + ); + } + } +} + +#[test] +fn for_init_top_level_yield_is_hoisted_before_the_loop() { + let mut stmts = vec![Stmt::For { + init: Some(Box::new(Stmt::Expr(yield_num(7.0)))), + condition: Some(Expr::GlobalGet(0)), + update: None, + body: vec![], + }]; + let mut next_id = 100; + hoist_yields_in_stmts(&mut stmts, &mut next_id); + assert!( + matches!( + &stmts[0], + Stmt::Let { + init: Some(Expr::Yield { .. }), + .. + } + ), + "init yield must be hoisted into a leading `let = yield`: {stmts:?}" + ); + match &stmts[1] { + Stmt::For { init: Some(i), .. } => assert_eq!(residual_yields(std::slice::from_ref(i)), 0), + other => panic!("expected the For after the hoisted let, got {other:?}"), + } +} + +/// In an `async function*`, `yield E` awaits `E` first. The pre-linearize +/// operand pass never sees loop headers, so the linearizer must add the +/// await when it moves a header yield into the loop (`yield promise` in a +/// loop condition delivered the promise itself). +#[test] +fn async_generator_header_yield_awaits_its_operand() { + for is_async in [false, true] { + super::linearize::set_linearize_async_generator(is_async); + let body = vec![Stmt::While { + condition: comma_yield(1.0), + body: vec![], + }]; + let mut states = Vec::new(); + let mut current = Vec::new(); + let mut state_num = 0; + let mut next_id = 100; + let mut catches = Vec::new(); + let mut finallys = Vec::new(); + linearize_body( + &body, + &mut states, + &mut current, + &mut state_num, + 90, + &mut next_id, + 91, + &mut catches, + &mut finallys, + ); + super::linearize::set_linearize_async_generator(false); + let await_state = states + .iter() + .find(|s| matches!(s.exit, StateExit::Await { .. })) + .map(|s| s.num); + let yield_state = states + .iter() + .find(|s| matches!(s.exit, StateExit::Yield { .. })) + .map(|s| s.num) + .expect("the header yield must become a Yield state"); + if is_async { + let await_state = await_state.expect("async header yield must await its operand"); + assert!( + await_state < yield_state, + "operand await must precede the yield" + ); + } else { + assert_eq!(await_state, None, "sync generators never await"); + } + } +} diff --git a/crates/perry-transform/src/generator/mod.rs b/crates/perry-transform/src/generator/mod.rs index cb2c340a81..4901c03672 100644 --- a/crates/perry-transform/src/generator/mod.rs +++ b/crates/perry-transform/src/generator/mod.rs @@ -501,3 +501,7 @@ pub fn transform_plain_async_closure_body( #[cfg(test)] #[path = "dispatch_growth_tests.rs"] mod dispatch_growth_tests; + +#[cfg(test)] +#[path = "loop_header_yield_tests.rs"] +mod loop_header_yield_tests; diff --git a/test-files/test_gap_10419_generator_loop_header_yield.ts b/test-files/test_gap_10419_generator_loop_header_yield.ts new file mode 100644 index 0000000000..94d845a53d --- /dev/null +++ b/test-files/test_gap_10419_generator_loop_header_yield.ts @@ -0,0 +1,514 @@ +// #10419: a `yield` in a loop CONDITION / UPDATE / INIT must suspend the +// generator at every nesting depth. #5933 fixed loops that are direct +// statements of the generator body; the same loop nested in `if` / `else` / +// `try` / `catch` / `finally` / `switch` / a label / another loop was emitted +// as an ordinary loop, so the residual yield never suspended: `[...g()]` was +// empty and a sent-value loop never terminated. Minifiers produce exactly this +// shape (lru-cache 11.5.2's `*#A` / `*#z` iterators). +// +// Every loop body calls tick(), which throws after 1000 steps, and every +// consumer caps its pulls, so a regression prints a wrong line instead of +// hanging the harness. + +let steps = 0; +function tick(): void { + if (++steps > 1000) throw new Error("runaway loop"); +} + +function take(it: Iterator, sends: any[] = [], limit = 25): string { + const out: string[] = []; + let r = it.next(); + let i = 0; + while (!r.done && i < limit) { + out.push(JSON.stringify(r.value)); + r = it.next(sends[i]); + i++; + } + out.push(r.done ? "done=" + JSON.stringify(r.value) : "(pull limit)"); + return out.join(" "); +} + +function run(name: string, mk: () => Iterator, sends?: any[]): void { + steps = 0; + try { + console.log(name + ":", take(mk(), sends)); + } catch (e) { + console.log(name + ": threw", (e as Error).message); + } +} + +function attempt(name: string, f: () => string): void { + steps = 0; + try { + console.log(name + ":", f()); + } catch (e) { + console.log(name + ": threw", String(e instanceof Error ? e.message : e)); + } +} + +// ── issue repro ────────────────────────────────────────────────────────── +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(JSON.stringify([...noIf()])); +console.log(JSON.stringify([...forInIf(1)])); +console.log(JSON.stringify([...whileInIf(1)])); +console.log(JSON.stringify([...forInTry()])); + +// ── loop kind x header position x container (each yields 3 2 1) ────────── +function* m_while_cond_top(n: number) { let t = 3; while (((yield t), --t > 0)) tick(); } +function* m_while_cond_if(n: number) { if (n) { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_else(n: number) { if (!n) tick(); else { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_try(n: number) { try { let t = 3; while (((yield t), --t > 0)) tick(); } finally { tick(); } } +function* m_while_cond_catch(n: number) { try { throw new Error("x"); } catch { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_finally(n: number) { try { tick(); } finally { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 3; while (((yield t), --t > 0)) tick(); } break; default: tick(); } } +function* m_while_cond_label(n: number) { blk: { let t = 3; while (((yield t), --t > 0)) tick(); if (n) break blk; tick(); } } +function* m_while_cond_labeled_loop(n: number) { let t = 3; lbl: while (((yield t), --t > 0)) { tick(); continue lbl; } } +function* m_while_cond_if_labeled_loop(n: number) { if (n) { let t = 3; lbl: while (((yield t), --t > 0)) { tick(); continue lbl; } } } +function* m_while_cond_loop(n: number) { for (let i = 0; i < n; i++) { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 3; while (((yield t), --t > 0)) tick(); } } } catch (e) { throw e; } } +function* m_dowhile_cond_top(n: number) { let t = 3; do tick(); while (((yield t), --t > 0)); } +function* m_dowhile_cond_if(n: number) { if (n) { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_else(n: number) { if (!n) tick(); else { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_try(n: number) { try { let t = 3; do tick(); while (((yield t), --t > 0)); } finally { tick(); } } +function* m_dowhile_cond_catch(n: number) { try { throw new Error("x"); } catch { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_finally(n: number) { try { tick(); } finally { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 3; do tick(); while (((yield t), --t > 0)); } break; default: tick(); } } +function* m_dowhile_cond_label(n: number) { blk: { let t = 3; do tick(); while (((yield t), --t > 0)); if (n) break blk; tick(); } } +function* m_dowhile_cond_labeled_loop(n: number) { let t = 3; lbl: do { tick(); continue lbl; } while (((yield t), --t > 0)); } +function* m_dowhile_cond_if_labeled_loop(n: number) { if (n) { let t = 3; lbl: do { tick(); continue lbl; } while (((yield t), --t > 0)); } } +function* m_dowhile_cond_loop(n: number) { for (let i = 0; i < n; i++) { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 3; do tick(); while (((yield t), --t > 0)); } } } catch (e) { throw e; } } +function* m_for_cond_top(n: number) { for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_if(n: number) { if (n) for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_else(n: number) { if (!n) tick(); else for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_try(n: number) { try { for (let t = 3; ((yield t), t > 1); t--) tick(); } finally { tick(); } } +function* m_for_cond_catch(n: number) { try { throw new Error("x"); } catch { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_finally(n: number) { try { tick(); } finally { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_switch(n: number) { switch (n) { case 0: break; case 1: { for (let t = 3; ((yield t), t > 1); t--) tick(); } break; default: tick(); } } +function* m_for_cond_label(n: number) { blk: { for (let t = 3; ((yield t), t > 1); t--) tick(); if (n) break blk; tick(); } } +function* m_for_cond_labeled_loop(n: number) { lbl: for (let t = 3; ((yield t), t > 1); t--) { tick(); continue lbl; } } +function* m_for_cond_if_labeled_loop(n: number) { if (n) { lbl: for (let t = 3; ((yield t), t > 1); t--) { tick(); continue lbl; } } } +function* m_for_cond_loop(n: number) { for (let i = 0; i < n; i++) { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_deep(n: number) { if (n) try { switch (n) { case 1: { for (let t = 3; ((yield t), t > 1); t--) tick(); } } } catch (e) { throw e; } } +function* m_for_update_top(n: number) { for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_if(n: number) { if (n) for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_else(n: number) { if (!n) tick(); else for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_try(n: number) { try { for (let t = 3; t > 0; (yield t), t--) tick(); } finally { tick(); } } +function* m_for_update_catch(n: number) { try { throw new Error("x"); } catch { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_finally(n: number) { try { tick(); } finally { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_switch(n: number) { switch (n) { case 0: break; case 1: { for (let t = 3; t > 0; (yield t), t--) tick(); } break; default: tick(); } } +function* m_for_update_label(n: number) { blk: { for (let t = 3; t > 0; (yield t), t--) tick(); if (n) break blk; tick(); } } +function* m_for_update_labeled_loop(n: number) { lbl: for (let t = 3; t > 0; (yield t), t--) { tick(); continue lbl; } } +function* m_for_update_if_labeled_loop(n: number) { if (n) { lbl: for (let t = 3; t > 0; (yield t), t--) { tick(); continue lbl; } } } +function* m_for_update_loop(n: number) { for (let i = 0; i < n; i++) { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_deep(n: number) { if (n) try { switch (n) { case 1: { for (let t = 3; t > 0; (yield t), t--) tick(); } } } catch (e) { throw e; } } +function* m_for_init_top(n: number) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } +function* m_for_init_if(n: number) { if (n) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_else(n: number) { if (!n) tick(); else { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_try(n: number) { try { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } finally { tick(); } } +function* m_for_init_catch(n: number) { try { throw new Error("x"); } catch { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_finally(n: number) { try { tick(); } finally { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } break; default: tick(); } } +function* m_for_init_label(n: number) { blk: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; if (n) break blk; tick(); } } +function* m_for_init_labeled_loop(n: number) { let t = 0; lbl: for (yield 3; t < 2; t++) { yield 2 - t; continue lbl; } } +function* m_for_init_if_labeled_loop(n: number) { if (n) { let t = 0; lbl: for (yield 3; t < 2; t++) { yield 2 - t; continue lbl; } } } +function* m_for_init_loop(n: number) { for (let i = 0; i < n; i++) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } } catch (e) { throw e; } } + +const matrix: Array<[string, (n: number) => Iterator]> = [ + ["while_cond_top", m_while_cond_top], + ["while_cond_if", m_while_cond_if], + ["while_cond_else", m_while_cond_else], + ["while_cond_try", m_while_cond_try], + ["while_cond_catch", m_while_cond_catch], + ["while_cond_finally", m_while_cond_finally], + ["while_cond_switch", m_while_cond_switch], + ["while_cond_label", m_while_cond_label], + ["while_cond_labeled_loop", m_while_cond_labeled_loop], + ["while_cond_if_labeled_loop", m_while_cond_if_labeled_loop], + ["while_cond_loop", m_while_cond_loop], + ["while_cond_deep", m_while_cond_deep], + ["dowhile_cond_top", m_dowhile_cond_top], + ["dowhile_cond_if", m_dowhile_cond_if], + ["dowhile_cond_else", m_dowhile_cond_else], + ["dowhile_cond_try", m_dowhile_cond_try], + ["dowhile_cond_catch", m_dowhile_cond_catch], + ["dowhile_cond_finally", m_dowhile_cond_finally], + ["dowhile_cond_switch", m_dowhile_cond_switch], + ["dowhile_cond_label", m_dowhile_cond_label], + ["dowhile_cond_labeled_loop", m_dowhile_cond_labeled_loop], + ["dowhile_cond_if_labeled_loop", m_dowhile_cond_if_labeled_loop], + ["dowhile_cond_loop", m_dowhile_cond_loop], + ["dowhile_cond_deep", m_dowhile_cond_deep], + ["for_cond_top", m_for_cond_top], + ["for_cond_if", m_for_cond_if], + ["for_cond_else", m_for_cond_else], + ["for_cond_try", m_for_cond_try], + ["for_cond_catch", m_for_cond_catch], + ["for_cond_finally", m_for_cond_finally], + ["for_cond_switch", m_for_cond_switch], + ["for_cond_label", m_for_cond_label], + ["for_cond_labeled_loop", m_for_cond_labeled_loop], + ["for_cond_if_labeled_loop", m_for_cond_if_labeled_loop], + ["for_cond_loop", m_for_cond_loop], + ["for_cond_deep", m_for_cond_deep], + ["for_update_top", m_for_update_top], + ["for_update_if", m_for_update_if], + ["for_update_else", m_for_update_else], + ["for_update_try", m_for_update_try], + ["for_update_catch", m_for_update_catch], + ["for_update_finally", m_for_update_finally], + ["for_update_switch", m_for_update_switch], + ["for_update_label", m_for_update_label], + ["for_update_labeled_loop", m_for_update_labeled_loop], + ["for_update_if_labeled_loop", m_for_update_if_labeled_loop], + ["for_update_loop", m_for_update_loop], + ["for_update_deep", m_for_update_deep], + ["for_init_top", m_for_init_top], + ["for_init_if", m_for_init_if], + ["for_init_else", m_for_init_else], + ["for_init_try", m_for_init_try], + ["for_init_catch", m_for_init_catch], + ["for_init_finally", m_for_init_finally], + ["for_init_switch", m_for_init_switch], + ["for_init_label", m_for_init_label], + ["for_init_labeled_loop", m_for_init_labeled_loop], + ["for_init_if_labeled_loop", m_for_init_if_labeled_loop], + ["for_init_loop", m_for_init_loop], + ["for_init_deep", m_for_init_deep], +]; + +for (const [name, g] of matrix) { + run(name, () => g(1)); +} +// the untaken branch / zero-iteration outer loop yields nothing +run("while_cond_if n=0", () => m_while_cond_if(0)); +run("for_update_loop n=0", () => m_for_update_loop(0)); +run("for_cond_loop n=2", () => m_for_cond_loop(2)); + +// ── minified lru-cache iterator shape ──────────────────────────────────── +class MiniLRU { + #n = 0; + #h = 0; + #a = 0; + #u: number[] = []; + #k: string[] = []; + #stale: boolean[] = []; + allowStale = false; + set(k: string, stale = false): this { + const i = this.#k.length; + this.#k.push(k); + this.#stale.push(stale); + this.#u.push(i + 1); + if (this.#n === 0) this.#h = i; + this.#a = i; + this.#n++; + return this; + } + #V(t: number): boolean { + return t < this.#k.length; + } + #p(t: number): boolean { + return this.#stale[t]; + } + *#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]; + } + *keys() { + for (const i of this.#A()) yield this.#k[i]; + } + *allKeys() { + for (const i of this.#A({ allowStale: true })) yield this.#k[i]; + } +} +{ + const c = new MiniLRU().set("a").set("b", true).set("c"); + console.log("lru keys:", JSON.stringify([...c.keys()]), JSON.stringify([...c.allKeys()])); + console.log("lru empty:", JSON.stringify([...new MiniLRU().keys()])); +} + +// ── sent values terminate the loop ─────────────────────────────────────── +function* sentBounded(n: number) { + let count = 0; + let v: any; + if (n) { + while ((v = yield count) !== "stop" && count < 5) count++; + } + return count; +} +run("sent bounded", () => sentBounded(1), [undefined, undefined, "stop"]); +run("sent bounded exhausts", () => sentBounded(1)); +function* sentUnbounded(n: number) { + let count = 0; + let v: any; + try { + if (n) while ((v = yield count) !== "stop") { tick(); count++; } + } finally { + count += 100; + } + return count; +} +run("sent unbounded", () => sentUnbounded(1), [1, 2, 3, "stop"]); +function* sentDoWhile(n: number) { + const got: any[] = []; + let v: any; + switch (n) { + case 1: + do { tick(); } while ((v = yield got.length) !== undefined && got.push(v) < 10); + } + return got; +} +run("sent do-while", () => sentDoWhile(1), ["x", "y"]); +function* sentForUpdate(n: number) { + let total = 0; + if (n) for (let i = 0; i < 10; i += (yield total) ?? 1) { tick(); total += i; } + return total; +} +run("sent for-update", () => sentForUpdate(1), [4, 5]); +function* sentForInit(n: number) { + if (n) for (let t = yield "init"; t > 0; t--) yield t; +} +run("sent for-init", () => sentForInit(1), [3]); +run("sent for-init none", () => sentForInit(1)); + +// ── return() / throw() while suspended in a header yield ────────────────── +function* retMid(log: string[], n: number) { + if (n) { + try { + for (let t = 0; ((yield t), t < 100); t++) tick(); + } finally { + log.push("finally"); + } + } + log.push("unreachable"); +} +attempt("return mid", () => { + const log: string[] = []; + const g = retMid(log, 1); + return JSON.stringify([g.next(), g.next(), g.return(42), g.next()]) + " " + log.join(","); +}); +function* throwMid(n: number) { + if (n) { + try { + while (((yield "w"), true)) tick(); + } catch (e) { + yield "caught " + e; + } + } + yield "after"; +} +attempt("throw mid", () => { + const g = throwMid(1); + return JSON.stringify([g.next(), g.next(), g.throw("boom"), g.next(), g.next()]); +}); +function* throwOuterCatch(n: number) { + try { + if (n) for (let i = 0; i < 100; (yield i), i++) tick(); + } catch (e) { + return "outer " + e; + } +} +attempt("throw outer", () => { + const g = throwOuterCatch(1); + return JSON.stringify([g.next(), g.next(), g.throw("bang"), g.next()]); +}); + +// ── break / continue around header yields ──────────────────────────────── +function* breakInSwitch(n: number) { + let t = 0; + switch (n) { + case 1: + while (((yield t), true)) { + tick(); + if (t++ >= 2) break; + } + yield "after"; + } +} +run("break in switch", () => breakInSwitch(1)); +function* labeledContinueUpdate(n: number) { + if (n) { + outer: for (let i = 0; i < 3; (yield i), i++) { + for (let j = 0; j < 2; j++) { + tick(); + if (j === 1) continue outer; + } + } + } +} +run("labeled continue update", () => labeledContinueUpdate(1)); +function* continueTryFinallyUpdate(log: string[], n: number) { + for (let i = 0; i < 3; (yield "u" + i), i++) { + try { + tick(); + if (i === 1) continue; + log.push("b" + i); + } finally { + log.push("f" + i); + } + } + if (n) { + for (let i = 0; i < 2; (yield "v" + i), i++) { + try { + if (i === 0) continue; + } finally { + log.push("g" + i); + } + } + } +} +{ + const log: string[] = []; + run("continue try/finally update", () => continueTryFinallyUpdate(log, 1)); + console.log(" log:", log.join(",")); +} +function* nestedHeaders(n: number) { + if (n) for (let i = 0; ((yield "i" + i), i < 2); i++) while (((yield "j" + i), false)) tick(); +} +run("nested headers", () => nestedHeaders(1)); + +function* closuresPerIteration(n: number) { + const fns: Array<() => number> = []; + if (n) for (let i = 0; ((yield i), i < 2); i++) fns.push(() => i); + if (n) for (let i = 0; i < 2; (yield "u" + i), i++) fns.push(() => i * 10); + return fns.map((f) => f()).join(","); +} +run("closures per iteration", () => closuresPerIteration(1)); + +// ── controls: shapes that already worked ───────────────────────────────── +function* bodyYieldInIf(n: number) { + if (n) for (let t = 0; t < 3; t++) yield t; +} +run("control body yield in if", () => bodyYieldInIf(1)); +function* forOfYieldIterable(n: number) { + if (n) for (const x of (yield "want") as number[]) yield x * 2; +} +run("control for-of yield iterable", () => forOfYieldIterable(1), [[1, 2]]); +function* noYieldHeaderInIf(n: number) { + let s = 0; + if (n) for (let i = 0; i < 4; i++) s += i; + yield s; +} +run("control no header yield", () => noYieldHeaderInIf(1)); + +// ── async generators (for await) ───────────────────────────────────────── +async function* aCondInIf(n: number) { + if (n) for (let t = 3; ((yield t), t > 1); t--) tick(); +} +async function* aWhileInTry(n: number) { + let t = 3; + try { + while (((yield t), --t > 0)) tick(); + } finally { + tick(); + } +} +async function* aDoWhileInSwitch(n: number) { + let t = 3; + switch (n) { + case 1: + do tick(); while (((yield t), --t > 0)); + } +} +async function* aUpdateInLabel(n: number) { + if (n) { + lbl: for (let t = 3; t > 0; (yield t), t--) { + tick(); + continue lbl; + } + } +} +async function* aPromiseOperand(n: number) { + for (let t = 3; ((yield Promise.resolve(t * 10)), t > 1); t--) tick(); + if (n) while (((yield Promise.resolve("p")), false)) tick(); +} +async function* aAwaitAndYield(n: number) { + let t = 2; + if (n) while (((yield await Promise.resolve(t)), t-- > 0)) tick(); +} +async function* aSent(n: number) { + let v: any; + let count = 0; + if (n) { + while ((v = yield count) !== "stop") { + tick(); + count++; + } + } + return count; +} +async function* aReturnMid(log: string[], n: number) { + if (n) { + try { + for (let t = 0; ((yield t), t < 100); t++) tick(); + } finally { + log.push("async finally"); + } + } +} + +// A plain async function's `for (let x = await p; …)` becomes a for-init yield +// after the await→yield rewrite, so it shares the init hoist. +async function forInitAwait(n: number): Promise { + const out: number[] = []; + for (let x = await Promise.resolve(3); x > 0; x--) out.push(x); + if (n) for (let y = await Promise.resolve(2); y > 0; y--) out.push(y * 10); + return out.join(","); +} + +async function collect(name: string, g: AsyncIterable): Promise { + steps = 0; + const out: string[] = []; + try { + for await (const v of g) { + out.push(v instanceof Promise ? "" : JSON.stringify(v)); + if (out.length > 25) break; + } + console.log(name + ":", out.join(" ")); + } catch (e) { + console.log(name + ": threw", (e as Error).message, out.join(" ")); + } +} + +async function main(): Promise { + await collect("async cond in if", aCondInIf(1)); + await collect("async while in try", aWhileInTry(1)); + await collect("async do-while in switch", aDoWhileInSwitch(1)); + await collect("async update in label", aUpdateInLabel(1)); + await collect("async promise operand", aPromiseOperand(1)); + await collect("async await and yield", aAwaitAndYield(1)); + + steps = 0; + const s = aSent(1); + const r: any[] = []; + r.push(await s.next()); + r.push(await s.next("a")); + r.push(await s.next("b")); + r.push(await s.next("stop")); + r.push(await s.next()); + console.log("async sent:", JSON.stringify(r)); + + console.log("async fn for-init await:", await forInitAwait(1)); + + const log: string[] = []; + const g = aReturnMid(log, 1); + const rr: any[] = []; + rr.push(await g.next()); + rr.push(await g.next()); + rr.push(await g.return(7)); + rr.push(await g.next()); + console.log("async return mid:", JSON.stringify(rr), log.join(",")); +} +main().catch((e) => console.log("async main threw", (e as Error).message));