Skip to content

majit: #25 de-conflate frame-input shape from true-portal; always-portal flip (default ON)#457

Merged
youknowone merged 3 commits into
mainfrom
nbody
Jul 11, 2026
Merged

majit: #25 de-conflate frame-input shape from true-portal; always-portal flip (default ON)#457
youknowone merged 3 commits into
mainfrom
nbody

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Splits the fused per-CodeObject is_portal bool into its two independent concepts and flips to always-portal input shape (default ON, PYRE_ALWAYS_PORTAL=0 to roll back).

What it does

  • is_true_portal (portal_jd is not None): keeps the jit_merge_point marker + jitdriver stamp + LOAD_GLOBAL/LOAD_CONST namespace const-fold split.
  • frame_inputs / frame_is_portal (reds=['frame','ec'] + frame-vable locals prologue): the INPUT SHAPE, now Portal for every drained jitcode under the flip.

Every drained per-code jitcode becomes Portal-shaped, so its whole body is walkable as a portal and the trace.rs !built_as_portal decline narrows to shapeless skeletons only.

The blocker it resolves

The prior always-portal attempt regressed inline_helper 22x: forcing the frame-vable prologue into leaf callees made callee_fast_path_inlinable reject them as non-static vable → strict-leaf declines → main loop stops compiling.

Fix = the fresh-frame register-to-register fold (f202ec29e57): a branchless leaf's own-frame getarrayitem_vable_r/setarrayitem_vable_r resolve through a per-slot OpRef shadow (FBW_CALLEE_LOCALS_OPREF) instead of the metainterp vable path, emitting no GC op — the runtime realization of the fresh_virtualizable translate-time elision (is_virtualizable_getset returns False when 'fresh_virtualizable' in flags). The 1fbd3ecdf48 commit adds the scalar setfield_vable no-op fold that completes it.

Commits

  • f202ec29e57 — strict fresh-frame vable fold machinery (inert until the flip; byte-identical 159×2).
  • 1fbd3ecdf48 — de-conflate frame-input shape; always-portal flip (default ON).

Verification

  • inline_helper 985375007 @ 0.18s (was TIMEOUT on the prior blocked attempt).
  • short_circuit_side_effects built_as_portal 3→0, stdout byte-identical.
  • check.py: dynasm 160/160, cranelift 160/160, wasm 159/160 (the 1 wasm fail synth/calls_closures is pre-existing — byte-identical wrong output under both PYRE_ALWAYS_PORTAL=1 and =0).
  • cargo test -p pyre-jit-trace -p pyre-jit: 0 failed.

