Skip to content

exceptions: GC-manage W_BaseException; FBW walker raise-across-call resume + setslice fold (#171) - #593

Merged
youknowone merged 8 commits into
mainfrom
issue171
Jul 17, 2026
Merged

youknowone merged 8 commits into
mainfrom
issue171

Conversation

@youknowone

@youknowone youknowone commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Five commits stacked on issue171, spanning full-body-walk (FBW) residual
folding and the exception GC lifecycle.

Commits

  1. jit: fold same-length setslice on the FBW walker — intercept the
    StoreSubscr residual for a same-length, step-1, non-negative constant-bounds
    Integer→Integer slice with exact-list target/source, storing elements through
    int_items and eliding the CALL_MAY_FORCE (falls through to the opaque
    residual for anything outside the gate). Gated PYRE_NEWLIST_VIRT (default-on).

  2. jit: resume at caller's handler for raise-across-call on the FBW walker —
    generalize the guard-capture catch-resume from FOR_ITER_NEXT to any residual
    CALL whose pc is covered by an exceptiontable entry
    (residual_call_catch_resume), and record a top-level uncaught raise against
    exit_frame_with_exception_descr (FINISH { exit_with_exception: true })
    instead of aborting the bridge.

  3. interp: keep the in-flight exception's traceback chain a GC root —
    publish the propagating exception in a thread-local IN_FLIGHT_EXCEPTION at
    record_application_traceback and forward its w_traceback chain from the
    registered extra-root walker, so a non-moving major cannot sweep the chain of
    an off-GC exception.

  4. interp: record/trace the tracer-replaced exception in handle_exception —
    when bytecode_trace_after_exception replaces the propagating error, re-derive
    the exception object from the possibly-replaced err before recording the
    traceback and tracing (pyopcode.py:144-149).

  5. interp_exceptions: GC-manage W_BaseException via oldgen-stable alloc —
    w_exception_new_empty allocates through try_gc_alloc_stable_raw
    (W_BASE_EXCEPTION_GC_TYPE_ID, non-moving oldgen) with note_alloc +
    write_barrier and a malloc_typed fallback (mirroring w_generator_new);
    the prebuilt singletons stay immortal via w_exception_new_empty_immortal.
    walk_in_flight_exception forwards the exception object slot so the collector
    recurses into W_BASE_EXCEPTION_GC_PTR_OFFSETS, keeping
    walk_raw_exception_roots for the off-GC fallback.

Effect

Commit 5 makes W_BaseException sweepable, which fixes both the exception leak
(off-GC malloc_typed exceptions were never reclaimed) and a gc.collect()
crash at majit-gc/src/trace.rs:934: a suspended generator's frame held a
w_traceback subgraph through an off-GC exception the collector marked but never
recursed into, so the subgraph was swept while reachable and finalizer_children
later dereferenced a freed header. Native test_exceptions went from ~70–80%
panics to 0/30.

Verification

  • pyre/check.py: dynasm 216/216, cranelift 216/216, wasm 215/215 (3/3 backend runs).
  • native test_exceptions: 0/30 panics (residual non-zero exit is a pre-existing
    ast.parse-returns-code-object gap in traceback caret rendering, orthogonal to GC).
  • pyre-object exception unit tests 6/6 (singleton idempotency + GC tid).

Follow-up

Raw-i64 JIT exception carriers (BH_LAST_EXC_VALUE, backend _store_exception
cells, JIT_EXC_VALUE/TYPE), PENDING_CALL_ERROR, and a scoped IN_FLIGHT
clear are not yet registered GC roots. They are unreachable today (pyre collects
only at safepoints, absent from the to_exc_object→publish→guard windows, and
IN_FLIGHT transitively roots the last-raised exception), becoming relevant only
under free-threading. Tracked separately.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved exception and traceback handling during execution and debugging.
    • Fixed propagation of uncaught exceptions in JIT-compiled code.
    • Prevented exceptions and their traceback chains from being lost during garbage collection.
    • Improved handling of exception replacement by trace hooks.
  • Performance

    • Added optimized slice assignment for eligible integer-based lists.
    • Improved allocation and lifecycle handling for built-in exception instances.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The changes add in-flight exception GC roots, update traceback handling for tracer-replaced exceptions, defer top-level JIT exception finishes, improve residual-call catch recovery, specialize eligible list slice stores, and use GC-aware exception allocation for regular and singleton instances.

Changes

Exception lifetime and propagation

Layer / File(s) Summary
GC-aware exception allocation
pyre/pyre-object/src/interp_exceptions.rs
Empty exceptions use stable GC allocation for regular instances and immortal allocation for cached singleton exceptions.
In-flight exception rooting and trace replacement
pyre/pyre-interpreter/src/eval.rs, pyre/pyre-interpreter/src/pytraceback.rs
Exception objects are published as GC roots during traceback and trace-hook processing, and later traceback dispatch uses any tracer-replaced exception.

JIT exception dispatch

Layer / File(s) Summary
Deferred uncaught exception finish
pyre/pyre-jit-trace/src/jitcode_dispatch.rs, pyre/pyre-jit-trace/src/trace.rs
Top-level raises stash exception payloads and are converted into reference-typed exception finishes with exit_with_exception enabled. Related tests validate the deferred payload state.
Residual-call catch recovery
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
Residual-call guard snapshots can resume through a call-specific exception-table catch.

Virtual list slice specialization

Layer / File(s) Summary
Specialized list slice stores
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
Eligible constant-slice stores on exact integer-strategy lists emit guarded per-element updates with concrete tracing and journaling.

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

Possibly related issues

  • youknowone/pyre#581 — Covers related GC rooting and lifetime handling for in-flight exception references across JIT/interpreter transitions.

Possibly related PRs

  • youknowone/pyre#563 — Modifies adjacent JIT exception propagation, traceback recording, and exception tracking paths.

Poem

A rabbit watched exceptions hop,
While GC roots kept traceback atop.
JIT raises queued their finish bright,
Lists sliced swiftly, element by byte.
Catch paths found their proper door—
“Hop!” said the hare, “and sweep no more!”

🚥 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 accurately reflects the main changes: exception GC handling, FBW raise-resume behavior, and setslice folding.
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 issue171

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
pyre/pyre-jit-trace/src/jitcode_dispatch.rs

ast-grep timed out on this file


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

github-actions Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 36cc36b).
Updated: 2026-07-17T18:42:22.486Z

