Skip to content

fix(runtime): root async trap state across step bodies - #7836

Merged
proggeramlug merged 3 commits into
mainfrom
fix/7794-async-step-roots
Aug 11, 2026
Merged

fix(runtime): root async trap state across step bodies#7836
proggeramlug merged 3 commits into
mainfrom
fix/7794-async-step-roots

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Closes #7794

Summary

  • Keep the activation result promise, the saved outer trap promise, and the saved outer step closure in mutable runtime handles across arbitrary async step-body calls.
  • Re-read all three addresses after the call before restoring INLINE_TRAP or forwarding a rejection.
  • Use the same rooted call path for fulfillment, rejection, first-call, and async-generator resumptions.
  • Add a deterministic relocation regression that failed on main because rejection forwarding targeted the pre-move promise.

Validation

  • cargo test -p perry-runtime --lib -- --test-threads=1: 2106 passed, 0 failed, 4 ignored.
  • Focused relocation regression: failed before the production change, passes after it.
  • Exact parity: test_gap_async_advanced, test_gap_async_throw_after_await_not_unhandled, test_gap_generators, and test_gap_generator_return_throw_finally all pass.
  • Forced moving collection plus relocation verification on a resumed async step: exit 0, answer=42, 6025 objects copied.
  • cargo fmt --all -- --check, git diff --check, and scripts/check_file_size.sh pass.
  • scripts/raw_handle_debt.py is already red on clean origin/main with the same 1013 versus 998 count and the same four unrelated module violations; this change adds none.

Performance

Quiet-mini timing is pending because /Users/perry/bench.lock is owned by triage-agent. This PR remains draft until the async hot-path cost is measured.

Summary by CodeRabbit

  • Bug Fixes

    • Improved async function and generator resumption when garbage collection moves promise objects.
    • Prevented stale references during suspended-step continuation and rejection handling.
    • Preserved ambient trap state correctly across async step execution.
    • Improved runtime-handle updates when objects are relocated during garbage collection.
  • Tests

    • Added coverage for relocated captured promises, prior trap promises, async step closures, and forwarded runtime handles.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf1683be-d892-4c82-887f-71f736947c0f

📥 Commits

Reviewing files that changed from the base of the PR and between a0f158e and 32b8034.

📒 Files selected for processing (3)
  • changelog.d/7836-async-step-roots.md
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/promise/async_step.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/gc/mod.rs
  • changelog.d/7836-async-step-roots.md

📝 Walkthrough

Walkthrough

Async step invocation now uses rooted mutable handles for captured promises, trap state, closures, and results. Resume paths share this logic. Relocation tests verify pointer rewriting, rejection forwarding, and trap restoration.

Changes

Async step relocation safety

Layer / File(s) Summary
Rooted step invocation
crates/perry-runtime/src/promise/async_step.rs
The shared helper roots captured activation and prior trap state, invokes the step closure, re-reads relocated pointers, restores INLINE_TRAP, and forwards rejected results.
Async step entry points
crates/perry-runtime/src/promise/async_step.rs
Initial calls, generator resumes, fulfillment thunks, and rejection thunks delegate to the shared helper.
Relocation validation
crates/perry-runtime/src/gc/mod.rs, crates/perry-runtime/src/promise/async_step.rs, changelog.d/7836-async-step-roots.md
Tests validate runtime-handle rewriting and resumed async-step behavior after object forwarding. The changelog records mutable-root handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AsyncStep
  participant RuntimeHandleScope
  participant StepClosure
  participant MovingGC
  AsyncStep->>RuntimeHandleScope: root activation, trap, closure, and result
  RuntimeHandleScope->>StepClosure: invoke async step
  StepClosure->>MovingGC: allocate during step execution
  MovingGC-->>RuntimeHandleScope: rewrite mutable roots
  RuntimeHandleScope-->>AsyncStep: provide relocated pointers
  AsyncStep->>AsyncStep: restore INLINE_TRAP and forward rejection
Loading

Possibly related PRs

  • PerryTS/perry#6727: Both changes modify async-step and generator resumption paths for GC-safe relocation.
  • PerryTS/perry#7375: Both changes root promise state and re-read relocated addresses across async operations.
  • PerryTS/perry#7529: Both changes modify async-step promise rooting and stale-pointer handling.

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary runtime fix for async trap state across step-body calls.
Description check ✅ Passed The description covers the change, linked issue, validation results, performance, and regression tests, although it omits the template checklist.
Linked Issues check ✅ Passed The changes satisfy issue #7794 by rooting relocated promises and trap state, re-reading them after calls, and safely forwarding rejections.
Out of Scope Changes check ✅ Passed The changelog entry, runtime helper, and relocation tests directly support the async-step garbage-collection fix and introduce no unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7794-async-step-roots

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Quiet-M1 performance check completed against base 82f0e96 and PR head a0f158e.

Methodology:

  • Built perry, perry-runtime-static, and perry-stdlib-static for both arms with isolated staged source and a target directory outside the repo.
  • Compiled both arms with the same saved base compiler, changing only PERRY_RUNTIME_DIR.
  • Confirmed the base/fix runtime archive hashes differed.
  • Ran one warmup plus 12 alternating A/B pairs per workload; every run exited 0 with identical output.

Results (median wall time):

  • asyncpipe: 3.435084 s -> 3.443230 s, +0.237% (median paired ratio +0.223%).
  • promise_all_chains: 0.195893 s -> 0.197088 s, +0.610% (median paired ratio +0.641%).

