Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
| Expr::AsyncStepDone { .. }
| Expr::CurrentStepClosure
| Expr::AsyncFirstCall { .. }
| Expr::AsyncGenResume { .. }
| Expr::ObjectGetOwnPropertyNames(..)
| Expr::MathHypot(..)
| Expr::RegExpExecGroups => super::misc_methods::lower(ctx, expr),
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-codegen/src/expr/misc_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,33 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
Ok(blk.call(DOUBLE, "js_async_first_call", &[(DOUBLE, &step_box)]))
}

// -------- #6709: async-generator activation entry --------
// Like AsyncFirstCall but delivers a caller-supplied value + is_error
// flag to the step closure (gen.next(v) / gen.throw(e)).
Expr::AsyncGenResume {
step_closure,
value,
is_error,
} => {
let step_box = lower_expr(ctx, step_closure)?;
let value_box = lower_expr(ctx, value)?;
let is_error_lit = if *is_error {
double_literal(f64::from_bits(crate::nanbox::TAG_TRUE))
} else {
double_literal(f64::from_bits(crate::nanbox::TAG_FALSE))
};
let blk = ctx.block();
Ok(blk.call(
DOUBLE,
"js_async_generator_resume",
&[
(DOUBLE, &step_box),
(DOUBLE, &value_box),
(DOUBLE, is_error_lit.as_str()),
],
))
}

// -------- Object.getOwnPropertyNames(obj) --------
// Returns ALL own keys (including non-enumerable ones from
// defineProperty), unlike Object.keys which skips them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,14 @@ pub(crate) fn declare_core(module: &mut LlModule) {
// the right pointer on the very first state. Saves/restores
// INLINE_TRAP across the call for nested-async composition.
module.declare_function("js_async_first_call", DOUBLE, &[DOUBLE]);
// #6709: async-generator activation entry. Like js_async_first_call but
// the caller supplies the resume value + is_error flag (gen.next(v) /
// gen.throw(e)) instead of the hard-coded (undefined, false).
module.declare_function(
"js_async_generator_resume",
DOUBLE,
&[DOUBLE, DOUBLE, DOUBLE],
);

// ========== Slugify ==========
module.declare_function("js_slugify", I64, &[I64]);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/analysis/value_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,7 @@ pub fn infer_expr_type<F: HirTypeFacts + ?Sized>(expr: &Expr, env: &F) -> Type {
Type::Promise(Box::new(Type::Named("WebAssembly.Module".to_string())))
}
Expr::AsyncFirstCall { .. }
| Expr::AsyncGenResume { .. }
| Expr::WebAssemblyInstantiate(_)
| Expr::WebCryptoDigest { .. }
| Expr::WebCryptoImportKey { .. }
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,20 @@ pub enum Expr {
step_closure: Box<Expr>,
},

/// #6709. Async-generator activation entry. Like `AsyncFirstCall`, but
/// invokes the step closure with a caller-supplied `value` + `is_error`
/// instead of `(undefined, false)`. Emitted by the async-generator
/// lowering for each `gen.next(v)` / `gen.throw(e)` so the shared state
/// machine resumes: a non-error call delivers `value` to `__sent`, an
/// error call routes `value` as a throw at the current suspend state. The
/// runtime helper (`js_async_generator_resume`) sets up the
/// `CURRENT_STEP_CLOSURE` / `trap_next` TLS exactly like `AsyncFirstCall`.
AsyncGenResume {
step_closure: Box<Expr>,
value: Box<Expr>,
is_error: bool,
},

