Skip to content

jit: store-loop header merge-point forward-select + list-subscript exact-type guards + int-subclass BigInt#477

Merged
youknowone merged 6 commits into
mainfrom
test-math-parity
Jul 10, 2026
Merged

jit: store-loop header merge-point forward-select + list-subscript exact-type guards + int-subclass BigInt#477
youknowone merged 6 commits into
mainfrom
test-math-parity

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Six commits on top of main (the FOR_ITER abort-free heap-store admission from this line already landed via #448 and was dropped during rebase).

Commits

  • jit: publish STORE_SUBSCR residual exception to backend cellsbh_store_subscr_fn publishes a raise into both BH_LAST_EXC_VALUE and the backend _store_exception cells, so a compiled trace's trailing GuardNoException side-exits instead of reading a stale 0 and swallowing the raise. Mirrors bh_store_attr_fn / jit_next.
  • jit: guard exact list type in subscript read/store specializations — walker-native list get/setitem gate on is_exact_list and emit a runtime walker_guard_exact_w_class (PtrEq on the canonical w_class) so a list subclass that shares ob_type == &LIST_TYPE but overrides __getitem__/__setitem__ side-exits to the generic residual rather than taking the direct-storage path.
  • jit: forward-select loop-header merge point for header-entry traces — a header entry selects the FIRST merge point at/after entry (min{mp_pc >= entry}) instead of the last one before it; the old max{<= entry} picked a preceding sibling loop's merge point, seeding that loop's green colors and leaving this loop's pycode color OpRef::NONE (the "born-between-loops" JitMergePointGreenKeyUnresolved abort). Body-guard bridge resumes keep max{<= entry}. header_entry_is_nested scopes the change out for a loop nested inside another still-active loop, keeping nested inner loops on the interpreted path (their bridges are separately miscompiled — see Split descr identity between metainterp and backend #36).
  • jit-trace: exact-w_class guard on residual/trait list setitem path — adds a default WalkerFrameOps::guard_exact_w_class and calls it in generated_list_setitem_by_strategy, extending the exact-type guard to the residual/trait store helper (defense-in-depth; the live getitem helper is retired-path only and documented in place).
  • interpreter: preserve BigInt magnitude for int-subclass instancesint_descr_new branches on is_long, cloning the BigInt into w_long_new for magnitudes that overflow i64 (the i64 fast path truncated them to a garbage machine word).
  • style: apply rustfmt to walker_frame_ops exact-w_class guard.

Verification

  • cargo build --release (dynasm) clean; python pyre/check.py --backend dynasmALL PASSED: dynasm 161/161.
  • Store-loop benches (prefix / histo / matfill / sieve / copy_scale): 0.78–0.95x regressions → 5–13x, driven by the merge-point fix letting the second sibling loop compile.
  • Nested adversarial shapes (for-in-while / for-in-for / while-in-for / triple / mixed siblings) byte-identical JIT vs PYRE_NO_JIT=1.

Codex parity review

Ran against the true fork point: Section 1 & 2 = None (no new parity regressions or mismatches). Section 3/4 findings are all documented adaptations or accurately-documented unreachable pre-existing items (dispositions recorded in .claude/codex-review-report.md). Follow-ups filed: #36 (nested-loop bridge exact-resume), #35 (rvmprof port).

commented by Claude

bh_store_subscr_fn stashed a raise from a user __setitem__ only in
BH_LAST_EXC_VALUE, not the backend _store_exception cells that a compiled
trace's trailing GUARD_NO_EXCEPTION reads. The guard then passed on a
stale 0 and the raise was swallowed, so a for-loop with a raising
subclass __setitem__ ran to completion under JIT. Also publish via
jit_publish_exception, mirroring bh_store_attr_fn and jit_next.

Assisted-by: Claude
The list subscript read (try_walker_specialize_subscr,
generated_binary_subscr_value) and store (try_walker_specialize_store_subscr,
generated_store_subscr_value) specializations gated on is_list and emitted
GuardClass(&LIST_TYPE); both check ob_type. A list subclass shares
ob_type == &LIST_TYPE but retags w_class and may override
__getitem__/__setitem__, so the recorded direct-storage IR bypassed the
override in compiled code. Gate on is_exact_list and add
walker_guard_exact_w_class so a subclass side-exits to the generic
residual that dispatches to the override, mirroring the tuple read path.
Tighten the shared tuple gate to is_exact_tuple for consistency.

Assisted-by: Claude
loop_header_merge_point_regs seeded the loop-header merge point's
pycode/frame/ec register colors from max{mp_pc <= entry}. For a second
sibling loop's header entry this selected the preceding loop's merge
point, leaving this loop's own pycode green color OpRef::NONE and
aborting the trace with JitMergePointGreenKeyUnresolved, so the loop
never compiled.

Header entries now select min{mp_pc >= entry}; body-guard bridge resume
(is_bridge_trace) keeps max{mp_pc <= entry}. Mirrors opimpl_jit_merge_point
(pyjitpl.py:1537) binding greenboxes from the merge-point op the frame is
about to execute. header_entry_is_nested excludes a loop nested inside a
still-active loop, which keeps the decline/abort path because the walker
miscompiles the bridges such an inner loop closes into.

Assisted-by: Claude
`generated_list_setitem_by_strategy` emitted only `guard_class(LIST_TYPE)`
+ strategy guard, so a trace recorded on an exact list could be re-entered
by a list subclass (shared `ob_type == &LIST_TYPE`, retagged `w_class`,
overriding `__setitem__`) and bypass the override via direct backing-store
write. Add a default `WalkerFrameOps::guard_exact_w_class` (getfield
`w_class` -> PtrEq canonical -> GuardTrue, skipped for null typeobj or
unescaped virtuals) and call it between `guard_class` and
`guard_list_strategy`, matching the walker-native store path ordering.
Both `MIFrame` and `WalkContext` inherit it via the default method.

The walker-native store/getitem paths already carry this guard; the live
getitem helper `generated_list_getitem_by_strategy` is reached only by the
retired executor-trait path, documented in place.

Assisted-by: Claude
`int_descr_new` built every subclass instance via `w_int_get_value` +
`w_int_new_unique`, truncating a value that overflows i64 (stored as a
`W_LongObject`) to a garbage machine word. Branch on `is_long`: clone the
`BigInt` into `w_long_new` for large magnitudes, keep the i64 fast path
otherwise. `w_class = cls` is set on either representation.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 10, 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: 37 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: 1fd99681-6726-46de-8c4b-3d451278f62d

📥 Commits

Reviewing files that changed from the base of the PR and between a67d249 and 0124a3a.

📒 Files selected for processing (7)
  • majit/majit-backend-wasm/src/lib.rs
  • pyre/pyre-interpreter/src/opcode_ops.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_helpers/typed_trace.rs
  • pyre/pyre-jit-trace/src/walker_frame_ops.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test-math-parity

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

Copy link
Copy Markdown

🤖 Codex parity review

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

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)

  • pyre/pyre-jit-trace/src/trace_helpers/typed_trace.rs:1818 ↔ pypy/objspace/descroperation.py:356 — the active trait tracer emits only guard_class(LIST_TYPE) before direct list-storage reads. A list subclass shares that payload type but PyPy dispatches __getitem__ through special-method lookup; a later subclass value can pass the guard and bypass an override. The patch correctly filters the trace-time object at typed_trace.rs:1901, but does not add the needed runtime w_class guard to this active path.

  • pyre/pyre-jit-trace/src/trace_helpers/typed_trace.rs:1888 ↔ pypy/objspace/descroperation.py:356 — exact-tuple filtering occurs only while recording. generated_tuple_getitem subsequently guards the storage ob_type, not canonical Python class identity, so a tuple subclass can enter a trace recorded for a plain tuple and bypass an overridden __getitem__. This existed before the patch; the new trace-time is_exact_tuple filter does not close the compiled-trace guard hole.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/trace.rs:430 ↔ rpython/jit/metainterp/pyjitpl.py:1537 — Pyre reconstructs merge-point register colours by decoding JitCode around a resume PC, whereas RPython’s MIFrame executes the jit_merge_point directly. The patch’s header/body selection is an implementation-language/resume-model adaptation.

  • pyre/pyre-jit-trace/src/trace.rs:421 ↔ rpython/jit/metainterp/pyjitpl.py:1537 — nested-loop header entries deliberately retain the old, declining behavior because Pyre’s walker cannot yet safely compile nested-loop closing bridges. This is an explicit conservative limitation, not a 1:1 PyPy port.

  • pyre/pyre-interpreter/src/opcode_ops.rs:680 ↔ rpython/jit/backend/llsupport/llmodel.py:194 — publishing a residual-call exception to both BH_LAST_EXC_VALUE and backend exception cells is required by Pyre’s shared Rust helper ABI; RPython keeps metainterpreter state and backend exception storage separate.

  • pyre/pyre-interpreter/src/typedef.rs:1294 ↔ pypy/objspace/std/intobject.py:1097 — Pyre preserves an int subclass’s value by allocating either Rust W_IntObject or W_LongObject and retagging w_class; PyPy uses allocate_instance/newbigint. The changed long-value branch is semantically aligned with PyPy’s W_LongObject branch.

@youknowone youknowone merged commit dad032c into main Jul 10, 2026
28 of 29 checks passed
@youknowone youknowone deleted the test-math-parity branch July 10, 2026 23:04
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