diff --git a/changelog.d/7375-await-promise-rooting.md b/changelog.d/7375-await-promise-rooting.md new file mode 100644 index 0000000000..1d5b71e27b --- /dev/null +++ b/changelog.d/7375-await-promise-rooting.md @@ -0,0 +1,19 @@ +### Fixed + +- **`await` polled a promise that the event-loop pump had already moved.** The + await loop's `wait` block calls `js_promise_run_microtasks_await_loop`, + `js_run_stdlib_pump` and `js_await_loop_tick_timers` — all of which allocate + and can drive an evacuating minor — then branches back to `check`, which + re-unboxes the *same* SSA value. After one pump the loop polled retired + from-space, and `js_promise_state` dereferenced it. + + The existing comment there ("unbox the promise in each block that uses it") + addresses LLVM's dominance requirement, which is a different problem: the box + itself named the pre-collection promise, so unboxing it again in every block + reproduced the stale address five times rather than fixing it. + + The promise now takes a temp root once, and every block re-reads the slot the + collector rewrites instead of reusing the register. + + Closes 4 of the 31 quarantine catches in #7341 — all `obj_type=5` + (`GC_TYPE_PROMISE`) with frame #1 in generated code. diff --git a/crates/perry-codegen/src/expr/fs_await.rs b/crates/perry-codegen/src/expr/fs_await.rs index 5e7b4d37d1..af6a9ecc91 100644 --- a/crates/perry-codegen/src/expr/fs_await.rs +++ b/crates/perry-codegen/src/expr/fs_await.rs @@ -93,6 +93,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // intermediate block. Hoisted to the entry block so the // slot dominates the merge block even when this Await is // itself nested inside an if-arm. + // #7341: root the promise for the whole await loop. + // + // `wait` calls `js_promise_run_microtasks_await_loop`, + // `js_run_stdlib_pump` and `js_await_loop_tick_timers`, every one of + // which allocates and can drive an evacuating minor — then branches + // back to `check`, which re-unboxes the SAME SSA value. The comment + // below about unboxing per block solves LLVM dominance, not GC + // movement: the box names the pre-collection promise, so after one + // pump the loop polls retired from-space and `js_promise_state` + // dereferences it. + // + // 4 of the 31 catches in #7341 are this, all `obj_type=5` + // (GC_TYPE_PROMISE) with frame #1 in generated code. The temp-root + // slot is what the collector rewrites, so every block re-reads it + // instead of reusing the register. + let promise_root = crate::expr::temp_root::temp_root_push_double(ctx, &promise_box); let result_slot = ctx.func.alloca_entry(DOUBLE); // Pre-seed with the boxed operand so the non-promise // branch just needs to jump to merge. @@ -144,6 +160,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // could hoist the unbox into (check is reachable from // both the initial branch AND from `wait`). ctx.current_block = check_idx; + let promise_box = crate::expr::temp_root::temp_root_get_double(ctx, &promise_root); let promise_handle = unbox_to_i64(ctx.block(), &promise_box); let state = ctx .block() @@ -178,6 +195,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let wait_for_event_label = ctx.block_label(wait_for_event_idx); let unsettled_exit_label = ctx.block_label(unsettled_exit_idx); + let promise_box = crate::expr::temp_root::temp_root_get_double(ctx, &promise_root); let promise_handle_wait = unbox_to_i64(ctx.block(), &promise_box); let state_after_tick = ctx.block() @@ -214,6 +232,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // === settled === ctx.current_block = settled_idx; + let promise_box = crate::expr::temp_root::temp_root_get_double(ctx, &promise_root); let promise_handle2 = unbox_to_i64(ctx.block(), &promise_box); let state2 = ctx .block() @@ -230,6 +249,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // would terminate the process because `js_throw` longjmps // through a non-existent handler frame. ctx.current_block = reject_idx; + let promise_box = crate::expr::temp_root::temp_root_get_double(ctx, &promise_root); let promise_handle3 = unbox_to_i64(ctx.block(), &promise_box); let reason = ctx .block() @@ -246,6 +266,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // === done === ctx.current_block = done_idx; + let promise_box = crate::expr::temp_root::temp_root_get_double(ctx, &promise_root); let promise_handle4 = unbox_to_i64(ctx.block(), &promise_box); let value = ctx .block()