Skip to content

jit: record the save/restore-exception pair on a no-exception bridge entry - #772

Merged
youknowone merged 1 commit into
mainfrom
perf-bridge
Jul 25, 2026
Merged

youknowone merged 1 commit into
mainfrom
perf-bridge

Conversation

@youknowone

@youknowone youknowone commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator

What

_prepare_exception_resumption records SAVE_EXC_CLASS + SAVE_EXCEPTION for the exception-guard descr flavor whether or not the deadframe carried an exception (exc_class = 0 and a null value when it did not), and prepare_resume_from_failure records RESTORE_EXCEPTION just before the entry guard (pyjitpl.py).

That pair is not decoration — it is what keeps the optimizer from deleting the entry guard. Resume data can place a removable operation ahead of the guard, and optimize_GUARD_NO_EXCEPTION (rewrite.py, pure.py) drops a GUARD_NO_EXCEPTION whose predecessor was removed. Upstream pins the hazard with test_guard_no_exception_incorrectly_removed_from_bridge (issue #2132).

The bridge sub-walk emitted the trio only on the routed pending-exception arm; the no-exception arm recorded a bare GuardNoException. This records the same trio on that arm.

Why it matters here

The removal shape is structurally present in this tree, not hypothetical:

  • both optimize_GUARD_NO_EXCEPTION ports exist — rewrite.rs keys on ctx.last_op_removed, pure.rs on last_emitted_was_removed;
  • seed_execution_context_for_walk records a heap-CSE-eligible GetfieldGcR immediately ahead of the entry guard.

If that guard is removed, the bridge is entered with a pending exception at the same source guard and runs the recorded no-exception continuation on a NULL raised-call result.

Scope — this closes a window, it does not fix an observed miscompile

Stated plainly: the guard survives today. A control build with this change reverted produces identical output on the added bench, so nothing observable changes. RestoreException is never folded by any pass, so it holds last_op_removed clear; remove_bridge_exception strips the trio again once it is consecutive with its results unused, which is the case on this arm — so the protection costs nothing at runtime.

Also drops stale upstream line-number suffixes from the surrounding comment.

Verification

  • check.py dynasm 308/308 + cranelift 308/308 on the parent commit.
  • New bench exc_bridge_entry_guard_not_removed → (28000, 4000) on dynasm, cranelift, no-JIT, pypy3 and cpython; MAJIT_STATS confirms it really compiles a bridge (bridges_compiled=1), so it is not a vacuous shape.
  • exc_mixed_classes_bridge_flavor unchanged at 2399980000, matching pypy3 on both backends.
  • No jitstats baseline drifts.

A check.py run taken while this machine was at load average ~118 (several sibling worktrees building concurrently) showed three chronic boundary perf gate-sitters failing — raise_catch, const_arg_call_resume, residual_raise_except_resume. Correctness was 306/306 non-perf and no jitstats drifted, so those are load artifacts rather than an effect of this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of exception-guard scenarios during JIT execution.
    • Added coverage for repeated exception handling across loop iterations.
  • Documentation

    • Expanded internal explanations for exception restoration and guard behavior.

…entry

`_prepare_exception_resumption` records SAVE_EXC_CLASS + SAVE_EXCEPTION for
the exception-guard descr flavor whether or not the deadframe carried an
exception (exc_class = 0 and a null value when it did not), and
`prepare_resume_from_failure` records RESTORE_EXCEPTION just before the
entry guard (pyjitpl.py). The pair keeps the optimizer from deleting that
guard: resume data can place a removable operation ahead of it, and
`optimize_GUARD_NO_EXCEPTION` (rewrite.py, pure.py) drops a
GUARD_NO_EXCEPTION whose predecessor was removed. Upstream pins this with
test_guard_no_exception_incorrectly_removed_from_bridge (issue #2132).

The bridge sub-walk emitted the trio only on the routed pending-exception
arm; the no-exception arm recorded a bare GuardNoException. Both
`optimize_GUARD_NO_EXCEPTION` ports exist here (rewrite.rs on
`ctx.last_op_removed`, pure.rs on `last_emitted_was_removed`), and
`seed_execution_context_for_walk` records a heap-CSE-eligible GetfieldGcR
immediately ahead of that guard, so the removal shape is reachable. Record
the same trio on the no-exception arm.

The guard survives today — no bench changes behaviour with or without this
commit, and the added bench passes either way — so this closes the removal
window rather than fixing an observed miscompile. RestoreException is never
folded by any pass, so it holds `last_op_removed` clear;
`remove_bridge_exception` strips the trio once it is consecutive and its
results unused, which is the case on this arm.

Also drops stale upstream line-number suffixes from the surrounding comment.

check.py dynasm 308/308 + cranelift 308/308 on the parent commit; with this
change the new bench and exc_mixed_classes_bridge_flavor are bit-identical
to pypy on both backends, and no jitstats baseline drifts.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1433fbb9-c9a9-4754-9333-7dca897693ac

📥 Commits

Reviewing files that changed from the base of the PR and between c38f0f2 and cf5763a.

📒 Files selected for processing (2)
  • pyre/bench/synth/exc_bridge_entry_guard_not_removed.py
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs

Walkthrough

Adds a standalone benchmark for repeated exception-guard bridge execution and expands documentation for the corresponding GuardNoException path in bridge_subwalk.rs. The Rust executable logic remains unchanged.

Changes

Exception guard handling

Layer / File(s) Summary
Exception benchmark execution
pyre/bench/synth/exc_bridge_entry_guard_not_removed.py
Defines exception-producing control flow, descending-loop counters, repeated aggregation, and top-level result printing.
Bridge exception-path documentation
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
Expands comments describing exception-state preservation in the null-pending-exception bridge branch without changing executable operations.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • youknowone/pyre#759: Covers related exception-guard bridge exception-state handling and handler-entry behavior.

Poem

A bunny hops through guards so bright,
Counting errors left and right.
The bridge keeps exceptions in sight,
While loops repeat from morn to night.
“Hop, test, and print”—what a delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: recording save/restore-exception handling on the no-exception bridge entry path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 perf-bridge

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.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit cf5763a).
Updated: 2026-07-25T03:37:28.372Z

Files in the reviewed diff
pyre/bench/synth/exc_bridge_entry_guard_not_removed.py
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

None.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs:447 ↔ rpython/jit/metainterp/pyjitpl.py:3149: Rust records nullary SAVE_EXC_CLASS/SAVE_EXCEPTION operations whose values are read by generated code from Pyre’s pending-exception storage; RPython’s record0 additionally attaches the trace-time concrete class/value to its frontend boxes. This is an IR/implementation representation adaptation; the added sequence and runtime ordering match upstream.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs:447 ↔ rpython/jit/metainterp/pyjitpl.py:3149: Pyre emits the save/restore sequence at the reconstructed walker entry immediately before GUARD_NO_EXCEPTION, whereas PyPy splits it between _prepare_exception_resumption and prepare_resume_from_failure. This accommodates Pyre’s bridge-frame reconstruction while preserving the required SAVE_EXC_CLASS → SAVE_EXCEPTION → RESTORE_EXCEPTION → GUARD_NO_EXCEPTION protection.

  • pyre/bench/synth/exc_bridge_entry_guard_not_removed.py:19 ↔ rpython/jit/metainterp/test/test_exception.py:662: The benchmark uses an ordinary Python function rather than PyPy’s @dont_look_inside residual-call directive, and replaces JitDriver loop control with a CPython-compatible hot-loop workload (pyre/bench/synth/exc_bridge_entry_guard_not_removed.py:31 ↔ rpython/jit/metainterp/test/test_exception.py:671). This is a CPython-compatible compiler/test-harness adaptation.

@youknowone
youknowone merged commit 934f49b into main Jul 25, 2026
17 of 19 checks passed
@youknowone
youknowone deleted the perf-bridge branch July 25, 2026 09:07
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.

1 participant