Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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