Skip to content

jit: close the branchy-inlined-callee bridge gap (overlay NULL-skip + vable-array re-assertion) - #653

Merged
youknowone merged 2 commits into
mainfrom
perf-bridge
Jul 19, 2026
Merged

jit: close the branchy-inlined-callee bridge gap (overlay NULL-skip + vable-array re-assertion)#653
youknowone merged 2 commits into
mainfrom
perf-bridge

Conversation

@youknowone

@youknowone youknowone commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Closes the branchy-inlined-callee bridge gap (#343 depth-1 shape): a hot s = f(s, x) loop whose data-branchy inlined callee guard-fails now compiles a bridge instead of deopting to the blackhole on every crossing. Two commits, both in setup_bridge_sym on the pyre side, no eval.rs/jitdriver.rs changes.

Commit 1 — overlay NULL-skip (10b621bd29)

overlay_local stamped a bridge local's concrete from the live frame slot, skipping only Void. A loop-carried local held in a register at an interior guard reads NULL (GcRef(0)) from locals_cells_stack_w (never written back); stamping that hole folds a later may-force residual's Ref arg to concrete NULL → MayForceNullRefArgUnsupported. Skip a GcRef(0) source too, matching the deferred-overlay seed loop, so the box stays symbolic and the residual reads the runtime value.

Commit 2 — re-assert the vable array image after the vsd correction (4c8c0bf555)

For the pure s = add(s, x) shape, sync_virtualizable_after_guard_failure correctly restores s into the heap frame at bridge entry — but the multi-frame vsd correction that runs next (clear_stack_above with the innermost callee's depth, in callee coordinates, applied to the root frame) then NULLs the root frame's register-resident loop-carried locals. setup_bridge_sym reads NULL and the bridge aborts.

Re-assert the resume-decoded vable array items into the live frame's locals_cells_stack_w, per item inside the vvals decode loop, immediately after each decode (before the alloc-heavy frame-register consume loops). Items are uniformly Ref so no boxing; each write roots the value in the fully-walked frame array before the next decode allocates. The vable statics keep the correction's last_instr/valuestackdepth. This restores the write_boxes-runs-at-setup ordering the earlier synchronize skips on the multi-frame path. The vsd correction itself (load-bearing for the interpreter/blackhole fallback) is untouched.

Evidence

  • probeE (s = add(s, x), branchy inlined callee): 9.5s → 0.024s (~396x), value correct; bridge compiles (loops_aborted 299 → 0, guard_failures 285887 → 201).
  • Acceptance bench bench/bridge_branchy_callee_regression.py: 874x → 0.9x PASS.
  • probeF (2-level nested callee): correct.
  • check.py ALL PASSED: dynasm 225/225, cranelift 225/225, wasm 224/224.
  • GC-stress (MAJIT_GC_STRESS=1, collection per alloc): probeE and a loop-carried GC-mobile Ref-object probe both match PYRE_NO_JIT; bridge still compiles. The write target is the GC-traced frame array (walked in full every collection), so no stale-address hazard — unlike the earlier off-heap-shadow approach that was reverted.

Known follow-up (pre-existing, not introduced here)

A virtual-typed vable array item is materialized by two non-shared caches (the guard-recovery sync path vs setup_bridge_sym's BridgeVirtualCache), so such a slot can hold a distinct physical object with identical field values. Commit 2 makes the frame agree with the trace (an improvement); unifying the two materializers is a separate ticket.

commented by Claude

Summary by CodeRabbit

  • Bug Fixes
    • Improved JIT bridge setup so decoded reference values from virtualizable array items are correctly re-applied before later overlay/concretization reads.
    • Tightened concrete stamping for bridge-local values to avoid propagating NULL (Ref(GcRef(0))) concretes, preventing invalid null references from affecting subsequent operations and improving runtime stability.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7526134-88c3-431a-a75a-7795b11fdb60

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8c0bf and 3ac9446.

📒 Files selected for processing (1)
  • pyre/pyre-jit-trace/src/state.rs

Walkthrough

Bridge setup now reasserts decoded virtualizable references into the live frame after resume-image correction and avoids concrete stamping for both Void and explicit NULL references when seeding bridge locals.

Changes

Bridge setup corrections

Layer / File(s) Summary
Reassert decoded virtualizable references
pyre/pyre-jit-trace/src/state.rs
Decoded virtualizable reference values are written back into the live frame after the resume array image is clobbered by stack correction.
Guard NULL bridge-local concretization
pyre/pyre-jit-trace/src/state.rs
The overlay skips ctx.try_set_opref_concrete for live source values that are Void or Ref(GcRef(0)).

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

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

A rabbit hopped through bridge-local space,
And kept NULL stamps from leaving a trace.
Fresh refs returned to the frame anew,
While empty values stayed safely true.
“Hop!” said the bunny, “the bridge is sound!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: skipping NULL overlay stamps and reasserting the vable array during bridge setup.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf-bridge

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

❤️ Share

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

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 3ac9446).
Updated: 2026-07-19T04:29:06.927Z

Files in the reviewed diff
pyre/pyre-jit-trace/src/state.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/state.rs:3519 ↔ rpython/jit/metainterp/virtualizable.py:108-113 — the new direct Ref store omits the required old-to-young GC write barrier. PyPy’s translated lst[j] = ... receives a GC-transform barrier; Pyre explicitly requires try_gc_write_barrier before such stores. A minor GC can otherwise miss a young object newly written into an old FixedObjectArray.

  • pyre/pyre-jit-trace/src/state.rs:3516-3519 ↔ rpython/jit/metainterp/virtualizable.py:109-113 — PyPy writes every physical array element and asserts the decoded-box count matches exactly. The patch silently returns for a null array or out-of-range slot, leaving a partial vable image instead of surfacing corrupted/inconsistent resume data.

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

  • majit/majit-metainterp/src/virtualizable.rs:1168-1171 ↔ rpython/jit/metainterp/virtualizable.py:108-113 — the pre-existing canonical write_boxes_to_heap() path also stores Ref array elements without a write barrier. This is outside the authoritative changed-file list and was already present on upstream/main.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/state.rs:8360-8365 ↔ rpython/jit/metainterp/pyjitpl.py:3428-3454 — Pyre reasserts each decoded array item during decoding because its later native clear_stack_above can clobber the root-frame image in multi-frame resumes. PyPy performs one synchronize_virtualizable() batch write after resume reconstruction. The earlier Pyre write timing is a moving-GC/off-heap-decoder adaptation; it is structurally acceptable once it preserves the same complete, barriered array image.

@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: 10b621bd29

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

if !matches!(
cv,
majit_ir::Value::Void
| majit_ir::Value::Ref(majit_ir::GcRef(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.

P2 Badge Preserve genuine PY_NULL local stamps

When a bridge local is genuinely unbound (for example after DELETE_FAST or a branch that skips assignment), the live frame slot is also PY_NULL; this file documents that frame-slot NULL means an uninitialized local, and the LOAD_FAST walker raises UnboundLocalError only when ctx.box_value(value) is Ref(PY_NULL) (trace_opcode.rs:7397-7403). This blanket skip leaves a non-constant local box unstamped in that case, so the bridge records a normal GuardNonnull/symbolic path instead of reproducing the interpreter's unbound-local exception, causing a miscompiled or perpetually deopting bridge for deleted/unassigned locals. Please distinguish stale frame-array holes from real unbound locals before dropping the NULL stamp.

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.

Caution

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

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

8734-8740: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Skip GcRef(0) when seeding kept-stack colors from the vable image.

The stack overlay mirrors the overlay_local logic by reading from live_local_values (the locals_cells_stack_w array). Just like loop-carried locals, kept stack temps can leave unmaterialized holes (GcRef(0)) in the frame array post-guard. Stamping these holes will poison the real vable box with concrete NULL, triggering the exact same MayForceNullRefArgUnsupported residual abort this PR intends to fix, just for stack slots instead of locals.

Please apply the same GcRef(0) check here as was added to overlay_local.

🐛 Proposed fix
                             if seed_bridge_locals {
                                 if let Some(&cv) = live_local_values.get(s) {
-                                    if !matches!(cv, majit_ir::Value::Void) {
+                                    if !matches!(
+                                        cv,
+                                        majit_ir::Value::Void | majit_ir::Value::Ref(majit_ir::GcRef(0))
+                                    ) {
                                         ctx.try_set_opref_concrete(v, cv);
                                     }
                                 }
                             }
🤖 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/state.rs` around lines 8734 - 8740, Update the
seed_bridge_locals handling around live_local_values to skip
majit_ir::Value::GcRef(0), matching the existing overlay_local logic. Only call
ctx.try_set_opref_concrete for non-Void values that are not the null GcRef
sentinel, while preserving all other stack-color seeding 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.

Outside diff comments:
In `@pyre/pyre-jit-trace/src/state.rs`:
- Around line 8734-8740: Update the seed_bridge_locals handling around
live_local_values to skip majit_ir::Value::GcRef(0), matching the existing
overlay_local logic. Only call ctx.try_set_opref_concrete for non-Void values
that are not the null GcRef sentinel, while preserving all other stack-color
seeding behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac1af268-32c9-43fb-9d11-37068dae940b

📥 Commits

Reviewing files that changed from the base of the PR and between 77009b9 and 10b621b.

📒 Files selected for processing (1)
  • pyre/pyre-jit-trace/src/state.rs

@youknowone youknowone changed the title jit: skip NULL frame-slot stamp in the bridge-setup local overlay jit: close the branchy-inlined-callee bridge gap (overlay NULL-skip + vable-array re-assertion) Jul 18, 2026

@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/f6a87117dca574726afa4830039bd112be7d2243/pyre-jit-trace/src/state.rs#L8364
P1 Badge Avoid restamping frame slots from stale fail values

When an earlier bridge_decode_box in this vable-array loop materializes a virtual, it can allocate and trigger a minor GC before later array items are decoded; those later objects are already safe in the live frame and get forwarded there, but val is decoded from the unforwarded raw fail_values/resume copy. Passing that val back into store_live_frame_array_slot can overwrite the forwarded locals_cells_stack_w slot with the stale pre-GC address, which is the same dangling-pointer hazard the code below explicitly avoids by reading concrete array values from the rooted live frame instead of decoded vable_array_values after allocations.

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

overlay_local resolves a bridge local from the vable image and then stamps its
concrete from the live frame slot, previously skipping only Void. A loop-carried
local held in a register at an interior guard reads NULL (GcRef(0)) from
locals_cells_stack_w because it was never written back; stamping that hole folds
a later may-force residual's Ref arg to concrete NULL and aborts with
MayForceNullRefArgUnsupported. Skip a GcRef(0) source too, matching the
deferred-overlay seed loop, so the box stays symbolic and the residual reads the
runtime value.

Assisted-by: Claude
…sd correction

At bridge setup the multi-frame vsd correction in decode_and_restore_guard_failure
(clear_stack_above with the innermost callee's depth applied to the root frame) nulls
the root frame's register-resident loop-carried locals that
sync_virtualizable_after_guard_failure had just restored from the resume decode.
setup_bridge_sym then reads those slots as NULL from locals_cells_stack_w, so a hot
`s = f(s, x)` loop whose branchy inlined callee guard fails stamps s's box with concrete
NULL, folds a later may-force residual's Ref arg to NULL, and aborts the bridge with
MayForceNullRefArgUnsupported — deopting to the blackhole on every crossing.

Re-assert the resume-decoded vable array items into the live frame's locals_cells_stack_w
per item inside the vvals decode loop, immediately after each item is decoded and before
the alloc-heavy frame-register consume loops. The array items are uniformly Ref so no
boxing is introduced; writing each item right after its decode roots it in the traced
frame before the next decode allocates. The vable statics keep the corrected
last_instr/valuestackdepth. This restores the write_boxes-runs-at-setup ordering the
earlier synchronize skips on the multi-frame path.

probeE (s = add(s, x), branchy inlined callee): 9.5s -> 0.024s, bridge compiles
(loops_aborted 299 -> 0, guard_failures 285887 -> 201); acceptance bench 874x -> 0.9x.

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: 3ac9446f29

ℹ️ 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 +8361 to +8365
store_live_frame_array_slot(
sym.concrete_vable_ptr as usize,
idx - vable_array_start,
val,
);

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 Preserve pending fields when reasserting vable virtuals

When a virtualizable_values array slot decodes as a virtual that also has deferred pending field writes, this overwrites the live frame slot with the object materialized by setup_bridge_sym. The guard-recovery path has already applied all pending fields before syncing the vable (pyre/pyre-jit/src/eval.rs:8231), but this bridge-side materializer does not replay generic rd_pendingfields (it only special-cases current-exception fields in seed_bridge_pending_current_exception), so the overwrite replaces the correct object with a stale copy missing those lazy field/array writes and the bridge can trace from corrupted object state.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant