Skip to content

jit: restrict the LOAD_LOCALS residual to portal jitcodes; record three converged jitstats baselines - #1014

Merged
youknowone merged 2 commits into
mainfrom
wasm-jit
Aug 4, 2026
Merged

jit: restrict the LOAD_LOCALS residual to portal jitcodes; record three converged jitstats baselines#1014
youknowone merged 2 commits into
mainfrom
wasm-jit

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Two commits, both follow-ups to #1009.

jit: restrict the LOAD_LOCALS residual to portal jitcodes

Addresses the P1 left on #1009's review.

In a non-portal inlined callee frame_var aliases the outermost frame — the
same aliasing the LoadGlobal namespace split documents, where it resolved the
caller's names table and produced a silent miscompile. Threading the callee's
own frame is not available as a remedy: an inlined callee has no materialised
frame at all (frame_ptr == 0, portal_frame_reg unseeded), which is what
inlining a virtualizable means.

The two opcodes differ in whether the receiver's identity can change the answer,
so they no longer share one arm:

  • load_locals returns that frame's own w_locals via
    get_or_create_w_locals, so an aliased receiver is a wrong value that no
    guard can catch, and there is no frame-free way to compute it. Residual in
    portal jitcodes, permanent decline otherwise.
  • load_build_class reads frame.get_builtin(), which with
    honor__builtins__ false is space.builtin for every frame
    (baseobjspace::frame_builtin_obj), so the answer does not depend on which
    frame asks. Unrestricted. That is a property of the flag rather than of this
    arm, so a const assertion on HONOR_BUILTINS now fails the build, naming
    this site, if the flag is ever flipped.

Restricting both was measured and rejected — it cost
slots_class_var_conflict loops_aborted 0 -> 5, list_length_hint_validate
guard_failures 1 -> 13427, and two fixtures' loops_compiled. Splitting them
costs none of that: every fixture the residual improved keeps its value.

bench: record the cranelift and wasm jitstats converged onto by #1003 and #960

Three baselines moved after #1003 and #960 merged. All three are a backend
converging onto a value another backend already recorded, not a regression:

fixture field before after already recorded by
exception_try_call_inlined_callee_raise (cranelift) guard_failures 402 201 dynasm
loops_compiled 3 2 dynasm
exception_try_call_inlined_callee_raise (wasm) guard_failures 821 201 dynasm
loops_aborted 1 0 dynasm
loops_compiled 3 2 dynasm
exc_in_loop_divzero_continue (wasm) bridges_compiled 0 3 dynasm, cranelift
guard_failures 360 603 dynasm, cranelift

Evidence that these are convergence:

  • All three backends' exception_try_call_inlined_callee_raise baselines are now
    byte-identical (bridges_compiled=1 guard_failures=201 loops_aborted=0 loops_compiled=2).
  • The declining walk reports the same census mechanism on dynasm and wasm
    (P2Drain::CompileRootRaise: 1) — the numbers and the reason agree.
  • exc_in_loop_divzero_continue's guard_failures rise accompanies
    bridges_compiled 0 -> 3: more compiled coverage, not less.
  • Both fixtures' output is byte-identical to python3.14 on every backend.

Verification

check.py: dynasm 371/371, cranelift 371/371, wasm 367/367.

Measured on a full rebuild at HEAD with all four LLBC fingerprints verified, and
artifact mtimes re-checked before and after each run to confirm no concurrent
rebuild intervened.

In a non-portal callee `frame_var` aliases the outermost frame, the same
aliasing the LoadGlobal namespace split documents, where it resolved the
caller's `names` table. Threading the callee's own frame is not available as a
remedy: an inlined callee has no materialised frame at all (`frame_ptr == 0`,
`portal_frame_reg` unseeded), which is what inlining a virtualizable means.

The two opcodes differ in whether the receiver's identity can change the answer,
so they no longer share one arm:

`load_locals` returns that frame's own `w_locals` via get_or_create_w_locals,
so an aliased receiver is a wrong value that no guard can catch, and there is no
frame-free way to compute it. It keeps the residual in portal jitcodes and
declines otherwise.

`load_build_class` reads frame.get_builtin(), which with honor__builtins__
false is space.builtin for every frame (baseobjspace::frame_builtin_obj), so
the answer does not depend on which frame asks. It stays unrestricted. That is
a property of the flag rather than of this arm, so a const assertion on
HONOR_BUILTINS now fails the build, naming this site, if the flag is ever
flipped.

Restricting both instead was measured and rejected: it cost
slots_class_var_conflict 0 -> 5, list_length_hint_validate guard_failures
1 -> 13427, and two fixtures' loops_compiled. Splitting them costs none of
that — every fixture the residual improved keeps its value.

check.py: dynasm 371/371, cranelift 371/371, wasm 366 passed. The one wasm
failure in that run, exception_try_call_inlined_callee_raise loops_compiled
3 -> 2, is inherited: that fixture's bytecode contains neither opcode, and the
same failure appears with the rejected variant above.

Assisted-by: Claude
…and #960

Three baselines moved when those two landed. Each is a backend converging onto
values the other backends already recorded, not a loss, so each is recorded
rather than investigated as a regression.

exception_try_call_inlined_callee_raise: cranelift and wasm both drop
loops_compiled 3 -> 2 and guard_failures to 201, which is what the dynasm
baseline already held. All three backends now carry byte-identical numbers for
this fixture (bridges_compiled=1 guard_failures=201 loops_aborted=0
loops_compiled=2), and wasm additionally loses its extra loops_aborted=1. The
declining walk reports the same census reason on dynasm and wasm,
P2Drain::CompileRootRaise=1.

exc_in_loop_divzero_continue: wasm was the outlier at bridges_compiled=0,
guard_failures=360 against dynasm's and cranelift's 3 and 603, and now matches
both. The guard_failures rise is a consequence of compiling those three bridges
rather than none, so it accompanies more compiled coverage, not less.

Both fixtures' output is byte-identical to python3.14 on every backend.

check.py at this tree: dynasm 371/371, cranelift 371/371, wasm 367/367.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 3, 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: 25 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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 Plus

Run ID: 275a7303-1e0a-40da-b39a-f2384bed9a86

📥 Commits

Reviewing files that changed from the base of the PR and between 21715c6 and 2db6410.

📒 Files selected for processing (4)
  • pyre/bench/synth/exc_in_loop_divzero_continue.wasm.jitstats
  • pyre/bench/synth/exception_try_call_inlined_callee_raise.cranelift.jitstats
  • pyre/bench/synth/exception_try_call_inlined_callee_raise.wasm.jitstats
  • pyre/pyre-jit/src/jit/codewriter.rs

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: 2db6410938

ℹ️ 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 +12375 to +12378
if is_locals && !is_true_portal {
push_fresh_ref(&mut current_state, &mut graph);
current_depth += 1;
emit_abort_permanent!(py_pc);

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 Thread the callee frame instead of declining LOAD_LOCALS

When a hot portal reaches an inlined non-portal class body containing LOAD_LOCALS, this branch puts abort_permanent in the callee jitcode; loop_inlines_abort_permanent_callee then declines resolvable caller loops up front, while other paths abort when they reach the opcode, so these loops can never JIT. This avoids the wrong-locals result but leaves the underlying frame-identity collapse in place as a permanent performance workaround. Non-portal jitcodes need their own red frame and should execute load_locals against that frame rather than retiring the trace.

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

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 2db6410).
Updated: 2026-08-03T23:18:53.707Z

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

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit/src/jit/codewriter.rs:12375 ↔ pypy/interpreter/pyopcode.py:777: non-portal LOAD_LOCALS now emits abort_permanent, whereas PyPy executes self.pushvalue(self.getorcreatedebug().w_locals). This avoids returning the caller’s locals, but incorrectly makes every reachable inlined callee containing LOAD_LOCALS permanently unjittable. The parity fix is to thread that callee’s own red frame, not decline it.

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

  • pyre/pyre-jit/src/jit/codewriter.rs:8941 ↔ pypy/interpreter/function.py:206: non-portal generated code has frame_var aliased to the outermost frame, while PyPy constructs and runs a distinct new_frame for each Python call. This frame-identity collapse is the underlying cause of the prior incorrect non-portal LOAD_LOCALS lowering and still leaves other per-frame state unavailable during inlining/resume.

  • pyre/pyre-interpreter/src/baseobjspace.rs:12658 ↔ pypy/interpreter/pyframe.py:199: Pyre fixes HONOR_BUILTINS to false; PyPy supports both the shared-builtin and per-frame-builtin branches. Consequently Pyre cannot match PyPy configurations that honor a module’s __builtins__ mapping.

4. Structural adaptations

  • pyre/pyre-interpreter/src/eval.rs:4215 ↔ pypy/interpreter/pyopcode.py:869: when __build_class__ is absent, Pyre raises NameError, while this PyPy source raises ImportError. This is the stated Python 3.14 versus PyPy-version exception-semantic adaptation; the patch’s LOAD_BUILD_CLASS residual path reaches that existing behavior.

@youknowone
youknowone merged commit 4d75883 into main Aug 4, 2026
18 of 19 checks passed
@youknowone
youknowone deleted the wasm-jit branch August 4, 2026 03:43
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