Skip to content

jit: propagate a handler-raised exception through the func-entry blackhole resume - #641

Merged
youknowone merged 1 commit into
mainfrom
ec-wiring
Jul 18, 2026
Merged

youknowone merged 1 commit into
mainfrom
ec-wiring

Conversation

@youknowone

@youknowone youknowone commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes a deterministic silent dynasm/cranelift JIT miscompile found by a fuzz
sweep (hunt-3), then confirmed as a new manifestation of the blackhole-failure
family (#15). jit_out != nojit_out on the same binary; NOJIT and CPython agree.

Symptom

A function with a hot return path and a cold exception path where an
except X as m: handler raises a second exception of a different class,
in a function that also has a sibling except clause, miscompiles: exactly
one iteration routes the propagated exception to the wrong except clause in
the caller.

def f(i):
    try:
        if i % 9 == 0:
            raise ValueError(i)
        return 1
    except ValueError as m:
        if m.args[0] % 2 == 0:
            raise                 # bare reraise -> ValueError
        raise KeyError            # a DIFFERENT class
    except KeyError:              # sibling clause
        return 100

def run(n):
    ve = ke = other = 0
    for i in range(n):
        try: other += f(i)
        except ValueError: ve += 1
        except KeyError: ke += 1
    return ve, ke, other

print(run(9000))
# CPython / NOJIT: (500, 500, 8000)   JIT (before): (501, 499, 8000)

Root cause

f compiles as a func-entry trace of its return path; the cold i % 9 == 0
iteration guard-fails into the blackhole, which replays the exception path,
raises the ValueError, catches it, raises the handler's KeyError, then reaches a
Void leave-frame that drops the exception (BlackholeResult::Failed →
invalidate_loop), so the caller matches the first exception's class. The
defect is in trace construction (the walker), not the backend exception cells:

  • When a shared catch landing's FrameState widens, the earlier incoming
    exception links' outgoing args were not remapped, corrupting the exception
    register kinds/positions. Remap every incoming exception link on widen.
  • The cleanup RERAISE could read a null symbolic placeholder instead of the
    live exception; load it from the semantic frame slot when handler entry
    carries a null placeholder.
  • carry_explicit_raise_value_on_catch_stack no longer swaps the
    last_exception / last_exc_value link kinds.
  • The dynamic DELETE_FAST bound check's exception arm is split explicitly and
    its successful continuation is kept off the generic per-op catch edge.

This also subsumes the #633 cluster-B DELETE_FAST reraise-sibling shape, so
its source-shape abort_permanent decline is removed — that family now
JIT-compiles instead of falling back to the interpreter.

Verification

  • Repro + synth/handler_reraise_second_exc → (500, 500, 8000), jit == nojit,
    both backends.
  • jit: fix five fuzz-found dynasm miscompiles (newslice, short-preamble hoist, bridge global fold, DELETE_FAST reraise, nested-break resume) #633 cluster-B bench named_reraise_sibling_hot → 38172 (jit == nojit ==
    CPython) with the decline gone; the hunt-2 B family sweeps (37 cases) pass via
    the real fix; the E family sweeps (412 cases) are unaffected.
  • python ./pyre/check.py --backend dynasm and --backend cranelift →
    ALL PASSED (224/224 each).
  • cargo test -p pyre-jit --features dynasm green, including two new tests
    (named_handler_cleanup_with_sibling_clause_emits_delete_fast_path,
    explicit_raise_stack_value_preserves_exception_link_kinds).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved exception handling across complex try/except flows, including re-raised exceptions and sibling handlers.
    • Fixed cleanup behavior for named exception handlers so valid execution paths are preserved.
    • Corrected handling of exceptions raised explicitly within handlers, maintaining accurate exception state.
    • Improved bound-check exception handling to prevent incorrect propagation or control-flow behavior.
  • Tests

    • Added coverage for handler cleanup, re-raising scenarios, and exception-state preservation.

…khole resume

A named `except X as m:` handler that raises a second exception of a
different class, in a function that also has a sibling `except` clause,
miscompiled under a func-entry trace of the return path: the cold exception
path deopts into the blackhole, which dropped the handler-raised exception at
a Void leave-frame (BlackholeResult::Failed -> invalidate_loop), so the caller
matched the first exception's class. jit output diverged from the interpreter
by one iteration.

- When a shared catch landing's FrameState widens, remap every incoming
  exception link's outgoing args so the exception register kinds and positions
  stay aligned.
- Load the cleanup RERAISE's exception from its semantic frame slot when the
  handler entry carries a null symbolic placeholder, instead of a fresh
  placeholder.
