Skip to content

jit: stop replaying an opcode's stack effect when the after-residual reconcile names the coordinate the mirror already holds - #1271

Merged
youknowone merged 3 commits into
mainfrom
agent/stdlib-foundations
Aug 16, 2026
Merged

jit: stop replaying an opcode's stack effect when the after-residual reconcile names the coordinate the mirror already holds#1271
youknowone merged 3 commits into
mainfrom
agent/stdlib-foundations

Conversation

@youknowone

@youknowone youknowone commented Aug 16, 2026

Copy link
Copy Markdown
Owner

test_dataclasses SIGSEGVs under the JIT on linux/arm64 (clean on darwin). It is
wrong code, not a GC fault: at the crash the virtualizable array holds two
adjacent operand-stack slots transposed, both objects live and well-formed.

capture_resumedata(after_residual_call=True) advances the walk operand-stack
mirror (ctx.vstack_boxes) to the post-call boundary with
reconcile_vstack_at_boundary. It ran even when the resume py_pc equalled
ctx.vstack_cur_pypc — the coordinate the mirror already holds.
step_vstack_mirror returns on exactly that equality, so there is no boundary
there, and reconciling replays the current opcode's stack effect a second
time. VstackOpClass::Swap/Copy are permutations, so the second application
is the identity: the two slots stay in pre-swap order for the rest of the walk.

dataclasses._process_class's

all_init_fields = [f for f in fields.values()
                   if f._field_type in (_FIELD, _FIELD_INITVAR)]

lowers to GET_ITER; LOAD_FAST_AND_CLEAR f; SWAP 2; BUILD_LIST 0; SWAP 2; FOR_ITER. The BUILD_LIST residual's guard reconciled at the second SWAP's
own py_pc, so every later guard snapshot published the accumulator list and the
iterator crossed in the virtualizable array. On guard failure
write_from_resume_data_partial wrote them crossed into
locals_cells_stack_w, the blackhole's LIST_APPEND read the
dict_valueiterator out of the accumulator slot, and w_list_append
switch_to_object_strategy segfaulted.

PYRE_VSTACK_DIAG=1 names it directly — the same prev_pypc reconciled twice:

