diff --git a/crates/perry-codegen-js/src/emit/exprs_more.rs b/crates/perry-codegen-js/src/emit/exprs_more.rs index e030fa83c0..cf5e449bb1 100644 --- a/crates/perry-codegen-js/src/emit/exprs_more.rs +++ b/crates/perry-codegen-js/src/emit/exprs_more.rs @@ -1035,6 +1035,11 @@ impl JsEmitter { self.emit_expr(val); self.output.push(')'); } + Expr::ForAwaitToArray(val) => { + self.output.push_str("Array.fromAsync("); + self.emit_expr(val); + self.output.push(')'); + } Expr::ArrayFromMapped { iterable, map_fn, diff --git a/crates/perry-codegen/src/collectors/escape_check.rs b/crates/perry-codegen/src/collectors/escape_check.rs index 72f580f5f5..e5f3aa02e7 100644 --- a/crates/perry-codegen/src/collectors/escape_check.rs +++ b/crates/perry-codegen/src/collectors/escape_check.rs @@ -395,6 +395,7 @@ pub fn check_escapes_in_expr( | Expr::IteratorToArray(operand) | Expr::GetIterator(operand) | Expr::ForOfToArray(operand) + | Expr::ForAwaitToArray(operand) | Expr::WeakRefNew(operand) | Expr::WeakRefDeref(operand) | Expr::FinalizationRegistryNew(operand) diff --git a/crates/perry-codegen/src/collectors/escape_news.rs b/crates/perry-codegen/src/collectors/escape_news.rs index df0f63d275..27b1a9636c 100644 --- a/crates/perry-codegen/src/collectors/escape_news.rs +++ b/crates/perry-codegen/src/collectors/escape_news.rs @@ -254,6 +254,7 @@ fn collect_used_new_fields_in_expr( | Expr::IteratorToArray(operand) | Expr::GetIterator(operand) | Expr::ForOfToArray(operand) + | Expr::ForAwaitToArray(operand) | Expr::WeakRefNew(operand) | Expr::WeakRefDeref(operand) | Expr::FinalizationRegistryNew(operand) diff --git a/crates/perry-codegen/src/collectors/i32_locals.rs b/crates/perry-codegen/src/collectors/i32_locals.rs index 4653e8bab8..71e3ebac69 100644 --- a/crates/perry-codegen/src/collectors/i32_locals.rs +++ b/crates/perry-codegen/src/collectors/i32_locals.rs @@ -1113,6 +1113,7 @@ pub fn collect_localset_ids_in_expr_filtered( | Expr::IteratorToArray(operand) | Expr::GetIterator(operand) | Expr::ForOfToArray(operand) + | Expr::ForAwaitToArray(operand) | Expr::WeakRefNew(operand) | Expr::WeakRefDeref(operand) | Expr::QueueMicrotask(operand) diff --git a/crates/perry-codegen/src/collectors/refs.rs b/crates/perry-codegen/src/collectors/refs.rs index 43c93e678c..12b0560e58 100644 --- a/crates/perry-codegen/src/collectors/refs.rs +++ b/crates/perry-codegen/src/collectors/refs.rs @@ -236,6 +236,7 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet) { | Expr::IteratorToArray(operand) | Expr::GetIterator(operand) | Expr::ForOfToArray(operand) + | Expr::ForAwaitToArray(operand) | Expr::WeakRefNew(operand) | Expr::WeakRefDeref(operand) | Expr::QueueMicrotask(operand) diff --git a/crates/perry-codegen/src/expr/arrays_finds.rs b/crates/perry-codegen/src/expr/arrays_finds.rs index 5bf43e9415..268b47becd 100644 --- a/crates/perry-codegen/src/expr/arrays_finds.rs +++ b/crates/perry-codegen/src/expr/arrays_finds.rs @@ -573,6 +573,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .block() .call(DOUBLE, "js_for_of_to_array", &[(DOUBLE, &v)])) } + Expr::ForAwaitToArray(o) => { + let v = lower_expr(ctx, o)?; + let undefined = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + Ok(ctx.block().call( + DOUBLE, + "js_array_from_async", + &[(DOUBLE, &v), (DOUBLE, &undefined), (DOUBLE, &undefined)], + )) + } Expr::WeakRefDeref(o) => { // `ref.deref()` — returns the wrapped target (or undefined if // collected; GC never clears the stub slot, so always returns diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index ce0c8a8c53..b4130c2094 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1575,6 +1575,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { | Expr::IteratorToArray(..) | Expr::GetIterator(..) | Expr::ForOfToArray(..) + | Expr::ForAwaitToArray(..) | Expr::WeakRefDeref(..) | Expr::Uint8ArrayNew(..) | Expr::Uint8ArrayLength(..) diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index 636c0542bd..695b50025c 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -2085,6 +2085,11 @@ pub enum Expr { /// else drives its `[Symbol.iterator]`. Without it the index loop read /// `.length` off a raw Map/Set handle (→ 0) and iterated zero times. ForOfToArray(Box), + /// Materialize an untyped `for await...of` receiver into a plain Array. + /// This routes through the async-iterator protocol first, then falls back + /// to the existing array/array-like behavior used by `Array.fromAsync`. + /// The lowering wraps this in `Await` before the index loop reads it. + ForAwaitToArray(Box), /// Array.from(iterable, mapFn, thisArg?) -> Array /// Creates a new array by applying mapFn to each element of the iterable. /// `this_arg` (#2773) binds `this` inside a non-arrow mapFn. diff --git a/crates/perry-hir/src/lower/stmt_loops.rs b/crates/perry-hir/src/lower/stmt_loops.rs index 6e8434bacd..8c50401e62 100644 --- a/crates/perry-hir/src/lower/stmt_loops.rs +++ b/crates/perry-hir/src/lower/stmt_loops.rs @@ -925,7 +925,11 @@ pub(crate) fn lower_stmt_for_of( } else if is_iterable_typed_array { Expr::ArrayFrom(Box::new(arr_expr)) } else if needs_runtime_iterator { - Expr::ForOfToArray(Box::new(arr_expr)) + if for_of_stmt.is_await { + Expr::Await(Box::new(Expr::ForAwaitToArray(Box::new(arr_expr)))) + } else { + Expr::ForOfToArray(Box::new(arr_expr)) + } } else { arr_expr }; diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index 6a7024697e..355c40da00 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -1299,7 +1299,11 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result { tag(h, 398); e.as_ref().hash(h); } Expr::GetIterator(e) => { tag(h, 11238); e.as_ref().hash(h); } Expr::ForOfToArray(e) => { tag(h, 11243); e.as_ref().hash(h); } + Expr::ForAwaitToArray(e) => { tag(h, 11244); e.as_ref().hash(h); } Expr::ArrayFromMapped { iterable, map_fn, this_arg } => { tag(h, 399); iterable.as_ref().hash(h); map_fn.as_ref().hash(h); this_arg.is_some().hash(h); if let Some(t) = this_arg { t.as_ref().hash(h); } } Expr::ParseInt { string, radix } => { tag(h, 400); string.as_ref().hash(h); radix.hash(h); } Expr::ParseFloat(e) => { tag(h, 401); e.as_ref().hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index 3b7b81d32b..7545f2bae8 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -243,6 +243,7 @@ where | Expr::IteratorToArray(v) | Expr::GetIterator(v) | Expr::ForOfToArray(v) + | Expr::ForAwaitToArray(v) | Expr::ObjectRest { object: v, .. } | Expr::ProxyRevoke(v) | Expr::ReflectOwnKeys(v) diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index c68139ffc4..82cd1f9b13 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -244,6 +244,7 @@ where | Expr::IteratorToArray(v) | Expr::GetIterator(v) | Expr::ForOfToArray(v) + | Expr::ForAwaitToArray(v) | Expr::ObjectRest { object: v, .. } | Expr::ProxyRevoke(v) | Expr::ReflectOwnKeys(v) diff --git a/test-parity/node-suite/globals/async-generator-for-await-variable.ts b/test-parity/node-suite/globals/async-generator-for-await-variable.ts new file mode 100644 index 0000000000..4c9e0b6e04 --- /dev/null +++ b/test-parity/node-suite/globals/async-generator-for-await-variable.ts @@ -0,0 +1,45 @@ +async function* letters() { + yield "a"; + await Promise.resolve(); + yield "b"; +} + +const collect = async (label: string, iterable: AsyncIterable) => { + const values: string[] = []; + for await (const value of iterable) { + values.push(String(value)); + if (values.length > 4) { + values.push("guard"); + break; + } + } + console.log(`${label}:`, values.join(",")); +}; + +await collect("direct", letters()); + +const held = letters(); +await collect("variable", held); + +const runner = { + async *run(prefix: string) { + yield `${prefix}1`; + await Promise.resolve(); + yield `${prefix}2`; + }, +}; +await collect("method result", runner.run("m")); + +const custom = { + async *[Symbol.asyncIterator]() { + yield "s1"; + yield Promise.resolve("s2"); + }, +}; +await collect("custom async iterable", custom); + +const promised: unknown[] = []; +for await (const value of [Promise.resolve(1), 2]) { + promised.push(value); +} +console.log("array promises:", promised.join(","));