Files in the reviewed diff
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/pytraceback.rs
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-object/src/interp_exceptions.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:23581 ↔ pypy/objspace/std/listobject.py:1792: the new setslice fast path reads source[j] and immediately writes target[start + j]. When source and target are the same list with overlapping ranges, later reads observe earlier writes. PyPy detects aliasing (other_items is items) and copies right-to-left. Example: a[1:3] = a[0:2] becomes [0, 0, 0, 3] here instead of PyPy’s [0, 0, 1, 3].

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/eval.rs:744 ↔ pypy/interpreter/executioncontext.py:240: IN_FLIGHT_EXCEPTION is set but is never cleared. Consequently, every thread permanently roots the most recently traceback-recorded exception and its traceback/frame graph after it has been caught. PyPy’s execution-context exception state has an explicit None clearing path at executioncontext.py:245-247; this TLS root has no equivalent lifecycle.

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

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:10193 ↔ rpython/jit/metainterp/pyjitpl.py:3262: top-level exception exits still omit store_token_in_vable() before FINISH. Upstream calls it before compile_exit_frame_with_exception; the prior upstream/main inline ctx.trace_ctx.finish(...) path omitted it too, so this is retained rather than introduced.

  • pyre/pyre-object/src/interp_exceptions.rs:1279 ↔ rpython/rtyper/exceptiondata.py:34: standard_exc_instance is process-global (OnceLock) while its w_class is selected from the per-thread EXC_CLASS_BY_KIND registry. The first thread to initialize an instance fixes another thread’s exception-class identity into the shared object. This was already present before the patch.

4. Structural adaptations

  • pyre/pyre-object/src/interp_exceptions.rs:574 ↔ pypy/module/exceptions/interp_exceptions.py:120: allocating ordinary exceptions in Pyre’s non-moving old generation is a Rust/raw-pointer adaptation; PyPy relies on its managed object-space allocation and does not need a stable-address branch.

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:12566 ↔ rpython/jit/metainterp/pyjitpl.py:2610: consulting CPython-style exception tables and converting Pyre’s code-unit PC to byte offsets is required by Pyre’s CPython-compatible compiler; RPython directly stores/restores JitCode frame PCs.

  • pyre/pyre-jit-trace/src/trace.rs:3193 ↔ rpython/jit/metainterp/pyjitpl.py:3266: deferring FINISH construction through TraceAction::Finish is a Rust tracing-pipeline adaptation of RPython’s direct history.record(FINISH, ...) call.

@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: 24e47a88ea