Composes with the orthodox strict-multiframe snapshot routing (#440/#453) that this branch is rebased onto.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 42 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: 1eca425a-9aef-4831-afd4-c2146e3ead83

📥 Commits

Reviewing files that changed from the base of the PR and between 91df2e6 and 5d10432.

📒 Files selected for processing (5)
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/pyjitcode.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nbody

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

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fbd3ecdf4

ℹ️ 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 +5864 to 5865
let frame_inputs = if is_true_portal || always_portal {
FrameInputs::Portal

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 Seed portal-shaped callees on inline_call paths

With PYRE_ALWAYS_PORTAL defaulting on, this makes non-portal callees carry the Portal input shape and emit the frame-vable locals prologue. However the existing inline_call_* descent still goes through run_sub_jitcode_walk, which only seeds the explicit argument slots (jitcode_dispatch.rs:19009-19028) and never seeds the callee's portal frame/ec registers or activates the fresh-frame fold added for try_walker_inline_user_call. Any non-portal callee entered by an inline_call_* whose body starts with the new LOAD_FAST/STORE_FAST vable ops will therefore read an unseeded frame (OpRef::NONE) and abort with VableBoxNotSeeded, so enabling the default Portal shape needs the same frame/fold seeding on this path.

Useful? React with 👍 / 👎.

Repository owner deleted a comment from github-actions Bot Jul 9, 2026
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit c4ba1ac).

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-jit/src/jit/codewriter.rs:5776 ↔ pypy/interpreter/pycode.py:275 and rpython/jit/codewriter/jtransform.py:990: “all drained per-code jitcodes” are now given FrameInputs::Portal and frame-vable operations. PyPy marks a newly created call frame fresh_virtualizable; is_virtualizable_getset() then returns False, so ordinary callee-frame accesses are direct operations, not portal virtualizable accesses.

  • pyre/pyre-jit-trace/src/pyjitcode.rs:193 ↔ pypy/module/pypyjit/interp_jit.py:66: redefining built_as_portal as an input-shape flag admits every callee body to portal full-body walking. PyPy has one portal dispatch loop whose JitDriver owns [frame, ec]; non-portal JitCodes are not made portal bodies merely to make them walkable.

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:4131 ↔ rpython/jit/metainterp/pyjitpl.py:1189: the new own-frame shortcut drops every matching setfield_vable_* without updating virtualizable state or synchronizing it. Upstream either has no vable op for a fresh frame or performs virtualizable_boxes[index] = valuebox followed by synchronize_virtualizable().

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:4266 ↔ rpython/jit/metainterp/pyjitpl.py:1219: own-frame getarrayitem_vable_* now reads a TLS HashMap and otherwise falls through to an unseeded-vable failure. Upstream’s fresh-frame translation emits a normal direct array read; its actual vable handler reads the frame’s virtualizable_boxes, not a per-thread side table.

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:4404 ↔ rpython/jit/metainterp/pyjitpl.py:1237: own-frame setarrayitem_vable_* updates TLS OpRef/concrete maps and emits no operation. Upstream’s fresh-frame case is translated before bytecode emission; a remaining vable write updates virtualizable_boxes and synchronizes it.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:7220 ↔ rpython/jit/codewriter/codewriter.py:74: JITCode shape and walk behavior are selected independently by the mutable process environment variable PYRE_ALWAYS_PORTAL. A JitCode built before an environment change can subsequently be walked with the opposite shape assumption. RPython’s make_jitcodes() produces a fixed translated JitCode body; it has no runtime mode that changes its calling/virtualizable shape.

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:7198 ↔ rpython/jit/metainterp/pyjitpl.py:65: the new FBW_CALLEE_LOCALS_OPREF: Vec<HashMap<...>> is a side table for per-frame SSA state. RPython stores execution state in each MIFrame’s register arrays and captures the actual frame stack; this new table is neither frame-owned nor represented in resume data.

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

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:12340 ↔ rpython/jit/metainterp/pyjitpl.py:1415: walker inlining still rejects every callee with an internal branch and supports only straight-line leaves. PyPy pushes and traces an MIFrame for an inlinable callee, including branchy bodies, and preserves that frame stack for guard/resume handling.

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:7182 ↔ rpython/jit/metainterp/pyjitpl.py:86: the pre-existing FBW_CALLEE_LOCALS_CONCRETE: Vec<HashMap<...>> keeps callee-local state in a TLS side table. Upstream stores the corresponding state in the individual MIFrame.registers_* arrays, so it naturally has per-frame identity and participates in frame-stack capture.

4. Structural adaptations

  • pyre/pyre-jit/src/eval.rs:8214 ↔ rpython/jit/backend/llsupport/llmodel.py:723: changing ref-field writes to a pointer-width store correctly reflects upstream’s dedicated write_ref_at_mem path rather than the integer/float byte-size path. The added null-pointer no-op is a Rust runtime-safety adaptation; upstream assumes a valid structure pointer and performs no equivalent guard.

…nert)

Add the register-to-register fold that lets a branchless leaf callee carrying
the portal frame-vable locals prologue stay strict-inlinable. Own-frame
getarrayitem_vable_r / setarrayitem_vable_r resolve through a per-slot OpRef
shadow instead of the metainterp vable path, emitting no GC op — the
fresh_virtualizable case (is_virtualizable_getset returns False when
'fresh_virtualizable' in flags, jtransform.py:990-993; pycode.py:275
fresh_frame = jit.hint(frame, access_directly=True,
fresh_virtualizable=True)).

- FBW_CALLEE_LOCALS_OPREF + FBW_STRICT_FOLD_FRAME_REG thread-local stacks,
  pushed/popped by InlineRecursionGuard alongside FBW_CALLEE_LOCALS_CONCRETE.
- Short-circuits in getarrayitem_vable_via_metainterp /
  setarrayitem_vable_via_metainterp: when the op reads/writes the active
  fold frame register, fold via the shadow.
- callee_fast_path_inlinable gains a callee_frame_reg param and accepts
  own-frame vable ops via inline_resolvable_seeded_frame_op.
