Skip to content

wasm: pick loop-header label by JUMP descr; jit: scope abort_permanent to loop body#558

Merged
youknowone merged 2 commits into
mainfrom
ec-wiring
Jul 15, 2026
Merged

wasm: pick loop-header label by JUMP descr; jit: scope abort_permanent to loop body#558
youknowone merged 2 commits into
mainfrom
ec-wiring

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Two related JIT correctness changes on the ec-wiring branch.

jit: scope loop abort_permanent decline to the loop body

loop_body_has_abort_permanent was over-broad: an except X as e anywhere in a
function marked the whole hot loop abort-permanent, so the loop never
JIT-compiled and the exc/resume path stayed in the interpreter. Scoping the
decline to the loop body lets these loops compile.

wasm: select loop-header label by terminal JUMP's descr

The wasm backend chose the loop-header label of a multi-label (peeled) trace by
taking the last LABEL, not the label the terminal JUMP actually targets.
When the JUMP's descr points at a non-last label, find_label_args (the
back-edge parallel move) and loop_label_idx (the emitted loop) bound to the
wrong label, so loop-carried locals cross-loaded — a loop-invariant
range-iterator local read back as an unrelated int and FOR_ITER raised
TypeError: not an iterator.

find_jump_target_label_index / find_loop_label_index now resolve the target
label by the JUMP descr's identity (falling back to the last label for
descr-less IR), and is_resumable_peeled declines the key-dispatch shape unless
the JUMP target is the last label (such shapes compile as a plain local loop).

This is confined to majit-backend-wasm/src/codegen.rs; the native dynasm and
cranelift backends do not link it and are unaffected.

Verification

  • wasm synthetic suite: 170/170 (no regression from the is_resumable_peeled change)
  • dynasm: 181/181; cranelift: 181/181 (the wasm change is inert on native)
  • The miscompile was pinned on a hot for _ in range(N): s += G loop whose
    peeled trace's terminal JUMP targets a non-last label; the corrupted
    loop-carried iterator (read back as an int) is corrected by the label-selection
    fix.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 14, 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: 14 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: bd42ea7c-9c3a-42cb-91fe-b701826cc559

📥 Commits

Reviewing files that changed from the base of the PR and between 048978b and 8f9242d.

📒 Files selected for processing (2)
  • majit/majit-backend-wasm/src/codegen.rs
  • pyre/pyre-jit-trace/src/trace.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ec-wiring

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: f4f1d5bc0d

ℹ️ 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 +3906 to +3908
if ops.iter().rposition(|op| op.opcode == OpCode::Label) != Some(loop_label) {
return false;
}

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 preamble state for non-last targets

When a peeled trace has real work before the JUMP target but the terminal JUMP targets a non-last LABEL (the shape this change says now compiles as a plain local loop), returning false here also makes compile_loop record has_preamble = false. That path publishes the first label as a normal key-0 entry, so a loop-closing bridge to that label can be accepted and tail-call the function entry instead of a label loader; the peeled preamble then runs again with loop-carried values, which can livelock or corrupt induction state. If key dispatch is unsupported for this shape, the loop still needs to be marked as having a preamble or its labels must not be published as non-peeled entries.

Useful? React with 👍 / 👎.

Comment on lines +2240 to +2242
let Some(merge_point) = crate::jitcode_runtime::decoded_ops(code)
.find(|op| op.opname == "jit_merge_point")
.map(|op| op.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.

P2 Badge Use the current loop's merge point

For a function with multiple hot loops, this always selects the first jit_merge_point in the whole jitcode rather than the one corresponding to the start_pc being traced. If the later hot loop contains an in-body abort_permanent and the first loop is clean, back_edge_end is computed from the first loop's back edge, the later marker falls after scan_end, and the upfront decline returns false; with PYRE_FULL_BODY_WALK enabled the authoritative walk can then hit the unsupported op and reproduce the frame/tail corruption this guard is meant to prevent. The scan should anchor on the merge point for the current start_pc.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🤖 Codex parity review

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

Files in the reviewed diff
majit/majit-backend-wasm/src/codegen.rs
pyre/pyre-jit-trace/src/trace.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)

  • majit/majit-backend-wasm/src/codegen.rs:4001 ↔ rpython/jit/metainterp/history.py:999 — unmatched or absent JUMP.descr still falls back to “the last LABEL.” PyPy resolves the terminating jump by its exact TargetToken; an external/nonlocal target must not silently become a local back-edge. This fallback existed in upstream/main (rposition(LABEL)).