ℹ️ 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 +21355 to +21359
let Some(src_item) = (unsafe { pyre_object::w_list_getitem(value_obj, j) }) else {
unreachable!("setslice specialization: source index {j} has no element");
};
pyre_object::gc_roots::pin_root(src_item);
let Some(displaced) = (unsafe { pyre_object::w_list_getitem(list_obj, tgt_index) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refresh roots after boxing slice items

When this specialized path runs with the moving/minor GC and w_list_getitem(value_obj, j) boxes an integer element, that allocation can relocate the target list. The code then immediately uses the stale list_obj for the displaced read, and later similarly uses pinned src_item/displaced without re-reading their forwarded shadow-stack slots after further boxing; a collection during slice assignment can therefore read or write through stale pointers. Re-read the forwarded operands/roots after each allocation before continuing.

Useful? React with 👍 / 👎.

`target[const_slice] = source` lowers to STORE_SUBSCR with a constant
`slice` key (`store_subscr_fn`).  When `source` is a virtualizable BUILD_LIST
temp the residual force-materialises it every iteration.

Intercept the StoreSubscr residual on the full-body-walk path for a
same-length, step-1, non-negative constant-bounds, Integer-to-Integer slice
with an exact-list target and source: guard both lists' class / exact w_class /
Integer strategy and int_items length, then read the source elements through
`getfield_gc(int_items)` + `getarrayitem_gc` and store them into the target's
`int_items` block, eliding the `CALL_MAY_FORCE`.  When the source is the freshly
virtualized list the optimizer folds the reads against its `SetarrayitemGc`
stores and the temporary DCEs.

Falls through to the opaque residual (always byte-correct) for anything outside
the gate: a non-constant / None / negative bound, a non-unit step, a
length-changing slice, a non-Integer-storage target or source, or a list
subclass.  Gated on `PYRE_NEWLIST_VIRT` (default-on).

Assisted-by: Claude
Generalize the guard-capture catch-resume from FOR_ITER_NEXT to any
residual CALL: rename GuardCaptureScope::foriter_next_catch_resume to
residual_call_catch_resume and set it whenever the residual CALL's pc is
covered by an exceptiontable entry (pycode::lookup_exceptiontable),
instead of only for is_for_iter_next.

A top-level uncaught raise is now recorded against
exit_frame_with_exception_descr rather than aborting the bridge:
fbw_terminate_with_raise stashes the exception box as an is_exception
FINISH payload behind FBW_FINISH_IS_EXCEPTION, and the Terminate arm in
full_body_walk_trace reads fbw_finish_is_exception() to build
TraceAction::Finish { exit_with_exception: true }.

Assisted-by: Claude
W_BaseException is allocated off-GC (w_exception_new_empty ->
malloc_typed), so the collector never reaches it as a root, while its
w_traceback chain is GC-managed oldgen. A non-moving major (enabled on
wasm) could sweep that chain and leave exc.w_traceback dangling.

Publish the exception being propagated in a thread-local
IN_FLIGHT_EXCEPTION at record_application_traceback, and forward its
w_traceback chain head from walk_global_prebuilt_roots (the registered
extra-root walker) so the traceback stays reachable across a collection.

Assisted-by: Claude
When bytecode_trace_after_exception replaces the propagating error (a
tracer or signal handler raised during exception handling),
record_application_traceback and exception_trace were passed the
exception object captured before the trace hook ran, not the
replacement. Re-derive the exception object from the possibly-replaced
`err` after the trace block and use it for both, matching
pyopcode.py:144-149 (`except OperationError as e: operr = e` then records
and traces `operr`).

Assisted-by: Claude
w_exception_new_empty allocates through try_gc_alloc_stable_raw
(W_BASE_EXCEPTION_GC_TYPE_ID, non-moving oldgen) with note_alloc +
write_barrier and a malloc_typed fallback, mirroring w_generator_new.
The prebuilt singletons memory_error_singleton / standard_exc_instance
route through a new w_exception_new_empty_immortal that keeps the
malloc_typed path, since they are cached in GC-invisible OnceLocks and
baked into JIT constant pools.

walk_in_flight_exception forwards the exception object slot itself so
the collector marks the GC-managed exception and recurses into its
W_BASE_EXCEPTION_GC_PTR_OFFSETS children, and retains walk_raw_exception_roots
for the off-GC (no-GC-installed) fallback.

Assisted-by: Claude

@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: 288bc59c05

ℹ️ 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 +574 to +577
let raw = crate::gc_hook::try_gc_alloc_stable_raw(
W_BASE_EXCEPTION_GC_TYPE_ID,
W_BASE_EXCEPTION_SIZE,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Root managed exceptions before allocating their args

When the GC hook is installed, this path makes every w_exception_new_empty() result an old-gen managed object. Callers such as w_exception_new() and PyError::to_exc_object() keep that pointer only in a Rust local and then call w_str_new()/w_list_new() to build args; a collection during those allocations cannot see the exception and can sweep it before w_exception_set_args(exc, ...) writes through it. Please pin/root the new exception, or build all allocating payloads before publishing it as a managed object.

Useful? React with 👍 / 👎.

// catches it, it lives only in the in-flight Rust `PyError`, so a
// safepoint's non-moving major would otherwise sweep its old-gen
// traceback chain (`tstate->current_exception` parity).
crate::eval::set_in_flight_exception(w_exc_object);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Publish in-flight exceptions before running trace hooks

This root is installed only when record_application_traceback runs, but handle_exception() materializes err.exc_object before calling bytecode_trace_after_exception, and a Python trace function or pending action can allocate and collect before execution reaches this line. With exceptions now old-gen managed, the original exception lives only in the PyError Rust local during that window and can be swept; root it before invoking the trace hook, and update the root if the hook replaces err.

Useful? React with 👍 / 👎.

@youknowone youknowone changed the title jit: fold same-length setslice on the FBW walker (#171) exceptions: GC-manage W_BaseException; FBW walker raise-across-call resume + setslice fold (#171) Jul 17, 2026
… payload

`inline_call_recursion_propagates_subraise_from_callee` and
`raise_r_emits_guard_class_when_concrete_exc_pinned_in_shadow` predate the
deferred-FINISH design (c691648): a top-level uncaught SubRaise now stashes
the exception as an `is_exception` finish payload for the FBW Terminate arm's
compile consumer instead of recording an inline FINISH in `walk()`. Assert the
stashed payload (`fbw_finish_is_exception` + `fbw_finish_payload_take`) rather
than an inline `Finish` op; `raise/r` still records its inline GuardClass.

Assisted-by: Claude
…pply-loop

The FBW setslice specialization applied the assignment as journaled setitems,
pinning fresh boxes onto the shadow stack but reusing stale `list_obj` /
`src_item` / `displaced` Rust locals after intervening `w_list_getitem` /
`w_int_new` boxes. A moving minor collection rewrites the shadow-stack slot but
not the Rust local, so those reads/writes could go through pre-move pointers.
Reload every live ref after each boxing allocation before its next use — walker
operands via `walker_concrete_ref_object`, pinned boxes via `shadow_stack_get`
with the slot index captured just before the pin — matching the push_roots /
pop_roots livevar reload discipline.

Assisted-by: Claude
…exception

`handle_exception` materialized the old-gen managed exception into the `PyError`
local, then ran `bytecode_trace_after_exception` (arbitrary Python, allocating)
before `record_application_traceback` published it as the in-flight GC root. A
collection during the trace hook could sweep the still-unrooted exception.
Publish `err.exc_object` via `set_in_flight_exception` before the trace hook;
`record_application_traceback` re-publishes any tracer-replaced operr afterward.

Assisted-by: Claude

@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

https://github.com/youknowone/pyre/blob/36cc36b6f824a69c03c2c8fe93c0ab73ed2f846d/pyre-jit-trace/src/trace.rs#L2971
P1 Badge Route P2 exception finishes as exceptions

When a multiframe/P2 outer continuation terminates via fbw_terminate_with_raise, this new finish_is_exception check is not used: drive_outer_continuation_and_map still consumes the same fbw_finish_payload_take() and always builds TraceAction::Finish { exit_with_exception: false } for non-void payloads. In that bridge/resume path, an uncaught raise across the resumed outer call is compiled as a normal return of the exception object instead of ExitFrameWithExceptionRef, so the caller observes a returned exception rather than a propagated raise.


https://github.com/youknowone/pyre/blob/36cc36b6f824a69c03c2c8fe93c0ab73ed2f846d/pyre-interpreter/src/eval.rs#L744-L745
P2 Badge Clear stale in-flight exception roots

The new TLS root is only ever assigned an exception object, so after a handled exception is pushed onto the handler stack / sys_exc_value, walk_in_flight_exception keeps the last propagated exception and its traceback graph rooted until another exception overwrites it. In long-running code that raises once and then continues, this permanently retains the traceback frames and their locals; clear this slot when propagation ends or when ownership moves to the normal exception roots.

ℹ️ 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".

@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: 1

🤖 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 `@pyre/pyre-interpreter/src/eval.rs`:
- Around line 726-776: Replace the single overwriteable IN_FLIGHT_EXCEPTION Cell
with a scoped, nestable root stack or guard-based mechanism. Update
set_in_flight_exception and the corresponding propagation lifecycle so each
raised exception is pushed before handling and its prior root is restored or
removed when propagation ends, preserving outer PyError exceptions during nested
trace-hook handling. Ensure walk_in_flight_exception visits every active
in-flight exception and its traceback roots.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9fe7ffd3-fcec-4d2d-9ec7-f5872440d04a

📥 Commits

Reviewing files that changed from the base of the PR and between 4617339 and 36cc36b.

📒 Files selected for processing (5)
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/pytraceback.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-object/src/interp_exceptions.rs

Comment on lines +726 to +776
thread_local! {
/// The exception currently being raised / propagated up the Rust call
/// stack. Between the raising frame's `record_application_traceback` and
/// the frame that finally catches it, the exception is held only in the
/// Rust `PyError` value in flight — not on any frame's value stack or in
/// `ec.sys_exc_value` yet. `W_BaseException` is std::alloc-backed (outside
/// the managed nursery/old-gen), so the collector never reaches it as a
/// root and never traces its slots; a dispatch-loop safepoint running the
/// non-moving old-gen major would sweep its old-gen traceback chain from
/// under it. Mirrors `tstate->current_exception`: keep the in-flight
/// exception's traceback chain a GC root until the exception is caught.
static IN_FLIGHT_EXCEPTION: Cell<PyObjectRef> = const { Cell::new(pyre_object::PY_NULL) };
}

/// Publish the exception now being raised / propagated so the GC root walker
/// keeps it (and its traceback chain) alive across a collection. Called from
/// `record_application_traceback`, the single chokepoint every raising frame
/// passes through.
pub fn set_in_flight_exception(exc: PyObjectRef) {
IN_FLIGHT_EXCEPTION.with(|c| c.set(exc));
}

fn walk_in_flight_exception(visitor: &mut dyn FnMut(&mut majit_ir::GcRef)) {
IN_FLIGHT_EXCEPTION.with(|c| {
let exc = c.get();
if exc.is_null() {
return;
}
// Forward the exception OBJECT slot itself. A GC-managed exception
// (oldgen-stable `w_exception_new_empty`) is marked here and the
// collector then recurses into its `W_BASE_EXCEPTION_GC_PTR_OFFSETS`
// slots via offset tracing, keeping the whole graph alive. The
// exception is oldgen-stable (non-moving), so the slot is not rewritten,
// but marking is what matters. Mirrors the value-stack walker, which
// forwards the on-stack exception slot then walks its raw children.
let slot = c.as_ptr();
// SAFETY: `slot` points at the `Cell<PyObjectRef>`'s interior, which is
// valid for the duration of this closure. `PyObjectRef` and `GcRef`
// share layout (`*mut PyObject`).
unsafe { visitor(&mut *(slot as *mut majit_ir::GcRef)) };
// Off-GC fallback: when the GC is not installed the exception is
// `malloc_typed`-immortal, so the visitor above is a no-op and the
// collector never traces its slots. Forward ALL its GC-managed children
// in place — the whole `W_BASE_EXCEPTION_GC_PTR_OFFSETS` set (args_w,
// w_cause, w_context, the w_traceback chain, w_dict, and the per-subclass
// slots). Read the object AFTER the visitor so a (hypothetically) moved
// exception is the live one. The slots are forwarded by address (not the
// barriered setter), so a moving child is relocated with no re-entry.
let exc = unsafe { *(slot as *const PyObjectRef) };
unsafe { walk_raw_exception_roots(exc, visitor) };
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Use a nested, scoped root instead of one overwriteable Cell.

A trace hook can raise or catch another exception, replacing this slot while the outer exception still exists only in its Rust PyError. A collection during that nested handling can sweep the outer exception or traceback. Preserve nested roots and restore/remove them when each propagation ends.

🤖 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 `@pyre/pyre-interpreter/src/eval.rs` around lines 726 - 776, Replace the single
overwriteable IN_FLIGHT_EXCEPTION Cell with a scoped, nestable root stack or
guard-based mechanism. Update set_in_flight_exception and the corresponding
propagation lifecycle so each raised exception is pushed before handling and its
prior root is restored or removed when propagation ends, preserving outer
PyError exceptions during nested trace-hook handling. Ensure
walk_in_flight_exception visits every active in-flight exception and its
traceback roots.

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