Skip to content

jit: exact-resume FOR_ITER gate widening and JIT traceback fidelity - #563

Merged
youknowone merged 18 commits into
mainfrom
nbody
Jul 15, 2026
Merged

youknowone merged 18 commits into
mainfrom
nbody

Conversation

@youknowone

@youknowone youknowone commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator

Widens the #57 FOR_ITER JIT decline gate as exact-resume coverage is proven per opcode group, and closes the JIT traceback-fidelity gaps uncovered along the way. Every increment is gated on adversarial byte-exact parity vs the interpreter on both backends.

FOR_ITER gate widening (exact-resume)

A committed un-journaled heap effect resumes forward exactly (the loop tail is not dropped); a raising effect propagates via forward-exception-delivery. The gate now admits, alongside the base no-heap-mutation set:

  • STORE_SUBSCR/STORE_ATTR/STORE_NAME/STORE_GLOBAL (slice-2) and LOAD_ATTR/LOAD_NAME
  • DELETE_SUBSCR/DELETE_ATTR (slice-3a)
  • STORE_DEREF (slice-3b)

Supporting exact-resume fixes: FOR_ITER resume slots for closure frames; in-flight FOR_ITER item delivery on the kept-stack Unsupported branch-flush for nested loops; class-body namespace local refresh after a JIT run.

