Skip to content

jit(fbw): emit ExecutionContext.enter/leave at the inlined-call push - #796

Merged
youknowone merged 18 commits into
mainfrom
fbw
Jul 29, 2026
Merged

jit(fbw): emit ExecutionContext.enter/leave at the inlined-call push#796
youknowone merged 18 commits into
mainfrom
fbw

Conversation

@youknowone

@youknowone youknowone commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Wires the walker's multi-frame inline push to the jit.virtual_ref machinery. That machinery was already ported in full and had zero production callersopimpl_virtual_ref / opimpl_virtual_ref_finish on both legs, bhimpl_virtual_ref{,_finish} (wired), the optimizer's VIRTUAL_REF → virtual-struct transform (optimizeopt/virtualize.rs:2029), the snapshot encode, and the resume-side consume_virtualref_boxes + continue_tracing all existed with virtualref_boxes permanently empty. The missing piece was the producer.

What this does

walker_ec_enter / walker_ec_leave port executioncontext.py:85-107 at the seeded inline level:

def enter(self, frame):
    frame.f_backref = self.topframeref
    self.topframeref = jit.virtual_ref(frame)

Both the recorded ops and the concrete stores run. The walk is also the interpreter executing this iteration, so a residual sys._getframe() inside the callee body reads the live ec; recording the ops alone would leave it seeing the CALLER. The concrete slot holds the JitVirtualRef, which force_vref (#785) resolves — a vref from virtual_ref_during_tracing carries forced = frame with virtual_token = TOKEN_NONE (virtualref.py:85-92), so that read is exact and cannot fail.

leave sits in the original's finally position. The sub-walk block is an expression that always completes, so a normal return, a raised exception, and a declined callee all reach it; every return Err decline gate sits before the enter.

Single virtualref_boxes

opimpl_virtual_ref{,_finish} move from MetaInterp onto TraceCtx, and the duplicate PyreSym.virtualref_boxes is retired — upstream has one MetaInterp.virtualref_boxes.

This was not cosmetic. Once a producer exists, the split is a correctness bug: state.rs's bridge resume restored pairs into the PyreSym copy, while every full-body-walker snapshot reads the TraceCtx one (build_snapshot_vable_vref_boxes). A parent's still-open virtual_ref scope was therefore dropped at bridge entry.

Residual-call bracket

The walker's residual calls now carry the vref halves (pyjitpl.py:2017 / :2049) next to the virtualizable halves already there. vrefs_before_residual_call stamps TOKEN_TRACING_RESCALL; vrefs_after_residual_call turns a vref the callee forced into VIRTUAL_REF_FINISH + ConstPtr(NULL) before the CALL op is recorded. This is the safety half and cannot be deferred past the producer — an unbracketed residual that hands the frame to Python would otherwise go undetected.

Known gaps, stated rather than hidden

  1. leave's escape branch is concrete-only. if frame.escaped or got_exception: f_back.mark_as_escaped(); frame_vref() runs at recording time but is not emitted into the IR. optimize_VIRTUAL_REF_FINISH leaves forced NULL on the normal path, so with two nested seeded inline levels A→B where B's frame escapes, a later compiled-code read of B.f_backref reaches a finished vref and force_pyframe_vref's expect panics. imp, virtualref: ImportRLock port; frame-chain vref reads through the vref #785's comment named this as the condition the VIRTUAL_REF emit owns re-checking; this is that re-check, and the answer is that the state stops being unreachable. escaped is a bit in PyFrame.flags, so the emit costs a getfield + guard per inline leave — the cost upstream pays for the same code.
  2. alloc_virtual_ref is a leaked Box, not a GC object. Ownership genuinely belongs to the collector (trace, resume data and the frame chain all hold the address), so freeing it is not correct. It costs 24 bytes per inlined call per trace recording — bounded by compilation events, not iterations — and converges with the sibling _dummy/JITFRAME divergence already documented at set_vref_gc_type_id.

Unseeded (register-resident) inline levels still take no vref: they have no frame object to take one of, where upstream's perform_call builds a frame for every inlined call. That is the next slice, and it is what trace.rs:1694's decline-to-legacy-replay is waiting on.

Verification

cargo check -p majit-metainterp -p pyre-interpreter -p pyre-jit-trace -p pyre-jit --features dynasm is clean (the one warning is pre-existing, in an untouched file). check.py has not been run locally on this commit — the machine has been at load 26-59 all session from sibling worktrees, where the perf gates are meaningless and builds get SIGTERM'd. CI's quiet runners are the judge here, and the two gaps above are exactly what I want the parity review to weigh in on.

Summary by CodeRabbit

  • Bug Fixes

    • Improved virtual-reference tracing/rollback and finish handling across tracing, inline calls, residual calls, and bridge recovery.
    • Corrected ExecutionContext enter/leave bracketing (including exception/escape paths) and added safety to prevent stale topframeref.
    • Strengthened generational-GC write barriers and fixed pointer-sized layout metadata to avoid invalid reference states.
  • New Features

    • Added allocator-aware virtualizable token forcing for JIT drivers, plus standard_virtualizable_heap_ptr.
    • Exposed additional virtual-ref stack/introspection APIs for tracing.
    • Added ExecutionContext topframeref field offset/descriptor support for JIT lowering.
  • Documentation

    • Refreshed notes describing virtual-ref bracketing behavior during residual and inlined calls.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR centralizes virtual-reference tracing in TraceCtx, adds execution-context enter/leave handling for inlined and bridge-carrier calls, propagates allocators through forcing, restores frame references across compiled execution, and updates virtual-reference allocation and GC write barriers.

Changes

Virtual-reference tracing and execution-context flow

Layer / File(s) Summary
TraceCtx virtual-reference ownership and recovery
majit/majit-metainterp/src/trace_ctx.rs, majit/majit-metainterp/src/pyjitpl.rs, pyre/pyre-jit-trace/src/state.rs, pyre/pyre-jit-trace/src/trace_opcode.rs
Virtual-reference recording, finishing, snapshots, bridge recovery, and validation use TraceCtx instead of PyreSym.
Execution-context enter and leave flow
pyre/pyre-interpreter/src/executioncontext.rs, pyre/pyre-jit-trace/src/descr.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/*, pyre/pyre-jit-trace/src/trace.rs
Inline calls, carrier walks, and may-force residual calls bracket execution-context and virtual-reference transitions, including exception-aware cleanup.
Allocator, GC, and runtime preservation
majit/majit-metainterp/src/{jitdriver,pyjitpl,virtualizable,virtualref}.rs, pyre/pyre-jit/src/eval.rs, majit/majit-backend-cranelift/src/compiler.rs, pyre/pyre-object/src/listobject.rs
Forcing accepts injected allocators, virtual references use typed GC allocation when available, reference writes add guarded barriers, arrays are initialized, and compiled execution restores topframeref.
Runtime layout and position integrity
majit/majit-gc/src/rewrite.rs, majit/majit-metainterp/src/optimizeopt/{heap,virtualize}.rs, pyre/pyre-jit-trace/src/state.rs
Raw position reservation, virtualizable ownership checks, pointer-sized vref descriptors, and materialized object class initialization are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant InlineWalker
  participant TraceCtx
  participant ExecutionContext
  participant CompiledExecutor
  InlineWalker->>TraceCtx: record virtual-reference enter
  TraceCtx->>ExecutionContext: update topframeref and frame-chain link
  InlineWalker->>TraceCtx: finalize virtual-reference leave
  CompiledExecutor->>ExecutionContext: save topframeref
  CompiledExecutor->>ExecutionContext: restore topframeref
Loading

Possibly related issues

Possibly related PRs

  • youknowone/pyre#238: Directly relates to the TraceCtx virtual-reference snapshot/restore APIs consumed by bridge recovery.
  • youknowone/pyre#749: Modifies the same bridge-carrier walk and execution-context leave paths.
  • youknowone/pyre#794: Targets the same virtual-reference box-stack lifecycle across TraceCtx and PyreSym.

Suggested reviewers: lifthrasiir

Poem

A rabbit hops where vrefs flow,
Through enter, leave, and bridges low.
GC bells ring, barriers gleam,
Frames return from every dream.
TraceCtx keeps the stack just right!

🚥 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 captures the main change: emitting ExecutionContext.enter/leave during the inlined-call push.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fbw
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fbw

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.

@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/feefdcd2e446b3498025dac2b1f53830c4dce5b3/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L1653
P1 Badge Emit the escape branch into the compiled trace

When a nested seeded callee frame escapes (for example, it is retained via sys._getframe()), this branch marks and forces frames only in the recording-time objects; the emitted IR proceeds directly to VIRTUAL_REF_FINISH with a null object. The optimizer therefore leaves the caller vref's forced field null on the compiled path, so a later escaped_frame.f_back reaches force_pyframe_vref and panics with InvalidVirtualRef. Emit the escaped/exception check, caller escape propagation, and frame-vref force before finishing the vref.

AGENTS.md reference: AGENTS.md:L14-L19

ℹ️ 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 +1063 to +1066
let vref_ptr = self
.metainterp_sd
.virtualref_info
.virtual_ref_during_tracing(virtual_obj_ptr as *mut u8);

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 Allocate tracing vrefs on the GC heap

In a long-lived process that continues compiling traces or bridges, every seeded inline call through this newly reachable producer allocates a 24-byte JitVirtualRef using Box::into_raw, and no trace retirement or other owner ever reclaims it. Compilation events are not a finite bound for dynamic workloads, so JIT memory grows permanently with the number of recorded inline calls; use the collector-owned allocation used by the upstream lltype.malloc(self.JIT_VIRTUAL_REF) implementation.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit f06af59).
Updated: 2026-07-29T12:41:16.168Z

Files in the reviewed diff
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/src/aarch64/assembler.rs
majit/majit-backend-dynasm/src/x86/assembler.rs
majit/majit-gc/src/collector.rs
majit/majit-gc/src/rewrite.rs
majit/majit-metainterp/src/jitdriver.rs
majit/majit-metainterp/src/optimizeopt/heap.rs
majit/majit-metainterp/src/optimizeopt/virtualize.rs
majit/majit-metainterp/src/pyjitpl.rs
majit/majit-metainterp/src/trace_ctx.rs
majit/majit-metainterp/src/virtualizable.rs
majit/majit-metainterp/src/virtualref.rs
pyre/pyre-interpreter/src/executioncontext.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit-trace/src/trace_opcode.rs
pyre/pyre-jit/src/eval.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)

4. Structural adaptations

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

ℹ️ 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 +1702 to +1704
let live = ctx.virtualref_boxes_len();
if live >= 2 {
ctx.stop_tracking_virtualref(live - 2);

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 Skip vrefs already stopped by the residual-call bracket

When a may-force residual exposes this callee frame, vrefs_after_residual_call has already called stop_tracking_virtualref and replaced the vref box with ConstPtr(NULL). Since the same exposure marks the frame escaped, this block calls stop_tracking_virtualref again; the resulting VIRTUAL_REF_FINISH(NULL, callee_frame) is lowered by optimize_virtual_ref_finish to field stores through a null vref pointer, so the compiled trace can crash on leave. Check whether the top pair still contains a live vref before emitting the forced finish.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

let escaped = unsafe {
let frame_vref = (*concrete_ec).topframeref;
(*concrete_ec).topframeref = concrete_f_backref;
let escaped = (*concrete_frame).escaped() || got_exception;

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 Record the frame escape branch in the trace

For seeded inline frames whose escaped state differs on a later compiled execution, this recording-time-only test permanently selects either the forced or NULL VIRTUAL_REF_FINISH form. In particular, when the recording run sees escaped == false but a compiled run lets a nested frame retain this frame through f_backref, the emitted NULL finish clears the token without preserving forced; a later force_pyframe_vref then reaches InvalidVirtualRef and panics. Emit the frame.escaped || got_exception decision and caller propagation into the trace rather than specializing it from the concrete recording frame.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

Comment thread pyre/pyre-jit-trace/src/descr.rs Outdated
index,
name: name.to_string(),
offset,
field_size: 8,

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 Use pointer width for the EC frame-link descriptor

On wasm32, every new SetfieldGc of ExecutionContext.topframeref uses this descriptor's 8-byte width. The wasm backend only substitutes the 4-byte pointer width when is_pointer_field() is true, but this descriptor is deliberately Unsigned, so it emits an i64 store and overwrites the adjacent w_tracefunc field after topframeref. Use the target pointer width or otherwise provide the backend with a pointer-width store while retaining the required barrier behavior.

Useful? React with 👍 / 👎.

@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: 80ccff44d6

ℹ️ 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 thread pyre/pyre-jit-trace/src/state.rs Outdated
Comment on lines +9105 to +9108
if published as usize != restored_top
&& unsafe {
majit_metainterp::virtualref::ptr_is_virtual_ref(published as *const u8)
}

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 Republish the resumed inline vref before tracing a bridge

When a guard fails inside an inlined callee, execute_assembler has already restored EC.topframeref to the saved portal frame at pyre-jit/src/eval.rs:7459-7460. That value is not a virtual ref, so this predicate refuses to install restored_top even though restored_virtualref_boxes contains the active callee scopes. The concrete bridge walk consequently exposes the portal frame to residual sys._getframe() calls and later closes the wrong concrete frame chain; republish the innermost restored vref whenever such a scope exists rather than requiring the currently published value to be another vref.

AGENTS.md reference: AGENTS.md:L32-L42

Useful? React with 👍 / 👎.

return gcref.0 as *mut u8;
}
}
Box::into_raw(Box::new(vref)) as *mut u8

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 Do not fall back to an untraced Box vref

Although the normal allocation is now GC-owned, the new allocation-failure fallback still creates a Box<JitVirtualRef>. In production, if alloc_oldgen_typed returns null under memory pressure, the collector treats this address as unowned and therefore never traces its forced field; once EC.topframeref holds it, the wrapped seeded frame has no other GC-visible edge and can be reclaimed during a subsequent collection, leaving force_vref to return a dangling frame pointer. Propagate the allocation failure or use a collector-visible rooted allocation instead of this fallback.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pyre/pyre-jit-trace/src/trace.rs (1)

1264-1292: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Save and restore virtualref_boxes around the non-committal carrier walk.

carrier_ec_leave pops ctx.virtualref_boxes; mid-loop break only leaves the prior pops undone, and the discard path later calls ctx.cut_trace(pre_pos), which only rewinds the recorder. A failed multi-frame carrier walk where the sub-walk recorded vref scope closures but was discarded leaves virtualref_boxes shorter than the restored trace; save it before the sub-walk and restore it before returning p2_drain_abort().

🤖 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-jit-trace/src/trace.rs` around lines 1264 - 1292, In the carrier
walk around drive_middle_frame_and_thread, save the current ctx.virtualref_boxes
state before any carrier_ec_leave calls. On the failed/discard path, restore
that saved state after discarding the sub-walk and before returning
p2_drain_abort(), ensuring the context matches the restored trace even when the
loop exits early.
pyre/pyre-jit/src/eval.rs (1)

7442-7461: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The topframeref bracket is not unwind-safe, unlike the CurrentFrameGuard it cites.

The comment models this on install_current_frame / CurrentFrameGuard, but those are RAII; this is a straight-line save/restore. run_compiled_detailed_with_bridge_keyed can panic (strict-mode resume_unwind, backend/blackhole paths), and on that unwind Line 7459 never runs — leaving exactly the poisoned state this change exists to prevent: a JitVirtualRef published in the live slot, read as a PyFrame by whatever resumes, carrying a FORCE_TOKEN for a dead JIT frame. A Drop-based guard closes the hole for free.

🛡️ Proposed fix
-    let ec_for_topframeref = frame_root.frame().execution_context as *mut PyExecutionContext;
-    let saved_topframeref = if ec_for_topframeref.is_null() {
-        std::ptr::null_mut()
-    } else {
-        unsafe { (*ec_for_topframeref).topframeref }
-    };
+    struct TopFrameRefGuard {
+        ec: *mut PyExecutionContext,
+        saved: *mut PyFrame,
+    }
+    impl Drop for TopFrameRefGuard {
+        fn drop(&mut self) {
+            if !self.ec.is_null() {
+                unsafe { (*self.ec).topframeref = self.saved };
+            }
+        }
+    }
+    let ec_for_topframeref = frame_root.frame().execution_context as *mut PyExecutionContext;
+    let _topframeref_guard = TopFrameRefGuard {
+        ec: ec_for_topframeref,
+        saved: if ec_for_topframeref.is_null() {
+            std::ptr::null_mut()
+        } else {
+            unsafe { (*ec_for_topframeref).topframeref }
+        },
+    };

and drop the manual restore at Lines 7459-7461 (note the guard must outlive only the outcome computation, so scope it accordingly).

🤖 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 `@majit/majit-metainterp/src/trace_ctx.rs`:
- Around line 1053-1085: Update TraceCtx’s active-trace root-walking path to
include every raw pointer stored in virtualref_boxes, forwarding and rewriting
virtual_obj_ptr and vref_ptr during collection. Ensure
vrefs_before_residual_call, vrefs_after_residual_call, and
opimpl_virtual_ref_finish read the updated pointers rather than stale cached
values, while preserving the existing virtualref_boxes pairing and indexing.

In `@majit/majit-metainterp/src/virtualref.rs`:
- Around line 498-501: Guard the gc_write_barrier call in continue_tracing with
majit_gc::gc_owns_object(vref_ptr), invoking the barrier only when the vref
address belongs to the GC. Preserve the existing tracing flow and avoid applying
the backend barrier to Box::into_raw fallback allocations.

In `@pyre/pyre-jit-trace/src/descr.rs`:
- Line 2700: Update the field_size expression for topframeref in the surrounding
descriptor definition to derive its size from the actual *mut PyFrame field type
rather than pyre_object::PyObjectRef, preserving the existing field(...)
configuration.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs`:
- Around line 47-51: Mark the `callee_ec` result from `record_op_with_descr` as
concrete before passing it to `walker_ec_leave`, using the pointer returned by
`root_sym.concrete_execution_context()`. Preserve the existing `GetfieldGcR`
recording and ensure residual-call and snapshot paths can resolve the stamped
execution context.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs`:
- Around line 2249-2251: Remove the redundant ca_concrete_frame declaration and
its seed-block assignment, then use concrete_callee_frame at the entered_ec and
leave sites. Preserve concrete_callee_frame as the single value carrying
concrete_frame_ptr outside the seed block.
- Around line 1712-1718: Capture the boolean returned by
TraceCtx::opimpl_virtual_ref_finish when processing callee_frame and assert it
succeeds without removing the call in release builds; use a let binding followed
by debug_assert!. Apply the same result-checking pattern to the live >= 2
stop_tracking_virtualref call so unbalanced virtual-reference operations are
detected at their source.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/trace.rs`:
- Around line 1264-1292: In the carrier walk around
drive_middle_frame_and_thread, save the current ctx.virtualref_boxes state
before any carrier_ec_leave calls. On the failed/discard path, restore that
saved state after discarding the sub-walk and before returning p2_drain_abort(),
ensuring the context matches the restored trace even when the loop exits early.
🪄 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 Plus

Run ID: dba4c575-efee-4d3d-967d-88d7c1612c68

📥 Commits

Reviewing files that changed from the base of the PR and between 948340d and 80ccff4.

📒 Files selected for processing (13)
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/virtualref.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs

Comment thread majit/majit-metainterp/src/trace_ctx.rs
Comment thread majit/majit-metainterp/src/virtualref.rs Outdated
index,
name: name.to_string(),
offset,
field_size: std::mem::size_of::<pyre_object::PyObjectRef>(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

field_size derived from the wrong type for topframeref.

topframeref is *mut PyFrame, not pyre_object::PyObjectRef. The two happen to be the same width today, so this is inert, but the shared field(...) helper hides the mismatch — a future fat-pointer/tagged representation on either side would silently desync the descr from the struct.

🤖 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-jit-trace/src/descr.rs` at line 2700, Update the field_size
expression for topframeref in the surrounding descriptor definition to derive
its size from the actual *mut PyFrame field type rather than
pyre_object::PyObjectRef, preserving the existing field(...) configuration.

Comment thread pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
Comment on lines +1712 to +1718
let live = ctx.virtualref_boxes_len();
if live >= 2 {
ctx.stop_tracking_virtualref(live - 2);
}
}
// `jit.virtual_ref_finish(frame_vref, frame)`.
ctx.opimpl_virtual_ref_finish(callee_frame);

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 | 🔵 Trivial | ⚡ Quick win

Discarding the opimpl_virtual_ref_finish result hides an unbalanced vref stack until the loop header.

TraceCtx::opimpl_virtual_ref_finish returns false when there is no vrefbox to pop; MetaInterp::opimpl_virtual_ref_finish asserts on exactly that. Here the bool is dropped, so an enter/leave imbalance surfaces later as the virtualref_boxes must be empty assert at the loop header (or not at all if the trace aborts first), far from the offending frame. The live >= 2 guard above has the same shape.

🛡️ Proposed fix
     // `jit.virtual_ref_finish(frame_vref, frame)`.
-    ctx.opimpl_virtual_ref_finish(callee_frame);
+    debug_assert!(
+        ctx.opimpl_virtual_ref_finish(callee_frame),
+        "walker_ec_leave: no vrefbox for this frame — enter/leave unbalanced"
+    );

Note debug_assert! would drop the call in release builds; use a let ok = ...; debug_assert!(ok, ...) binding instead.

🤖 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-jit-trace/src/jitcode_dispatch/inline_call.rs` around lines 1712 -
1718, Capture the boolean returned by TraceCtx::opimpl_virtual_ref_finish when
processing callee_frame and assert it succeeds without removing the call in
release builds; use a let binding followed by debug_assert!. Apply the same
result-checking pattern to the live >= 2 stop_tracking_virtualref call so
unbalanced virtual-reference operations are detected at their source.

Comment on lines +2249 to +2251
// The seeded callee frame's runtime object, for the `enter`/`leave`
// bracket below — the OpRef alone cannot carry it out of the seed block.
let mut ca_concrete_frame = std::ptr::null_mut::<pyre_interpreter::PyFrame>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ca_concrete_frame duplicates the existing concrete_callee_frame.

Both are declared in the same outer scope and both are assigned concrete_frame_ptr inside the seed block (Line 2406 and Line 2423), so they are always identical. The doc comment's rationale ("the OpRef alone cannot carry it out of the seed block") is already satisfied by concrete_callee_frame. Two names for one value invites them diverging later.

♻️ Proposed cleanup
-    // The seeded callee frame's runtime object, for the `enter`/`leave`
-    // bracket below — the OpRef alone cannot carry it out of the seed block.
-    let mut ca_concrete_frame = std::ptr::null_mut::<pyre_interpreter::PyFrame>();

and drop the assignment at Line 2423, using concrete_callee_frame at the entered_ec / leave sites.

Also applies to: 2423-2423

🤖 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-jit-trace/src/jitcode_dispatch/inline_call.rs` around lines 2249 -
2251, Remove the redundant ca_concrete_frame declaration and its seed-block
assignment, then use concrete_callee_frame at the entered_ec and leave sites.
Preserve concrete_callee_frame as the single value carrying concrete_frame_ptr
outside the seed block.

youknowone added a commit that referenced this pull request Jul 28, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 28, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

Assisted-by: Claude

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pyre/pyre-jit/src/eval.rs (1)

3858-3892: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

force_pyframe's forcing path was not migrated to the new allocator-aware force_virtualizable_token.

This function was updated to call driver.force_virtualizable_token(token) so virtualizable materialization uses the registered blackhole_allocator instead of silently falling back to NullAllocator (per the new doc comment: a jit.virtual_ref frame decoded through NullAllocator leaves forced null). The sibling force_pyframe (same file, lines ~3894-3940) still calls driver.meta_interp_mut().force_virtualizable_token(token) directly at its force closure, bypassing the fix. If a frame forced via that path can also contain nested virtual refs, it is exposed to the same null-forced bug this PR just fixed elsewhere.

🐛 Proposed fix
         let mut force = |ptr: *mut u8| {
             info.force_virtualizable_if_necessary(ptr, |token| {
-                driver.meta_interp_mut().force_virtualizable_token(token);
+                driver.force_virtualizable_token(token);
             });
         };
🤖 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-jit/src/eval.rs` around lines 3858 - 3892, Update the sibling
force_pyframe function’s force closure to call
driver.force_virtualizable_token(token) instead of
driver.meta_interp_mut().force_virtualizable_token(token). Keep the token
extraction and surrounding virtual-reference forcing logic unchanged so this
path also uses the registered blackhole_allocator.
majit/majit-metainterp/src/pyjitpl.rs (1)

2957-2982: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize concurrent take_back_all_descrs writes.

all_descrs() now returns the same process-wide Mutex<Vec<DescrRef>>, so different MetaInterp instances snap the same base list, append different new DescrRefs, and then race through if all_descrs.len() >= slot.len() { *slot = all_descrs; }. The longer/equal write replaces the shared slot and can discard another in-flight compilation’s unique descriptors, breaking descr_index integrity. Use the existing publish-style PUBLISH serialization for these write-backs, or an append-only merge instead of length comparison.

🤖 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 `@majit/majit-metainterp/src/pyjitpl.rs` around lines 2957 - 2982, Serialize
take_back_all_descrs write-backs using the existing PUBLISH mechanism so
concurrent MetaInterp instances cannot replace the shared descriptor list with
stale snapshots. Update take_back_all_descrs to acquire the same publish-style
serialization around the length check and assignment, preserving the monotonic
write-back behavior.
🤖 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 `@majit/majit-metainterp/src/virtualref.rs`:
- Around line 158-195: Remove the Box::into_raw fallback from alloc_virtual_ref
and make a registered GC type plus successful old-generation allocation
mandatory. Propagate allocation or registration failure through the callers
instead of returning an untracked JitVirtualRef, while preserving the existing
initialization and creation write barrier for successful allocations.

---

Outside diff comments:
In `@majit/majit-metainterp/src/pyjitpl.rs`:
- Around line 2957-2982: Serialize take_back_all_descrs write-backs using the
existing PUBLISH mechanism so concurrent MetaInterp instances cannot replace the
shared descriptor list with stale snapshots. Update take_back_all_descrs to
acquire the same publish-style serialization around the length check and
assignment, preserving the monotonic write-back behavior.

In `@pyre/pyre-jit/src/eval.rs`:
- Around line 3858-3892: Update the sibling force_pyframe function’s force
closure to call driver.force_virtualizable_token(token) instead of
driver.meta_interp_mut().force_virtualizable_token(token). Keep the token
extraction and surrounding virtual-reference forcing logic unchanged so this
path also uses the registered blackhole_allocator.
🪄 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 Plus

Run ID: 5523b2f8-6317-44db-bcf1-72ca1a58e988

📥 Commits

Reviewing files that changed from the base of the PR and between 80ccff4 and 05dafb6.

📒 Files selected for processing (16)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/virtualizable.rs
  • majit/majit-metainterp/src/virtualref.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs

Comment on lines +158 to +195
///
/// `lltype.malloc(self.JIT_VIRTUAL_REF)` is a GC allocation, and it has to be
/// one here too: `forced` is a traced slot (`gc_ptr_offsets = [16]`, registered
/// with [`set_vref_gc_type_id`]), so once `ExecutionContext.topframeref` holds
/// the vref instead of the frame, this object is the only edge keeping the
/// frame it wraps reachable. A host-heap allocation is invisible to the
/// collector — the root walker's `gc_current_object_address` early-out returns
/// an unowned address unchanged — which drops that edge and lets a live frame
/// be collected out from under the walk.
///
/// Old-gen, not nursery: `virtualref_boxes`, the recorded ops' concrete stamps
/// and the interpreter's `f_backref` chain all hold this address as a raw
/// pointer that no root walker forwards, and MiniMark's old-gen is mark-sweep,
/// so the address is stable across a minor collection. The `forced` frame may
/// be young, hence the creation write barrier.
///
/// The `Box` fallback covers the window before `set_vref_gc_type_id` has run
/// (the id still reads its unset sentinel) and an old-gen allocation failure;
/// it is leaked, and reclamation is what it gives up.
fn alloc_virtual_ref(real_object: *mut u8) -> *mut u8 {
let vref = Box::new(JitVirtualRef {
let vref = JitVirtualRef {
super_: ObjectHeader {
typeptr: JIT_VIRTUAL_REF_VTABLE,
},
virtual_token: TOKEN_NONE,
forced: real_object,
});
Box::into_raw(vref) as *mut u8
};
let type_id = vref_gc_type_id();
if type_id != VREF_GC_TYPE_ID_UNSET {
let gcref = majit_gc::alloc_oldgen_typed(type_id, std::mem::size_of::<JitVirtualRef>());
if gcref.0 != 0 {
unsafe { std::ptr::write(gcref.0 as *mut JitVirtualRef, vref) };
// Creation write barrier: an old-gen vref may point at a young frame.
majit_gc::gc_write_barrier(gcref);
return gcref.0 as *mut u8;
}
}
Box::into_raw(Box::new(vref)) as *mut u8

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 | 🟠 Major | 🏗️ Heavy lift

Do not silently fall back to a host-heap vref.

Box::into_raw is still used when old-generation allocation fails. That object is recognized as a JitVirtualRef, but the collector cannot trace its forced field, so the materialized frame may be collected while topframeref still holds the vref. The fallback also leaks every allocation. Make GC registration/allocation an invariant or propagate allocation failure; the ownership guard below does not restore the missing GC edge.

As per coding guidelines, the generated JIT must preserve interpreter semantics and upstream storage ownership.

🤖 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 `@majit/majit-metainterp/src/virtualref.rs` around lines 158 - 195, Remove the
Box::into_raw fallback from alloc_virtual_ref and make a registered GC type plus
successful old-generation allocation mandatory. Propagate allocation or
registration failure through the callers instead of returning an untracked
JitVirtualRef, while preserving the existing initialization and creation write
barrier for successful allocations.

Source: Coding guidelines

youknowone added a commit that referenced this pull request Jul 28, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

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: 87103f0c38

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

// unseeded (register-resident) inline has none, which is the remaining gap
// between this chain and upstream's, where `perform_call` builds a frame
// for every inlined call (`pyjitpl.py:2445-2476, 1862-1874`).
let entered_ec = callee_frame_seeded && !ca_concrete_frame.is_null() && {

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 Create a frame for every inlined callee

When a strict inline is deeper than fbw_max_multiframe_depth() or fails a seeding precondition but remains inlined via the single-frame fallback, this predicate skips both ExecutionContext.enter and the matching leave. That callee therefore has no independent red frame: frame inspection observes the caller, and guard-failure resume collapses the callee's globals/locals onto the caller boundary. Build and thread a frame for every inline level, or decline the inline when that is impossible.

AGENTS.md reference: AGENTS.md:L32-L42

Useful? React with 👍 / 👎.

})
.max();
let mut max_raw_pos = max_result_pos;
if let Some(result_high_water) = max_result_pos {

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 Reserve operand positions when a trace has no results

When a trace or bridge contains only void operations but still references input or fail boxes—for example LABEL(input0); SETFIELD_GC(input0, ConstPtr); JUMPmax_result_pos is None, so this conditional skips scanning those operands and initializes next_pos to 0. Replacing the ConstPtr then emits LOAD_FROM_GC_TABLE at raw position 0, colliding with input0 in backend SSA and potentially compiling the store against the wrong value. Include arguments and fail arguments in the high-water calculation even when no result position exists.

Useful? React with 👍 / 👎.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
majit/majit-metainterp/src/trace_ctx.rs (1)

2093-2102: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fall back to the box's own type, not Type::Int.

When virtualizable_slot_type(i) returns None (no virtualizable_info, or an index outside the layout derived from a stale virtualizable_array_lengths), a Ref slot — including the identity slot at [-1] — gets declared Int. That declaration reaches remove_consts_and_duplicates's debug_assert!(tp == declared) and, on the registration side, the typed LABEL inputargs.

🐛 Proposed fix
-                .map(|(i, &opref)| (opref, self.virtualizable_slot_type(i).unwrap_or(Type::Int)))
+                .map(|(i, &opref)| {
+                    let tp = self
+                        .virtualizable_slot_type(i)
+                        .or_else(|| opref.ty())
+                        .unwrap_or(Type::Int);
+                    (opref, tp)
+                })
🤖 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 `@majit/majit-metainterp/src/trace_ctx.rs` around lines 2093 - 2102, Update
collect_virtualizable_typed_boxes to use each box’s own type when
virtualizable_slot_type(i) returns None, rather than defaulting to Type::Int.
Preserve the existing slot type when available and ensure fallback typing covers
Ref and identity slots consistently for downstream declarations.
🤖 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 `@majit/majit-gc/src/rewrite.rs`:
- Around line 2976-2993: Make the operand/failarg reservation scan unconditional
rather than guarding it with max_result_pos. Update the logic around
reserve_later_box in the trace rewrite flow to compute max_raw_pos from every
valid non-constant operand and failarg, including Void-only traces where
max_result_pos is None, while preserving the existing high-water comparison when
result positions exist.

---

Outside diff comments:
In `@majit/majit-metainterp/src/trace_ctx.rs`:
- Around line 2093-2102: Update collect_virtualizable_typed_boxes to use each
box’s own type when virtualizable_slot_type(i) returns None, rather than
defaulting to Type::Int. Preserve the existing slot type when available and
ensure fallback typing covers Ref and identity slots consistently for downstream
declarations.
🪄 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 Plus

Run ID: ca2dc2a8-b35e-4633-b669-81212f57307c

📥 Commits

Reviewing files that changed from the base of the PR and between 05dafb6 and 87103f0.

📒 Files selected for processing (19)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/virtualizable.rs
  • majit/majit-metainterp/src/virtualref.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs

Comment on lines +2976 to +2993
if let Some(result_high_water) = max_result_pos {
let mut reserve_later_box = |pos: OpRef| {
if !pos.is_none() && !pos.is_constant() && pos.raw() > result_high_water {
max_raw_pos =
Some(max_raw_pos.map_or(pos.raw(), |old: u32| old.max(pos.raw())));
}
};
for op in &ops {
for arg in op.getarglist() {
reserve_later_box(arg.to_opref());
}
if let Some(fail_args) = op.getfailargs() {
for arg in fail_args {
reserve_later_box(arg.to_opref());
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reserve operand positions even when there are no result positions.

For a Void-only trace with a non-constant operand/failarg, max_result_pos is None, so Line 2976 skips the reservation scan and next_pos becomes 0. A generated LoadFromGcTable can then reuse an input box’s SSA position, aliasing backend values. Compute the high-water mark across valid operands/failargs unconditionally.

Proposed fix
-        if let Some(result_high_water) = max_result_pos {
-            let mut reserve_later_box = |pos: OpRef| {
-                if !pos.is_none() && !pos.is_constant() && pos.raw() > result_high_water {
-                    max_raw_pos =
-                        Some(max_raw_pos.map_or(pos.raw(), |old: u32| old.max(pos.raw())));
-                }
-            };
-            for op in &ops {
-                for arg in op.getarglist() {
-                    reserve_later_box(arg.to_opref());
-                }
-                if let Some(fail_args) = op.getfailargs() {
-                    for arg in fail_args {
-                        reserve_later_box(arg.to_opref());
-                    }
-                }
+        let mut reserve_box = |pos: OpRef| {
+            if !pos.is_none() && !pos.is_constant() {
+                max_raw_pos = Some(max_raw_pos.map_or(pos.raw(), |old| old.max(pos.raw())));
+            }
+        };
+        for op in &ops {
+            for arg in op.getarglist() {
+                reserve_box(arg.to_opref());
+            }
+            if let Some(fail_args) = op.getfailargs() {
+                for arg in fail_args {
+                    reserve_box(arg.to_opref());
+                }
             }
         }

As per coding guidelines, the generated JIT must preserve interpreter semantics.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(result_high_water) = max_result_pos {
let mut reserve_later_box = |pos: OpRef| {
if !pos.is_none() && !pos.is_constant() && pos.raw() > result_high_water {
max_raw_pos =
Some(max_raw_pos.map_or(pos.raw(), |old: u32| old.max(pos.raw())));
}
};
for op in &ops {
for arg in op.getarglist() {
reserve_later_box(arg.to_opref());
}
if let Some(fail_args) = op.getfailargs() {
for arg in fail_args {
reserve_later_box(arg.to_opref());
}
}
}
}
let mut reserve_box = |pos: OpRef| {
if !pos.is_none() && !pos.is_constant() {
max_raw_pos = Some(max_raw_pos.map_or(pos.raw(), |old| old.max(pos.raw())));
}
};
for op in &ops {
for arg in op.getarglist() {
reserve_box(arg.to_opref());
}
if let Some(fail_args) = op.getfailargs() {
for arg in fail_args {
reserve_box(arg.to_opref());
}
}
}
🤖 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 `@majit/majit-gc/src/rewrite.rs` around lines 2976 - 2993, Make the
operand/failarg reservation scan unconditional rather than guarding it with
max_result_pos. Update the logic around reserve_later_box in the trace rewrite
flow to compute max_raw_pos from every valid non-constant operand and failarg,
including Void-only traces where max_result_pos is None, while preserving the
existing high-water comparison when result positions exist.

Source: Coding guidelines

youknowone added a commit that referenced this pull request Jul 29, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

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: 8d3f4da58d

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

is_quasi_immutable: false,
flag: ArrayFlag::Unsigned,
virtualizable: false,
index_in_parent: 0,

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 Give each ExecutionContext field a distinct parent index

When a trace both manipulates sys_exc_value and enters or leaves a seeded inline frame, both descriptors currently use index_in_parent = 0. The optimizer uses that value as the shared StructPtrInfo field slot for parent-bound descriptors, so a value cached for sys_exc_value can be returned for topframeref (or conversely), publishing an exception object as the frame chain or corrupting the active exception state. Set the parent index from index so these two physical fields occupy separate slots.

Useful? React with 👍 / 👎.

Comment on lines +9384 to +9387
let restored_top = restored_virtualref_boxes
.last()
.map(|&(_, vref_ptr)| vref_ptr)
.unwrap_or(sym.concrete_vable_ptr as usize);

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 Republish materialized frames for stopped vref pairs

When an inline frame was forced by a residual call during recording, stop_tracking_virtualref preserves its pair but replaces the vref box and concrete pointer with NULL; a later guard therefore restores a nonempty restored_virtualref_boxes whose final vref_ptr is 0. This calculation makes restored_top zero and the following condition skips republishing anything, leaving the assembler guard's saved portal frame in EC.topframeref while the bridge resumes inside the callee, so sys._getframe() observes the portal and carrier leave closes the wrong chain. Fresh evidence beyond the prior live-vref republishing comment is the stopped-pair representation at TraceCtx::stop_tracking_virtualref; select the paired materialized virtual frame when the restored vref pointer is null.

AGENTS.md reference: AGENTS.md:L32-L42

Useful? React with 👍 / 👎.

@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

♻️ Duplicate comments (3)
majit/majit-metainterp/src/virtualref.rs (1)

158-196: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Untracked Box fallback in alloc_virtual_ref remains unresolved.

This reproduces a previously-flagged major issue on this same function that has not been marked addressed: when the GC type id is unset or old-gen allocation fails, the code still falls back to a leaked, GC-invisible Box::into_raw allocation for JitVirtualRef. The struct's own doc block acknowledges this: "The Box fallback covers the window before set_vref_gc_type_id has run (the id still reads its unset sentinel) and an old-gen allocation failure; it is leaked, and reclamation is what it gives up." Since forced is the only traced edge keeping a live frame reachable once topframeref holds the vref, a host-heap Box vref can let the collector reclaim a still-referenced frame out from under the walk, and the allocation is leaked either way. As per coding guidelines, this deviation should be fixed as a translator/generation defect (mandatory GC registration + propagated allocation failure) rather than left as a silent fallback.

🤖 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 `@majit/majit-metainterp/src/virtualref.rs` around lines 158 - 196, The
fallback in alloc_virtual_ref must be removed: require set_vref_gc_type_id to
register a valid GC type before allocation, use alloc_oldgen_typed for every
JitVirtualRef, and propagate registration or allocation failure instead of
returning a leaked Box::into_raw object. Preserve the existing write barrier for
successfully allocated old-generation references.

Source: Coding guidelines

majit/majit-gc/src/rewrite.rs (1)

2957-3005: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Operand/failarg reservation scan is still skipped when max_result_pos is None.

This reproduces the previously-flagged, still-open major issue on this exact block: the reservation loop over op.getarglist()/op.getfailargs() only runs if let Some(result_high_water) = max_result_pos, so a Void-only trace with a non-constant operand/failarg leaves max_raw_pos at None and next_pos becomes 0, entirely skipping the high-water reservation the comment describes: "rewrite.py:106-116 replaces a ConstPtr argument with a fresh LOAD_FROM_GC_TABLE box. RPython boxes retain distinct identity when their producer was optimized away, so pyre's numeric namespace must reserve argument and guard-fail positions too. Forced virtuals can carry producerless constant boxes as allocation lengths; reusing such a raw position would alias an Int box with the new Ref box in backend SSA." A freshly-generated LoadFromGcTable op can then reuse position 0 (or any low position), aliasing an existing input box in backend SSA. The fix from the earlier review — computing max_raw_pos from operands/failargs unconditionally rather than only when max_result_pos.is_some() — has not been applied.

🐛 Proposed fix (unconditional reservation scan)
-        let mut max_raw_pos = max_result_pos;
-        if let Some(result_high_water) = max_result_pos {
-            let mut reserve_later_box = |pos: OpRef| {
-                if !pos.is_constant()
-                    && matches!(pos.ty(), Some(Type::Int | Type::Float | Type::Ref))
-                    && pos.raw() > result_high_water
-                {
-                    max_raw_pos =
-                        Some(max_raw_pos.map_or(pos.raw(), |old: u32| old.max(pos.raw())));
-                }
-            };
-            for op in &ops {
-                for arg in op.getarglist() {
-                    reserve_later_box(arg.to_opref());
-                }
-                if let Some(fail_args) = op.getfailargs() {
-                    for arg in fail_args {
-                        reserve_later_box(arg.to_opref());
-                    }
-                }
-            }
-        }
+        let mut max_raw_pos = max_result_pos;
+        let mut reserve_box = |pos: OpRef| {
+            if !pos.is_constant()
+                && matches!(pos.ty(), Some(Type::Int | Type::Float | Type::Ref))
+            {
+                max_raw_pos = Some(max_raw_pos.map_or(pos.raw(), |old: u32| old.max(pos.raw())));
+            }
+        };
+        for op in &ops {
+            for arg in op.getarglist() {
+                reserve_box(arg.to_opref());
+            }
+            if let Some(fail_args) = op.getfailargs() {
+                for arg in fail_args {
+                    reserve_box(arg.to_opref());
+                }
+            }
+        }
🤖 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 `@majit/majit-gc/src/rewrite.rs` around lines 2957 - 3005, Make the operand and
failarg reservation scan in the next_pos initialization unconditional: do not
gate the loop over op.getarglist() and op.getfailargs() on max_result_pos being
Some. Initialize the high-water state from max_result_pos when present, scan all
typed non-constant operands and failargs regardless, and preserve the existing
exclusions for constants, untyped values, and Void sentinels before computing
next_pos.

Source: Coding guidelines

pyre/pyre-jit-trace/src/descr.rs (1)

2666-2697: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

index_in_parent is hardcoded to 0 for both fields — likely identity bug for topframeref.

The field closure takes an index: u32 parameter that's threaded into SimpleFieldDescrSpec.index, but index_in_parent is hardcoded to 0 for every call site, so both sys_exc_value (index: 0) and topframeref (index: 1) report the same index_in_parent. This was harmless when the group had only one field (where 0 was trivially correct); adding a second field without updating index_in_parent looks like an oversight. If any consumer (heap-cache field identity, GC rewrite per-field bitstrings, etc.) uses index_in_parent to distinguish sibling fields on the same struct, this collision could alias topframeref reads/writes with sys_exc_value's.

🐛 Proposed fix
     let field = |index: u32, field_key: &str, offset: usize| SimpleFieldDescrSpec {
         index,
         field_key: field_key.to_string(),
         name: format!("ExecutionContext.{field_key}"),
         offset,
         field_size: std::mem::size_of::<pyre_object::PyObjectRef>(),
         field_type: Type::Ref,
         is_immutable: false,
         is_quasi_immutable: false,
         flag: ArrayFlag::Unsigned,
         virtualizable: false,
-        index_in_parent: 0,
+        index_in_parent: index,
     };
🔎 Verification
#!/bin/bash
# Confirm how index_in_parent is consumed for SimpleFieldDescr, and whether
# other multi-field groups vary it per field.
rg -n "index_in_parent" -C3 majit/majit-ir/src/descr.rs
rg -n "struct SimpleFieldDescrSpec" -A20 majit/majit-ir/src/descr.rs
rg -n "make_simple_descr_group\(" -A15 majit/majit-ir/src -g '!descr.rs' 2>/dev/null

Also note: this same closure sets field_size: std::mem::size_of::<pyre_object::PyObjectRef>() for topframeref, which is actually *mut PyFrame. Sizes are identical today so this stays inert, but it's the same class of issue flagged in a previous review on this exact line for the (then single-field) group.

🤖 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-jit-trace/src/descr.rs` around lines 2666 - 2697, Update the
EC_DESCR_GROUP field closure so index_in_parent is derived from the field’s
index rather than hardcoded to zero, ensuring sys_exc_value and topframeref have
distinct sibling identities. Also set field_size from the actual field type used
by each ExecutionContext field, including the *mut PyFrame type for topframeref,
instead of assuming PyObjectRef for both.
🤖 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 `@majit/majit-metainterp/src/trace_ctx.rs`:
- Around line 1099-1118: Update the minor-collection GC root walker to visit
every entry in TraceCtx::virtualref_boxes, including both the stored OpRef and
its associated raw pointer, so cached virtual-reference pointers remain valid
before consumers such as is_virtual_ref, tracing_before_residual_call, and
sanity comparison use them.

In `@pyre/pyre-jit-trace/src/trace_opcode.rs`:
- Around line 2975-2979: Update build_framestack_snapshot to retain the
vable_boxes returned by list_of_boxes_virtualizable(ctx) and obtain vref_boxes
through the available vref-only helper, removing the call to
ctx.build_snapshot_vable_vref_boxes() so the vable snapshot is not rebuilt or
discarded.

---

Duplicate comments:
In `@majit/majit-gc/src/rewrite.rs`:
- Around line 2957-3005: Make the operand and failarg reservation scan in the
next_pos initialization unconditional: do not gate the loop over op.getarglist()
and op.getfailargs() on max_result_pos being Some. Initialize the high-water
state from max_result_pos when present, scan all typed non-constant operands and
failargs regardless, and preserve the existing exclusions for constants, untyped
values, and Void sentinels before computing next_pos.

In `@majit/majit-metainterp/src/virtualref.rs`:
- Around line 158-196: The fallback in alloc_virtual_ref must be removed:
require set_vref_gc_type_id to register a valid GC type before allocation, use
alloc_oldgen_typed for every JitVirtualRef, and propagate registration or
allocation failure instead of returning a leaked Box::into_raw object. Preserve
the existing write barrier for successfully allocated old-generation references.

In `@pyre/pyre-jit-trace/src/descr.rs`:
- Around line 2666-2697: Update the EC_DESCR_GROUP field closure so
index_in_parent is derived from the field’s index rather than hardcoded to zero,
ensuring sys_exc_value and topframeref have distinct sibling identities. Also
set field_size from the actual field type used by each ExecutionContext field,
including the *mut PyFrame type for topframeref, instead of assuming PyObjectRef
for both.
🪄 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 Plus

Run ID: 3c3f7ae6-ebd0-4a3f-9ac1-73ef558a77b7

📥 Commits

Reviewing files that changed from the base of the PR and between 87103f0 and 8d3f4da.

📒 Files selected for processing (19)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/virtualizable.rs
  • majit/majit-metainterp/src/virtualref.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs

Comment thread majit/majit-metainterp/src/trace_ctx.rs
Comment on lines 2975 to +2979
let vable_boxes = self.list_of_boxes_virtualizable(ctx);
let vref_boxes = Self::build_virtualref_boxes(self.sym(), ctx);
// `pyjitpl.py:2623` passes `self.virtualref_boxes` to the snapshot
// alongside the virtualizable boxes; the vable half here comes from
// the trait leg's own mirror, so take only the vref half.
let (_, vref_boxes) = ctx.build_snapshot_vable_vref_boxes();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect TraceCtx::build_snapshot_vable_vref_boxes to see if it duplicates
# the vable-box construction that list_of_boxes_virtualizable also performs.
rg -n "fn build_snapshot_vable_vref_boxes" -A 40 majit/majit-metainterp/src/trace_ctx.rs

Repository: youknowone/pyre

Length of output: 2451


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the vable-box helper definitions and call sites.
rg -n "list_of_boxes_virtualizable|build_snapshot_vable_vref_boxes|build_vable_snapshot_boxes|build_vref_snapshot_boxes" -S .

printf '\n--- trace_opcode.rs 2950-3005 ---\n'
sed -n '2950,3005p' pyre/pyre-jit-trace/src/trace_opcode.rs

printf '\n--- pyjitpl.rs relevant helpers ---\n'
rg -n "pub fn build_vable_snapshot_boxes|pub fn build_vref_snapshot_boxes|fn build_vable_snapshot_boxes|fn build_vref_snapshot_boxes" -A 80 pyre/pyre-jit-trace/src/pyjitpl.rs

Repository: youknowone/pyre

Length of output: 10080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- trace_opcode.rs 3107-3124 ---'
sed -n '3107,3124p' pyre/pyre-jit-trace/src/trace_opcode.rs

printf '%s\n' '--- majit-metainterp/pyjitpl dispatch helpers ---'
sed -n '8755,8810p' majit/majit-metainterp/src/pyjitpl/dispatch.rs

printf '%s\n' '--- compare builders in trace_opcode.rs ---'
sed -n '3270,3340p' pyre/pyre-jit-trace/src/trace_opcode.rs

Repository: youknowone/pyre

Length of output: 7522


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- trace_opcode.rs build_framestack_snapshot call sites ---'
rg -n "build_framestack_snapshot|build_snapshot_vable_vref_boxes" pyre/pyre-jit-trace/src -S

printf '%s\n' '--- trace_opcode.rs 2920-2990 ---'
sed -n '2920,2990p' pyre/pyre-jit-trace/src/trace_opcode.rs

printf '%s\n' '--- exact list_of_boxes_virtualizable implementation ---'
sed -n '3107,3215p' pyre/pyre-jit-trace/src/trace_opcode.rs

printf '%s\n' '--- pyjitpl dispatcher full builder references ---'
sed -n '8725,8755p' majit/majit-metainterp/src/pyjitpl/dispatch.rs

Repository: youknowone/pyre

Length of output: 12829


Avoid rebuilding the vable snapshot boxes in build_framestack_snapshot.

list_of_boxes_virtualizable(ctx) reads the canonically mirrored ctx.virtualizable_boxes and produces the trace-local vable snapshot list. Calling ctx.build_snapshot_vable_vref_boxes() afterwards reconstructs the same vable snapshot from ctx.virtualizable_boxes and discards it, wasting vable snapshot allocation on every guard capture. Keep using the returned vable_boxes and call a vref-only helper for vref_boxes.

🤖 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-jit-trace/src/trace_opcode.rs` around lines 2975 - 2979, Update
build_framestack_snapshot to retain the vable_boxes returned by
list_of_boxes_virtualizable(ctx) and obtain vref_boxes through the available
vref-only helper, removing the call to ctx.build_snapshot_vable_vref_boxes() so
the vable snapshot is not rebuilt or discarded.

youknowone added a commit that referenced this pull request Jul 29, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

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/ca4a6d35eabf590854874d79c3b7fd7d390d70f4/pyre-jit-trace/src/trace.rs#L1663
P1 Badge Restore carrier vrefs when discarding the bridge walk

When the reconstructed callee walk returns Err, None, or a non-return outcome—or when a deeper return is followed by a middle/root reconstruction failure—this call closes the restored carrier scope before the code decides whether the bridge will be committed. The abort epilogue explicitly replays execution from the original guard, but discard_bridge_carrier_walk retains the shortened vref prefix and does not undo walker_ec_leave's concrete topframeref/escape mutations, so replay resumes inside the callee with its active frame scope already removed; frame inspection then observes the caller and the eventual leave can close the wrong chain. Defer these carrier leaves until commit, or restore the complete pre-walk vref and concrete EC state on every abort.

AGENTS.md reference: AGENTS.md:L32-L42

ℹ️ 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-jit-trace/src/jitcode_dispatch/inline_call.rs`:
- Around line 3045-3067: Refresh or root the concrete frame pointer across the
inline callee sub-walk before the leave-time use in inline_call.rs:3045-3067,
using the callee OpRef shadow or equivalent shadow-stack pinning so
walker_ec_leave receives a valid frame after residual calls or GC. Apply the
corresponding pointer-safety update at residual_call.rs:459-484 wherever
INLINE_CONCRETE_FRAME is read, preserving the existing residual-call behavior.
🪄 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 Plus

Run ID: 559893bd-d954-4300-bf7d-42e51bf937fe

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3f4da and abee4e8.

📒 Files selected for processing (20)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/src/virtualizable.rs
  • majit/majit-metainterp/src/virtualref.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/listobject.rs

Comment thread pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
youknowone added a commit that referenced this pull request Jul 29, 2026
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

Assisted-by: Claude
`got_exception` was set for every callee outcome other than `SubReturn`,
including a tracing decline and a loop transition. `leave`'s caller passes
it only for an exception exit, so a decline permanently marked the caller
escaped and forced a vref that never needed forcing. Now `SubRaise` only.

Gate both vref halves of the residual-call bracket on `is_may_force`, the
walker's `check_forces_virtual_or_virtualizable()`. `do_residual_call`
runs the whole preparation block only for `assembler_call or
effectinfo.check_forces_...` (pyjitpl.py:2007); stamping a call that
cannot force left a token nothing would clear.

Record the escape branch's force instead of performing it only concretely:
a leaving frame that escaped emits `VIRTUAL_REF_FINISH(vrefbox,
virtualbox)`, the non-null form `optimize_VIRTUAL_REF_FINISH` lowers to
storing the virtual into `vref.forced` (virtualize.py:141-151), by way of
`stop_tracking_virtualref`. Previously the NULL form was emitted, leaving
`forced` NULL with `virtual_token` cleared, so a later read through a
deeper escaped frame's `f_backref` reached `InvalidVirtualRef`. Upstream
writes `forced` at runtime through `frame_vref()`'s force instead; that
route needs the `jit_force_virtual` lowering, which is unwired, and both
forms are already understood by the optimizer.

Assisted-by: Claude
`opimpl_virtual_ref_finish`'s nesting assert was vacuous. For a
non-constant `virtual_obj` it derived the compared pointer from
`lastbox_ptr`, the value it then compared against, so
`assert_eq!(virtual_obj_ptr, lastbox_ptr)` could never fire — disabling
exactly the mismatched-nesting check `pyjitpl.py:1823` exists to make.
The spelling was inherited from the `MetaInterp` copy, where the method
had no callers; the inlined-call enter/leave puts it on a live path.
Source the pointer from `concrete_of_opref` instead — pyre's
`getref_base()` — so both sides are derived independently. An unstamped
box is skipped as unknown rather than failed.

Replace the inline `leave` profile-hook comment's assertion with the
reason it holds: `is_being_profiled` is a portal-driver green
(`interp_jit.py:68 greens = ['next_instr', 'is_being_profiled',
'pycode']`), so a trace recorded with profiling off is only entered with
profiling off, and enabling profiling selects a different green key
rather than reusing the trace.

Assisted-by: Claude
…zers

`materialize_concrete_virtual_ptr` and `materialize_virtual_object` computed
the fresh object's `w_class` as `get_instantiate(descr.vtable() as *PyType)`.
`VRefSizeDescr::vtable()` returns the `jit_virtual_ref_vtable` type-id
constant, not a `PyType` pointer, so materializing a virtual `JitVirtualRef`
on a guard failure dereferenced the constant; the offset-8 write would also
have clobbered `JitVirtualRef.virtual_token`.

Both sites now read `SizeDescr::w_class_obj()`, which returns `None` for a
descr whose vtable word is not a `PyType` — the same accessor the dynasm,
wasm and GC-rewrite allocation paths already use.

Assisted-by: Claude
…cross an assembler run

`ExecutionContext::gettopframe_raw` returned `self.topframeref` unforced.
`executioncontext.py:68/72/446/451` all read that slot as `self.topframeref()`
— with the parens — and the only unforced reads upstream are `:88` and `:96`,
which move the vref along rather than dereference it.  "raw" here is about
skipping `gettopframe`'s virtualizable force, not the vref force; without it a
`JitVirtualRef` reaches `bh_call_fn_impl`, which dereferences it at `PyFrame`
field offsets.

`leave` runs from a `finally` upstream, and a guard failure inside an inlined
callee resumes into that callee's own `MIFrame` level, so `topframeref` is
balanced however a frame is left.  Pyre carries `enter`/`leave` as
walker-recorded field ops rather than jitcode, so the ops after a failing guard
never run.  Save and restore the slot around
`run_compiled_detailed_with_bridge_keyed`, the way `install_current_frame` /
`CurrentFrameGuard` bracket a frame: a balanced run restores what it saved, an
unbalanced exit restores the caller.

`alloc_virtual_ref` now allocates through the GC, matching
`lltype.malloc(self.JIT_VIRTUAL_REF)`.  `forced` is a traced slot, so once
`topframeref` holds the vref this object is the only edge keeping the frame
reachable; a host-heap allocation is invisible to the collector.  Old-gen
(mark-sweep, non-moving) keeps the raw addresses in `virtualref_boxes` and the
recorded ops' concrete stamps valid, and the creation write barrier covers the
old-to-young `forced` edge.

Assisted-by: Claude
A carrier frame's `enter` — and the `virtual_ref` scope it opened — belongs to
the parent trace.  `rebuild_state_after_failure` restores the still-open pairs
(`pyjitpl.py:3433 self.virtualref_boxes = virtualref_boxes`), and upstream's
resume continues inside that frame's `execute_frame`, so its `finally:
ec.leave(...)` still runs and closes the scope.  Pyre's carrier sub-walk enters
the callee body directly with nothing standing in for that `finally`, so the
bridge finished with `ec.topframeref` still naming the resumed frame's vref.  In
compiled code that vref carries a live FORCE_TOKEN and a null `forced`
(`virtualize.py optimize_VIRTUAL_REF`), so a later `gettopframe` forced a vref
whose scope no guard still encodes and the force yielded null.

`carrier_ec_leave` closes one scope through the existing `walker_ec_leave`,
sourcing the frame box from the restored pair rather than from anything the
bridge built — that box is what `opimpl_virtual_ref_finish`'s identity assert
compares against.  The carrier drain calls it as the deepest sub-walk returns
and again after each middle frame, innermost first; the `SubRaise` route passes
`got_exception=true`.

`ExecutionContext.topframeref` is a live heap field, so unlike the RPython
locals `rebuild_state_after_failure` reconstructs, whatever the dead trace last
stored there is still in it.  A CALL_ASSEMBLER callee trace reaches its guard
without passing through `execute_assembler`, so an abandoned callee level's vref
survives into the bridge walk and its JIT frame is already gone — `cpu.force`
then asserts on a null `jf_force_descr`.  The resume data names what the slot
should hold: the innermost still-open scope, whose vref the `continue_tracing`
loop has just repaired, or the resumed frame when no scope is open.  Rewrite
only when the slot holds some other vref.

327 bench fixtures and `check.py` dynasm 322/322 pass; cranelift and wasm still
fail on GC type ids.

Assisted-by: Claude
`VREF_GC_TYPE_ID` starts at `u32::MAX`, not 0, so the `type_id != 0` guard let
`alloc_virtual_ref` reach `alloc_oldgen_typed` with the unset sentinel whenever
a vref was built before `set_vref_gc_type_id` ran.  Zero is a legitimate id, so
name the sentinel and compare against it.

Assisted-by: Claude
…f/f_backref concrete stores

`PyreBlackholeAllocator::bh_setfield_gc_r` and `bh_setinteriorfield_gc_r`
performed the ref store without the generational barrier that
`llmodel.py:723 bh_setfield_gc_r` carries through `:495 write_ref_at_mem`.
The cranelift backend's implementation of the same trait methods already
called it. Blackhole resume materializes virtuals into the old generation
(`ResumeDataDirectReader` -> `VirtualInfo::allocate` ->
`allocate_with_vtable` -> `bh_new_with_vtable`) and fills their ref fields
through these setters, so a nursery value stored there did not join
`old_objects_pointing_to_young` and the slot was not forwarded by the next
minor collection.

`VirtualRefInfo::continue_tracing` writes `vref.forced` on an old-gen vref
without the barrier its `alloc_virtual_ref` counterpart already carries.

`walker_ec_enter`'s concrete `f_backref` store is the recording-time shadow
of a `SetfieldGc` on a `Type::Ref` field, whose emitted form is barriered.

Assisted-by: Claude
…h the resume allocator

`drive_bridge_carrier_walk` emitted `carrier_ec_leave` only on the
compile-bound success paths, so setup failures, walk errors and abort paths
left restored vref scopes open and the `f_backref` chain grew without bound.
Each carrier frame now leaves immediately after its walk, with a drain
epilogue, mirroring `pyframe.py execute_frame`'s `finally: ec.leave(...)`.

`ResidualFrameChainGuard::enter` replaced an already-published tracing vref
with the raw frame; a tracing vref carries `forced = frame`
(`virtualref.py:85-92`), so the guard now declines that case.

`MetaInterp::force_virtualizable_token` decoded through `NullAllocator`,
leaving `forced` null after the writeback. `compile.py:966-1000
ResumeGuardForcedDescr.force_now` materializes through the same resume
allocator ordinary guard failure uses; the registered blackhole allocator is
now threaded through.

Assisted-by: Claude
…unwind-safe topframeref

- `walker_ec_leave` skips the forced `VIRTUAL_REF_FINISH` when the tracked
  vref box is already `ConstPtr(NULL)`, which `vrefs_after_residual_call`
  installs when a may-force residual exposed the frame.
- The non-committal bridge carrier walk closes only the scopes it opened:
  the drain moved inside the walk, bounded by the entry depth and run before
  `cut_trace`, and `virtualref_boxes` is restored to the surviving snapshot
  prefix. The unbounded post-walk wrapper is gone.
- `execute_assembler`'s `ec.topframeref` save/restore became a `Drop` guard so
  a panic unwind cannot leave a `JitVirtualRef` published in the live slot.
- `continue_tracing` barriers `vref.forced` only when the collector owns the
  vref, matching the GC-allocated arm of `alloc_virtual_ref`.
- `carrier_ec_leave` stamps the concrete value on the `execution_context`
  `GetfieldGcR` it records.
- Guard-failure resume republishes the innermost restored vref whenever a
  scope is still open, not only when the published value is itself a vref.

Assisted-by: Claude
…tore

- The cranelift `bh_new_array` cleared nothing: the nursery is not
  zero-filled, so a resumed frame's `locals_cells_stack_w` came back holding
  the recycled bytes of the previous tenant, and the GC's per-item walk read
  them as children. Clear the fixed and the variable part and re-store the
  length, as `gct_do_malloc_varsize_clear` does. The inline allocators keep
  their clearing from the rewriter's ZERO_ARRAY, so the memclear stays out of
  the shared allocation helper. The dynasm path already allocates old-gen
  (zero-filled) and the wasm nursery zero-fills on reset.

- `vable_write_array_item`'s `Type::Ref` arm stored without the write barrier
  that `llmodel.py:495-497 write_ref_at_mem` implies for every blackhole ref
  store. Arm whichever side the collector owns, as the sibling
  `VirtualizableInfo::write_array_item` already does; the barrier argument is
  the array block base, before the items offset.

Assisted-by: Claude
…vref layout target-correct

`sys._getframe(N)` inside an inlined callee resolved one level too far up,
returning the caller. Two fixtures caught it: a wrong count in
`getframe_while_escaping_read_frame_identity` and `KeyError: 'base'` (exit 1) in
`getframe_inline_subwalk_multiframe`.

- `OptHeap::writes_into_virtualizable` reported true for a SETFIELD_GC whose
  target was merely read off the virtualizable frame, so `emit_lazy_setfield`
  dropped `ec.topframeref = callee_vref`. The preamble lost the publish while
  the peeled body kept it, which is why every other iteration was wrong.
  `virtualizable.py:81-84` permits indirection only through an array field, so
  the indirect branch is now restricted to SETARRAYITEM_GC.

- With the publish restored the vref is forced where it never was before, which
  exposed the rest:
  * `VIRTUAL_REF_FINISH` now runs before CALL_ASSEMBLER so the call stays
    adjacent to GUARD_NOT_FORCED and the backends install `jf_force_descr`.
  * `force_pyframe` reads `vable_token` only from the standard virtualizable
    (`pyjitpl.py:3326-3334`); an inlined callee materialized through a virtual
    reference is an ordinary frame whose token slot is not in its resume image.
  * `JitVirtualRef` field offsets come from `offset_of!` and its identity word
    is pointer-sized, per `virtualref.py:17-23`, instead of hardcoded 8/16 and
    u64. The old layout was wrong on wasm32, where a pointer is four bytes.
  * The GC rewriter reserves argument and guard-fail box positions, so a
    producerless constant box position is no longer reused for a fresh
    reference (`rewrite.py:106-116`).

pyre/check.py: dynasm 3 -> 1, cranelift 3 -> 1, wasm 18 -> 2 failures. The
remainder are the `ast_compile_roundtrip` cpython/pypy oracle mismatch and one
wasm GC panic in `inline_chain_depth_typeflip`.

Assisted-by: Claude
The argument/failarg scan added alongside the result scan admitted the
`VoidOp(u32::MAX)` sentinel, which pinned `next_pos` at `u32::MAX` and
overflowed the first `next_pos += 1` in `emit`. Both scans now select on
`ty()` being Int/Float/Ref, and test `is_constant()` before `raw()`,
which panics on the inline Const variants.

Assisted-by: Claude
The struct's identity word and both reference slots are pointer-sized, and
the type registration derives its size and traced offset from the struct,
but the surrounding comments still described the earlier fixed 8/16-byte
spelling. They also carried line numbers of files in this repo, which the
comment convention excludes.

Also record what the rewriter's `next_pos` sentinel guard prevents in a
release build, where the overflow wraps to 0 instead of panicking.

Assisted-by: Claude
The earlier narrowing to Int/Float/Ref also dropped `TempVar`, which the
original scan counted. Excluding it lowers the mark past the sentinel range
and lets a rewriter-introduced box take a position that reaches the backend
through resume data rather than through `pos`/args/failargs. Only the
`VoidOp(u32::MAX)` sentinel needs excluding, so both scans now share one
predicate that does exactly that.

Assisted-by: Claude
`COND_CALL_GC_WB` had three paths that emitted zero bytes and returned:

- A missing write-barrier descriptor returned early on both arches.
  Descriptor *resolution* already falls back to the current MiniMark
  layout, but the emitter's own `None` arm was untouched. Now `expect`,
  matching the descriptor assert at opassembler.py:917-919 and
  x86/assembler.py:2399-2401. Every dynasm `set_gc_allocator` call site
  installs `MiniMarkGC`, whose `get_write_barrier_descr` returns `Some`.

- A base argloc that was not `Loc::Reg` returned early on both arches.
  Upstream aarch64 gets a register from `ARMRegisterManager.return_constant`
  (aarch64/regalloc.py:70); the shared `RegisterManager::return_constant`
  follows the llsupport spelling (llsupport/regalloc.py:625) and can return
  a bare `Loc::Immed`. Now panics, like the paired lowered `GcStore`.

- aarch64 card marking ran the whole sequence under `if let Some(loc_index)`
  where `loc_index` was narrowed to `Loc::Reg`, so an immediate index
  produced an empty `card_mark` body. `nbody` emits 46 `COND_CALL_GC_WB_ARRAY`
  ops and every one carries an `Immed` index, so that body was always empty
  on aarch64 while x86 emitted the card bits. Ports the immediate arm from
  x86/assembler.py:2382-2386; A64 has no or-to-memory form, so the `OR8`
  becomes ldrb/orr/strb. Both arches now end the index match with
  x86/assembler.py:2387-2388's `AssertionError` instead of falling through.

`pyre/check.py --backend dynasm`: 333 passed. `nested_loop` (2.1x, a bench
that sits on the 2x gate; it emits no write barriers at all) and
`synth/ast_compile_roundtrip` (baseline cpython/pypy output mismatch, pyre
not run) are unrelated.

Assisted-by: Claude
…arrier decline

Neither path is reachable from `pyre/check.py`, so both get a unit test.

`cond_call_gc_wb_array_immed_index_marks_same_card_as_reg_index` allocates
three old-gen card arrays, sets CARDS_SET by hand so the emitted
`b.ne =>card_mark` is taken on the first check and the array helper is never
entered, then drives each index through the compiled immediate arm, the
compiled register arm, and `do_write_barrier_card`, comparing `dirty_cards`
after every index. A `ConstInt` index reaches the emitter as `Loc::Immed`
because both `CondCallGcWb*` prepare sites call `make_sure_var_in_reg` with
no selected register (llsupport/regalloc.py:625). Negative control: with the
immediate arm disabled the compiled cards read `[]` against `[0]`.

`write_barrier_ignores_unmanaged_jitframe_with_flag_byte_set` pins the
`is_managed_heap_object` guard in `do_write_barrier`. A jitframe from the
`libc::calloc` fallback has no GcHeader, so the inline flag test reads
`jit_wb_if_flag_byteofs` out of bytes that are not a header; the test sets
that bit explicitly rather than depending on the host allocator, and asserts
the block neither enters `remembered_set` nor has the byte cleared. Negative
control: removing the guard fails the `remembered_set` assertion.

Also extends `do_write_barrier`'s doc comment to name the jitframe case —
it previously justified the guard only by interpreter `Box::into_raw` call
sites, which made the JIT path look like dead weight.

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/f06af59666ec46d2d4c90cb0af6efcf8b512b966/pyre-jit-trace/src/trace.rs#L1660
P1 Badge Defer carrier leave until the resumed frame exits

When drive_bridge_carrier_subwalk returns None or Err because bridge tracing cannot continue, the resumed callee has not returned or raised—it will continue through the bridge-abort recovery path. This unconditional leave nevertheless pops its restored vref scope and concretely rewrites EC.topframeref to the caller; the later discard restores only virtualref_boxes, not that concrete frame chain. Consequently, resumed interpreter/blackhole code inside the callee can expose the caller through sys._getframe() and eventually leave the wrong frame. Call carrier_ec_leave only for terminal SubReturn/SubRaise outcomes, while preserving the active callee on tracing aborts.

AGENTS.md reference: AGENTS.md:L24-L30

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

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