- Split the dynamic DELETE_FAST bound check's exception arm explicitly and keep
  its successful continuation off the generic per-op catch edge.
- Drop the source-shape DELETE_FAST reraise-sibling abort_permanent decline;
  the cleanup path now compiles correctly, restoring JIT coverage for it.

Regression bench: synth/handler_reraise_second_exc.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 18, 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

Run ID: 3a829703-3ea2-4016-890d-fc3d0b335c6a

📥 Commits

Reviewing files that changed from the base of the PR and between a7fe462 and 4e3174c.

📒 Files selected for processing (2)
  • pyre/bench/synth/handler_reraise_second_exc.py
  • pyre/pyre-jit/src/jit/codewriter.rs

Walkthrough

The PR adds a benchmark for nested exception-handler re-raises and updates JIT frame-state merging, handler exception-value loading, cleanup control flow, and exception-edge handling, with regression tests for named-handler cleanup and explicit raise stack metadata.

Changes

Exception-handler correctness

Layer / File(s) Summary
Benchmark and handler shape
pyre/bench/synth/handler_reraise_second_exc.py
Adds f, run, and N to exercise bare re-raises, converted exceptions, sibling handlers, and aggregated results.
Exception-link state merging
pyre/pyre-jit/src/jit/codewriter.rs
Tracks incoming exception links and remaps their arguments when catch targets receive merged frame states; preserves explicit raise exception-link kinds.
Handler and exception-edge control flow
pyre/pyre-jit/src/jit/codewriter.rs
Loads handler exception values from frame slots when needed, preserves bound-check continuations, avoids duplicate catch wiring, removes obsolete cleanup detection, and adds regression coverage.

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

Possibly related issues

  • youknowone/pyre#595 — Directly relates to JIT exception-handler re-raise, nested-handler correctness, and exception-state handling.

Possibly related PRs

  • youknowone/pyre#48 — Modifies related exception-pair metadata and handler/catch lowering in codewriter.rs.
  • youknowone/pyre#135 — Addresses related JIT catch/handler framestate and exception-edge behavior.
  • youknowone/pyre#563 — Touches related JIT exception propagation and blackhole exception tracking.

Suggested reviewers: lifthrasiir

Poem

A rabbit hops through handlers bright,
Re-raising errors into night.
Links are merged, states align,
Bound checks follow every sign.
Tests guard each exception trail—
JIT paths thump their sturdy tail.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main JIT fix: propagating a handler-raised exception through func-entry blackhole resume.
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 ec-wiring

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 4e3174c).
Updated: 2026-07-18T06:08:25.129Z

Files in the reviewed diff
pyre/bench/synth/handler_reraise_second_exc.py
pyre/pyre-jit/src/jit/codewriter.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)

  • pyre/pyre-jit/src/jit/codewriter.rs:9358 ↔ pypy/interpreter/pyopcode.py:1361: WITH_EXCEPT_START only pushes a fresh symbolic value ("push_fresh_ref") and emits neither the required __exit__ call nor an abort/deopt; PyPy calls call_contextmanager_exit_function(...). This code is unchanged from upstream/main.

4. Structural adaptations

  • pyre/pyre-jit/src/jit/codewriter.rs:694 ↔ rpython/flowspace/flowcontext.py:139: Pyre coalesces multiple exceptional predecessors by handler PC and retains "incoming_exception_links" to rewrite old links when the landing state widens; RPython creates one EggBlock per exception edge. The new remapping at codewriter.rs:1505 preserves the required target-input alignment.
  • pyre/pyre-jit/src/jit/codewriter.rs:9290 ↔ pypy/interpreter/pyopcode.py:1365: CPython-3.11-compatible RERAISE stack/lasti bytecode has no direct RPython flowspace equivalent. The patch’s "getarrayitem_vable_r" recovery of a null symbolic exception slot is a Rust virtual-frame adaptation; it preserves PyPy’s TOS exception value before the pop.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e3174c9d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +11718 to +11721
joinpoints
.entry(py_pc + 1)
.or_insert_with(Vec::new)
.insert(0, bound_block.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not hide queued joinpoints after DELETE_FAST

When DELETE_FAST is immediately followed by a PC that is already a branch target, e.g. if c: del x; return x, that target block is already present in joinpoints[py_pc + 1] and queued for later walking. Inserting the DELETE_FAST success block at index 0 makes emit_mark_label_pc! pick this new block when the queued branch-target block is later popped (start_pc == py_pc skips the merge path), so the original branch target is never emitted and its predecessor still points at an empty/orphan block. Register the continuation without displacing existing pending joinpoints, or merge with the existing candidate.

Useful? React with 👍 / 👎.

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