The async-heavy regression is below 1% in both subjects. Marking ready for review.

@proggeramlug
proggeramlug marked this pull request as ready for review August 11, 2026 10:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
crates/perry-runtime/src/promise/async_step.rs (2)

1408-1412: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why RELOCATION_CASE needs no mutable-root scanner.

RELOCATION_CASE is a thread-local side table that holds raw heap addresses of promises and closures. The repository rule requires a gc_register_mutable_root_scanner registration for such tables. This table is #[cfg(test)]-only and must retain pre-forwarding addresses for the test to work, so a scanner would defeat it. Add a short comment stating that exemption so a later reader does not treat it as a missed registration.

Note also that this test mutates the process-global INLINE_TRAP, so it must run under RUST_TEST_THREADS=1.

As per coding guidelines: "When adding a runtime or side-table cache containing raw heap pointers, register a mutable-root scanner with gc_register_mutable_root_scanner in the same change." and "Run perry-runtime tests single-threaded with RUST_TEST_THREADS=1 because they share process-global side tables and are not parallel-safe."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/promise/async_step.rs` around lines 1408 - 1412, Add
a concise comment immediately above RELOCATION_CASE documenting that its
cfg(test)-only raw-address table intentionally lacks a mutable-root scanner
because the test requires pre-forwarding addresses; also note that the
associated test must run with RUST_TEST_THREADS=1 due to mutating the
process-global INLINE_TRAP.

Source: Coding guidelines


908-924: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm the step closure pointer needs no re-read.

call_async_step_body roots captured_trap_next but not step. This is correct only if step is never used after js_closure_call2 returns. The current body satisfies that. Add a short comment so a later change does not reintroduce a stale step use after the call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/promise/async_step.rs` around lines 908 - 924, Add a
short comment in call_async_step_body documenting that step is intentionally not
rooted or re-read because it is not used after js_closure_call2 returns. Keep
the existing rooting of captured_trap_next unchanged and make the comment
explicit that future post-call use of step would require re-reading it.
crates/perry-runtime/src/gc/mod.rs (1)

61-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the helper’s root coverage.

The symbols resolve in test builds. Add a doc comment stating that this helper rewrites runtime handle roots only and does not rewrite the installed INLINE_TRAP.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/mod.rs` around lines 61 - 66, Update the test
helper test_rewrite_runtime_handles_for_forwarded_objects with a doc comment
stating that it rewrites runtime handle roots only and does not rewrite the
installed INLINE_TRAP.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/7836-async-step-roots.md`:
- Around line 3-4: Update the final clause of the changelog entry to describe
preventing use of pre-collection addresses or stale pointers, rather than
preventing restoration or dereferencing of relocated objects. Preserve the
existing explanation of mutable roots and moving collections, and do not add a
version header.

In `@crates/perry-runtime/src/promise/async_step.rs`:
- Around line 1442-1467: Update the relocation test teardown around
relocating_step and TrapGuard::drop to snapshot each forwarded source object’s
first payload word before forwarding, restore those words afterward, and
explicitly clear GC_FLAG_FORWARDED on all three source objects. Add the minimal
local cleanup needed since the forwarding API has no clear helper, while
preserving the existing INLINE_TRAP and RELOCATION_CASE restoration.

---

Nitpick comments:
In `@crates/perry-runtime/src/gc/mod.rs`:
- Around line 61-66: Update the test helper
test_rewrite_runtime_handles_for_forwarded_objects with a doc comment stating
that it rewrites runtime handle roots only and does not rewrite the installed
INLINE_TRAP.

In `@crates/perry-runtime/src/promise/async_step.rs`:
- Around line 1408-1412: Add a concise comment immediately above RELOCATION_CASE
documenting that its cfg(test)-only raw-address table intentionally lacks a
mutable-root scanner because the test requires pre-forwarding addresses; also
note that the associated test must run with RUST_TEST_THREADS=1 due to mutating
the process-global INLINE_TRAP.
- Around line 908-924: Add a short comment in call_async_step_body documenting
that step is intentionally not rooted or re-read because it is not used after
js_closure_call2 returns. Keep the existing rooting of captured_trap_next
unchanged and make the comment explicit that future post-call use of step would
require re-reading it.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cc758c4-2681-4c7a-a4e2-1fdfd18e1de3

📥 Commits

Reviewing files that changed from the base of the PR and between 82f0e96 and a0f158e.

📒 Files selected for processing (3)
  • changelog.d/7836-async-step-roots.md
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/promise/async_step.rs

Comment thread changelog.d/7836-async-step-roots.md Outdated
Comment thread crates/perry-runtime/src/promise/async_step.rs
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in 32b8034:

  • The relocation fixture now snapshots and restores each forwarded source object's first payload word and explicitly clears GC_FLAG_FORWARDED during teardown.
  • The test asserts that cleanup happened before returning.
  • Documented the test-only side-table scanner exemption, the runtime-handle-only helper coverage, and why the step pointer needs no post-call re-read.
  • Reworded the changelog entry in terms of avoiding stale pre-collection addresses.

Validation:

  • Focused relocation test: passed.
  • Full single-threaded perry-runtime library suite: 2,106 passed, 0 failed, 4 ignored.
  • cargo fmt --check, git diff --check, and scripts/check_file_size.sh: passed.
  • raw_handle_debt.py remains at the pre-existing 1,013 vs 998 baseline with the same four unrelated module violations; no increase from this patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GC: async_step_fulfill_thunk holds two bare *mut Promise locals across the step-body call (asyncpipe SIGBUS)

1 participant