- Strict path seeds each param into slot i and activates the fold before the
  sub-walk.

Gated on PYRE_ALWAYS_PORTAL (fbw_always_portal_enabled); default OFF leaves
callee_portal_frame_reg = u16::MAX so every new path is inert and byte-
identical. dynasm/cranelift 159/159; inline_helper 985375007 @ 0.24s.

Assisted-by: Claude
…tal flip (default ON)

Split the fused `is_portal` bool into its two independent RPython concepts:
  - `is_true_portal` (portal_jd is not None): jit_merge_point marker +
    jitdriver stamp + LOAD_GLOBAL/LOAD_CONST namespace const-fold split.
  - `frame_inputs` / `frame_is_portal` (reds=['frame','ec'] + frame-vable
    locals prologue): the INPUT SHAPE.

Under the always-portal flip (default ON, PYRE_ALWAYS_PORTAL=0 to roll back)
every drained per-code jitcode is Portal-shaped, so its whole body is walkable
as a portal and the trace.rs `!built_as_portal` decline narrows to shapeless
skeletons only. A branchless leaf's frame-vable prologue folds via the
fresh-frame register-to-register fold (fresh_virtualizable,
jtransform.py:990-993), so it stays strict-inlinable — no VableBoxNotSeeded
abort, no 22x inline_helper regression.

codewriter.rs:
- frame_inputs = Portal when is_true_portal || always_portal; frame_is_portal
  = matches!(Portal) gates the ~20 frame-vable dual-write sites (getfield/
  setfield/getarrayitem/setarrayitem_vable on frame_var). Gating on
  matches!(Portal), not has_frame(), keeps a Frame callee (flag=0) prologue-
  free and byte-identical to the fused bool.
- jit_merge_point marker + LOAD_GLOBAL namespace split stay is_true_portal.
- finalize_jitcode `built_as_portal: frame_is_portal` records the input shape.

jitcode_dispatch.rs:
- setfield_vable_via_metainterp scalar fold: a leaf's valuestackdepth /
  last_instr setfield on its own fresh frame folds to a no-op (completes the
  getarrayitem/setarrayitem fold from the prior commit).
- fbw_always_portal_enabled default ON.

trace.rs / pyjitcode.rs: decline message + built_as_portal doc updated to
"no portal input shape".

Witnesses: inline_helper 985375007 @ 0.19s (was TIMEOUT on the prior blocked
attempt); short_circuit_side_effects built_as_portal=false 3->0, stdout
byte-identical; dynasm + cranelift check.py 159/159 default-on and with
PYRE_ALWAYS_PORTAL=0 rollback.

Assisted-by: Claude
bh_setfield_gc_r routed through the descriptor-sized byte writer shared
with the Int/Float field stores. On wasm32 the Ref FieldDescrInfo reports
field_size 8 while a pointer is 4 bytes, so the store overwrote the
adjacent pointer field of the resumed virtual PyFrame (its
locals_cells_stack_w), zeroing the frame's locals array pointer; the
callee then read locals through a null array.

Write the Ref field with a pointer-width usize store, matching
llmodel.py:723 bh_setfield_gc_r. Int and Float keep the descriptor-sized
writer.

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: 5d10432b91

ℹ️ 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 Portal-shaped under the always-portal flip, so a later portal
// walk of any body is admitted (`run_perfn_walk` no longer declines
// on `!built_as_portal`). Only shapeless skeletons carry `false`.
built_as_portal: frame_is_portal,

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 Rebuild late portals before admitting them

When PYRE_ALWAYS_PORTAL is on, a function first compiled through the callee path reaches this metadata assignment with frame_is_portal=true but is_true_portal=false, so the earlier is_true_portal/portal_jd_index gates do not emit jit_merge_point or loop_header ops for backward jumps. CallControl::get_jitcode also does not rebuild an existing payload when that same code is later registered as a portal, so marking it walkable here lets run_perfn_walk drive a stale callee-shaped body as a root portal; a hot loop function that was first seen as a callee can then be walked without the merge-point machinery needed to close/compile the loop. Either rebuild/stamp a true portal body on late registration or keep these payloads declined.

Useful? React with 👍 / 👎.

@youknowone youknowone merged commit 9e1dc2e into main Jul 11, 2026
29 of 30 checks passed
@youknowone youknowone deleted the nbody branch July 11, 2026 14:16
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