// Crypto operations
CryptoRandomBytes(Box<Expr>), // crypto.randomBytes(size) -> string (hex)
CryptoRandomUUID, // crypto.randomUUID() -> string
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/stable_hash/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ impl SH for Expr {
Expr::AsyncStepDone { value, step_closure, } => { tag(h, 442); value.as_ref().hash(h); step_closure.as_ref().hash(h); }
Expr::CurrentStepClosure => tag(h, 443),
Expr::AsyncFirstCall { step_closure } => { tag(h, 444); step_closure.as_ref().hash(h); }
Expr::AsyncGenResume { step_closure, value, is_error } => { tag(h, 12510); step_closure.as_ref().hash(h); value.as_ref().hash(h); is_error.hash(h); }
Expr::TaggedTemplateStrings { site_id, cooked, raw } => { tag(h, 445); site_id.hash(h); cooked.hash(h); raw.hash(h); }
Expr::TemplateRaw(e) => { tag(h, 446); e.as_ref().hash(h); }
Expr::RegisterClassParentDynamic { class_name, parent_expr, } => { tag(h, 447); class_name.hash(h); parent_expr.as_ref().hash(h); }
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/walker/expr_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,14 @@ where
Expr::AsyncFirstCall { step_closure } => {
f(step_closure);
}
Expr::AsyncGenResume {
step_closure,
value,
..
} => {
f(step_closure);
f(value);
}

// ─── Buffer family ───────────────────────────────────────────────
Expr::BufferFrom { data, encoding } => {
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/walker/expr_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,14 @@ where
Expr::AsyncFirstCall { step_closure } => {
f(step_closure);
}
Expr::AsyncGenResume {
step_closure,
value,
..
} => {
f(step_closure);
f(value);
}
Expr::BufferFrom { data, encoding } => {
f(data);
if let Some(e) = encoding {
Expand Down
15 changes: 14 additions & 1 deletion crates/perry-runtime/src/object/async_generator_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,20 @@ fn process_one_queued_request(state_id: usize) {
}

fn finish_after_pending_result(state_id: usize, out: *mut Promise, fulfilled: bool, value: f64) {
// #6709: settle THIS request's promise BEFORE draining the queue, so its
// reactions are enqueued ahead of the next request's. Spec order is
// AsyncGeneratorResolve (settle the front request's promise) *then*
// AsyncGeneratorDrainQueue (resume the next). Draining first let a
// synchronously-completing next request — e.g. the terminal
// `{value: undefined, done: true}` after the last `yield` — resolve and
// fire its `.then` before this (non-terminal) one, reordering the results
// of three eagerly-queued `iter.next()` calls. This only surfaced once
// async-generator `.next()` began returning a *pending* Promise for every
// `yield` (the spec `AsyncGeneratorYield(? Await(value))` tick) so this
// pending path is now always taken; before #6709 `.next()` resolved
// synchronously (busy-wait) and hit the immediate path, which already
// deferred the drain via `schedule_drain`.
settle_out(out, fulfilled, value);
let has_queue = STATES.with(|states| {
states
.borrow()
Expand All @@ -482,7 +496,6 @@ fn finish_after_pending_result(state_id: usize, out: *mut Promise, fulfilled: bo
} else {
mark_inactive(state_id);
}
settle_out(out, fulfilled, value);
}

fn finish_after_immediate_queued_result(
Expand Down
40 changes: 40 additions & 0 deletions crates/perry-runtime/src/promise/async_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,46 @@ pub extern "C" fn js_async_first_call(step_closure_nanbox: f64) -> f64 {
result
}

/// #6709. Entry point for an async-generator activation. Like
/// `js_async_first_call`, but the caller supplies the resume `value` and an
/// `is_error` flag instead of the hard-coded `(undefined, false)`.
///
/// Each `gen.next(v)` / `gen.throw(e)` / `.return(v)` on a lowered async
/// generator starts a fresh async-step activation that resumes the SHARED
/// state machine (the generator's `__state`/`__done`/`__sent` boxes persist
/// across calls). The activation's step body reads its resumed value from the
/// first arg (delivered to `__sent`) and routes an error (`is_error = true`) at
/// the current suspend state; it returns either an already-settled Promise
/// (synchronous yield/completion), a fresh pending Promise (the body suspended
/// at an inner `await` via `js_async_step_chain`), or a rejected Promise (an
/// uncaught throw). The `trap_next = null` reset mirrors `js_async_first_call`
/// so a nested async fn/gen the body invokes cannot prematurely settle THIS
/// activation's result promise; `prev` is restored on exit for composition.
#[no_mangle]
pub extern "C" fn js_async_generator_resume(
step_closure_nanbox: f64,
value: f64,
is_error: f64,
) -> f64 {
let ptr = crate::value::js_nanbox_get_pointer(step_closure_nanbox)
as *mut crate::closure::ClosureHeader;
let prev = INLINE_TRAP.with(|c| {
let old = c.get();
c.set(InlineTrap {
trap_next: std::ptr::null_mut(),
current_step: ptr as usize,
});
old
});
let result = crate::closure::js_closure_call2(ptr, value, is_error);
INLINE_TRAP.with(|c| c.set(prev));
result
}

#[used]
static KEEP_JS_ASYNC_GENERATOR_RESUME: extern "C" fn(f64, f64, f64) -> f64 =
js_async_generator_resume;

// Thread-local single-slot cache for async-step thunks. Keyed by the
// step closure pointer. When the same step closure is used across
// multiple promise-of-promise awaits (the simple-probe shape), we
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-transform/src/async_to_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ fn alloc_local(next_id: &mut LocalId) -> LocalId {
// processed recursively so awaits inside a nested `if (cond) { x = y +
// await z; }` are hoisted into the inner block, not the outer scope.

fn hoist_awaits_in_stmts(stmts: &mut Vec<Stmt>, next_id: &mut LocalId) {
pub(crate) fn hoist_awaits_in_stmts(stmts: &mut Vec<Stmt>, next_id: &mut LocalId) {
let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
for stmt in std::mem::take(stmts) {
let mut hoisted: Vec<Stmt> = Vec::new();
Expand Down
100 changes: 100 additions & 0 deletions crates/perry-transform/src/generator/linearize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ pub struct State {
pub enum StateExit {
/// Yield a value and advance to next_state
Yield { value: Expr, next_state: u32 },
/// #6709: async-generator `await` suspend point. Distinct from `Yield`:
/// the value is awaited on the microtask queue (via `AsyncStepChain`) and
/// the generator resumes WITHOUT returning `{value, done}` to the
/// consumer — the enclosing `.next()`/`.throw()` result Promise stays
/// pending until a real consumer `Yield` or completion is reached. Only
/// produced when linearizing an `async function*` body.
Await { value: Expr, next_state: u32 },
/// Goto another state (non-yielding transition)
Goto(u32),
/// Function is done
Expand Down Expand Up @@ -469,6 +476,99 @@ pub fn linearize_body(
});
}

// #6709: async-generator `await` suspend points. After
// `hoist_awaits_in_stmts` every remaining `await` sits in a
// statement-level position (bare `await x;`, `let v = await x;`,
// `return await x;`, `throw await x;`); split each into its own
// `StateExit::Await` state so the enclosing `.next()`/`.throw()`
// step driver suspends on the microtask queue instead of
// busy-waiting. The awaited (settled/rejected) value is delivered
// back through `__sent`, mirroring the two-way `yield` resume
// channel. Only fires for async generators (`Expr::Await` never
// reaches the linearizer otherwise — plain async fns rewrite
// `await`→`yield` upstream, sync generators have no `await`).
Stmt::Expr(Expr::Await(inner)) if linearize_async_generator() => {
let await_val = (**inner).clone();
let this_state = *state_num;
*state_num += 1;
states.push(State {
num: this_state,
body: std::mem::take(current),
exit: StateExit::Await {
value: await_val,
next_state: *state_num,
},
});
// Bare `await x;` — result discarded, no continuation binding.
}
Stmt::Let {
id,
init: Some(Expr::Await(inner)),
mutable,
ty,
name,
} if linearize_async_generator() => {
let await_val = (**inner).clone();
let this_state = *state_num;
*state_num += 1;
states.push(State {
num: this_state,
body: std::mem::take(current),
exit: StateExit::Await {
value: await_val,
next_state: *state_num,
},
});
// Resumed value (the settled await result) arrives via __sent.
current.push(Stmt::Let {
id: *id,
init: Some(Expr::LocalGet(sent_id)),
mutable: *mutable,
ty: ty.clone(),
name: name.clone(),
});
}
Stmt::Return(Some(Expr::Await(inner))) if linearize_async_generator() => {
let await_val = (**inner).clone();
let this_state = *state_num;
*state_num += 1;
states.push(State {
num: this_state,
body: std::mem::take(current),
exit: StateExit::Await {
value: await_val,
next_state: *state_num,
},
});
// `return await x` — the awaited value (__sent) is the return.
current.push(Stmt::Return(Some(make_iter_result(
Expr::LocalGet(sent_id),
true,
))));
let cont_state = *state_num;
*state_num += 1;
states.push(State {
num: cont_state,
body: std::mem::take(current),
exit: StateExit::Done,
});
}
Stmt::Throw(Expr::Await(inner)) if linearize_async_generator() => {
let await_val = (**inner).clone();
let this_state = *state_num;
*state_num += 1;
states.push(State {
num: this_state,
body: std::mem::take(current),
exit: StateExit::Await {
value: await_val,
next_state: *state_num,
},
});
// `throw await x` — throw the awaited value (__sent).
current.push(Stmt::Throw(Expr::LocalGet(sent_id)));
}

// #34: `return yield* inner` — delegation in return position.
// The earlier catch-all arm (`Return(Some(Yield { .. }))`) ignored
// the `delegate` flag and yielded `inner` itself as a single
Expand Down
Loading
Loading