4. Structural adaptations

  • majit/majit-backend-wasm/src/codegen.rs:3986 ↔ rpython/jit/backend/x86/regalloc.py:1403 — Rust uses Arc::ptr_eq to model PyPy’s object-identity match between JUMP.descr and LABEL.descr, then emits structured Wasm control flow. The patch’s target-based selection is parity-correct; Wasm’s loop construction is the implementation-language adaptation.

  • pyre/pyre-jit-trace/src/trace.rs:2272 ↔ rpython/jit/metainterp/pyjitpl.py:2949 — Pyre statically declines a full-body walk when its generated JitCode contains abort_permanent, with exception-table-aware scan bounds. PyPy dynamically raises SwitchToBlackhole and continues in the blackhole interpreter. This remains a transitional walker/CPython-bytecode adaptation; the patch narrows the pre-existing over-decline to the actual loop body outside try blocks.

full_body_walk_trace declines a loop trace up front when
loop_body_has_abort_permanent finds an abort_permanent opcode. The scan
ran from the first jit_merge_point to the end of the per-CodeObject
jitcode, so a marker in post-loop code declined the hot loop even though
the walk never reaches it. `except X as e` lowers its implicit cleanup to
DELETE_FAST (an abort_permanent) in the post-loop handler, so any hot loop
in a function with an `except ... as e` failed to compile and ran
interpreted.

Scope the scan to the loop body: from the first jit_merge_point up to the
loop's back-edge (the last goto-family op whose decoded label targets at or
before the header). A loop whose header is covered by an exception handler
(lookup_exceptiontable) keeps the full-tail scan, so a post-loop
abort_permanent still declines it — compiled-loop delivery of an uncaught
raise into the handler is not yet supported. In-loop markers (the SWAP a
chained comparison lowers to) stay before the back-edge and still decline.

PYRE_FBW_LOOPBODY_SCAN_FULL opts back into the full-tail scan.

Assisted-by: Claude
find_label_args and the loop-header index used the last LABEL of a
multi-label trace. A terminal JUMP whose descr targets an earlier LABEL
then bound the parallel move and the emitted `loop` to the wrong label,
cross-loading loop-carried locals. find_jump_target_label_index /
find_loop_label_index resolve the target LABEL by descr Arc identity and
fall back to the last LABEL for descr-less IR. is_resumable_peeled declines
the key-dispatch shape unless the JUMP target is the last LABEL.

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

ℹ️ 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 +2294 to +2298
let loop_in_try = unsafe {
pyre_interpreter::pycode::w_code_lookup_exceptiontable(
w_code as pyre_object::PyObjectRef,
(start_pc as u32) * 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.

P2 Badge Scan exception handlers nested in loop bodies

When a hot loop contains an inner try/except ... as e but the loop header itself is outside the protected range, this lookup returns None because it probes only start_pc. Handler cleanup can be laid out after the normal loop backedge, and DELETE_FAST cleanup is emitted as abort_permanent (pyre/pyre-jit/src/jit/codewriter.rs:11292-11308); with loop_in_try == false, scan_end cuts at the normal backedge and misses that marker. In that exception path the full-body walk can execute handler side effects and then abort/replay instead of declining up front.

Useful? React with 👍 / 👎.

@youknowone youknowone merged commit 09c32a8 into main Jul 15, 2026
30 checks passed
@youknowone youknowone deleted the ec-wiring branch July 15, 2026 05:24
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