Skip to content

jit: close gh#152 deep-kept operand-stack resume gap (RPython register/liveness channel)#436

Merged
youknowone merged 7 commits into
mainfrom
nbody
Jul 9, 2026
Merged

jit: close gh#152 deep-kept operand-stack resume gap (RPython register/liveness channel)#436
youknowone merged 7 commits into
mainfrom
nbody

Conversation

@youknowone

@youknowone youknowone commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Closes the deep-kept operand-stack resume gap in the FOR_ITER residency path
(gh#152 layer-2 census: the kept_stack_deep_var_* trio + short_circuit_side_effects).

What

Three commits, all gh#152:

  1. majit: route successful-compile trace teardown to abort_trace_live — fix the
    loops_aborted success-teardown double-count. abort_trace(false) was overloaded as
    generic trace-session teardown and bumped stats.loops_aborted even on a successful
    compile (a NEW-DEVIATION vs RPython's aborted_tracing, which is reached only on the
    SwitchToBlackhole unwind). The four success edges now route to abort_trace_live(false)
    (live teardown, no accounting); loops_aborted reads 0 on all loop-nest benches with
    loops_compiled/bridges_compiled/guard_failures unchanged.

  2. synth: correct mutation-count comment — the kept_stack_deep_var_shortcircuit_mutate
    canary appends 3× per iteration (g, h, conditional g/h); comment corrected to match.

  3. jit: deep-kept operand-stack resume via register/liveness channel; delete FOR_ITER hazard gate — the core fix. A depth>1 kept operand stack could not be reconstructed at
    a compiled interior branch guard, causing an ResidualCallArgUnbound abort (walk declined,
    permanent interpreter fallback) or a SIGSEGV when the reload was forced on.

    Root cause (RPython parity): pyre restored the deep operand stack through the vable
    positional channel (a shadow that pop_value NULL-clears), whereas RPython's
    get_list_of_active_boxes (rpython/jit/metainterp/pyjitpl.py:177-234) captures guard
    resume boxes from the register file registers_r[index] via the per-PC -live- set.

    The fix:

    • Delete the foriter_deep_kept_call_hazard gate (a FOR_ITER inlining: walker coverage gap (iteration_protocol abort) + liveness gap (polymorphic deopt crash) #342 NEW-DEVIATION with no RPython
      backing) that withheld the FOR_ITER per-iteration getarrayitem_vable_r reload for loop
      bodies with a CALL across an in-body conditional branch. The reload is now unconditional
      for portals, matching w_iterator = self.peekvalue() (pypy/interpreter/pyopcode.py:1303).
    • Recover deep kept operand slots from the register/liveness channel: in
      walker_capture_snapshot_for_last_guard_impl, a slot the walk mirror leaves NULL across a
      not-taken branch merge is filled from ctx.registers_r[color], where color is the
      pcdep_color_slots[guard_py_pc] inverse (new state::semantic_slot_color_for_ref_slot).
      Capture-only (transient snapshot overlay, never the live shadow). Fills Type::Ref slots;
      an unboxed-int kept temp stays a hole (follow-up: int-bank NEW_W_INT capture).

    Arm-independence is correct by construction: the recovery is keyed to the guard's resume PC
    liveness, matching RPython's per-PC -live- set.

Results

The census 4 benches, previously 1.10×–1.21× slower than the interpreter (pure residency
loss), now all beat it after the fix:

bench census (before) post-fix
kept_stack_deep_var_condexpr 1.10× slower 2.50× faster
kept_stack_deep_var_nested_call 1.15× slower 1.40× faster
kept_stack_deep_var_shortcircuit 1.21× slower 1.35× faster
short_circuit_side_effects 0.50× (partial) 1.77× faster

The deep_var trio's shared ResidualCallArgUnbound abort is gone; the built_as_portal=false
structural-decline log still fires on the short-circuit pair but is now benign (the trait-tracer
fallback is a net win). That structural decline is a separate architectural item (pyre's
one-artifact-per-CodeObject model vs RPython's independent per-greenkey portal/callee tokens),
not a residency defect on any current bench.

Verification

  • pyre/check.py 155/155 on dynasm and cranelift (flagless, no env gates).
  • kept_stack_deep_var_{condexpr,nested_call,shortcircuit} + the shortcircuit_mutate canary
    bit-exact (len(log)=120000, no double-delivery / dropped iteration).
  • cargo test -p pyre-jit-trace 297/0, -p pyre-jit 260/0.
  • Loop-nest steady-state stats (nbody/fannkuch/spectral_norm/nested_loop) unchanged
    except loops_aborted → 0.
  • Codex parity review: 0 regressions introduced by this patch.

Issue body refreshed at the corresponding commit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved trace recovery and stack resumption, including better handling of deep-kept values during JIT execution.
    • Updated type(x) behavior so it more closely matches Python/PyPy expectations.
  • Bug Fixes

    • Prevented successful trace compilation paths from being counted as aborts.
    • Fixed cases where pending abort state could leak into unrelated trace events.
    • Improved subtype checks for partially initialized types.
    • Simplified iterator reload behavior in portal execution for more reliable results.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR changes success-path teardown in JIT trace merge points to avoid abort-accounting double counts, adds a type(x) shortcut to skip __init__ calls, introduces deep-kept operand-stack slot recovery for Ref/Int banks, removes a portal iterator reload hazard check, and adds a null-MRO subtype fallback walk.

Changes

Trace success-path teardown

Layer / File(s) Summary
clear_pending_abort helper
majit/majit-metainterp/src/pyjitpl.rs
Adds public clear_pending_abort method resetting staged pending-abort fields, used in compile teardown.
Success edges use live-teardown
majit/majit-metainterp/src/jitdriver.rs
Four success edges replace abort_trace(false) with abort_trace_live(false) plus clear_pending_abort() to avoid abort-accounting on success.

type(x) init skip shortcut

Layer / File(s) Summary
type_call_type_x_shortcut and call-site gating
pyre/pyre-interpreter/src/call.rs
New helper detects type(x) calls and gates out __init__ execution in three call paths.

Deep-kept operand-stack slot recovery

Layer / File(s) Summary
Inverse slot-color lookup helpers
pyre/pyre-jit-trace/src/state.rs
Adds semantic_slot_color_for_slot and Ref/Int-bank wrappers for per-PC color lookup.
stack_sync augmentation with Ref/Int recovery
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
Adds deepkept_int_recovery_enabled() env flag and fills stack_sync holes via Ref-bank then optional Int-bank recovery.
Unconditional portal iterator reload
pyre/pyre-jit/src/jit/codewriter.rs, pyre/bench/synth/kept_stack_deep_var_shortcircuit_mutate.py
Removes hazard-scan gating for portal iterator reload, making it unconditional; updates related bench comment.

Null-MRO subtype fallback

Layer / File(s) Summary
find_best_base and null-MRO fallback walk
pyre/pyre-object/src/typeobject.rs
Adds find_best_base helper and uses it to walk base chains when MRO pointer is null instead of returning false immediately.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JitDriver
  participant TraceState
  participant PendingAbort

  JitDriver->>TraceState: abort_trace_live(false)
  JitDriver->>PendingAbort: clear_pending_abort()
  JitDriver->>JitDriver: clear_trace_session()
  JitDriver->>JitDriver: mark compile_trace_success
Loading

Possibly related issues

Possibly related PRs

  • youknowone/pyre#311: Both PRs modify the merge-point/CloseLoop tracing control flow in majit/majit-metainterp.
  • youknowone/pyre#365: Both PRs touch kept-stack/guard resume logic in pyre-jit-trace and codewriter.rs.
  • youknowone/pyre#421: Both PRs modify ForIterNext iterator reload logic and the same kept-stack benchmark.

Poem

A rabbit hops through traces clean,
No double-count abort routine.
type(x) skips its init call,
Deep slots recovered, holes not at all.
Bases walked when MRO's null —
Hop hop hop, the code review's full! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title names the core deep-kept operand-stack fix and register/liveness channel, which matches the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Codex parity review

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

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-object/src/typeobject.rs:735 ↔ pypy/objspace/std/typeobject.py:1341: Rust find_best_base silently skips non-type bases and does not reject incomplete type bases; PyPy checks if not w_candidate.hasmro: raise TypeError("Cannot extend an incomplete type ..."). The new null-MRO _issubtype_slow_and_wrong fallback is directionally closer, but this error path is not ported.

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

  • pyre/pyre-interpreter/src/call.rs:2023 ↔ pypy/objspace/std/typeobject.py:726: Rust’s call_with_kwargs type path calls unwrap_static_new(new_fn) / call_callable directly; PyPy binds __new__ with space.get(w_newdescr, space.w_None, w_type=self) before space.call_obj_args.
  • pyre/pyre-interpreter/src/call.rs:2354 ↔ pypy/objspace/std/typeobject.py:726: Rust’s no-frame type_descr_call_impl uses lookup_in_type(w_type, "__new__") and call_function_impl directly; PyPy descriptor-binds __new__ first via space.get(...).
  • pyre/pyre-interpreter/src/call.rs:2399 ↔ pypy/objspace/std/typeobject.py:741: Rust raises __init__() should return None, not 'X'; PyPy raises the shorter __init__() should return None.

4. Structural adaptations

  • majit/majit-metainterp/src/pyjitpl.rs:6935 ↔ rpython/jit/metainterp/pyjitpl.py:3119: Rust splits trace teardown into abort_trace_live() plus optional accounting, while PyPy reaches the success path by raising ContinueRunningNormally; this is an explicit control-flow adaptation.
  • pyre/pyre-interpreter/src/call.rs:2045 ↔ pypy/objspace/std/typeobject.py:735: The new type(x) __init__ skip matches PyPy’s condition, but Rust must spell it as a helper over raw type pointers and kwargs presence.
  • pyre/pyre-jit/src/jit/codewriter.rs:9568 ↔ pypy/interpreter/pyopcode.py:1303: Rust reloads portal FOR_ITER iterators with getarrayitem_vable_r; PyPy source says w_iterator = self.peekvalue(). This is the Pyre virtualizable-frame lowering of the same stack read.
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:10242 ↔ rpython/jit/metainterp/pyjitpl.py:216: Rust reconstructs kept-stack guard boxes via pcdep_color_slots and registers_r[color]; PyPy iterates liveness directly and stores self.registers_*[index].
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:10312 ↔ rpython/jit/metainterp/pyjitpl.py:216: Rust gates Int-bank kept-stack recovery behind PYRE_FBW_DEEPKEPT_INT and boxes raw ints for the Ref-typed vable array; PyPy always appends self.registers_i[index] to resume storage.

@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: 87599d818d

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

// NOT reached on success (the loop/bridge counter was already
// bumped at backend-compile time). Call the live-teardown half
// only, not the accounting half.
self.meta.abort_trace_live(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 Clear pending abort payload after successful trace teardown

When this success path tears down a live trace, abort_trace_live still stages pending_abort_green_key/pending_abort_permanent for a subsequent aborted_tracing call. No aborted_tracing follows on successful compile, and the later re-observe path calls abort_trace_live with no live ctx, so the staged key can survive until an unrelated path that reports an abort after consuming its own ctx; with abort hooks/diagnostics enabled this can fire on_trace_abort for the previously successful greenkey. The bridge finish helper explicitly clears these fields after success for the same reason, so this success teardown should do that as well.

Useful? React with 👍 / 👎.

@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: 152de8f2fc

ℹ️ 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 +10215 to +10218
if box_op != OpRef::NONE
&& !opref_is_null_const_ptr(box_op)
&& box_op.ty() == Some(majit_ir::Type::Ref)
{

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 Decline unboxed kept-stack slots before compiling

When a FOR_ITER body keeps an unboxed int temporary below an in-body short-circuit/conditional branch, this Ref-only recovery leaves that operand-stack slot as a NULL hole in the virtualizable snapshot; the removed hazard gate previously forced these call+branch loop bodies to abort before consuming the iterator, but now the trace can compile and later resume with the missing slot. Please either conservatively decline when the guard-PC slot resolves to a non-Ref OpRef, or synthesize the boxed NEW_W_INT value before allowing the snapshot.

Useful? React with 👍 / 👎.

Four trace-close success edges in JitDriver called abort_trace(false),
which runs both the live history teardown and aborted_tracing accounting.
On a successful compile the accounting must not fire: raise_if_successful()
raises ContinueRunningNormally (pyjitpl.py:3095-3123), bypassing the
`except SwitchToBlackhole` handler that reaches aborted_tracing, and the
loop/bridge counter was already bumped at backend-compile time. Calling
the accounting half here bumped stats.loops_aborted on success and
double-counted the re-observe edge.

Change the four success edges (CompileTrace arm, the bridge Compiled
close, the CloseLoopWithArgs bridge Compiled close, and the
take_compile_trace_success re-observe) to abort_trace_live(false) so only
the live teardown runs. loops_aborted now reads 0 on nbody/fannkuch/
spectral_norm/nested_loop; loops_compiled, bridges_compiled, and
guard_failures are unchanged.

Assisted-by: Claude
…cuit_mutate

The loop appends three times per iteration (g, h, and one conditional
g/h), so len(log) for f(40000) is 120000, not the 80000 the "2 mutations
per iteration" comment implied. State the 3x invariant.

Assisted-by: Claude
…lete FOR_ITER hazard gate

Restore deep kept operand-stack slots at a branch-guard resume from the
guard-PC register file instead of the pop-cleared vable positional shadow,
porting get_list_of_active_boxes (rpython/jit/metainterp/pyjitpl.py:177-234),
which captures resume boxes from registers_r[index] via the per-PC -live- set.

In walker_capture_snapshot_for_last_guard_impl (jitcode_dispatch.rs), a kept
operand slot the walk mirror (ctx.vstack_boxes) leaves NULL across a not-taken
branch merge is recovered from ctx.registers_r[color], where color is the
pcdep_color_slots[guard_py_pc] inverse (new state::semantic_slot_color_for_ref_slot).
The recovery is capture-only (transient snapshot overlay, never the live shadow)
and fills only Type::Ref slots; an unboxed-int kept temp is left a hole.

Delete the foriter_deep_kept_call_hazard gate (codewriter.rs) that withheld the
FOR_ITER per-iteration getarrayitem_vable_r reload for loop bodies with a CALL
across an in-body conditional branch. The reload is now unconditional for
portals, matching w_iterator = self.peekvalue() (pypy/interpreter/pyopcode.py:1303).

kept_stack_deep_var_{condexpr,nested_call,shortcircuit} and the shortcircuit
mutate canary run resident without the deep-guard SIGSEGV; check.py 155/155 on
dynasm and cranelift.

Assisted-by: Claude
Reformat the `iter_value` binding after the `foriter_deep_kept_call_hazard`
gate removal collapsed the condition to a single `if is_portal`. No logic
change; `cargo fmt --check` clean.

Assisted-by: Claude
The four successful-compile teardown edges (CompileTrace, both bridge-Compiled
paths, and the re-observed-success path) call abort_trace_live(false) for live
cleanup but fire no aborted_tracing, so the pending_abort_green_key/permanent
that abort_trace_live stages was never consumed. A later unrelated abort would
then take() that stale key and fire on_trace_abort for the earlier
successfully-compiled greenkey.

Add MetaInterp::clear_pending_abort() and call it on each success edge, matching
the existing bridge-FINISH success path (pyjitpl.rs) which already cleared these
fields inline for the same reason. No aborted_tracing follows on success
(raise_if_successful raises ContinueRunningNormally, pyjitpl.py:3095-3123).

check.py 155/155 dynasm and cranelift; aborted_tracing tests pass.

Assisted-by: Claude
…allback

type.__call__ suppresses __init__ when self is the `type` builtin with no
keyword arguments and exactly one positional argument (type(x) returns the
class of x from __new__). Add `type_call_type_x_shortcut` and gate the three
plain-instantiation __init__ dispatch sites in call.rs on it
(typeobject.py:735-736).

w_type_issubtype falls back to a find_best_base base-chain walk when mro_w is
null instead of returning false, matching _issubtype_slow_and_wrong for a
partially initialised type (typeobject.py:1640-1655, find_best_base
1335-1354; the "incomplete type" raise is omitted since the fn returns bool).

Assisted-by: Claude
…ult OFF)

Port the i-bank half of get_list_of_active_boxes (pyjitpl.py:206-210,
add_box_to_storage(registers_i[index])) to the deep-kept operand-stack
recovery in walker_capture_snapshot_for_last_guard_impl. A bank-0 (Int) stack
pcdep entry names the Int-bank color owning the slot; registers_i[color] holds
the raw int, boxed into a W_IntObject via wrapint so the uniformly Ref-typed
vable array carries a Ref.

Generalize semantic_slot_color_for_ref_slot into a bank-generic
semantic_slot_color_for_slot core; add semantic_slot_color_for_int_slot
(bank=0) alongside the ref (bank=1) delegate.

Gated default-OFF: flag-off is byte-identical to leaving the int a hole
(resume re-materializes it from its defining IR). On the current frontend the
operand stack is uniformly Ref-banked so no bank-0 stack entry exists and the
block fires nowhere; the real int-hole (a Ref-bank color whose OpRef is
Int-typed) cannot be boxed at capture time — wrapint emits into a settled
trace and trips store_final_boxes_in_guard (resume.py:397) — so it must be
synthesized at operand-stack push time, a frontend change tracked separately.
This is the RPython-parity read channel for when bank-0 stack entries exist.

Assisted-by: Claude

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

🤖 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/pyre-jit-trace/src/jitcode_dispatch.rs`:
- Around line 10242-10243: Replace the new HashSet side table in the stack
synchronization logic with a small Vec-based structure that matches the existing
stack/list shape. In the code around stack_sync and covered in
jitcode_dispatch.rs, keep the bounded operand-stack index tracking in a simple
linear collection and update the membership checks accordingly instead of using
a Rust-native set. Ensure the rest of the ported path continues to follow the
original RPython/PyPy data-structure style.
🪄 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: 81626490-3d6d-4f20-afc5-f638d55a0840

📥 Commits

Reviewing files that changed from the base of the PR and between 69df974 and 2c2ba2f.

📒 Files selected for processing (8)
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • pyre/bench/synth/kept_stack_deep_var_shortcircuit_mutate.py
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-object/src/typeobject.rs

Comment on lines +10242 to +10243
let mut covered: std::collections::HashSet<usize> =
stack_sync.iter().map(|&(idx, _)| idx).collect();

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 | 🟠 Major | ⚡ Quick win

Avoid introducing a HashSet side table here.

covered only tracks bounded operand-stack vable indices; a small Vec keeps this closer to the existing stack/list shape and avoids adding a Rust-native container in the ported path.

As per coding guidelines, **/*.rs: “When porting RPython/PyPy code, do not casually introduce Rust-native containers like HashMap, HashSet, or BTreeMap; match the original RPython data structure shape instead.” <coding_guidelines>

Proposed refactor
-                let mut covered: std::collections::HashSet<usize> =
-                    stack_sync.iter().map(|&(idx, _)| idx).collect();
+                let mut covered: Vec<usize> = stack_sync.iter().map(|&(idx, _)| idx).collect();
                 let mut augmented = stack_sync;
                 for s in 0..depth {
                     let vidx = nvs + nlocals + s;
                     if covered.contains(&vidx) {
                         continue;
@@
                                 augmented.push((vidx, box_op));
-                                covered.insert(vidx);
+                                covered.push(vidx);
@@
                                     let boxed = crate::state::wrapint(ctx.trace_ctx, raw);
                                     augmented.push((vidx, boxed));
-                                    covered.insert(vidx);
+                                    covered.push(vidx);
📝 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.

Suggested change
let mut covered: std::collections::HashSet<usize> =
stack_sync.iter().map(|&(idx, _)| idx).collect();
let mut covered: Vec<usize> = stack_sync.iter().map(|&(idx, _)| idx).collect();
let mut augmented = stack_sync;
for s in 0..depth {
let vidx = nvs + nlocals + s;
if covered.contains(&vidx) {
continue;
}
🤖 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 10242 - 10243,
Replace the new HashSet side table in the stack synchronization logic with a
small Vec-based structure that matches the existing stack/list shape. In the
code around stack_sync and covered in jitcode_dispatch.rs, keep the bounded
operand-stack index tracking in a simple linear collection and update the
membership checks accordingly instead of using a Rust-native set. Ensure the
rest of the ported path continues to follow the original RPython/PyPy
data-structure style.

Source: Coding guidelines

@youknowone youknowone merged commit fab987c into main Jul 9, 2026
29 of 30 checks passed
@youknowone youknowone deleted the nbody branch July 9, 2026 03:07
youknowone added a commit that referenced this pull request Jul 9, 2026
…r/liveness channel) (#436)

* majit: route successful-compile trace teardown to abort_trace_live

Four trace-close success edges in JitDriver called abort_trace(false),
which runs both the live history teardown and aborted_tracing accounting.
On a successful compile the accounting must not fire: raise_if_successful()
raises ContinueRunningNormally (pyjitpl.py:3095-3123), bypassing the
`except SwitchToBlackhole` handler that reaches aborted_tracing, and the
loop/bridge counter was already bumped at backend-compile time. Calling
the accounting half here bumped stats.loops_aborted on success and
double-counted the re-observe edge.

Change the four success edges (CompileTrace arm, the bridge Compiled
close, the CloseLoopWithArgs bridge Compiled close, and the
take_compile_trace_success re-observe) to abort_trace_live(false) so only
the live teardown runs. loops_aborted now reads 0 on nbody/fannkuch/
spectral_norm/nested_loop; loops_compiled, bridges_compiled, and
guard_failures are unchanged.

Assisted-by: Claude

* synth: correct mutation-count comment in kept_stack_deep_var_shortcircuit_mutate

The loop appends three times per iteration (g, h, and one conditional
g/h), so len(log) for f(40000) is 120000, not the 80000 the "2 mutations
per iteration" comment implied. State the 3x invariant.

Assisted-by: Claude

* jit: deep-kept operand-stack resume via register/liveness channel; delete FOR_ITER hazard gate

Restore deep kept operand-stack slots at a branch-guard resume from the
guard-PC register file instead of the pop-cleared vable positional shadow,
porting get_list_of_active_boxes (rpython/jit/metainterp/pyjitpl.py:177-234),
which captures resume boxes from registers_r[index] via the per-PC -live- set.

In walker_capture_snapshot_for_last_guard_impl (jitcode_dispatch.rs), a kept
operand slot the walk mirror (ctx.vstack_boxes) leaves NULL across a not-taken
branch merge is recovered from ctx.registers_r[color], where color is the
pcdep_color_slots[guard_py_pc] inverse (new state::semantic_slot_color_for_ref_slot).
The recovery is capture-only (transient snapshot overlay, never the live shadow)
and fills only Type::Ref slots; an unboxed-int kept temp is left a hole.

Delete the foriter_deep_kept_call_hazard gate (codewriter.rs) that withheld the
FOR_ITER per-iteration getarrayitem_vable_r reload for loop bodies with a CALL
across an in-body conditional branch. The reload is now unconditional for
portals, matching w_iterator = self.peekvalue() (pypy/interpreter/pyopcode.py:1303).

kept_stack_deep_var_{condexpr,nested_call,shortcircuit} and the shortcircuit
mutate canary run resident without the deep-guard SIGSEGV; check.py 155/155 on
dynasm and cranelift.

Assisted-by: Claude

* jit: rustfmt the FOR_ITER iterator-reload block

Reformat the `iter_value` binding after the `foriter_deep_kept_call_hazard`
gate removal collapsed the condition to a single `if is_portal`. No logic
change; `cargo fmt --check` clean.

Assisted-by: Claude

* majit: clear pending_abort payload on successful trace teardown

The four successful-compile teardown edges (CompileTrace, both bridge-Compiled
paths, and the re-observed-success path) call abort_trace_live(false) for live
cleanup but fire no aborted_tracing, so the pending_abort_green_key/permanent
that abort_trace_live stages was never consumed. A later unrelated abort would
then take() that stale key and fire on_trace_abort for the earlier
successfully-compiled greenkey.

Add MetaInterp::clear_pending_abort() and call it on each success edge, matching
the existing bridge-FINISH success path (pyjitpl.rs) which already cleared these
fields inline for the same reason. No aborted_tracing follows on success
(raise_if_successful raises ContinueRunningNormally, pyjitpl.py:3095-3123).

check.py 155/155 dynasm and cranelift; aborted_tracing tests pass.

Assisted-by: Claude

* interp: type(x) one-arg __init__ skip + null-mro subtype base-chain fallback

type.__call__ suppresses __init__ when self is the `type` builtin with no
keyword arguments and exactly one positional argument (type(x) returns the
class of x from __new__). Add `type_call_type_x_shortcut` and gate the three
plain-instantiation __init__ dispatch sites in call.rs on it
(typeobject.py:735-736).

w_type_issubtype falls back to a find_best_base base-chain walk when mro_w is
null instead of returning false, matching _issubtype_slow_and_wrong for a
partially initialised type (typeobject.py:1640-1655, find_best_base
1335-1354; the "incomplete type" raise is omitted since the fn returns bool).

Assisted-by: Claude

* jit: deep-kept Int-bank recovery channel (PYRE_FBW_DEEPKEPT_INT, default OFF)

Port the i-bank half of get_list_of_active_boxes (pyjitpl.py:206-210,
add_box_to_storage(registers_i[index])) to the deep-kept operand-stack
recovery in walker_capture_snapshot_for_last_guard_impl. A bank-0 (Int) stack
pcdep entry names the Int-bank color owning the slot; registers_i[color] holds
the raw int, boxed into a W_IntObject via wrapint so the uniformly Ref-typed
vable array carries a Ref.

Generalize semantic_slot_color_for_ref_slot into a bank-generic
semantic_slot_color_for_slot core; add semantic_slot_color_for_int_slot
(bank=0) alongside the ref (bank=1) delegate.

Gated default-OFF: flag-off is byte-identical to leaving the int a hole
(resume re-materializes it from its defining IR). On the current frontend the
operand stack is uniformly Ref-banked so no bank-0 stack entry exists and the
block fires nowhere; the real int-hole (a Ref-bank color whose OpRef is
Int-typed) cannot be boxed at capture time — wrapint emits into a settled
trace and trips store_final_boxes_in_guard (resume.py:397) — so it must be
synthesized at operand-stack push time, a frontend change tracked separately.
This is the RPython-parity read channel for when bank-0 stack entries exist.

Assisted-by: Claude
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