prev=932 new=933 class=ResultToTos  (BUILD_LIST, cur:=933)
prev=933 new=933 class=Swap(2)      (SWAP applied #1)
prev=933 new=934 class=Swap(2)      (SWAP applied #2 -> cancels)

The fix adds py_pc != ctx.vstack_cur_pypc to that advance.

Verification

linux/arm64 test_dataclasses 3/3 OK (Ran 276 tests … OK (skipped=2)); SIGSEGV 3/3 before
linux/arm64 CPython suite gate PASS 214, FAIL 0, CRASH 0, TIMEOUT 0
darwin test_dataclasses JIT 3/3 + JIT-off 1/1 OK
darwin CPython suite gate PASS 217, no regressions
darwin check.py dynasm 436/436, cranelift 436/436

Measured at base 9dbc228b34e; the branch has since been rebased onto
e27b07851c2, whose only delta is unrelated.

Two reds seen during verification are inherited, not this branch's:

  • wasm synth/short_circuit_value_kept_stack 5.3–5.5x > gate 3.7x — the gate is
    tuned to a ubuntu measurement (3.2x) and CI runs wasm only on ubuntu; an
    origin/main in-place control arm on darwin measured 5.7/6.1x for the same
    row.
  • linux/arm64 test.test_urllib2 IMPORTERROR — reproduces with PYRE_NO_JIT=1,
    and the container clone is pinned to a base that predates its fix
    (bcbd4b26b32).

No regression fixture: a hand-written comprehension with the same bytecode shape
does not reproduce on either platform, so coverage rests on test_dataclasses
in the CPython suite gate.

The first and last commits are an add/revert pair for the three env-gated
blackhole diagnostics used during the investigation; they cancel out.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PuYePQknDcMCUsy8omQ1fh

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue that could apply stack changes twice when resuming execution after a residual call.
    • Improved execution reliability when the resume location matches the current program location.

`MAJIT_BH_ROOT_CHECK` reports a register bank resized while a root
registration still names its buffer. `push_resume_ref_roots` and
`push_bh_regs` root a bank by the raw `(pointer, length)` of its `Vec`
buffer and document the same precondition — the bank is sized once and
only indexed afterwards. `ref_bank_registration_len` answers whether that
still holds for a given buffer, across both stacks.

`MAJIT_BH_CALL_ARGS` reports a residual call's ref arguments by the
register index each came out of, plus `num_regs_r` and the bank length,
so an argument can be told apart as resume-seeded, written by an opcode
of this run, or read out of the jitcode's constant pool.

`MAJIT_BH_VABLE` reports a virtualizable array read by its index
register rather than only the index value, plus the resolved array and
its length. `handler_getarrayitem_vable_r` takes the index from
`registers_i`, and an index register the resume section never named
reads whatever `setposition` left there; zero is in bounds, so the
existing bounds assert stays silent.

All three follow the `MAJIT_GC_BH_PROBE` / `MAJIT_BH_NULL_ARG` shape: a
`OnceLock`-cached env read and an `eprintln!`, off by default.

Assisted-by: Claude
…reconcile names the coordinate the mirror already holds

`capture_resumedata(after_residual_call=True)` advances the walk-level
operand-stack mirror to the post-call boundary before the guard supplies
the virtualizable snapshot. The advance ran unconditionally, including
when the resume `py_pc` equals `vstack_cur_pypc` — the coordinate the
mirror is already at. `step_vstack_mirror` returns on that same equality,
so there is no boundary there and `reconcile_vstack_at_boundary` replays
the current opcode's stack effect a second time.

`VstackOpClass::Swap` and `Copy` are permutations: applying `SWAP i`
twice is the identity. In `dataclasses._process_class` the comprehension
`[f for f in fields.values() if f._field_type in (_FIELD, _FIELD_INITVAR)]`
lowers to `LOAD_FAST_AND_CLEAR; SWAP 2; BUILD_LIST 0; SWAP 2; FOR_ITER`,
and the `BUILD_LIST` residual's guard reconciles at the second `SWAP`'s
own py_pc. The mirror then held operand slots 1 and 2 in their pre-swap
order for the rest of the walk, and every later guard snapshot published
the accumulator list and the iterator crossed in the virtualizable array.
On guard failure `LIST_APPEND` read the iterator out of the accumulator
slot and `w_list_append` segfaulted.

Gate the advance on `py_pc != ctx.vstack_cur_pypc`, which is what "the
same transition the following walk step would make" already meant.

Assisted-by: Claude
This reverts commit fb8972dfb4cf3ea7412ffb3785fea270e75aea3e.
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 28eead21-f7cd-4905-9ed2-b02ec7ab8be8

📥 Commits

Reviewing files that changed from the base of the PR and between e27b078 and 766471a.

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

Included review availability: Your plan includes up to 2 reviews per rolling hour; 0 remain after this review.


Walkthrough

The residual-call resume path now guards vstack reconciliation with a Python PC comparison. Matching PCs no longer apply the same opcode stack permutation twice.

Changes

Resume reconciliation

Layer / File(s) Summary
Conditional vstack reconciliation
pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs
The path reconciles the vstack only when py_pc differs from ctx.vstack_cur_pypc.

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

Merge Risk: 🟡 Moderate · up to 76647

The fix prevents duplicate JIT stack-effect replay that can corrupt resumed operand-stack ordering and crash affected executions, and the reported platform suites pass; however, required dynasm-enabled Rust checks were not run, so merge readiness remains incomplete until they pass or the omission is explicitly accepted.

Possibly related PRs

Poem

A rabbit checks the Python PC,
“Same place? No stack move for me.”
The residual call hops away,
No duplicate effects today.
Carrots cheer the guarded trail!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main fix: preventing duplicate opcode stack-effect replay during vstack reconciliation.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/stdlib-foundations

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 766471a).
Updated: 2026-08-16T11:07:32.014Z

Files in the reviewed diff
pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.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)

None.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs:603 ↔ rpython/jit/metainterp/pyjitpl.py:194 — Rust’s flattened-JitCode operand-stack shadow has no direct RPython counterpart. The added py_pc != ctx.vstack_cur_pypc prevents replaying an already-applied opcode effect; PyPy snapshots the already-maintained MIFrame.registers_* at the post-residual -live- point. This is a fundamental Rust-walker adaptation and restores, rather than departs from, that one-transition semantics.

@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: 766471a4d6

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

// idempotent: a `SWAP` applied twice is the identity, so the two
// slots it exchanged keep their pre-swap order for the rest of the
// walk and every later guard snapshot publishes them crossed.
if after_residual_call && ctx.vstack_valid && py_pc != ctx.vstack_cur_pypc {

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 same-coordinate reconciliation bookkeeping

For a same-PC after-residual capture that still needs depth normalization or hole filling, this condition skips the entire reconciliation rather than only the duplicate opcode effect. reconcile_vstack_at_boundary already suppresses a repeated SWAP via repeat_boundary, while deliberately retaining box resizing, shadow hole filling, vstack_depth publication, and vstack_last_ref clearing; bypassing it can therefore leave stale mirror state that the next real boundary uses to build later guard snapshots. Keep the same-coordinate handling inside the reconciler and suppress only the effect that must not be replayed.

AGENTS.md reference: AGENTS.md:L309-L311

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit 7e0e61c into main Aug 16, 2026
15 of 17 checks passed
@youknowone
youknowone deleted the agent/stdlib-foundations branch August 16, 2026 12:48
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