JIT traceback fidelity (#9)

A JIT-compiled frame that exits with a residual exception previously dropped its own Python traceback entry.

  • Fix A — uncaught propagation: record the frame at the blackhole exit-with-exception, translating the faulting JitCode PC to the raise line.
  • Fix C — caught-in-frame reraise/chaining: route_to_catch transferred a residual into a zero-cost handler without recording the frame; a reraise (raise), raise ... from, or implicit __context__ chain then escaped with the fault/raise line missing and the chained exception carrying no compiled-frame traceback. A generic record_caught_exception callback on the blackhole (raw refs only — no interpreter dependency) records at handler entry; the bottommost exit suppresses the duplicate cleanup entry when the escaping exception is the already-recorded caught object.

Correctness fixes uncovered by the wider gate

  • Self-recursive CALL_ASSEMBLER fold value corruption at depth: the post-call guard snapshot sourced stack slot 0 from the stale pre-call operand-stack mirror [callable, ...], so deep-recursion guard-fail recovery substituted the callable for the result. Advance the mirror to the post-call state before the guard; decline the exception-guard bridge compile from the post-call resume state.
  • STORE_DEREF method-load slot mirror: method-form LOAD_ATTR pushes [method, self] but the mirror kept only self, so a cell-related guard reconstructed the method slot from a stale value and the tail call invoked the wrong object. Mirror both method-load slots at the vable stack-write chokepoint.
  • Raising-DELETE stale exception: a residual delete that raised left the backend _store_exception cell populated after propagating out, so the next JIT'd function resurfaced it. Drain the cell at the bottommost unhandled frame exit.

Loop admission

scope loop abort_permanent decline to the loop body: an except X as e handler lowers its cleanup to a post-loop DELETE_FAST (abort_permanent) that wrongly declined the hot loop; scope the scan to the loop body so such loops compile (and their caught-reraise tracebacks are exercised).

Verification

pyre/check.py --backend dynasm,cranelift green across the increments (the sole failure on the current base is synth/list_insert_pop_index, a pre-existing objspace error-message parity issue from #552 that fails at interpreter level, unrelated to this branch — fixed as a follow-up commit here). Adversarial byte-exact repros (values, final container state, exception type/message, and full multi-frame tracebacks incl. __cause__/__context__ chains) pass on both backends.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved exception tracking and traceback reporting during JIT execution and exception propagation.
    • Fixed GC-related issues in iteration and class construction (namespace now rebuilt safely after JIT execution).
    • Corrected JIT recovery for frame/operand-stack state after interruptions and loop resumes.
    • Ensured dict/set operations validate hashability up-front: unhashable keys/values now raise TypeError without partially mutating the result.
  • Improvements
    • Expanded JIT support for FOR_ITER loop bodies, including method-call patterns and common STORE_*/DELETE_* cases.
    • Python compatibility checks now prefer Python 3.14 when available.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 23 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: 2204fefe-49c5-4a70-a2d5-0ae11f2bae78

📥 Commits

Reviewing files that changed from the base of the PR and between d58c7c0 and fdccc16.

📒 Files selected for processing (13)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-metainterp/src/blackhole.rs
  • pyre/check.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.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/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

Walkthrough

The PR updates JIT frame-slot translation, FOR_ITER eligibility and resume recovery, operand-stack mirroring, blackhole exception traceback handling, GC rooting, container hash validation, class namespace relocation, and checker interpreter selection.

Changes

JIT execution and resume handling

Layer / File(s) Summary
Frame layout and slot translation
pyre/pyre-jit/src/jit/codewriter.rs, pyre/pyre-jit/src/eval.rs, pyre/pyre-jit-trace/src/...
JIT slots and reconstructed stack positions now account for deref slots and physical frame layout.
FOR_ITER eligibility and exact resume
pyre/pyre-jit/src/eval.rs, pyre/pyre-jit-trace/src/trace.rs
FOR_ITER bodies allow additional mutations and attribute loads, with refined abort, flush, and resume behavior.
Operand-stack mirroring and guards
pyre/pyre-jit-trace/src/jitcode_dispatch.rs, majit/majit-backend-cranelift/src/compiler.rs
Stack mirrors advance across CALL_ASSEMBLER, method-form LOAD_ATTR values are captured, and receiver version tags are guarded.
JIT shape encoding limits
pyre/pyre-jit/src/eval.rs
Constant encoding overflow is detected and routed through interpreter fallback.

Blackhole exception propagation

Layer / File(s) Summary
Caught-exception bookkeeping
majit/majit-metainterp/src/blackhole.rs
Blackhole interpreters record caught exception values and invoke an inherited callback at catch handlers.
JIT exception traceback and recovery
pyre/pyre-jit/src/call_jit.rs
Resume paths attach tracebacks, suppress caught reraises, drain backend exception state, and bypass exception bridges.

Interpreter runtime safety

Layer / File(s) Summary
GC-safe object and namespace handling
pyre/pyre-interpreter/src/builtins.rs, pyre/pyre-interpreter/src/call.rs
Iterable values are rooted across collection, and class-body locals are reloaded after relocation.
Pre-insertion hash validation
pyre/pyre-interpreter/src/builtins.rs, pyre/pyre-interpreter/src/opcode_ops.rs, pyre/pyre-interpreter/src/typedef.rs
Dictionary and set paths reject unhashable keys or elements before insertion.

Interpreter selection

Layer / File(s) Summary
Host interpreter selection
pyre/check.py
The checker probes Python 3.14, Python 3, and Python in order.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • youknowone/pyre#205 — Covers overlapping JIT trace correctness and blackhole exception-resume changes.
  • youknowone/pyre#389 — Relates to expanded FOR_ITER support and operand-stack materialization.
  • youknowone/pyre#570 — Relates to the widened FOR_ITER JIT eligibility gate.

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

A rabbit hops through frames of light,
Roots each value snug and tight.
Catches leap and tracebacks bloom,
FOR_ITER finds its proper room.
Slots align, exceptions flee—
JIT paths thump with harmony.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the two main themes: widened FOR_ITER JIT admission and improved traceback fidelity.
✨ 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: 2a593d5c90

ℹ️ 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 thread pyre/pyre-jit/src/eval.rs
| I::StoreAttr { .. }
| I::StoreName { .. }
| I::StoreGlobal { .. }
| I::StoreDeref { .. }

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 Keep StoreDeref loops out until cell slots flush correctly

Allowing STORE_DEREF admits loop bodies in frames with closure/freevar cells, but the forward-resume path still restores the outer CALL result at locals_cells_stack_w[nlocals] and sets valuestackdepth = nlocals + 1 in flush_walk_end_state_after_outer_call, rather than using nlocals + ncells. For a nonlocal loop body where an inlined call aborts before the STORE_DEREF (or before the tail), this overwrites the first cell/freevar slot instead of the operand stack slot, corrupting the resumed frame; keep these shapes interpreted or make the flush path use the physical stack base.

Useful? React with 👍 / 👎.

Comment on lines +2076 to +2078
let caught_reraise = exc_value != 0 && last_caught_exception_value == exc_value;
if caught_reraise {
err.attach_tb = 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.

P2 Badge Distinguish explicit raise from bare reraise

This treats any exception object that was caught in the blackhole frame as a bare RERAISE, but an except ... as e: raise e path raises the same object through handler_raise, not handler_reraise. In that case Python should append the current raise site to the traceback; setting attach_tb = false skips that recording when the frame exits, so JIT blackhole exits lose the frame for explicit re-raises of the caught object. Track whether the raising opcode was actually reraise/ instead of using object identity.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

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

Files in the reviewed diff
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-metainterp/src/blackhole.rs
pyre/check.py
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/call.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/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit/src/call_jit.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/codewriter.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

@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: 6a5f92bae8

ℹ️ 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 +2296
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 traced loop's merge point for abort scans

When a function has sibling hot loops, full_body_walk_trace is called with the selected loop's start_pc, but this helper now anchors the scan to the first jit_merge_point in the whole JitCode. For a later loop, back_edge_end is computed from the first loop and an abort_permanent inside the later loop lies beyond scan_end, so the defensive decline is skipped and the authoritative walker can hit the in-loop abort corruption this guard is meant to avoid. Use the merge point corresponding to start_pc, not the first one in the function.

Useful? React with 👍 / 👎.

(
bank,
color,
frame_layout_slot(slot, nlocals, stack_base_absolute),

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 Keep pcdep stack slots in the space bridge mirrors read

Publishing pcdep_color_slots with operand-stack slots shifted by ncells breaks bridge and inline-resume paths that still build compact [locals | operand stack] mirrors. For example, setup_bridge_sym bounds its mirror by nlocals + stack_depth, and reconstruct_inline_recipe sizes registers_r the same way; in a frame with a pure cell/freevar, a live stack slot is now published as stack_base_absolute + depth, so it is dropped as out-of-prefix and guard-fail resumes lose kept operand values. Either keep this metadata compact for those consumers or update their indexing/bounds to physical frame slots.

Useful? React with 👍 / 👎.

Comment on lines +1933 to +1935
let frame_ptr = bh.virtualizable_ptr as *mut PyFrame;
let jitcode_index = bh.jitcode.try_index().map(|v| v as i32);
let last_opcode_position = bh.last_opcode_position;

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 Record tracebacks before advancing past a frame

In a multi-frame blackhole chain where the exception has no handler in the current frame, this captures the frame info but then releases the frame and moves to the caller; the new traceback recording only runs in the bottommost None arm. An exception raised in an inlined/blackholed callee and escaping through compiled callers therefore records only the outermost frame, omitting the callee frames that Python tracebacks must include. Record the current frame before the Some(caller) propagation as well.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyre/pyre-jit-trace/src/jitcode_dispatch.rs (1)

4717-4768: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the physical stack base here. STORE_DEREF and MAKE_CELL also write through setarrayitem_vable_r, but they target the cell/freevar region in locals_cells_stack_w, not an operand-stack push. Comparing against raw nlocals can still fold those closure writes into ctx.vstack_last_ref / ctx.vstack_boxes; gate this on the stack base (nlocals + ncells) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch.rs` around lines 4717 - 4768, Update
the vstack write handling around ctx.vstack_last_ref and ctx.vstack_boxes to use
the physical operand-stack base, nlocals + ncells, rather than raw nlocals.
Ensure STORE_DEREF and MAKE_CELL writes in the cell/freevar region are excluded,
while retaining the existing method-form LOAD_ATTR mirroring and last-reference
behavior for true operand-stack writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/check.py`:
- Around line 23-30: The PYTHON3 selection can accept an older or shimmed
interpreter, causing version-sensitive baseline and stdlib mismatches. Update
the candidate selection around PYRE_CHECK_PYTHON3 and PYTHON3 to validate each
executable by probing sys.implementation.name and sys.version_info, accepting
only CPython 3.14; apply the same requirement to the environment override or
reject it when invalid, while preserving the existing fallback behavior.

In `@pyre/pyre-interpreter/src/call.rs`:
- Around line 3222-3233: Update the class-body execution flow around
run_with_jit() and the earlier __prepare__ call to explicitly pin every
PyObjectRef that remains live afterward, including w_namespace, bases,
w_metaclass, extra_kwargs, and w_orig_bases. Re-read each reference from its
shadow-stack/frame slot after the allocating call and before later uses, while
preserving the existing body_ns refresh and mapping_namespace handling.

In `@pyre/pyre-jit-trace/src/trace.rs`:
- Around line 2289-2330: Update loop_body_has_abort_permanent to select the
jit_merge_point associated with start_pc rather than the first decoded merge
point. Use start_pc to identify the correct loop header and then restrict
back-edge and abort_permanent scanning to that loop, preserving the existing
exception-table and full-scan behavior.

In `@pyre/pyre-jit/src/call_jit.rs`:
- Around line 588-590: The last_instruction calculations in
pyre/pyre-jit/src/call_jit.rs at lines 588-590, 1948-1955, and 2080-2087 eagerly
dereference frame_ptr through map_or; replace map_or with map_or_else at all
three sites so the unsafe (*frame_ptr).last_instr read occurs only for None,
while preserving i64::from for Some values.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/jitcode_dispatch.rs`:
- Around line 4717-4768: Update the vstack write handling around
ctx.vstack_last_ref and ctx.vstack_boxes to use the physical operand-stack base,
nlocals + ncells, rather than raw nlocals. Ensure STORE_DEREF and MAKE_CELL
writes in the cell/freevar region are excluded, while retaining the existing
method-form LOAD_ATTR mirroring and last-reference behavior for true
operand-stack writes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1091b3cf-6272-4254-8217-27ee8d44f7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 7103208 and 6a5f92b.

📒 Files selected for processing (11)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-metainterp/src/blackhole.rs
  • pyre/check.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

Comment thread pyre/check.py
Comment on lines +23 to +30
# pyre's compat target is CPython 3.14; its native modules (`_sre.MAGIC`) and the
# vendored `lib-python/3` are coupled to that version. Prefer a version-matched
# oracle so a stale `python3` on PATH (an older system CPython) does not diverge
# from pypy on version-sensitive error text and trip a spurious cpython-vs-pypy
# baseline mismatch.
PYTHON3 = os.environ.get("PYRE_CHECK_PYTHON3") or next(
(cand for cand in ("python3.14", "python3", "python") if shutil.which(cand)),
"python3",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
for candidate in python3.14 python3 python; do
  path="$(command -v "$candidate" 2>/dev/null || true)"
  if [ -n "$path" ]; then
    "$path" -c 'import sys; print(sys.executable, sys.implementation.name, sys.version_info[:3])'
  fi
done

Repository: youknowone/pyre

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section with line numbers.
sed -n '1,120p' pyre/check.py | cat -n

# Find where PYTHON3 is used.
rg -n "\bPYTHON3\b|PYRE_CHECK_PYTHON3|python3\.14|version-sensitive|compat target" pyre/check.py pyre -g '!**/__pycache__/**'

Repository: youknowone/pyre

Length of output: 7599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact uses of PYTHON3/PYPY3 and any version checks nearby.
sed -n '730,1045p' pyre/check.py | cat -n

# Look for any explicit CPython/version validation in the repo.
rg -n "sys\.implementation|version_info|CPython 3\.14|3\.14|PYRE_CHECK_PYTHON3|PYRE_CHECK_PYPY3" pyre -g '!**/__pycache__/**'

Repository: youknowone/pyre

Length of output: 35967


Pin the CPython 3.14 oracle more tightly

shutil.which() still allows an older python3 or shim to be selected here, and that path feeds both the baseline comparisons and stdlib lookup. Probe the candidate with sys.implementation.name and sys.version_info before accepting it, or require PYRE_CHECK_PYTHON3 to point at a known 3.14 interpreter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/check.py` around lines 23 - 30, The PYTHON3 selection can accept an
older or shimmed interpreter, causing version-sensitive baseline and stdlib
mismatches. Update the candidate selection around PYRE_CHECK_PYTHON3 and PYTHON3
to validate each executable by probing sys.implementation.name and
sys.version_info, accepting only CPython 3.14; apply the same requirement to the
environment override or reject it when invalid, while preserving the existing
fallback behavior.

Comment on lines +3222 to +3233
// A minor collection during the body (a hot class-level loop compiles
// and runs through the JIT portal) can promote the namespace young ->
// old, relocating it. The frame's `debugdata.w_locals` slot and the
// shadow-stack pin are forwarded, keeping the object alive at its new
// address, but this stack-local `body_ns` copy is not — it is left
// pointing at the freed nursery slot. Re-read the forwarded object
// from the frame before any downstream use; a stale read would
// dereference reclaimed memory (`resolve_dict_backing` / `w_dict_items`
// below, then `__set_name__`).
let body_ns = frame.get_w_locals();
let mapping_namespace = mapping_namespace.map(|_| body_ns);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Pin and re-read all live object references to prevent use-after-free.

While this change correctly re-reads body_ns from the frame to survive GC relocation during run_with_jit(), several other local PyObjectRef variables remain unrooted and are not updated. Specifically, w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases are held by value and used after frame.run_with_jit() (e.g., w_namespace at line 3397, bases at line 3366).

Because these variables are not explicitly pinned and re-read from the GC shadow stack, a minor collection during the JIT portal execution will leave them pointing to reclaimed nursery slots, causing memory corruption when later accessed.

All PyObjectRef state that lives across run_with_jit() (and other prior allocating calls like the __prepare__ invocation) must be explicitly pinned to the shadow stack and re-read before subsequent use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/call.rs` around lines 3222 - 3233, Update the
class-body execution flow around run_with_jit() and the earlier __prepare__ call
to explicitly pin every PyObjectRef that remains live afterward, including
w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases. Re-read each
reference from its shadow-stack/frame slot after the allocating call and before
later uses, while preserving the existing body_ns refresh and mapping_namespace
handling.

Comment on lines +2289 to 2330
fn loop_body_has_abort_permanent(w_code: *const (), start_pc: usize) -> bool {
let Some(pjc) = crate::state::pyjitcode_for_code(w_code) else {
return false;
};
let code = pjc.jitcode.code.as_slice();
let mut seen_merge_point = false;
for op in crate::jitcode_runtime::decoded_ops(code) {
if op.opname == "jit_merge_point" {
seen_merge_point = true;
} else if seen_merge_point && op.opname == "abort_permanent" {
return true;
let Some(merge_point) = crate::jitcode_runtime::decoded_ops(code)
.find(|op| op.opname == "jit_merge_point")
.map(|op| op.pc)
else {
return false;
};

let mut back_edge_end: Option<usize> = None;
let mut first_abort_permanent = None;
for op in crate::jitcode_runtime::decoded_ops(code).filter(|op| op.pc > merge_point) {
if op.opname == "abort_permanent" && first_abort_permanent.is_none() {
first_abort_permanent = Some(op.pc);
}
if op.opname.starts_with("goto") && op.argcodes.ends_with('L') {
let target = u16::from_le_bytes([code[op.next_pc - 2], code[op.next_pc - 1]]) as usize;
if target <= merge_point {
back_edge_end = Some(back_edge_end.map_or(op.pc, |end| end.max(op.pc)));
}
}
}
false

// `start_pc` is a code-unit index; the exception table lookup takes byte offsets (×2).
let loop_in_try = unsafe {
pyre_interpreter::pycode::w_code_lookup_exceptiontable(
w_code as pyre_object::PyObjectRef,
(start_pc as u32) * 2,
)
}
.is_some();

let scan_end = if loop_in_try || std::env::var_os("PYRE_FBW_LOOPBODY_SCAN_FULL").is_some() {
code.len()
} else {
back_edge_end.unwrap_or(code.len())
};
first_abort_permanent.is_some_and(|pc| pc < scan_end)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'fn find_loop_header_pcs' -A 25 pyre/pyre-jit/src/jit/codewriter.rs
rg -n 'jit_merge_point' pyre/pyre-jit/src/jit/codewriter.rs
rg -n 'fn resume_jitcode_pc_for' -A 15 pyre/pyre-jit-trace/src/state.rs

Repository: youknowone/pyre

Length of output: 3584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the helper in trace.rs and nearby context.
sed -n '2260,2355p' pyre/pyre-jit-trace/src/trace.rs | cat -n

# Inspect resume_jitcode_pc_for and surrounding state handling.
sed -n '1,260p' pyre/pyre-jit-trace/src/state.rs | cat -n

# Inspect loop-header emission context in codewriter.
sed -n '7390,7565p' pyre/pyre-jit/src/jit/codewriter.rs | cat -n

Repository: youknowone/pyre

Length of output: 32295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for call sites / uses of loop_body_has_abort_permanent and start_pc.
rg -n 'loop_body_has_abort_permanent|resume_jitcode_pc_for\(|start_pc' pyre/pyre-jit-trace/src pyre/pyre-jit/src | sed -n '1,220p'

# Search for the surrounding doc comment about corruption scenario.
rg -n 'corruption scenario|abort_permanent|loop body' pyre/pyre-jit-trace/src/trace.rs -A 10 -B 10

Repository: youknowone/pyre

Length of output: 31662


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every call site and inspect the surrounding logic.
rg -n 'loop_body_has_abort_permanent\(' pyre/pyre-jit-trace/src/trace.rs -A 20 -B 20

# Inspect the helper definition with line numbers in context.
sed -n '2280,2345p' pyre/pyre-jit-trace/src/trace.rs | cat -n

# Inspect any nearby doc comments that describe the intended loop-body semantics.
sed -n '2220,2289p' pyre/pyre-jit-trace/src/trace.rs | cat -n

Repository: youknowone/pyre

Length of output: 12674


Resolve the merge point from start_pc
decoded_ops(...).find(|op| op.opname == "jit_merge_point") always picks the first loop header in the jitcode. In a code object with multiple loop headers, that can scan the wrong back-edge and miss an abort_permanent in the loop named by start_pc.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/trace.rs` around lines 2289 - 2330, Update
loop_body_has_abort_permanent to select the jit_merge_point associated with
start_pc rather than the first decoded merge point. Use start_pc to identify the
correct loop header and then restrict back-edge and abort_permanent scanning to
that loop, preserving the existing exception-table and full-scan behavior.

Comment on lines +588 to +590
let last_instruction =
pyre_jit_trace::state::python_pc_for_jitcode_pc_public(jitcode_index, opcode_position)
.map_or(unsafe { (*frame_ptr).last_instr as i64 }, i64::from);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Defer raw pointer dereference using map_or_else.

The map_or method evaluates its default argument eagerly, causing an unnecessary memory read of (*frame_ptr).last_instr even when the Option resolves to Some. Use map_or_else to defer the unsafe memory read until it is actually needed.

  • pyre/pyre-jit/src/call_jit.rs#L588-L590: replace map_or with map_or_else(|| unsafe { (*frame_ptr).last_instr as i64 }, i64::from)
  • pyre/pyre-jit/src/call_jit.rs#L1948-L1955: replace map_or with map_or_else(|| unsafe { (*frame_ptr).last_instr as i64 }, i64::from)
  • pyre/pyre-jit/src/call_jit.rs#L2080-L2087: replace map_or with map_or_else(|| unsafe { (*frame_ptr).last_instr as i64 }, i64::from)
📍 Affects 1 file
  • pyre/pyre-jit/src/call_jit.rs#L588-L590 (this comment)
  • pyre/pyre-jit/src/call_jit.rs#L1948-L1955
  • pyre/pyre-jit/src/call_jit.rs#L2080-L2087
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit/src/call_jit.rs` around lines 588 - 590, The last_instruction
calculations in pyre/pyre-jit/src/call_jit.rs at lines 588-590, 1948-1955, and
2080-2087 eagerly dereference frame_ptr through map_or; replace map_or with
map_or_else at all three sites so the unsafe (*frame_ptr).last_instr read occurs
only for None, while preserving i64::from for Some values.

@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

https://github.com/youknowone/pyre/blob/cd2c480cebda04c5d7fe5e00fcdf8e01402f443d/pyre-interpreter/src/opcode_ops.rs#L356
P1 Badge Root MAP_ADD operands before hashing

When this path is reached from the interpreter, map_add has already popped both key and value off the Python frame before calling this helper, so this new try_hash_value(key) can run a user __hash__ and trigger a moving GC while those objects are only raw Rust locals. If either object moves, the following w_dict_store(dict, key, value) uses stale pointers. Pin dict/key/value across the hash call, or keep the operands rooted until after the store.


https://github.com/youknowone/pyre/blob/cd2c480cebda04c5d7fe5e00fcdf8e01402f443d/pyre-interpreter/src/opcode_ops.rs#L328
P1 Badge Keep collected set-update items rooted while hashing

For SET_UPDATE over an arbitrary iterable, collect_iterable roots yielded objects only until it returns the Vec; this new per-item try_hash_value can then execute Python and trigger GC while the current item and the remaining Vec entries are unrooted raw pointers. A generator yielding young user objects whose __hash__ allocates can leave w_set_add or later iterations using stale references. Keep the shadow-stack roots alive through the hash/add loop or pin each item before hashing it.

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

@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: d58c7c0a3d

ℹ️ 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 thread pyre/pyre-interpreter/src/opcode_ops.rs Outdated
key: PyObjectRef,
value: PyObjectRef,
) -> Result<(), PyError> {
crate::builtins::try_hash_value(key)?;

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 checked insertion instead of a preflight hash

With a user-defined key in a MAP_ADD path, this preflight calls __hash__, and the following w_dict_store computes object_key_for(key), invoking the same hash hook a second time through the infallible store path. That makes dict displays/comprehensions run side-effecting or stateful __hash__ methods twice (and a second hash error is swallowed by the raw store) instead of once as in PyPy/CPython; use w_dict_store_checked or an equivalent single checked insertion, and apply the same pattern to the other new pre-hash-before-raw-store sites.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

♻️ Duplicate comments (2)
pyre/check.py (1)

23-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate that the selected executable is actually CPython 3.14.

shutil.which() only proves that a command exists; python3/python may resolve to an older CPython, PyPy, or a shim, and PYRE_CHECK_PYTHON3 bypasses validation entirely. That can still produce version-mismatched baseline comparisons and stdlib discovery. Probe candidates with sys.implementation.name and sys.version_info before accepting them, or fail clearly when the override is not CPython 3.14.

#!/bin/bash
set -euo pipefail

for candidate in "${PYRE_CHECK_PYTHON3:-}" python3.14 python3 python; do
  [ -n "$candidate" ] || continue
  path="$(command -v "$candidate" 2>/dev/null || true)"
  [ -n "$path" ] || continue
  "$path" -c 'import sys; print(sys.executable, sys.implementation.name, sys.version_info[:3])'
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/check.py` around lines 23 - 30, Validate the executable selected by
PYRE_CHECK_PYTHON3 or the fallback candidates before assigning PYTHON3: probe
each candidate using sys.implementation.name and sys.version_info, accept only
CPython 3.14, and reject an invalid explicit override with a clear failure.
Continue searching fallback candidates when an unqualified candidate is
unavailable or mismatched, and fail clearly if none qualify.
pyre/pyre-interpreter/src/call.rs (1)

3222-3233: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Pin and re-read all live object references to prevent use-after-free.

While this change correctly re-reads body_ns from the frame to survive GC relocation during run_with_jit(), several other local PyObjectRef variables remain unrooted and are not updated. Specifically, w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases are held by value and used after frame.run_with_jit() (e.g., w_namespace at line 3397, bases at line 3366).

Because these variables are not explicitly pinned and re-read from the GC shadow stack, a minor collection during the JIT portal execution will leave them pointing to reclaimed nursery slots, causing memory corruption when later accessed.

All PyObjectRef state that lives across run_with_jit() (and other prior allocating calls like the __prepare__ invocation) must be explicitly pinned to the shadow stack and re-read before subsequent use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/call.rs` around lines 3222 - 3233, Update the
class-creation flow surrounding frame.run_with_jit() and the earlier __prepare__
call to explicitly pin every live PyObjectRef that survives those allocations,
including w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases.
Re-read each reference from its shadow-stack/frame slot before later use,
alongside the existing body_ns refresh, preserving the current behavior while
preventing stale relocated-object references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@majit/majit-backend-cranelift/src/compiler.rs`:
- Around line 24740-24806: Add the #[ignore] attribute to both
guard_eval_breaker_deopts_when_bitmask_set and
guard_eval_breaker_cross_trace_inherits_bitmask, which mutate the process-global
eval_breaker_word address override. Leave their test logic unchanged and mark
them for explicit serial execution.

In `@pyre/pyre-interpreter/src/opcode_ops.rs`:
- Around line 349-356: Prevent moving-GC use-after-free around every
try_hash_value call by pinning referenced PyObjectRef locals and each unrooted
vector element on the shadow stack, then re-reading them with shadow_stack_get
before subsequent stores or uses. Apply this in
pyre/pyre-interpreter/src/opcode_ops.rs lines 349-356 (map_add_value), 310-310,
and 322-328; pyre/pyre-interpreter/src/builtins.rs lines 5748-5748 and
5766-5766; and pyre/pyre-interpreter/src/typedef.rs lines 2057-2063, 2129-2134,
3493-3499, 13895-13901, 13987-13993, and 14067-14073, preserving the existing
w_dict_store and w_set_add behavior.

---

Duplicate comments:
In `@pyre/check.py`:
- Around line 23-30: Validate the executable selected by PYRE_CHECK_PYTHON3 or
the fallback candidates before assigning PYTHON3: probe each candidate using
sys.implementation.name and sys.version_info, accept only CPython 3.14, and
reject an invalid explicit override with a clear failure. Continue searching
fallback candidates when an unqualified candidate is unavailable or mismatched,
and fail clearly if none qualify.

In `@pyre/pyre-interpreter/src/call.rs`:
- Around line 3222-3233: Update the class-creation flow surrounding
frame.run_with_jit() and the earlier __prepare__ call to explicitly pin every
live PyObjectRef that survives those allocations, including w_namespace, bases,
w_metaclass, extra_kwargs, and w_orig_bases. Re-read each reference from its
shadow-stack/frame slot before later use, alongside the existing body_ns
refresh, preserving the current behavior while preventing stale relocated-object
references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a12676fe-141a-4daf-948e-bc39ebac628d

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5f92b and d58c7c0.

📒 Files selected for processing (13)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-metainterp/src/blackhole.rs
  • pyre/check.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.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/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

@coderabbitai coderabbitai 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (2)
pyre/check.py (1)

23-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate that the selected executable is actually CPython 3.14.

shutil.which() only proves that a command exists; python3/python may resolve to an older CPython, PyPy, or a shim, and PYRE_CHECK_PYTHON3 bypasses validation entirely. That can still produce version-mismatched baseline comparisons and stdlib discovery. Probe candidates with sys.implementation.name and sys.version_info before accepting them, or fail clearly when the override is not CPython 3.14.

#!/bin/bash
set -euo pipefail

for candidate in "${PYRE_CHECK_PYTHON3:-}" python3.14 python3 python; do
  [ -n "$candidate" ] || continue
  path="$(command -v "$candidate" 2>/dev/null || true)"
  [ -n "$path" ] || continue
  "$path" -c 'import sys; print(sys.executable, sys.implementation.name, sys.version_info[:3])'
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/check.py` around lines 23 - 30, Validate the executable selected by
PYRE_CHECK_PYTHON3 or the fallback candidates before assigning PYTHON3: probe
each candidate using sys.implementation.name and sys.version_info, accept only
CPython 3.14, and reject an invalid explicit override with a clear failure.
Continue searching fallback candidates when an unqualified candidate is
unavailable or mismatched, and fail clearly if none qualify.
pyre/pyre-interpreter/src/call.rs (1)

3222-3233: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Pin and re-read all live object references to prevent use-after-free.

While this change correctly re-reads body_ns from the frame to survive GC relocation during run_with_jit(), several other local PyObjectRef variables remain unrooted and are not updated. Specifically, w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases are held by value and used after frame.run_with_jit() (e.g., w_namespace at line 3397, bases at line 3366).

Because these variables are not explicitly pinned and re-read from the GC shadow stack, a minor collection during the JIT portal execution will leave them pointing to reclaimed nursery slots, causing memory corruption when later accessed.

All PyObjectRef state that lives across run_with_jit() (and other prior allocating calls like the __prepare__ invocation) must be explicitly pinned to the shadow stack and re-read before subsequent use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/call.rs` around lines 3222 - 3233, Update the
class-creation flow surrounding frame.run_with_jit() and the earlier __prepare__
call to explicitly pin every live PyObjectRef that survives those allocations,
including w_namespace, bases, w_metaclass, extra_kwargs, and w_orig_bases.
Re-read each reference from its shadow-stack/frame slot before later use,
alongside the existing body_ns refresh, preserving the current behavior while
preventing stale relocated-object references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@majit/majit-backend-cranelift/src/compiler.rs`:
- Around line 24740-24806: Add the #[ignore] attribute to both
guard_eval_breaker_deopts_when_bitmask_set and
guard_eval_breaker_cross_trace_inherits_bitmask, which mutate the process-global
eval_breaker_word address override. Leave their test logic unchanged and mark
them for explicit serial execution.

In `@pyre/pyre-interpreter/src/opcode_ops.rs`:
- Around line 349-356: Prevent moving-GC use-after-free around every
try_hash_value call by pinning referenced PyObjectRef locals and each unrooted
vector element on the shadow stack, then re-reading them with shadow_stack_get
before subsequent stores or uses. Apply this in
pyre/pyre-interpreter/src/opcode_ops.rs lines 349-356 (map_add_value), 310-310,
and 322-328; pyre/pyre-interpreter/src/builtins.rs lines 5748-5748 and
5766-5766; and pyre/pyre-interpreter/src/typedef.rs lines 2057-2063, 2129-2134,
3493-3499, 13895-13901, 13987-13993, and 14067-14073, preserving the existing
w_dict_store and w_set_add behavior.

---

Duplicate comments:
In `@pyre/check.py`:
- Around line 23-30: Validate the executable selected by PYRE_CHECK_PYTHON3 or
the fallback candidates before assigning PYTHON3: probe each candidate using
sys.implementation.name and sys.version_info, accept only CPython 3.14, and
reject an invalid explicit override with a clear failure. Continue searching
fallback candidates when an unqualified candidate is unavailable or mismatched,
and fail clearly if none qualify.

In `@pyre/pyre-interpreter/src/call.rs`:
- Around line 3222-3233: Update the class-creation flow surrounding
frame.run_with_jit() and the earlier __prepare__ call to explicitly pin every
live PyObjectRef that survives those allocations, including w_namespace, bases,
w_metaclass, extra_kwargs, and w_orig_bases. Re-read each reference from its
shadow-stack/frame slot before later use, alongside the existing body_ns
refresh, preserving the current behavior while preventing stale relocated-object
references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a12676fe-141a-4daf-948e-bc39ebac628d

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5f92b and d58c7c0.

📒 Files selected for processing (13)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-metainterp/src/blackhole.rs
  • pyre/check.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.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/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
🛑 Comments failed to post (2)
majit/majit-backend-cranelift/src/compiler.rs (1)

24740-24806: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add #[ignore] to tests mutating process-global state to prevent CI flakiness and SIGSEGVs.

These tests override the global eval_breaker_word address to point to a stack-local AtomicUsize. If cargo test runs them in parallel with other tests compiling JIT traces, the other traces will bake in a dangling pointer, leading to undefined behavior (SIGSEGVs) or flaky test failures.

Consistent with other process-global tests in this file (e.g., test_host_loop_external_jump_to_middle_label_uses_label_selector), please add an #[ignore] attribute instructing them to be run serially.

🛠️ Proposed fix to isolate test execution
+    #[ignore = "sets the process-global eval_breaker_word override; run serially: \
+                `cargo test -p majit-backend-cranelift guard_eval_breaker_ \
+                --features dynasm -- --ignored --test-threads=1`"]
     #[test]
     fn guard_eval_breaker_deopts_when_bitmask_set() {
         assert_inert_eval_breaker_guard();
 
         let test_word = AtomicUsize::new(0);
         majit_ir::eval_breaker_word::set_addr_override_for_test(
             &test_word as *const AtomicUsize as usize,
         );
         let _clear_override = ClearEvalBreakerAddrOverride;
 
         let (backend, token) = compile_eval_breaker_guard_trace(518);
         let frame = backend.execute_token(&token, &[]);
         assert!(
             backend.get_latest_descr(&frame).is_finish(),
             "a zero eval-breaker word must stay on the guard fast path"
         );
 
         test_word.fetch_or(majit_ir::eval_breaker_word::EB_STW, Ordering::Relaxed);
         assert_ne!(
             test_word.load(Ordering::Relaxed) & majit_ir::eval_breaker_word::EB_STW,
             0
         );
         let frame = backend.execute_token(&token, &[]);
         assert!(
             !backend.get_latest_descr(&frame).is_finish(),
             "the STW bit must use the existing guard-failure exit"
         );
         test_word.fetch_and(!majit_ir::eval_breaker_word::EB_STW, Ordering::Relaxed);
 
         let (backend_after_clear, token_after_clear) = compile_eval_breaker_guard_trace(519);
         let frame = backend_after_clear.execute_token(&token_after_clear, &[]);
         assert!(
             backend_after_clear.get_latest_descr(&frame).is_finish(),
             "a freshly compiled trace must pass after the STW bit clears"
         );
 
         test_word.fetch_or(majit_ir::eval_breaker_word::EB_ASYNC, Ordering::Relaxed);
         let frame = backend_after_clear.execute_token(&token_after_clear, &[]);
         assert!(
             !backend_after_clear.get_latest_descr(&frame).is_finish(),
             "the async bit must use the same guard-failure exit"
         );
     }
 
+    #[ignore = "sets the process-global eval_breaker_word override; run serially: \
+                `cargo test -p majit-backend-cranelift guard_eval_breaker_ \
+                --features dynasm -- --ignored --test-threads=1`"]
     #[test]
     fn guard_eval_breaker_cross_trace_inherits_bitmask() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    #[ignore = "sets the process-global eval_breaker_word override; run serially: \
                `cargo test -p majit-backend-cranelift guard_eval_breaker_ \
                --features dynasm -- --ignored --test-threads=1`"]
    #[test]
    fn guard_eval_breaker_deopts_when_bitmask_set() {
        assert_inert_eval_breaker_guard();

        let test_word = AtomicUsize::new(0);
        majit_ir::eval_breaker_word::set_addr_override_for_test(
            &test_word as *const AtomicUsize as usize,
        );
        let _clear_override = ClearEvalBreakerAddrOverride;

        let (backend, token) = compile_eval_breaker_guard_trace(518);
        let frame = backend.execute_token(&token, &[]);
        assert!(
            backend.get_latest_descr(&frame).is_finish(),
            "a zero eval-breaker word must stay on the guard fast path"
        );

        test_word.fetch_or(majit_ir::eval_breaker_word::EB_STW, Ordering::Relaxed);
        assert_ne!(
            test_word.load(Ordering::Relaxed) & majit_ir::eval_breaker_word::EB_STW,
            0
        );
        let frame = backend.execute_token(&token, &[]);
        assert!(
            !backend.get_latest_descr(&frame).is_finish(),
            "the STW bit must use the existing guard-failure exit"
        );
        test_word.fetch_and(!majit_ir::eval_breaker_word::EB_STW, Ordering::Relaxed);

        let (backend_after_clear, token_after_clear) = compile_eval_breaker_guard_trace(519);
        let frame = backend_after_clear.execute_token(&token_after_clear, &[]);
        assert!(
            backend_after_clear.get_latest_descr(&frame).is_finish(),
            "a freshly compiled trace must pass after the STW bit clears"
        );

        test_word.fetch_or(majit_ir::eval_breaker_word::EB_ASYNC, Ordering::Relaxed);
        let frame = backend_after_clear.execute_token(&token_after_clear, &[]);
        assert!(
            !backend_after_clear.get_latest_descr(&frame).is_finish(),
            "the async bit must use the same guard-failure exit"
        );
    }

    #[ignore = "sets the process-global eval_breaker_word override; run serially: \
                `cargo test -p majit-backend-cranelift guard_eval_breaker_ \
                --features dynasm -- --ignored --test-threads=1`"]
    #[test]
    fn guard_eval_breaker_cross_trace_inherits_bitmask() {
        let test_word = AtomicUsize::new(0);
        majit_ir::eval_breaker_word::set_addr_override_for_test(
            &test_word as *const AtomicUsize as usize,
        );
        let _clear_override = ClearEvalBreakerAddrOverride;

        let (backend_a, token_a) = compile_eval_breaker_guard_trace(520);
        test_word.fetch_or(majit_ir::eval_breaker_word::EB_ASYNC, Ordering::Relaxed);
        let (backend_b, token_b) = compile_eval_breaker_guard_trace(521);

        let frame_a = backend_a.execute_token(&token_a, &[]);
        assert!(
            !backend_a.get_latest_descr(&frame_a).is_finish(),
            "the first trace must observe the stable test-local word"
        );
        let frame_b = backend_b.execute_token(&token_b, &[]);
        assert!(
            !backend_b.get_latest_descr(&frame_b).is_finish(),
            "a trace compiled after arming must bake the same test-local word address"
        );
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@majit/majit-backend-cranelift/src/compiler.rs` around lines 24740 - 24806,
Add the #[ignore] attribute to both guard_eval_breaker_deopts_when_bitmask_set
and guard_eval_breaker_cross_trace_inherits_bitmask, which mutate the
process-global eval_breaker_word address override. Leave their test logic
unchanged and mark them for explicit serial execution.
pyre/pyre-interpreter/src/opcode_ops.rs (1)

349-356: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Pin local object references and vector elements before try_hash_value to prevent use-after-free.

The addition of try_hash_value introduces an allocating call (which can trigger a moving minor GC) immediately before using local PyObjectRef variables (dict, set, key, value, item) and iterating over unrooted native Vec elements. Because Pyre uses a shadow-stack GC, local Rust variables and heap Vecs are not automatically updated when nursery objects relocate. These references must be explicitly pinned to the shadow stack before try_hash_value and re-read via shadow_stack_get before subsequent use; otherwise, they will point to freed memory.

  • pyre/pyre-interpreter/src/opcode_ops.rs#L349-L356: pin dict, key, and value to the shadow stack before try_hash_value, then re-read them before w_dict_store.
  • pyre/pyre-interpreter/src/opcode_ops.rs#L310-L310: pin set and value to the shadow stack before try_hash_value, then re-read them before w_set_add.
  • pyre/pyre-interpreter/src/opcode_ops.rs#L322-L328: pin set and every element in the unrooted items vector, re-reading the active item and set across try_hash_value.
  • pyre/pyre-interpreter/src/builtins.rs#L5748-L5748: pin dict, val, and all unrooted keys elements, re-reading the active key/dict/val across try_hash_value.
  • pyre/pyre-interpreter/src/builtins.rs#L5766-L5766: pin dict and all unrooted elems elements, re-reading them across try_hash_value.
  • pyre/pyre-interpreter/src/typedef.rs#L2057-L2063: pin obj and all unrooted items elements, re-reading them across try_hash_value.
  • pyre/pyre-interpreter/src/typedef.rs#L2129-L2134: pin set_obj and all unrooted items elements, re-reading them across try_hash_value.
  • pyre/pyre-interpreter/src/typedef.rs#L3493-L3499: pin d, value, and all unrooted items elements, re-reading them across try_hash_value.
  • pyre/pyre-interpreter/src/typedef.rs#L13895-L13901: pin args[0] and args[1] before try_hash_value, and re-read them before w_set_add.
  • pyre/pyre-interpreter/src/typedef.rs#L13987-L13993: pin args[0] and all unrooted other_items elements, re-reading them across try_hash_value.
  • pyre/pyre-interpreter/src/typedef.rs#L14067-L14073: pin args[0] and all unrooted other_items elements, re-reading them across try_hash_value.
📍 Affects 3 files
  • pyre/pyre-interpreter/src/opcode_ops.rs#L349-L356 (this comment)
  • pyre/pyre-interpreter/src/opcode_ops.rs#L310-L310
  • pyre/pyre-interpreter/src/opcode_ops.rs#L322-L328
  • pyre/pyre-interpreter/src/builtins.rs#L5748-L5748
  • pyre/pyre-interpreter/src/builtins.rs#L5766-L5766
  • pyre/pyre-interpreter/src/typedef.rs#L2057-L2063
  • pyre/pyre-interpreter/src/typedef.rs#L2129-L2134
  • pyre/pyre-interpreter/src/typedef.rs#L3493-L3499
  • pyre/pyre-interpreter/src/typedef.rs#L13895-L13901
  • pyre/pyre-interpreter/src/typedef.rs#L13987-L13993
  • pyre/pyre-interpreter/src/typedef.rs#L14067-L14073
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/opcode_ops.rs` around lines 349 - 356, Prevent
moving-GC use-after-free around every try_hash_value call by pinning referenced
PyObjectRef locals and each unrooted vector element on the shadow stack, then
re-reading them with shadow_stack_get before subsequent stores or uses. Apply
this in pyre/pyre-interpreter/src/opcode_ops.rs lines 349-356 (map_add_value),
310-310, and 322-328; pyre/pyre-interpreter/src/builtins.rs lines 5748-5748 and
5766-5766; and pyre/pyre-interpreter/src/typedef.rs lines 2057-2063, 2129-2134,
3493-3499, 13895-13901, 13987-13993, and 14067-14073, preserving the existing
w_dict_store and w_set_add behavior.

The full-body walker discarded w_type and version_tag from load_attr_fast_path, so its folded instance-storage read had only class and map guards. On wasm this left trace re-entry blind to class mutation and kept the stale instance slot after a property was installed.

Record the live version_tag getfield and GuardValue before the map/storage read, then replace the guarded box in the heap cache so promotion state matches the existing trait tracer path.

Assisted-by: Claude
Remove the #57 gate's LOAD_ATTR exclusion because the landed exact-resume path uses forward-exception-delivery / CALL-forward to resume a mid-body method-call abort exactly instead of the FBW refuse-drop. The method-load-then-call partial-drop hazard therefore no longer applies.

Only LOAD_ATTR is admitted; heap-store widening remains a separate later slice.

Assisted-by: Claude
Remove the #57 gate's abort-free restriction on the STORE_SUBSCR, STORE_ATTR, STORE_NAME, STORE_GLOBAL, and LOAD_NAME group. The landed exact-resume path uses forward-exception-delivery or CALL-forward to resume a mid-body abort after a committed un-journaled store exactly instead of taking the FBW refuse-drop path, so the partial-iteration drop hazard no longer applies.

Assisted-by: Claude
The codewriter published operand-stack color mappings in its compact fast-locals-plus-stack space, while runtime resume consumers treated those slots as physical locals_cells_stack_w indices. Pure cell and free-variable slots therefore shifted every resumed operand by ncells; argument-cells did not because they overlap fast locals.

Translate runtime-facing stack metadata through stack_base, use the same nlocals + ncells base in the callee rebuild, and keep dependent slot lookups aligned. This clears the range_iterator-as-method-target crash in JIT-compiled closure-frame FOR_ITER loops surfaced by LOAD_ATTR admission.

Assisted-by: Claude
…-flush for nested loops

The kept-stack Unsupported branch-flush plain-flushed at the inner FOR_ITER header without re-delivering the consumed item. For a nested inner FOR_ITER over a non-inlinable iterator, this dropped v=0 before its body ran.

Select delivery versus plain flush from the existing body-completed predicates, mirroring Shape A's item-delivering flush when the body has not run.

Assisted-by: Claude
…ss_inner

build_class_inner holds `body_ns` (the class namespace object) in a
stack-local across `frame.run_with_jit()`. A hot class-level loop compiles
and runs through the JIT portal; a minor collection during it can promote
the namespace young -> old, relocating it. The frame's `debugdata.w_locals`
slot and the shadow-stack pin are forwarded — the object stays alive at its
new address — but the stack-local `body_ns` copy is not, so it is left
pointing at the freed nursery slot. The downstream `resolve_dict_backing` /
`w_dict_items` mirror into class_ns then reads reclaimed memory, and the
stranded value later surfaces as a SIGSEGV in `__set_name__` / getattr.

Re-read the forwarded namespace from `frame.get_w_locals()` after the run,
and rebind `mapping_namespace` to it. A large nursery (no minor collection)
hid the bug.

Assisted-by: Claude
collect_iterable accumulated yielded elements into a raw Vec<PyObjectRef>
on the malloc heap across baseobjspace::next calls. When the iterable is a
generator whose body allocates (e.g. a genexpr calling a JIT callee that
boxes a fresh int), each next can trigger a moving minor collection. The raw
Vec is not a GC root and conservative C-stack scanning does not reach its
heap buffer, so earlier nursery-boxed elements were stranded / not forwarded;
a consumer such as builtin_sum then read stale pointers, giving SIGSEGV or a
wrong answer (a big nursery masked it).

Pin the iterator and every yielded element on the gc_roots shadow stack — a
real root the collector walks and updates on relocation — reloading the
iterator from its slot before each next, then read the forwarded element
slots back out into the result Vec after the loop. Mirrors the translator's
push_roots/pop_roots bracket around a collecting call.

collect_iterable is a residual loop-bearing helper (not trace-inlined); full
check.py stays 181/181 on both dynasm and cranelift.

Assisted-by: Claude
The guard-failure bridge entry seeding in `run_perfn_walk` gated its
EC-color-collision operand reseed behind `CAN_BE_TAGGED`. That gate was
added with the tagged-int machinery and is inert on flag-false builds,
so the reseed never ran there.

Under free register coloring a leaf callee (`def cap(): return CELL + 1`)
places its LOAD_DEREF result in the portal EC register color. When the
cell payload's class flips mid-loop (float->int, int->str, int->bignum)
the deref-class guard fails and a bridge resumes at the guard offset. The
base entry seed writes the stale `ec_box` (a `ConstPtr` to the
trace-time cell payload) into that color; the gated-off reseed left it
there instead of overwriting it with the live resume operand
(`bridge_registers_r` names the real deref result). The residual add then
read the stale `ConstPtr` as its LHS and dereferenced the now-dead
trace-time payload object -> SIGSEGV in the eager `bh_binary_op`.

The reseed of a non-frame reserved color that the bridge names as a
genuine operand is a general correctness fix (the frame color keeps its
unconditional skip to preserve the standard-virtualizable identity), so
drop the tagged-int gate.

Assisted-by: Claude
…ception

The interpreter records each propagating frame through handle_exception and record_application_traceback. The compiled deopt ExitFrameWithExceptionRef path omitted that boundary record, dropping the materialized JIT frame from uncaught exception tracebacks. Mirror the interpreter record at both bottommost blackhole exits, using the faulting JitCode position mapped back to the Python PC so the traceback line remains exact.

This does not reconstruct unmaterialized inlined callees; that remains separate work.

Assisted-by: Claude
… the frame

A raising DELETE residual left the backend _store_exception cell holding the exception after blackhole propagation returned ExitFrameWithExceptionRef and the outer interpreter caught it. The next JIT entry's GUARD_NO_EXCEPTION then resurfaced that stale value.

Drain the backend cell at the bottommost unhandled-exception exit in blackhole_resume_via_rd_numb, after in-frame handler and caller routing have failed.

Assisted-by: Claude
…bodies

Admit the delete opcodes through the same forward-exact-resume path as the slice-2 stores. A committed un-journaled delete resumes forward without dropping the tail; a raising delete propagates with a bit-exact traceback via the landed exit-frame recording.

STORE_DEREF intentionally remains declined because its separate resume bug is still open.

Assisted-by: Claude
The post-call guard snapshot sourced stack slot 0 from the stale pre-call mirror [callable, NULL, arg0, arg1], so deep-recursion guard-fail recovery substituted the callable for the result. Advance the caller mirror explicitly after the CALL_ASSEMBLER result register writeback and before the paired guards; the register file was already correct.

Preserve the returned callee JitFrame for Cranelift exception recovery, and leave exception-guard failures to blackhole recovery because the bridge callback has no exception-value channel.

Assisted-by: Claude
Method-form LOAD_ATTR left only its final self box in the walk vstack mirror. After a committed STORE_DEREF, a later guard refilled the lower method slot from the stale receiver, so resumed CALL invoked the list itself.

Mirror both exact LOAD_ATTR method slots at the vable stack-write chokepoint, then admit STORE_DEREF loop bodies and cover the branch-plus-call gate shape.

Assisted-by: Claude
…me reraise/chain escape

route_to_catch transferred a residual exception into a zero-cost exception-table
handler without recording the compiled frame's traceback entry. A handler that
reraised (bare raise) or raised a new exception (implicit __context__ chaining)
then escaped with the faulting line missing, and for chaining the __context__
exception carried no compiled-frame traceback; Fix A only recorded the cleanup
RERAISE, yielding the except-clause line instead of the fault/raise lines.

Add a record_caught_exception callback on the blackhole interpreter (generic,
raw frame/exception refs only). pyre-jit installs record_caught_blackhole_traceback,
which translates the faulting JitCode PC through the JitCode map and records via
record_application_traceback on handler entry. Track the caught exception value so
that when the same object later escapes through the handler cleanup, the bottommost
exit suppresses the duplicate entry (attach_tb=false); Fix A's uncaught path is
unchanged.

Assisted-by: Claude
The cpython baseline oracle resolved to the first `python3` on PATH. On a host
with an older system CPython that emits version-sensitive error text (e.g.
list.insert/pop with a float index: "integer argument expected, got float" on
3.9 vs "'float' object cannot be interpreted as an integer" on 3.14/pypy), it
diverged from the pypy oracle and tripped a spurious cpython-vs-pypy baseline
mismatch (BASEFAIL) on synth/list_insert_pop_index even though pyre matches 3.14.
Prefer python3.14 (the compat target) when present, falling back to python3 then
python; the PYRE_CHECK_PYTHON3 override is unchanged.

Assisted-by: Claude
MAP_ADD, SET_ADD, and several direct builtin insertion paths bypassed the existing hashability gate and stored list keys or elements. Check try_hash_value before mutating comprehension accumulators, set methods and constructors, dict construction, and dict.fromkeys.

Assisted-by: Claude
…nt encoding

unsupported_jit_shape returns a new ConstEncodingOverflow decline when an upper
bound on a frame's constant pool and register file cannot fit the single-byte
register-or-constant index encoding (assembler.py:72; check_result
count_regs[kind] + len(constants) <= 256). Such a frame runs in the interpreter
before tracing; the decline is recorded in the frame-shape census.

The constant-pool bound flattens nestable Tuple/Frozenset/Slice constants — a
module body like html.entities expands a few code.constants entries into
thousands of ConstRef pool slots — while Code constants count as one ref (they
belong to a separate callee frame). The register bound is max_stackdepth plus
local/cell/free slots plus headroom for the vable red args and per-opcode
temporaries. Both are upper bounds, so no unencodable frame is admitted.

Without the gate, assembling such a frame overruns the encoding and the majit
assembler asserts mid-emission (touch_ref_reg_or_pool_slot); the FOR_ITER gate
widening on this branch admitted the <module> frames of test_html and
test_sundry, which crashed there.

Assisted-by: Claude
…ey hash check

416dc05 added `try_hash_value(...)?` calls to several dict/set insert paths
(SET_ADD, SET_UPDATE, MAP_ADD, the set/frozenset/dict constructors,
dict.fromkeys, set.add/update/symmetric_difference_update). `try_hash_value`
can run a user `__hash__`, which can allocate and trigger a moving minor
collection that relocates young nursery objects. The new call sites held the
container (dict/set) and key/value/item as raw `PyObjectRef` locals across
that collection point without shadow-stack rooting, then passed the
(potentially stale, post-relocation) pointers to `w_set_add`/`w_dict_store`.
Before 416dc05 these paths had no collection point between receiving the
pointers and the store, so this was a genuine regression.

Root every such site with `pyre_object::gc_roots::{push_roots, pin_root,
shadow_stack_len, shadow_stack_get}` (the existing pattern used by
`collect_iterable` / `sort_rooted_items` / the memoryview constructors):
pin the container and key/value/item before the hash check, then reload from
the shadow stack before the store. For a loop over a `Vec` returned by
`collect_iterable` (which pops its own roots before returning), the whole
vec is re-pinned for the loop's duration, not just the current element,
since an earlier iteration's hash call can also strand later elements that
were never rooted.

Sites fixed:
- opcode_ops.rs: set_add_value, set_update_value (SET branch),
  map_add_value, dict_update_value (generic-mapping tail loop)
- builtins.rs: builtin_set_from_items, builtin_dict_ctor (both the
  keys()-mapping loop and the pairs loop)
- typedef.rs: frozenset_descr_new, set_descr_init, dict.fromkeys
  (init_dict_type), and the three init_set_type sites (add, update,
  symmetric_difference_update)

Also closes a second, related gap in dict_update_value (used by the
DICT_UPDATE bytecode, e.g. `{**mapping}`): the generic-mapping tail loop
called raw `getitem` + `w_dict_store` with no hash check at all, silently
accepting an unhashable key from a non-dict mapping. Added the same
try_hash_value + rooting there, matching the sibling paths' reject-before-
mutate behavior and error message.

Added a set_update_value unit test mirroring the existing
map_add_value/set_add_value unhashable-reject test.

Verified: cargo test -p pyre-interpreter --features dynasm (380 passed);
cargo test -p pyre-jit --features dynasm --test gc_stress (6 passed);
CI's clippy sandbox check
(CLIPPY_CONF_DIR=ci/clippy-sandbox cargo clippy -p pyre-interpreter --no-deps
--features sandbox,dynasm -- -A clippy::all -D clippy::disallowed_methods
-D clippy::disallowed_types) clean; python3 pyre/check.py --backend
dynasm,cranelift (188/188 both, no BASEFAIL); all touched-site unhashable
repros still raise `TypeError: unhashable type: '<type>'` and hashable
controls stay byte-exact vs a python3.14 oracle on both backends with JIT on
and off; a dict/set-comprehension and set.update/symmetric_difference_update
repro over instances whose `__hash__` allocates and calls `gc.collect()`
mid-hash stays byte-exact under a forced full collection.

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

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

// dereference reclaimed memory (`resolve_dict_backing` / `w_dict_items`
// below, then `__set_name__`).
let body_ns = frame.get_w_locals();
let mapping_namespace = mapping_namespace.map(|_| body_ns);

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 Refresh the namespace passed to the metaclass

When __prepare__ returns a namespace and the class body runs a hot JIT loop that triggers a moving GC, this only refreshes body_ns/mapping_namespace from the frame; the later metaclass path still reads the original w_namespace local (if let Some(w_prepared_dict) = w_namespace), which can still point at the pre-forwarding address. In that scenario a custom metaclass (or an exact prepared dict that is replayed after the body) can dereference a stale namespace even though the frame’s debugdata.w_locals was updated, so the prepared namespace used downstream needs to be refreshed/rooted as well.

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit a15b2e9 into main Jul 15, 2026
29 of 30 checks passed
@youknowone
youknowone deleted the nbody branch July 15, 2026 12: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