Skip to content

jit: run a tracing abort through convert_and_run_from_pyjitpl; gate write barriers on the collector's descriptor - #895

Merged
youknowone merged 28 commits into
mainfrom
aheui
Jul 31, 2026
Merged

jit: run a tracing abort through convert_and_run_from_pyjitpl; gate write barriers on the collector's descriptor#895
youknowone merged 28 commits into
mainfrom
aheui

Conversation

@youknowone

@youknowone youknowone commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Two independent lines of work, both grounded in RPython/PyPy sources.

1. Run a tracing abort's framestack through convert_and_run_from_pyjitpl

pyjitpl.py:2949 run_blackhole_interp_to_cancel_tracing hands the aborted
metainterp.framestack to blackhole.py:1799 convert_and_run_from_pyjitpl,
which converts it into a blackhole chain and runs it until the bottommost
frame's jit_merge_point raises ContinueRunningNormally. Half-executed
opcodes therefore finish instead of being skipped. pyre had only a narrow
band-aid (abort_at_next_merge_point); this wires up the orthodox consumer the
in-code TODO named.

Because the walk's framestack is local to the dispatcher and the abort arm has
no native state, it is staged in two hops: TraceCtx::aborted_framestack
MetaInterp::pending_abort_blackholeJitDriver::run_pending_abort_blackhole,
called from the jit_merge_point! expansion — the first point holding state.

Three coverage bugs surfaced on the way, each also latent on the guard-failure
resume path:

  • call_result_reg assumed RPython's single-slot return tail, but
    inline_call_typed closes every BC_INLINE_CALL with three slots, so
    blackhole.py:1655's code[position-1] read the NO_RETURN_REG sentinel.
  • BC_NEW had a wired handler but no opname→byte entry.
  • The macro dispatch JitCode carried no jitdriver_sd, so
    blackhole.py:1764's walk ran past the root and never captured the terminal
    image. Fixed at the root per call.py:147-148, which sets both directions.

stb.raising_exception is now carried, with the value read from
MetaInterp::last_exc_value where blackhole.py:1811-1814 reads it.

The conversion previously declined for any state with a virtualizable, which
turned out to disable it almost everywhere: aheui is the only frontend without
a [.. ; virt] array
. Upstream never declines, and that decline is now gone.
The remaining array_lens decline (flattened fixed [int]) stays — no frontend
declares one and there is no sym-side per-element collector.

⚠️ The example suites are neutral evidence here — they never abort (MAJIT_LOG=1
prints zero abort-blackhole lines). Forcing aborts with MAJIT_STEP_LIMIT=200
on a virt-array frontend reproduces the torn-state defect
(index out of bounds: the len is 21 but the index is 21, stackpos underflowed)
with the conversion off, and passes with it on. Pinned as
majit-metainterp/tests/abort_blackhole_virt_array.rs, its own test binary so it
can set the env knob, and verified to fail under MAJIT_ABORT_BLACKHOLE=0.

2. Gate write barriers on the collector's descriptor

rewrite.py:393 gates the entire write-barrier section on
self.gc_ll_descr.write_barrier_descr is not None, and
aarch64/assembler.py:975-980 gates the jitframe barrier on if gcrootmap and wbdescr. pyre had neither gate on aarch64: GcRewriterImpl.wb_descr was built
unconditionally from WriteBarrierDescr::for_current_gc() instead of asking the
collector, and reload_frame_if_necessary re-applied the barrier without
checking. The x86 path already had the second gate.

This was latent until #796 replaced a silent None => return in
emit_write_barrier_fastpath_for_base with an assertion — correct in itself, a
barrier assembling to zero bytes is invisible until it corrupts memory. After
that, a collector reporting no descriptor (the gc.py:156 GcLLDescr_boehm case)
took the assertion on every compile, so every trace was dropped and the frontend
fell back to the interpreter. Bisected: 314019bf78^ 0 panics vs 314019bf78
8326.

Note the assertion text names COND_CALL_GC_WB, but the reaching caller is
reload_frame_if_necessarygenop_call_malloc_nursery_headerless — the
allocator, with no barrier op in the trace at all.

MiniMarkGC::get_write_barrier_descr returns exactly what the backends were
constructing inline, card-marking adjustment included, so the production path is
unchanged.

Verification

  • pyre/check.py: dynasm 342/342, cranelift 342/342, wasm 339/339 — ALL PASSED
  • aheui corpus 55/55; logo 996310 bytes / exit 42 / md5 7fcdbfff0af449c4283c008e3ca317ce, jit output byte-identical to --no-jit
  • majit-metainterp 1427 passed, majit-gc 215 passed + 2 new gate tests
  • cargo check --workspace --exclude pyre and cargo fmt --check clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved JIT recovery after aborts, panics, and mid-operation resumes.
    • Corrected typed return handling, virtualizable state preservation, and integer arithmetic behavior.
    • Improved garbage-collection fallback behavior and write-barrier safety.
    • Improved liveness tracking and resumed compilation consistency.
  • Improvements

    • Made generated dispatcher output and serialized switch data deterministic.
    • Added clearer diagnostics for invalid switch configurations.
  • Tests

    • Expanded coverage for recovery, garbage collection, liveness, dispatcher graphs, and optimizer ordering.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 326c4a3d-4a3f-46a1-b77d-1b3817c4d610

📥 Commits

Reviewing files that changed from the base of the PR and between d923491 and 88c5a36.

📒 Files selected for processing (2)
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs

Walkthrough

The PR adds deferred abort blackhole recovery, installed-GC-box routing, optional write barriers, liveness cache reconstruction, deterministic translation output, runtime typing fixes, optimizer coverage, bigint arithmetic updates, and release CI validation.

Changes

Runtime barrier configuration

Layer / File(s) Summary
Installed GC-box routing and optional barriers
majit/majit-gc/*, majit/majit-backend-*/**
GC operations use boxed allocators only when a GC box is installed. Other paths use synchronized GC fallbacks. Optional descriptors control barrier emission.

Tracing and blackhole recovery

Layer / File(s) Summary
Deferred abort and blackhole recovery
majit/majit-metainterp/**, majit/majit-macros/src/jit_interp/*, majit/majit-metainterp/tests/*
Tracing preserves abort state and resumes through multi-frame blackhole conversion. Typed inline-call returns, virtualizable state, exception state, and abort tests are updated.

Liveness and translation outputs

Layer / File(s) Summary
Liveness decoding and cache reconstruction
majit/majit-translate/src/codewriter/liveness.rs, pyre/pyre-jit-trace/src/*, pyre/pyre-jit/src/jit/assembler.rs
Liveness records rebuild interning maps during assembler initialization, publication, and resume. Register-bank keys are canonicalized.
Deterministic and typed translation outputs
majit/majit-translate/src/codewriter/*, majit/majit-translate/src/front/mir.rs, majit/majit-translate/tests/*, .github/workflows/pyre-ci.yml
Switch serialization is key-sorted. Field-read typing follows the projected value type. Dispatcher checks accept expanded output while preserving arm grouping.

Optimizer and runtime validation

Layer / File(s) Summary
Optimizer, arithmetic, and runtime validation
majit/majit-metainterp/src/optimizeopt/*, pyre/pyre-object/src/longobject.rs, pyre/pyre-jit-trace/src/*, pyre/bench/*
Heap-store ordering tests, bigint result construction, overflow specialization, blackhole coverage, and benchmark sizing are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant Tracer
  participant TraceCtx
  participant JitDriver
  participant BlackholeInterpreter
  Tracer->>TraceCtx: preserve aborted framestack and captured state
  TraceCtx->>JitDriver: stage PendingAbortBlackhole
  JitDriver->>BlackholeInterpreter: convert preserved frames
  BlackholeInterpreter-->>JitDriver: return resume PC or completion sentinel
  JitDriver-->>Tracer: resume merge-point execution
Loading

Possibly related PRs

  • youknowone/pyre#447: Extends the same GC routing changes across the Cranelift, DynASM, wasm, and GC components.
  • youknowone/pyre#311: Shares JIT state-transfer changes used during blackhole and tracing recovery.
  • youknowone/pyre#602: Shares liveness-record decoding and state metadata updates.

Suggested reviewers: lifthrasiir

Poem

A rabbit checks the GC box bright,
Then guides each abort through blackhole night.
Barriers wait for descriptors true,
Liveness maps rebuild anew.
Stable graphs pass the CI lane—
Hop, hop, no lost state again!

🚥 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 clearly and concisely describes both primary changes: tracing abort conversion and write-barrier gating by the collector descriptor.
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.
✨ 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 aheui

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 30, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 88c5a36).
Updated: 2026-07-31T09:14:39.874Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/src/aarch64/assembler.rs
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/src/lib.rs
majit/majit-gc/src/lib.rs
majit/majit-gc/src/rewrite.rs
majit/majit-macros/src/jit_interp/codegen_state.rs
majit/majit-macros/src/jit_interp/mod.rs
majit/majit-metainterp/src/blackhole.rs
majit/majit-metainterp/src/jit_state.rs
majit/majit-metainterp/src/jitdriver.rs
majit/majit-metainterp/src/lib.rs
majit/majit-metainterp/src/optimizeopt/heap.rs
majit/majit-metainterp/src/optimizeopt/virtualize.rs
majit/majit-metainterp/src/pyjitpl.rs
majit/majit-metainterp/src/pyjitpl/dispatch.rs
majit/majit-metainterp/src/resume_box_reader.rs
majit/majit-metainterp/src/trace_ctx.rs
majit/majit-metainterp/tests/abort_blackhole_virt_array.rs
majit/majit-translate/src/codewriter/flatten.rs
majit/majit-translate/src/codewriter/jitcode.rs
majit/majit-translate/src/codewriter/liveness.rs
majit/majit-translate/src/front/mir.rs
majit/majit-translate/tests/test_make_jitcodes_produces_graph_keyed_output.rs
pyre/bench/synth/int_mul_ovf_bignum_promote.py
pyre/pyre-jit-trace/src/assembler.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_runtime.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit/src/jit/assembler.rs
pyre/pyre-object/src/longobject.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)

  • pyre/pyre-jit-trace/src/state.rs:496 ↔ rpython/jit/codewriter/assembler.py:236: encode_liveness deduplicates inputs, but this code writes the original slice lengths at lines 511–513. Duplicate register indices therefore produce a count larger than the encoded bitset and corrupt subsequent liveness-record decoding. Upstream keys and counts sets/frozensets, so cardinality always matches the bitset.

  • majit/majit-metainterp/src/jitdriver.rs:1946 ↔ rpython/jit/metainterp/pyjitpl.py:2949: abort-time blackhole conversion is declined for any fixed-array state layout. Upstream unconditionally converts and runs the complete frame stack; this fallback can still resume from the source-PC handoff after a mid-opcode abort.

4. Structural adaptations

  • majit/majit-metainterp/src/blackhole.rs:765 ↔ rpython/jit/metainterp/blackhole.py:1653: Rust’s unified BC_INLINE_CALL has three typed result slots, unlike upstream’s typed call opcode ending in one destination byte. The added result-kind slot selection is a necessary bytecode-format adaptation.

  • majit/majit-metainterp/src/jitdriver.rs:1890 ↔ rpython/jit/metainterp/pyjitpl.py:2949: Rust defers convert_and_run_from_pyjitpl until the macro expansion has both the driver and native state available; upstream performs it directly from MetaInterp.

  • majit/majit-gc/src/lib.rs:1389 ↔ rpython/memory/gctransform/framework.py:147: the process-global GC plus test-only per-thread allocator boxes are Rust ownership/free-threading scaffolding. Upstream owns one translation-time GCData.

  • majit/majit-translate/src/codewriter/liveness.rs:441 ↔ rpython/jit/codewriter/assembler.py:29: reconstructing liveness-position maps from serialized bytes is needed because Pyre splits build-time and runtime assembly; upstream retains one live Assembler and its map.

@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: 81e1f3e55a

ℹ️ 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 majit/majit-metainterp/src/jitdriver.rs Outdated
// time; snapshot it here because
// `abort_trace_live` below tears down the
// tracing state it belongs to.
last_exc_value: self.meta.last_exc_value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Capture the dispatcher's raised exception

When a NotInTrace residual call raises, dispatch.rs:5695-5707 deliberately leaves the exception in BH_LAST_EXC_VALUE and returns an abort with raising_exception=true; it does not copy that value into MetaInterp::last_exc_value. Staging self.meta.last_exc_value here therefore usually records zero, so the new blackhole conversion starts without the pending exception and continues as though the call succeeded, potentially swallowing the exception and executing the remaining opcode tail. Capture the dispatch machine/TLS exception value that triggered the abort instead.

AGENTS.md reference: AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

Comment on lines +2096 to +2098
if let Some(__bh_pc) = #driver.run_pending_abort_blackhole(
&mut #state, #env,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run abort conversion for default merge-point markers

This call is generated only when the marker explicitly supplies ; state; the overwhelmingly used zero-argument form (for example majit/examples/tlr/src/jit_interp.rs:76) selects the other expansion, which merely calls the merge wrapper and never consumes pending_abort_blackhole. Consequently a NotInTrace or virtualizable-escape abort in those portals still drops the captured frame stack and continues through the native body rather than completing the aborted frames, leaving the semantic mismatch this change is intended to fix. The default expansion also needs access to the configured state and must invoke the conversion.

AGENTS.md reference: AGENTS.md:L14-L18

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

Caution

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

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

145-165: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid re-decoding the full liveness buffer on every publish_state call.

publish_state is invoked after every single -live- encode from pyre_jit::Assembler::encode_liveness_info — including cache hits where all_liveness is byte-for-byte unchanged from the last call. Rebuilding all_liveness_positions via a full decode_liveness_records pass on every call makes total build-time cost grow roughly with the square of the number of -live- sites sharing one buffer, since each of N appends now triggers an O(current buffer size) redecode.

Skip the rebuild when the incoming bytes match what's already stored — a self-contained fix requiring no caller changes.

⚡ Proposed fix to skip redundant redecode on unchanged bytes
 pub fn publish_state(
     insns: &IndexMap<String, u8>,
     all_liveness: &[u8],
     all_liveness_length: usize,
     num_liveness_ops: usize,
 ) {
     ASSEMBLER_STATE.with(|r| {
         let mut asm = r.borrow_mut();
         asm.insns = insns.clone();
+        let liveness_unchanged = asm.all_liveness.as_slice() == all_liveness;
         asm.all_liveness.clear();
         asm.all_liveness.extend_from_slice(all_liveness);
         asm.all_liveness_length = all_liveness_length;
         asm.num_liveness_ops = num_liveness_ops;
-        // The dedup table describes the *previous* buffer, so rebuild it from
-        // the one just installed. Clearing instead would leave the mirror
-        // unable to see any record in the fresh bytes, and the next
-        // `intern_liveness` would append a duplicate of one already there.
-        asm.all_liveness_positions = liveness_positions_of(all_liveness);
+        // The dedup table describes the *previous* buffer, so rebuild it from
+        // the one just installed. Clearing instead would leave the mirror
+        // unable to see any record in the fresh bytes, and the next
+        // `intern_liveness` would append a duplicate of one already there.
+        // Skip the O(n) redecode when the bytes are unchanged (the common
+        // cache-hit case from `encode_liveness_info`).
+        if !liveness_unchanged {
+            asm.all_liveness_positions = liveness_positions_of(all_liveness);
+        }
     });
     crate::state::publish_liveness_info(all_liveness.to_vec());
 }
🤖 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/assembler.rs` around lines 145 - 165, Update
publish_state to compare the incoming all_liveness bytes with the buffer already
stored in assembler state before rebuilding all_liveness_positions. Only call
liveness_positions_of when the bytes differ; preserve the existing buffer, dedup
table, and metadata updates for unchanged inputs.
majit/majit-metainterp/src/blackhole.rs (1)

764-833: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add equivalent Ref/Float return-slot regression coverage.

inline_call_grouped always emits the generic BC_INLINE_CALL 3-slot return tail, including for inline_call_ir_r and inline_call_irf_f, but the current blackhole test only exercises the Int-result branch through setup_return_value_i_reads_inline_calls_int_return_slot. Add analogous blackhole/interp coverage for setup_return_value_r/setup_return_value_f to avoid relying on indirect coverage for the same Sentinel-as-register-index OOB path.

🤖 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-metainterp/src/blackhole.rs` around lines 764 - 833, Add
blackhole/interpreter regression tests alongside
setup_return_value_i_reads_inline_calls_int_return_slot for inline_call_ir_r and
inline_call_irf_f. Exercise setup_return_value_r and setup_return_value_f with
inline_call_grouped’s generic three-slot BC_INLINE_CALL tail, asserting each
result is written to its correct return register and does not treat
NO_RETURN_REG as a register index.
🤖 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 12239-12246: Update the CondCallGcWb/CondCallGcWbArray codegen arm
to require rw.wb_descr via an expect with the message "COND_CALL_GC_WB emitted
without a write barrier descriptor", then read all jit_wb_* fields from that
descriptor without unwrap_or(0) fallbacks. Preserve the existing field
extraction and drop(rw) flow once the invariant is enforced.

In `@majit/majit-metainterp/src/blackhole.rs`:
- Around line 8373-8387: Remove the redundant new_with_vtable/d>r insertion from
either this block or the later arraylen_gc/rd>i, cast_ptr_to_int/r>i, and
new_with_vtable/d>r loop in the same function, while retaining exactly one
mapping to BC_NEW_WITH_VTABLE.

In `@majit/majit-metainterp/src/jitdriver.rs`:
- Around line 2079-2087: Update the JitException::ExitFrameWithExceptionRef
branch to handle a None result from deliver_blackhole_exception explicitly
instead of propagating it with ?. Make the no-exception-surface case fail loudly
using the same diagnostic behavior as the ContinueRunningNormally arm, while
preserving the existing writeback and resume flow when a resume PC is returned.

---

Outside diff comments:
In `@majit/majit-metainterp/src/blackhole.rs`:
- Around line 764-833: Add blackhole/interpreter regression tests alongside
setup_return_value_i_reads_inline_calls_int_return_slot for inline_call_ir_r and
inline_call_irf_f. Exercise setup_return_value_r and setup_return_value_f with
inline_call_grouped’s generic three-slot BC_INLINE_CALL tail, asserting each
result is written to its correct return register and does not treat
NO_RETURN_REG as a register index.

In `@pyre/pyre-jit-trace/src/assembler.rs`:
- Around line 145-165: Update publish_state to compare the incoming all_liveness
bytes with the buffer already stored in assembler state before rebuilding
all_liveness_positions. Only call liveness_positions_of when the bytes differ;
preserve the existing buffer, dedup table, and metadata updates for unchanged
inputs.
🪄 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 Plus

Run ID: f0e04b97-7873-45c9-a6bf-a310762a36af

📥 Commits

Reviewing files that changed from the base of the PR and between d873f13 and 81e1f3e.

📒 Files selected for processing (25)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-macros/src/jit_interp/codegen_state.rs
  • majit/majit-macros/src/jit_interp/mod.rs
  • majit/majit-metainterp/src/blackhole.rs
  • majit/majit-metainterp/src/jit_state.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/pyjitpl/dispatch.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/tests/abort_blackhole_virt_array.rs
  • majit/majit-translate/src/codewriter/flatten.rs
  • majit/majit-translate/src/codewriter/jitcode.rs
  • majit/majit-translate/src/codewriter/liveness.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/tests/test_make_jitcodes_produces_graph_keyed_output.rs
  • pyre/pyre-jit-trace/src/assembler.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/jit/assembler.rs

Comment thread majit/majit-backend-cranelift/src/compiler.rs
Comment on lines +8373 to +8387
// Plain struct allocation (`blackhole.py:1301-1310 bhimpl_new` /
// `bhimpl_new_with_vtable`) — a `#[jit_interp]` frontend's declared
// `struct_allocs` literal lowers straight to it, so any resume that walks
// forward over one needs the byte dispatchable. The handlers were already
// wired in `wire_bhimpl_handlers`; only the opname→byte entries were
// missing, which turns into a `dispatch_step: unwired opcode` panic.
insns.insert("new/d>r".to_string(), majit_translate::insns::BC_NEW);
insns.insert(
"new_with_vtable/d>r".to_string(),
majit_translate::insns::BC_NEW_WITH_VTABLE,
);
insns.insert(
"new_array/id>r".to_string(),
majit_translate::insns::BC_NEW_ARRAY,
);

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

Duplicate insns.insert("new_with_vtable/d>r", ...).

This key is inserted here and again later in the same function (the arraylen_gc/rd>i / cast_ptr_to_int/r>i / new_with_vtable/d>r loop further down) with the identical byte value. Harmless (IndexMap overwrite, same value both times) but redundant — worth dropping one of the two.

♻️ Proposed dedup
     insns.insert("new/d>r".to_string(), majit_translate::insns::BC_NEW);
-    insns.insert(
-        "new_with_vtable/d>r".to_string(),
-        majit_translate::insns::BC_NEW_WITH_VTABLE,
-    );
     insns.insert(
         "new_array/id>r".to_string(),
         majit_translate::insns::BC_NEW_ARRAY,
     );
📝 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
// Plain struct allocation (`blackhole.py:1301-1310 bhimpl_new` /
// `bhimpl_new_with_vtable`) — a `#[jit_interp]` frontend's declared
// `struct_allocs` literal lowers straight to it, so any resume that walks
// forward over one needs the byte dispatchable. The handlers were already
// wired in `wire_bhimpl_handlers`; only the opname→byte entries were
// missing, which turns into a `dispatch_step: unwired opcode` panic.
insns.insert("new/d>r".to_string(), majit_translate::insns::BC_NEW);
insns.insert(
"new_with_vtable/d>r".to_string(),
majit_translate::insns::BC_NEW_WITH_VTABLE,
);
insns.insert(
"new_array/id>r".to_string(),
majit_translate::insns::BC_NEW_ARRAY,
);
// Plain struct allocation (`blackhole.py:1301-1310 bhimpl_new` /
// `bhimpl_new_with_vtable`) — a `#[jit_interp]` frontend's declared
// `struct_allocs` literal lowers straight to it, so any resume that walks
// forward over one needs the byte dispatchable. The handlers were already
// wired in `wire_bhimpl_handlers`; only the opname→byte entries were
// missing, which turns into a `dispatch_step: unwired opcode` panic.
insns.insert("new/d>r".to_string(), majit_translate::insns::BC_NEW);
insns.insert(
"new_array/id>r".to_string(),
majit_translate::insns::BC_NEW_ARRAY,
);
🤖 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-metainterp/src/blackhole.rs` around lines 8373 - 8387, Remove the
redundant new_with_vtable/d>r insertion from either this block or the later
arraylen_gc/rd>i, cast_ptr_to_int/r>i, and new_with_vtable/d>r loop in the same
function, while retaining exactly one mapping to BC_NEW_WITH_VTABLE.

Comment thread majit/majit-metainterp/src/jitdriver.rs

@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: 3dcc792359

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


let mut bh_builder = BackEdgeBhBuilder::lease();
let staticdata = &self.meta.staticdata;
let outcome = drive_multi_frame_blackhole(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Supply a portal runner for recursive abort frames

When a tracing abort occurs with an inlined recursive portal on the captured framestack, that frame can reach its jit_merge_point while it still has a caller. handle_jitexception then routes ContinueRunningNormally through handle_jitexception_in_portal, but this conversion path ultimately calls run_forever_with_portal with portal_runner=None, so handle_jitexception_dispatch panics at expect("ContinueRunningNormally requires portal_runner") instead of re-entering the recursive portal. Thread the driver's portal runner through the new abort conversion; this is also what the repository's TL recursive_portal_call! path will require when the default merge-point consumer is wired.

AGENTS.md reference: AGENTS.md:L194-L196

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

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

}
}
}
if code[pos - 1] == crate::jitcode::NO_RETURN_REG && pos >= 3 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle inline-call tails before the VSD early return

When an int/ref BC_INLINE_CALL is followed by the five-byte valuestackdepth sync, the branch above returns code[pos - 6] before this new three-slot detection runs. That byte is the inline call's return_f slot, which is NO_RETURN_REG for int/ref returns, so _setup_return_value_* indexes register 255 and panics during blackhole resume. Apply the return-kind slot adjustment to the position before the VSD branch returns; the new test only covers an inline call with no sync.

Useful? React with 👍 / 👎.

Comment on lines +2033 to +2037
state.restore_banked3(
&meta,
&terminal.registers_i[int_base..],
&terminal.registers_r[ref_base..],
&terminal.registers_f[float_base..],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the vable identity offset when restoring refs

For a #[jit_interp] state combining a virtualizable array with ref(T) scalar fields, slicing at ref_scalar_base removes the virtualizable identity before calling the generated restore_banked3. That implementation indexes ref scalar j at num_vable_identity_slots + j (codegen_state.rs:1200-1207), so with one ref scalar it reads past the slice and panics, while multiple fields are shifted. The existing guard-failure CRN path explicitly uses per-field writeback when num_vable_identity_slots != 0; this abort writeback needs the same treatment or must retain the identity prefix.

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

ℹ️ 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 +1960 to +1961
if let Some(values) = virt_array_values {
state.writeback_virt_array_state_fields_from_values(&values);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the recursive callee's virtualizable state

When tracing aborts inside an inlined recursive portal with a [.. ; virt] field, virt_array_values belongs to the callee's fresh state, whose identity is explicitly distinct from the caller (majit/examples/tl/src/jit_interp.rs:519-550). This writes those elements into the outer native state, and the subsequent drive_multi_frame_blackhole also supplies one virtualizable_ptr with per_frame=None, so every reconstructed caller/callee frame accesses the same virtualizable. Once the default merge-point consumer and portal runner are wired, completing the callee can therefore overwrite the caller's stack and later caller-tail operations can continue against the callee's storage; retain the fresh owner and thread each frame's own virtualizable identity/state through the abort conversion instead.

AGENTS.md reference: AGENTS.md:L32-L42

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

ℹ️ 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 +1946 to +1953
if !layout.array_lens.is_empty() {
if crate::majit_log_enabled() {
eprintln!(
"[bh] abort-blackhole declined: unseeded state shape (arrays={})",
layout.array_lens.len(),
);
}
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Seed fixed-array slots before running the abort chain

For any #[jit_interp] state containing a fixed [int] field, this discards the pending conversion and falls back to the source-pc handoff, which is unsafe when tracing aborts midway through an opcode: already-applied effects can be replayed while the unfinished tail is skipped. This is a supported and reachable state shape—the repository declares cells: [int] in jit_interp_float_state_field.rs:407-415—so the claim that no frontend uses it is incorrect; capture and seed the flattened array elements rather than declining the conversion.

AGENTS.md reference: AGENTS.md:L14-L18

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

ℹ️ 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 majit/majit-gc/src/lib.rs
/// null pointer and fall back to their non-GC path).
pub fn alloc_nursery_typed(type_id: u32, payload_size: usize) -> GcRef {
match ACTIVE_ALLOC_NURSERY_TYPED.get() {
Some(_) if !gc_box_installed() => standalone_alloc_nursery_typed(type_id, payload_size),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor registered allocator hooks before using the singleton

When an embedding registers set_active_alloc_nursery_typed(Some(f)) directly without separately calling note_gc_box_installed()—a requirement absent from the setter's contract—this arm ignores f and allocates from gc_sync instead. The object consequently belongs to the process singleton rather than the embedding's active collector, so it may not be traced or reclaimed by the intended GC; the analogous rooted allocator branch has the same defect. Preserve the registered allocator's ownership or explicitly record which hooks are singleton-forwarding hooks.

AGENTS.md reference: AGENTS.md:L148-L155

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

Caution

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

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

145-165: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Full liveness-map rebuild runs on every -live- emission, including cache hits.

publish_state now rebuilds all_liveness_positions by decoding the entire all_liveness buffer on every call. The writer side calls publish_state unconditionally at the end of encode_liveness_info (pyre/pyre-jit/src/jit/assembler.rs), including on a cache hit where the buffer did not change. A -live- marker is emitted at nearly every branch point across every assembled jitcode, so this full re-decode (bounded by a 64 KiB buffer) can run many thousands of times during one build, each time redoing work for bytes that were already decoded on the previous call.

Limit the rebuild to when the buffer actually grew (skip the publish_state call, or at least the liveness_positions_of rebuild, on a cache hit in encode_liveness_info).

⚡ Proposed fix sketch
-        pyre_jit_trace::assembler::publish_state(
-            &self.insns,
-            &self.all_liveness,
-            self.all_liveness_length,
-            self.num_liveness_ops,
-        );
-        pos
+        if !matches!(&self.all_liveness_positions.get(&key), Some(&p) if p == pos) || /* cache miss */ true {
+            // Only publish when the buffer actually grew (cache miss branch above),
+            // so publish_state's rebuild does not run on every cache-hit call.
+        }
+        pos

(Adjust so the publish_state call sits inside the miss branch of encode_liveness_info in pyre/pyre-jit/src/jit/assembler.rs, rather than after the shared if/else.)

🤖 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/assembler.rs` around lines 145 - 165, Move the
publish_state call in encode_liveness_info into the cache-miss branch so it runs
only when new liveness bytes are appended; leave cache-hit handling unchanged
and avoid rebuilding all_liveness_positions for unchanged buffers.
majit/majit-backend-dynasm/src/aarch64/assembler.rs (1)

4284-4303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: the PYRE_TRACE_CALL_DIAG diagnostic block is duplicated.

generate_quick_failure and genop_call_with_arglocs both repeat the same sequence: read PYRE_TRACE_CALL_DIAG, compare to self.trace_id, push volatile registers, load the debug-call args, blr, and pop volatile registers. Extract this into one helper (for example emit_trace_call_diag(&mut self, tag: i64)) to remove the duplication.

This is debug-only scaffolding, so the practical payoff is limited, but it is a straightforward extraction.

Also applies to: 5918-5933

🤖 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-dynasm/src/aarch64/assembler.rs` around lines 4284 -
4303, Extract the duplicated PYRE_TRACE_CALL_DIAG handling from
generate_quick_failure and genop_call_with_arglocs into a shared helper such as
emit_trace_call_diag(&mut self, tag: i64). Keep the helper responsible for
environment matching, volatile-register preservation, loading the diagnostic
arguments, invoking dynasm_debug_validate_oldgen_freeblocks, and restoring
registers; pass each caller’s existing diagnostic tag and replace both inline
blocks with the helper call.
🤖 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-metainterp/src/jitdriver.rs`:
- Around line 1955-1972: Move the empty-framestack check around
framestack.frames before the virt-array writeback block in the relevant
deoptimization flow. Return None immediately when no root frame exists, then
perform writeback and clear self.meta.single_pass_virt_array_values only after a
root frame is confirmed; preserve the existing vinfo and chain-processing logic
for non-empty framestacks.
- Around line 2039-2054: Update the abort-terminal write-back closure around
restore_banked3 to branch on layout.num_vable_identity_slots, matching the
guard-failure CRN path. Only use dense restore_banked3 for layouts with zero
virtualizable identity slots; preserve the native state fields for
virtualizable-array layouts while retaining the existing terminal and recovery
behavior.

---

Outside diff comments:
In `@majit/majit-backend-dynasm/src/aarch64/assembler.rs`:
- Around line 4284-4303: Extract the duplicated PYRE_TRACE_CALL_DIAG handling
from generate_quick_failure and genop_call_with_arglocs into a shared helper
such as emit_trace_call_diag(&mut self, tag: i64). Keep the helper responsible
for environment matching, volatile-register preservation, loading the diagnostic
arguments, invoking dynasm_debug_validate_oldgen_freeblocks, and restoring
registers; pass each caller’s existing diagnostic tag and replace both inline
blocks with the helper call.

In `@pyre/pyre-jit-trace/src/assembler.rs`:
- Around line 145-165: Move the publish_state call in encode_liveness_info into
the cache-miss branch so it runs only when new liveness bytes are appended;
leave cache-hit handling unchanged and avoid rebuilding all_liveness_positions
for unchanged buffers.
🪄 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 Plus

Run ID: 40b2ccfc-7509-4d27-afd8-7b31a64ab157

📥 Commits

Reviewing files that changed from the base of the PR and between 81e1f3e and dcbe9dc.

📒 Files selected for processing (31)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-macros/src/jit_interp/codegen_state.rs
  • majit/majit-macros/src/jit_interp/mod.rs
  • majit/majit-metainterp/src/blackhole.rs
  • majit/majit-metainterp/src/jit_state.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/pyjitpl/dispatch.rs
  • majit/majit-metainterp/src/resume_box_reader.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/tests/abort_blackhole_virt_array.rs
  • majit/majit-translate/src/codewriter/flatten.rs
  • majit/majit-translate/src/codewriter/jitcode.rs
  • majit/majit-translate/src/codewriter/liveness.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/tests/test_make_jitcodes_produces_graph_keyed_output.rs
  • pyre/bench/synth/int_mul_ovf_bignum_promote.py
  • pyre/pyre-jit-trace/src/assembler.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_runtime.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/jit/assembler.rs
  • pyre/pyre-object/src/longobject.rs

Comment on lines +1955 to +1972
// `writeback_virt_array_state_fields_from_values`, run here rather than
// by the `jit_merge_point!` hook's own virt-array writeback: the chain
// is about to re-execute the tail of the aborted opcodes against the
// live struct, so it has to see the walk's elements, not the
// trace-start ones.
if let Some(values) = virt_array_values {
state.writeback_virt_array_state_fields_from_values(&values);
self.meta.single_pass_virt_array_values = None;
}
// `jitdriver.rs seed_deopt_vinfo_ptr`: a state-field machine's
// `bh_clear_vable_token` is inert, so a non-null vinfo is safe and lets
// mid-body vable-array opcodes resolve; a real heap virtualizable
// (`token_offset > 0`, e.g. PyFrame) keeps the null-vinfo resume
// contract and reaches the chain with `virtualizable_info` unset.
let vinfo_ptr = seed_deopt_vinfo_ptr(self.meta.virtualizable_info());
let Some(root) = framestack.frames.first_mut() else {
return None;
};

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

Move the empty-framestack decline before the virt-array write-back.

Lines 1960-1963 mutate native state and clear self.meta.single_pass_virt_array_values. Line 1970 can then return None, which the doc comment at Line 1909 defines as "declined before the chain ran". The array_lens decline at Line 1946 returns before any mutation, so the two decline paths are not equivalent.

Check framestack.frames first, then apply the write-back.

🩹 Proposed fix
+        if framestack.frames.is_empty() {
+            if crate::majit_log_enabled() {
+                eprintln!("[bh] abort-blackhole declined: empty framestack");
+            }
+            return None;
+        }
         // `writeback_virt_array_state_fields_from_values`, run here rather than
         // by the `jit_merge_point!` hook's own virt-array writeback: the chain
         // is about to re-execute the tail of the aborted opcodes against the
         // live struct, so it has to see the walk's elements, not the
         // trace-start ones.
         if let Some(values) = virt_array_values {
             state.writeback_virt_array_state_fields_from_values(&values);
             self.meta.single_pass_virt_array_values = None;
         }
@@
         let vinfo_ptr = seed_deopt_vinfo_ptr(self.meta.virtualizable_info());
-        let Some(root) = framestack.frames.first_mut() else {
-            return None;
-        };
+        let root = &mut framestack.frames[0];
📝 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
// `writeback_virt_array_state_fields_from_values`, run here rather than
// by the `jit_merge_point!` hook's own virt-array writeback: the chain
// is about to re-execute the tail of the aborted opcodes against the
// live struct, so it has to see the walk's elements, not the
// trace-start ones.
if let Some(values) = virt_array_values {
state.writeback_virt_array_state_fields_from_values(&values);
self.meta.single_pass_virt_array_values = None;
}
// `jitdriver.rs seed_deopt_vinfo_ptr`: a state-field machine's
// `bh_clear_vable_token` is inert, so a non-null vinfo is safe and lets
// mid-body vable-array opcodes resolve; a real heap virtualizable
// (`token_offset > 0`, e.g. PyFrame) keeps the null-vinfo resume
// contract and reaches the chain with `virtualizable_info` unset.
let vinfo_ptr = seed_deopt_vinfo_ptr(self.meta.virtualizable_info());
let Some(root) = framestack.frames.first_mut() else {
return None;
};
if framestack.frames.is_empty() {
if crate::majit_log_enabled() {
eprintln!("[bh] abort-blackhole declined: empty framestack");
}
return None;
}
// `writeback_virt_array_state_fields_from_values`, run here rather than
// by the `jit_merge_point!` hook's own virt-array writeback: the chain
// is about to re-execute the tail of the aborted opcodes against the
// live struct, so it has to see the walk's elements, not the
// trace-start ones.
if let Some(values) = virt_array_values {
state.writeback_virt_array_state_fields_from_values(&values);
self.meta.single_pass_virt_array_values = None;
}
// `jitdriver.rs seed_deopt_vinfo_ptr`: a state-field machine's
// `bh_clear_vable_token` is inert, so a non-null vinfo is safe and lets
// mid-body vable-array opcodes resolve; a real heap virtualizable
// (`token_offset > 0`, e.g. PyFrame) keeps the null-vinfo resume
// contract and reaches the chain with `virtualizable_info` unset.
let vinfo_ptr = seed_deopt_vinfo_ptr(self.meta.virtualizable_info());
let root = &mut framestack.frames[0];
🤖 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-metainterp/src/jitdriver.rs` around lines 1955 - 1972, Move the
empty-framestack check around framestack.frames before the virt-array writeback
block in the relevant deoptimization flow. Return None immediately when no root
frame exists, then perform writeback and clear
self.meta.single_pass_virt_array_values only after a root frame is confirmed;
preserve the existing vinfo and chain-processing logic for non-empty
framestacks.

Comment on lines +2039 to +2054
let writeback = |state: &mut S, resume_pc: usize| {
let Some(terminal) = terminal.as_ref() else {
return;
};
let int_base = layout.int_scalar_base.min(terminal.registers_i.len());
let ref_base = layout.ref_scalar_base.min(terminal.registers_r.len());
let float_base = layout.float_scalar_base.min(terminal.registers_f.len());
let meta = S::build_meta(state, resume_pc, env);
state.restore_banked3(
&meta,
&terminal.registers_i[int_base..],
&terminal.registers_r[ref_base..],
&terminal.registers_f[float_base..],
);
state.recover_after_compiled_run();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the helper signature and the sibling branch it is used in.
rg -n -B5 -A30 'fn writeback_live_state_scalars_from_blackhole' --type=rs
rg -n -C6 'num_vable_identity_slots\s*==\s*0' --type=rs

Repository: youknowone/pyre

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files '*.rs' | head -80

echo "== helper signature =="
rg -n -B5 -A40 'fn writeback_live_state_scalars_from_blackhole' '*.rs' || true

echo "== sibling branch occurrences =="
rg -n -C8 'num_vable_identity_slots\s*==\s*0' '*.rs' || true

echo "== target function context =="
rg -n -B20 -A120 'fn run_pending_abort_blackhole|writeback\(|drive_multi_frame_blackhole' majit/majit-metainterp/src/jitdriver.rs || true

echo "== jit writerback doc/context =="
rg -n -B6 -A25 'writeback_live_scalar_state_field|restore_banked3|liveness' majit/majit-* src 2>/dev/null | head -240 || true

Repository: youknowone/pyre

Length of output: 3357


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file exists =="
ls -l majit/majit-metainterp/src/jitdriver.rs 2>/dev/null || true
ls -l majit/majit-*/*.rs majit/majit-*/src/*.rs 2>/dev/null | wc -l

echo "== helper signature =="
rg -n -B5 -A40 'fn writeback_live_state_scalars_from_blackhole' . --glob '*.rs' || true

echo "== sibling branch occurrences =="
rg -n -C8 'num_vable_identity_slots\s*==\s*0' . --glob '*.rs' || true

echo "== target function context =="
rg -n -B20 -A140 'fn run_pending_abort_blackhole|writeback\(|drive_multi_frame_blackhole|restore_banked3' majit/majit-metainterp/src/jitdriver.rs || true

echo "== state writeback doc/liveness snippets =="
rg -n -B8 -A35 'writeback_live_scalar_state_field|restore_banked3|liveness_info|single_pass_virt_array_values|num_vable_identity_slots' . --glob '*.rs' | head -280 || true

Repository: youknowone/pyre

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== helper signature =="
rg -n -B5 -A50 'fn writeback_live_state_scalars_from_blackhole' . --glob '*.rs' || true

echo "== sibling branch occurrences =="
rg -n -C8 'num_vable_identity_slots\s*==\s*0' . --glob '*.rs' || true

echo "== target writes around function =="
rg -n -C8 'run_pending_abort_blackhole|drive_multi_frame_blackhole|single_pass_virt_array_values|virtualizable_ptr|seed_deopt_vinfo_ptr|frames.first_mut|staged_all_liveness|all_liveness' majit/majit-metainterp/src/jitdriver.rs || true

echo "== restore/writeback definitions =="
rg -n -B10 -A70 'fn writeback_live_scalar_state_field|fn restore_banked3|fn writeback_live_state_scalars_from_blackhole' . --glob '*.rs' || true

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact run_pending_abort_blackhole block =="
sed -n '1934,2108p' majit/majit-metainterp/src/jitdriver.rs

echo "== guard failure terminal block =="
sed -n '4248,4280p' majit/majit-metainterp/src/jitdriver.rs

echo "== writeback_live_scalar_state_field definitions =="
rg -n -B8 -A85 'fn writeback_live_scalar_state_field|fn writeback_live_ref_scalar_state_field|fn writeback_live_float_scalar_state_field' majit/majit-* --glob '*.rs' || true

Repository: youknowone/pyre

Length of output: 32197


Gate abort-terminal write-back on layout for virtualizable arrays.

run_pending_abort_blackhole supports the [.. ; virt] state shape and leaves populated native state fields when terminal.is_some() removes the staged single-pass values. The terminal write-back then needs the same num_vable_identity_slots == 0 branch as guard-failure CRN: dense restore_banked3 does not apply to virtualizable-array layouts, so this path can clobber native fields with terminal-register values that are not native state fields.

🤖 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-metainterp/src/jitdriver.rs` around lines 2039 - 2054, Update the
abort-terminal write-back closure around restore_banked3 to branch on
layout.num_vable_identity_slots, matching the guard-failure CRN path. Only use
dense restore_banked3 for layouts with zero virtualizable identity slots;
preserve the native state fields for virtualizable-array layouts while retaining
the existing terminal and recovery behavior.

…projected place

`resolve_place`'s positional-aggregate branch hardcoded `ty: Ref(None)` on
the `FieldRead __pos_<N>` it emits, while the genuine-Ref tuple branch
directly below derives the same read's type from `place_ty`. A
`match (FieldlessEnum, bool)` scrutinee is built as a positional aggregate
and its `.0` read back to feed the switch, so the read produced a Ref-typed
switch value and `flatten.py:280 assert kind == 'int'` rejected the graph
(`pyre_object::dictmultiobject::dict_view_iterator_type_for_kind`).

Derive the type from `place_ty` in both branches, and name the graph and
block in the flatten assert.

Assisted-by: Claude
The `slow_generated_jitcodes_preserve_complete_dispatcher_graph` anchors
asserted 111 arms, 118 discriminants and 3 grouped arms; the dispatcher now
has 115, 119 and 2. The test self-ignores under `debug_assertions`, so the
`cargo test --all` job never ran it and the drift went unnoticed.

Make the two totals floors, since an added opcode raises them and only a
removal lowers them. Drop the grouped-arm count: the sorted
`mir_groups == source_groups` comparison two lines above already pins arm
membership and which variants share an arm, so it restated that equality
while failing whenever an Or-pattern was split.

Run the test from the Linux pyre/check.py job, which has the downloaded
build/llbc/*.ullbc and a warm release profile.

Assisted-by: Claude
`assembler.py:29-31` gives one Assembler one `all_liveness` buffer and one
`all_liveness_positions` dict. pyre hands the buffer over in three places and
carried none of the dict with it:

- `Assembler::resuming_build_time_liveness` seeded the build-time bytes with
  an empty dict, so the first runtime `_encode_liveness` of a triple already
  in the prefix appended a second copy.
- `AssemblerState::new` seeded the same bytes the same way.
- `publish_state` cleared the dict on every mirror publish.

The `-live-` operand is 2 bytes, so each duplicate spends part of one shared
64 KiB budget.

Add `decode_liveness_records`, which walks the buffer back into the records
`_encode_liveness` appended, and build the dict from it at all three points.
Also canonicalise `intern_liveness`'s key: `encode_liveness` sorts and dedups
before writing, so two orderings of one set encode identically while the raw
slice key made them distinct entries.

Assisted-by: Claude
…afe today

The default forwards a no-collect request to the collecting form, which is
only correct for a non-moving allocator. State which implementors reach it:
MiniMarkGC moves but is headered, so `alloc_nursery_headerless`'s own default
panics first, and the headerless boxes the backends install grow their pool.
A moving headerless box would have to override it.

Assisted-by: Claude
The map is a lookup table whose in-memory order never matters, but it is
written to build artefacts (aheui-jit's opcode_descrs.bin, pyre-jit-trace's
jit_metadata.json) and a HashMap serializes in iteration order. Five
consecutive builds of one unchanged aheui tree produced five different
opcode_descrs.bin, differing only in the 8-byte key/value pairs of this map;
that defeats the codegen cache and makes an A/B bisection impossible to
attribute to a source change.

Emit the key order const_keys_in_order already carries (jitcode.py:135
`sorted(as_dict.keys())`). The serialized shape is unchanged — still a map —
so existing artefacts stay readable. Verified: two rebuilds of aheui now give
byte-identical opcode_descrs.bin, opcode_jitcodes.bin and jit_metadata.json.

Assisted-by: Claude
`run_to_end` checked `is_too_long()` after every jitcode step and returned
`TraceAction::Abort` there, in the middle of a source opcode. The jitdriver's
Abort arm hands a source-level pc back through `single_pass_outcome`, taken
from the walk's root frame i0, which dispatch advances before running the
opcode arm — so the interpreter resumed past an opcode whose side effects had
only partly run. aheui's pi.jinseo lost the `size += 1` half of a linked-list
push whose `head = node` had already run, leaving a chain one node longer than
its size, and then spun in the compiled loop that reads the emptied storage.

Add `TraceCtx::abort_at_next_merge_point`: the overflow sets the flag and the
walk keeps stepping until the `BC_JIT_MERGE_POINT` arm consumes it and returns
Abort. A `Finish` that ends the walk with the flag still set is downgraded to
Abort so a trace past the limit is not committed.

RPython answers the same overflow with `SwitchToBlackhole(ABORT_TOO_LONG)`,
whose handler (pyjitpl.py:2949 `run_blackhole_interp_to_cancel_tracing` ->
blackhole.py:1799 `convert_and_run_from_pyjitpl`) finishes the current jitcode
frame in the blackhole; pyre has no such consumer yet.

aheui snippet corpus 55/55 jit==naive (was 54/55); logo 996310 bytes, exit 42,
md5 7fcdbfff0af449c4283c008e3ca317ce.

Assisted-by: Claude
`test_setfield_on_distinct_array_loads_forces_prior_lazy_set`: two structs
loaded from one array at distinct constant indices, then stored through the
same field descr. heap.py:81-83 `possible_aliasing` must force the first
store before the second takes the descr's single `_lazy_set` slot.

`test_guard_forces_size_store_paired_with_virtual_head_store`: a linked-list
push leaves two lazy sets pending at a guard, and heap.py:617-621 splits them
by whether the stored value is virtual — the `.head` store keeps its lazy set
and goes to pendingfields, the `.size` store is forced and emitted before the
guard.

The second test runs the full `__init__.py:15-22` pipeline so OptVirtualize is
present and the `NEW` result is still virtual at the heap pass; it needs a
SizeDescr with a non-empty `all_fielddescrs()`, which the existing
`TestSizeDescr` does not provide, so add `TestVirtualSizeDescr` /
`virtual_size_descr` alongside it.

Assisted-by: Claude
…pyjitpl

Wire the consumer `pyjitpl.py:2949 run_blackhole_interp_to_cancel_tracing`
calls: `blackhole.py:1799 convert_and_run_from_pyjitpl`.  The abort arm of
`merge_point` previously published only the walk's root-frame source pc, which
names a resume position only when the walk stopped at a source-opcode boundary.

The walk's framestack is local to `trace_jitcode_with_args_and_runtime`, and
the abort arm holds the sym but not native `state`, so the conversion is staged
in two hops:

- `TraceCtx` gains `aborted_framestack`, `abort_resume_jitcode_pc` and
  `abort_after_panic`.  On `TraceAction::Abort` the walk sets the top frame's
  `pc` to the instruction boundary it stopped at and moves the framestack onto
  the ctx; a panic-sourced abort publishes nothing.
- The `merge_point` Abort arm moves it into `MetaInterp::pending_abort_blackhole`
  together with the sym's scalar and ref-scalar state-field image.
- `JitDriver::run_pending_abort_blackhole`, called from the `jit_merge_point!`
  expansion, seeds the root frame's identity registers from that image, runs
  `drive_multi_frame_blackhole`, and maps `ContinueRunningNormally` /
  `DoneWithThisFrame*` / `ExitFrameWithExceptionRef` to a resume pc plus a
  `restore_banked3` + `recover_after_compiled_run` writeback from the terminal
  frame's banks.

Gated by `MAJIT_ABORT_BLACKHOLE` (default on).  It declines, leaving the
source-pc handoff in place, when the state layout has virtualizable identity
slots or flattened fixed `[int]` arrays.

`JitState::collect_ref_scalar_state_field_values` is added as the ref-bank
sibling of `collect_scalar_state_field_values`, generated by `#[jit_interp]`.

Three blackhole fixes the conversion path exercised:

- `call_result_reg` now takes the return kind and, when the byte before the
  resume position is `NO_RETURN_REG`, steps back over `BC_INLINE_CALL`'s three
  return slots (`return_i`, `return_r`, `return_f`) instead of reading the
  sentinel as a register index.  Covered by a new test.
- `handle_jitexception` captures the terminal image on the "no portal found"
  path before releasing the interpreter; a `#[jit_interp]` dispatch JitCode
  carries no `jitdriver_sd`, so its chain always ends there.
- `build_inline_call_only_bh_builder` gains the `new/d>r`,
  `new_with_vtable/d>r` and `new_array/id>r` opname-to-byte entries; the
  handlers were already wired.

The `is_too_long` merge-point deferral is kept and its comment rewritten to
describe it as the fallback path's protection rather than the fix.

Assisted-by: Claude
…bort's exception

`call.py:147-148 grab_initial_jitcodes` sets both directions:

    jd.mainjitcode = self.get_jitcode(jd.portal_graph)
    jd.mainjitcode.jitdriver_sd = jd

`JitDriver::register_dispatch_jitcode` set only the first.  The slot exists on
the metainterp-side `JitCode` (it is `majit_translate`'s, and `codewriter.rs:1036`
fills it on the translation path); the comment claiming otherwise is corrected.
Without the back-pointer `blackhole.py:1764`'s walk runs off the bottom of a
`#[jit_interp]` frontend's chain instead of stopping at its root, so
`blackhole.py:1767-1769` never publishes the bottommost frame's terminal image.
`handle_jitexception`'s no-portal arm is reverted to upstream's shape now that
the walk terminates there.

`run_pending_abort_blackhole` passed `last_exc_value = 0` and
`raising_exception = false`.  The flag was already on the drained
`SwitchToBlackhole` (`history.py:37-43`) and the Abort arm discarded it by
mapping to `stb.reason`; it now keeps the whole `stb`.  The value comes from
`MetaInterp::last_exc_value`, which is what `blackhole.py:1811-1814` reads,
snapshotted at the abort because `abort_trace_live` follows.

Adds `handle_jitexception_publishes_terminal_only_when_a_frame_names_its_jitdriver`,
which asserts the image is published exactly when a frame names its jitdriver.

Assisted-by: Claude
…tates

`run_pending_abort_blackhole` declined whenever the state declared a
virtualizable (`num_vable_identity_slots != 0`).  Upstream
`blackhole.py:1799 convert_and_run_from_pyjitpl` has no such condition, and the
decline covered nearly the whole tree: aheui is the only `#[jit_interp]`
frontend without a `[.. ; virt]` array, so the conversion was inactive for
braininterp, dualtape, i64env, spcount, tiny2, tiny3, tinyframe, tl, tla, tlc,
tlr and the cel machines.

The abort arm now stages, alongside the framestack, the walk's virtualizable
element values (`TraceCtx::collect_virtualizable_element_values`) and the
trace's virtualizable identity (`TraceCtx::virtualizable_heap_ptr`).  Before
the chain runs, `run_pending_abort_blackhole`:

- pushes those elements into native `state` via
  `writeback_virt_array_state_fields_from_values` — the walk mutates the
  trace-ctx shadow, not the live struct, and the chain's
  `getarrayitem_vable_*` read the real object;
- seeds the identity into the slot `StateFieldLayout::vable_identity_slot`
  (new) names, and passes it to `drive_multi_frame_blackhole` with the vinfo
  pointer `seed_deopt_vinfo_ptr` allows — null for a `token_offset > 0` heap
  virtualizable, which keeps its existing resume contract.

The remaining decline is a flattened fixed `[int]` array (`array_lens`), which
has no sym-side per-element collector.

`ContinueRunningNormally` with no green pc now asserts and logs instead of
silently returning `None`: the chain has already run the aborted opcodes'
tails, so falling back to the walk's source pc would execute them twice.

Adds `tests/abort_blackhole_virt_array.rs`, a `stackpos: int, stack: [int; virt]`
stack machine driven with `MAJIT_STEP_LIMIT=200` so every trace aborts inside an
opcode arm.  It is its own test binary because that knob is a process-wide
`LazyLock`.  Under `MAJIT_ABORT_BLACKHOLE=0` the same run panics with
`index out of bounds: the len is 21 but the index is 21` from `PUSH` — one half
of `stack[stackpos] = v; stackpos += 1` committed and the other lost.

Assisted-by: Claude
…descriptor

`reload_frame_if_necessary` re-applied the non-array write barrier fast
path on the reloaded jitframe unconditionally. `aarch64/assembler.py:975-980
_reload_frame_if_necessary` reads the descriptor first and applies the
barrier only `if gcrootmap and wbdescr`; the x86 path already spells that
as `if let Some(wb) = wb_descr`.

A collector that needs no write barrier reports none (`gc.py:156
GcLLDescr_boehm.write_barrier_descr = None`), and
`emit_write_barrier_fastpath_for_base` asserts a descriptor is present.
The function is reached from every collecting helper call, including
`genop_call_malloc_nursery_headerless`, not only from a `COND_CALL_GC_WB`
the GC rewriter emitted, so such a collector took the assertion on each
compile and the trace was dropped.

Assisted-by: Claude
…o descriptor

`GcRewriterImpl.wb_descr` was a plain `WriteBarrierDescr` that both backends
built from `WriteBarrierDescr::for_current_gc()` rather than asking the
active collector, so the rewriter emitted `COND_CALL_GC_WB*` for a collector
that has no write barrier.

`rewrite.py:393` gates the whole barrier section on
`self.gc_ll_descr.write_barrier_descr is not None`, and `rewrite.py:405-412`
is its `else`: `consider_setfield_gc` / `consider_setarrayitem_gc` still run,
no barrier is emitted, and the store reaches the ordinary lowering.
`gen_write_barrier_array` (rewrite.py:956) reads the descriptor unguarded
because that gate has already passed.

Make the field `Option<WriteBarrierDescr>`, source it from
`gc.get_write_barrier_descr()` (`gc.py:401 self.write_barrier_descr =
WriteBarrierDescr(self)`, `None` at `gc.py:156` for `GcLLDescr_boehm`) in
both backends, and add the gate. `MiniMarkGC::get_write_barrier_descr`
returns the same value the backends were constructing inline, card-marking
adjustment included, so the production path is unchanged.

Assisted-by: Claude
The doc comment cited `gc.py:268`, which is
`self.jit_wb_if_flag = GCClass.JIT_WB_IF_FLAG` inside
`WriteBarrierDescr.__init__`, not the `gc_ll_descr` attribute this accessor
models. That attribute is assigned at `gc.py:401` and defaulted to `None` at
`gc.py:156` for `GcLLDescr_boehm`; upstream reads it directly and has no
`get_write_barrier_descr` accessor.

Assisted-by: Claude
`build_inline_call_only_bh_builder` now registers `new/d>r`,
`new_with_vtable/d>r` and `new_array/id>r`, so `new/d>r` and `new_array/id>r`
no longer appear in the overlay-only gap that
`production_bh_builder_overlay_only_gap_snapshot` pins. The snapshot still
listed them and failed.

Update the literal and record why they left, as the comment already does for
`vtable_method_ptr/rd>i`.

Assisted-by: Claude
… a frontend

The comment on the trace_limit deferral illustrated the partially-applied
opcode with a specific aheui program. State the shape instead: a push that
stores its element and then bumps the length can commit the store and lose
the bump.

Assisted-by: Claude
…as run

`run_pending_abort_blackhole` used `None` for two different things: the
conversion declined before running anything, and the chain ran but produced no
resume pc. The caller treats `None` as the first, falling back to the
source-pc handoff — a pc dispatch advanced before the aborted opcodes' arms.
Taking that fallback after the chain executed those arms against the real heap
runs them a second time, which is the double-application the conversion exists
to prevent.

Two paths returned `None` post-chain:

- `ContinueRunningNormally` carrying no green pc. Unreachable for a declared
  jitdriver, but it returned `None` under a `debug_assert`.
- `ExitFrameWithExceptionRef` when `JitState::deliver_blackhole_exception`
  answers `None`, which is its default and no frontend overrides it, so any
  exception escaping every converted frame took this path.

Both now end the dispatch loop through the existing `single_pass_finish` +
`usize::MAX` "no forward pc" outcome. `blackhole.py:1799
convert_and_run_from_pyjitpl` does not return to its caller at all — it
propagates to `warmspot.py:961 handle_jitexception` — so upstream has no
counterpart to either answer.

State the invariant on the function: `None` means declined before the chain
ran, and only that.

Assisted-by: Claude
…conversion

`abort_blackhole_enabled()` gated the Abort arm's staging of the aborted
framestack for `pyjitpl.py:2949 run_blackhole_interp_to_cancel_tracing`.
RPython has no switch there.  The gate defaulted to enabled, so removing
it leaves runtime behaviour unchanged for an unset environment.

Reword the two `jitdriver.rs` doc paragraphs and the `dispatch.rs`
deferral comment that listed the knob among the reasons the conversion
may not run, and restate the `abort_blackhole_virt_array` module doc's
counter-example in terms of short-circuiting the conversion rather than
the removed variable.

Assisted-by: Claude
`run_to_end` set `abort_at_next_merge_point` on overflow and kept
stepping until `BC_JIT_MERGE_POINT` cashed the flag in, so the abort
landed only at a position where no source opcode was half-executed.
`pyjitpl.py:2861-2867 _interpret` calls `blackhole_if_trace_too_long()`
directly after `run_one_step()` and raises `SwitchToBlackhole(
ABORT_TOO_LONG)` there (`pyjitpl.py:2831`); the mid-opcode landing is
what `pyjitpl.py:2949 run_blackhole_interp_to_cancel_tracing` handles.

Move the check below the terminal-action test, matching the upstream
order in which a step that raises its own exception never reaches
`blackhole_if_trace_too_long` — so a closing step commits its trace even
when that step pushed the recorded op count past `trace_limit`. This
drops the `too_long` override that made a pending overflow outrank a
`CloseLoop`/`Finish`, and the post-loop recheck the deferral needed.

Remove `TraceCtx::abort_at_next_merge_point` and, with the merge-point
arm that set it gone, `TraceCtx::abort_resume_jitcode_pc`. The aborting
walk's top frame now always publishes `code_cursor` as its `frame.pc`
for `blackhole.py:1804 copy_data_from_miframe`, which is where
`pyjitpl.py:3863 self.pc = position` leaves `MIFrame.pc` after the
handler decodes an instruction's operands.

Assisted-by: Claude
`replay_pending_fields` and the generated `setup_bridge_sym` gated their
tracing on `AHEUI_BRIDGE_DIAG`. Both print generic bridge-entry replay
state (pendingfields, rd_virtuals, failarg counts) and sit next to
`MAJIT_BRIDGE_DEBUG` in the same files. No `AHEUI_`-prefixed name
remains under `majit/`.

Assisted-by: Claude
`jit_bigint_add_int_int` / `sub_int_int` / `mul_int_int` computed the exact
result in i128 and handed it to `rbigint.fromlong`, and `jit_bigint_add` /
`jit_bigint_sub` carried an i128 fast path that did the same. `fromlong` goes
through `args_from_long`, which builds a `Vec<Digit>` before `RBigInt::new`
copies it into the GC digit block.

Call `RBigInt::add_int_int_bigint_result` / `sub_int_int_bigint_result` /
`mul_int_int_bigint_result` (rbigint.py:717/788/873) instead, and drop the
i128 arms from `jit_bigint_add` / `jit_bigint_sub` so they reach
`RBigInt::add` / `sub` (rbigint.py:681/755). The sign-zero early returns stay:
they spell the "return the other operand itself" identity of rbigint.py:684-687
and :758-759 in the raw-pointer ABI.

synth/int_mul_ovf_bignum_promote steady-state, startup-subtracted CPU time,
min of 5, dynasm: 69.9ns -> 37.0ns per iteration at 8M iterations
(pypy3 ratio 5.50x -> 3.05x).

Assisted-by: Claude
The overflowing Add/Sub/Mul arm emitted `CallI(jit_bigint_fits_int, payload)`
+ `GuardFalse` before boxing the W_LongObject. The guard is answered by the
`GuardOverflow` two ops earlier: the payload comes from
`add/sub/mul_int_int_bigint_result`, which is the exact int-pair result, so a
value that just overflowed a machine int cannot fit one back.

`newlong_from_rbigint` (objspace.py:316-320) reaches the same conclusion at
trace time through `rbigint.toint()`'s `numdigits() >
MAX_DIGITS_THAT_CAN_FIT_IN_INT` test (rbigint.py:470); PyPy's own expected
trace for this loop shape carries no such call
(pypy/module/pypyjit/test_pypy_c/test_misc.py:52-72 test_factorial).
`try_walker_specialize_binary_op_long_int_pow` already omits its result-fits
guard on the same grounds.

synth/int_mul_ovf_bignum_promote optimized trace: 52 -> 48 ops (steady loop
21 -> 19). Startup-subtracted CPU time, min of 9, dynasm, 8M/16M iterations:
32.7ns / 31.4ns per iteration (pypy3 ratio 2.88x / 2.75x).

Assisted-by: Claude
Every allocation, write barrier and GC ownership query opened with a
thread-local read plus a `RefCell` borrow of the backend's per-thread GC box.
That box is installed only by `set_gc_allocator` -> `install_gc_box`, which is
the test path; production calls `install_gc_standalone` and leaves the cell
`None` on every thread, so the probe always missed and every caller paid a
`_tlv_get_addr` for it.

Add a per-backend process-global `GC_BOX_INSTALLED: AtomicBool`, set by
`install_gc_box` (cranelift: `set_cranelift_active_gc(Some(..))`) and checked
before each probe. It is never cleared: `clear_gc_allocator` can run on one
thread while another still owns a box.

RPython has no per-thread allocator — `gctransform/framework.py` weaves every
`malloc` against the single translation-time `gcdata`.

Gated: dynasm 12 sites, cranelift 6, wasm 4.

synth/int_mul_ovf_bignum_promote steady state, startup-subtracted CPU time,
min of 9, dynasm: 44.6ns -> 37.4ns per iteration at 8M, 41.4ns -> 35.4ns at
16M.

Assisted-by: Claude
The three per-backend `GC_BOX_INSTALLED` statics tracked the same fact, so
replace them with one in majit-gc behind `note_gc_box_installed` /
`gc_box_installed`.

Add `standalone_alloc_nursery_typed` and
`standalone_alloc_nursery_collecting_typed_rooted`: the allocation each
backend's hook performs once no per-thread box owns the request, spelled once
in majit-gc. The dynasm trampolines fall through to them, and
`alloc_nursery_typed` / `alloc_nursery_collecting_typed_rooted` call them
directly when no box is installed instead of going out through the hook. Both
are `#[inline]`; without it the entry points trade an indirect call for a
cross-crate call.

The hook is still read for its presence, so an uninstalled backend keeps
returning null.

Assisted-by: Claude
…rations

At 20000 the promoting loop was ~0.8% of the run: the measurement was the
2.4M-iteration warm-up, and the whole exec came to 0.01s against a 0.01s pypy
startup and a 0.08s dynasm one, so `max-pypy-ratio` compared two numbers that
were mostly startup subtraction error. It read 9.3-9.8x on CI (both ubuntu and
macos, and on main) against a gate of 8, and 2.75x locally.

check.py's gate policy (check.py:1890-1897) covers this case: a gate is only
tightened when the baseline is healthy, and where it is not the workload is
sized up instead.

pypy exec is now ~18x its startup. Measured through check.py: dynasm 2.3x,
cranelift 2.8x, wasm 5.4x. The gate stays at 8.

Assisted-by: Claude
`publish_state` rebuilt `all_liveness_positions` from the whole buffer on
every call: `decode_liveness_records` over every record the buffer holds,
three `Vec<u8>` allocated per record, then a rehash into a fresh IndexMap.
It is called from `Assembler::intern_liveness` once per `-live-` marker
interned, and the build-time drain's prefix is already ~21.5 KiB, so the
cost is `interns * buffer_len`.

`state::intern_liveness` is the only consumer of the mirror's dict, so
derive it there on first use after each publish. `publish_state` and
`AssemblerState::new` leave the field `None`; the dict a reader-side
intern derives still describes the bytes it is asked about, so the
duplicate append the rebuild was added to prevent stays prevented.

The affected quantity is JIT compile time: on
`bench/synth/inlined_helper_arith_hot`, N 40000 -> 4000000 moves pyre's
user CPU 0.31s -> 0.36s against a 0.04s startup. Startup-subtracted
ratios against pypy3, dynasm/cranelift/wasm on aarch64: that bench
8.0x -> 2.9x/3.7x/3.3x, `residual_raise_except_resume` -> 2.0x/2.5x/2.1x,
`raise_catch` 1.7x -> 1.0x. check.py: dynasm 348/348, cranelift 348/348,
wasm 344/344.

Assisted-by: Claude
`visit_pool` ran once per exit layout of every trace of every compiled
loop and tested membership with `Vec::contains`, so one walk was
quadratic in the number of distinct const pools, and that count grows
with the compiled code. The walk itself runs on every collection.

Replace the `Vec<usize>` with an `IndexSet<usize>` and fold the
membership test into the insert.

Measured on 30 renamed copies of `bench/synth/inlined_helper_arith_hot`
at N=3000, dynasm/aarch64, user CPU:

    loops    13    26    52   104   208   390
    before 0.09  0.14  0.27  0.64  2.06  8.37s
    after  0.09  0.15  0.26  0.53  1.24  3.18s

`sample` over the 390-loop run puts `rd_consts_root_walker_area` at 5339
of 7781 main-thread samples before and 104 after. check.py: dynasm
349/349, cranelift 349/349, wasm 345/345.

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

Caution

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

⚠️ Outside diff range comments (1)
majit/majit-metainterp/src/pyjitpl/dispatch.rs (1)

7956-7993: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Apply abort-framestack recovery on both single-pass trace paths.

trace_jitcode_from_merge_point also builds a StandaloneFrameStack and returns machine.run_to_end(...) directly, so mid-opcode aborts through merge-point tracing bypass the new panic-flag check, final pc update, and aborted_framestack stash. Root the parked ref_values in the blackhole conversion path too; this stash keeps the active frame bank across a call boundary where unrooted raw GC pointers can become live to collection.

🤖 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-metainterp/src/pyjitpl/dispatch.rs` around lines 7956 - 7993,
Apply the same Abort recovery logic to trace_jitcode_from_merge_point before
returning machine.run_to_end(...): update the final pc, honor and clear
abort_after_panic, and stash the StandaloneFrameStack in aborted_framestack
while preserving the fallback behavior. In the blackhole conversion path, root
the parked ref_values across call boundaries using the existing rooting
mechanism.
♻️ Duplicate comments (1)
majit/majit-backend-cranelift/src/compiler.rs (1)

12278-12284: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A missing write-barrier descriptor still degrades to a no-op barrier.

wb_mask_raw defaults to 0, so flag_ext & mask_val is always 0 and needs_wb is always false. Every CondCallGcWb / CondCallGcWbArray store then skips the barrier slow path. The dynasm sibling fails loud with .expect("COND_CALL_GC_WB emitted without a write barrier descriptor"). Now that gc_rewriter sources wb_descr from gc.get_write_barrier_descr() (Line 8145), the rewriter only emits these ops when a descriptor exists, so reaching this arm without one is an invariant violation.

🛡️ Proposed fix to fail loud instead of dropping the barrier
-                    let wb = rw.as_ref().and_then(|r| r.wb_descr.as_ref());
-                    let wb_byteofs = wb.map(|d| d.jit_wb_if_flag_byteofs as i32).unwrap_or(0);
-                    let wb_mask_raw = wb.map(|d| d.jit_wb_if_flag_singlebyte).unwrap_or(0);
-                    let wb_cards_set = wb.map(|d| d.jit_wb_cards_set).unwrap_or(0);
-                    let wb_card_shift = wb.map(|d| d.jit_wb_card_page_shift).unwrap_or(0);
-                    let wb_cards_singlebyte =
-                        wb.map(|d| d.jit_wb_cards_set_singlebyte).unwrap_or(0);
+                    let wb = rw
+                        .as_ref()
+                        .and_then(|r| r.wb_descr.as_ref())
+                        .expect("COND_CALL_GC_WB emitted without a write barrier descriptor");
+                    let wb_byteofs = wb.jit_wb_if_flag_byteofs as i32;
+                    let wb_mask_raw = wb.jit_wb_if_flag_singlebyte;
+                    let wb_cards_set = wb.jit_wb_cards_set;
+                    let wb_card_shift = wb.jit_wb_card_page_shift;
+                    let wb_cards_singlebyte = wb.jit_wb_cards_set_singlebyte;
🤖 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 12278 - 12284,
Update the write-barrier descriptor extraction in the compiler arm handling
CondCallGcWb and CondCallGcWbArray so a missing wb_descr fails immediately with
the same invariant-style expectation used by the dynasm sibling, rather than
defaulting wb_mask_raw and related values to zero. Preserve the existing
descriptor-based field extraction when wb_descr is present.
🤖 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 1709-1718: Handle the Err branch of try_borrow_mut in the via_box
logic so a reentrant borrow does not bypass the synchronized-GC fallback unless
the boxed allocator invariant guarantees that addr belongs to its address space.
Prefer routing the borrow failure through the raw mirror, consistent with
gc_owns_object_via_active_runtime and gc_is_nursery_object_via_active_runtime;
otherwise add a concise comment documenting the invariant.

In `@majit/majit-metainterp/src/optimizeopt/heap.rs`:
- Around line 4583-4604: Strengthen the SetfieldGc assertions in the affected
optimizer tests around result and the emitted guard: verify each expected
store’s descriptor, target object, and value rather than only counting
operations. In the size-store test, assert no emitted SetfieldGc targets d_head,
and verify the guard’s resume data retains d_head as a pending field while
preserving the existing ordering checks.

---

Outside diff comments:
In `@majit/majit-metainterp/src/pyjitpl/dispatch.rs`:
- Around line 7956-7993: Apply the same Abort recovery logic to
trace_jitcode_from_merge_point before returning machine.run_to_end(...): update
the final pc, honor and clear abort_after_panic, and stash the
StandaloneFrameStack in aborted_framestack while preserving the fallback
behavior. In the blackhole conversion path, root the parked ref_values across
call boundaries using the existing rooting mechanism.

---

Duplicate comments:
In `@majit/majit-backend-cranelift/src/compiler.rs`:
- Around line 12278-12284: Update the write-barrier descriptor extraction in the
compiler arm handling CondCallGcWb and CondCallGcWbArray so a missing wb_descr
fails immediately with the same invariant-style expectation used by the dynasm
sibling, rather than defaulting wb_mask_raw and related values to zero. Preserve
the existing descriptor-based field extraction when wb_descr is present.
🪄 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 Plus

Run ID: f4a50d17-22a0-48a8-b231-2cdc99f2aeb3

📥 Commits

Reviewing files that changed from the base of the PR and between dcbe9dc and d923491.

📒 Files selected for processing (31)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/rewrite.rs
  • majit/majit-macros/src/jit_interp/codegen_state.rs
  • majit/majit-macros/src/jit_interp/mod.rs
  • majit/majit-metainterp/src/blackhole.rs
  • majit/majit-metainterp/src/jit_state.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/pyjitpl/dispatch.rs
  • majit/majit-metainterp/src/resume_box_reader.rs
  • majit/majit-metainterp/src/trace_ctx.rs
  • majit/majit-metainterp/tests/abort_blackhole_virt_array.rs
  • majit/majit-translate/src/codewriter/flatten.rs
  • majit/majit-translate/src/codewriter/jitcode.rs
  • majit/majit-translate/src/codewriter/liveness.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/tests/test_make_jitcodes_produces_graph_keyed_output.rs
  • pyre/bench/synth/int_mul_ovf_bignum_promote.py
  • pyre/pyre-jit-trace/src/assembler.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_runtime.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/jit/assembler.rs
  • pyre/pyre-object/src/longobject.rs

Comment on lines +1709 to +1718
let via_box = majit_gc::gc_box_installed().then(|| {
CRANELIFT_ACTIVE_GC.with(|cell| {
let mut guard = match cell.try_borrow_mut() {
Ok(guard) => guard,
Err(_) => return Some(addr),
};
guard.as_deref_mut().map(|gc| gc.id_or_identityhash(addr))
})
});
if let Some(r) = via_box {
if let Some(Some(r)) = via_box {

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

A reentrant borrow now short-circuits the synchronized-GC fallback.

cell.try_borrow_mut() returns Some(addr) on failure. The outer if let Some(Some(r)) then returns addr and never reaches the gc_sync path at Line 1721. This is only correct when a reentrant borrow implies a boxed allocator owns the address space. Add a short comment that states this invariant, or route the borrow error through the raw mirror like gc_owns_object_via_active_runtime and gc_is_nursery_object_via_active_runtime do.

🤖 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 1709 - 1718,
Handle the Err branch of try_borrow_mut in the via_box logic so a reentrant
borrow does not bypass the synchronized-GC fallback unless the boxed allocator
invariant guarantees that addr belongs to its address space. Prefer routing the
borrow failure through the raw mirror, consistent with
gc_owns_object_via_active_runtime and gc_is_nursery_object_via_active_runtime;
otherwise add a concise comment documenting the invariant.

Comment on lines +4583 to +4604
let setfields: Vec<&Op> = result
.iter()
.filter(|o| o.opcode == OpCode::SetfieldGc)
.collect();
assert_eq!(
setfields.len(),
2,
"both stores must survive; emitted = {:?}",
result.iter().map(|o| o.opcode).collect::<Vec<_>>(),
);
let guard_at = result
.iter()
.position(|o| o.opcode == OpCode::GuardTrue)
.expect("guard must be emitted");
let last_setfield_at = result
.iter()
.rposition(|o| o.opcode == OpCode::SetfieldGc)
.unwrap();
assert!(
last_setfield_at < guard_at,
"both stores must be emitted before the guard they precede",
);

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

Assert the identity and pending state of each store.

The first test only counts two SetfieldGc operations. It can pass if the optimizer duplicates one store and drops the other. Assert each store's descriptor, target object, and value.

The second test only checks the size-store order. It can pass if the virtual d_head store is also emitted. Assert that no emitted SetfieldGc uses d_head. Also assert that the guard resume data contains the d_head pending field.

These checks are required to prove the stated lazy-store and deoptimization contracts.

Also applies to: 4744-4758

🤖 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-metainterp/src/optimizeopt/heap.rs` around lines 4583 - 4604,
Strengthen the SetfieldGc assertions in the affected optimizer tests around
result and the emitted guard: verify each expected store’s descriptor, target
object, and value rather than only counting operations. In the size-store test,
assert no emitted SetfieldGc targets d_head, and verify the guard’s resume data
retains d_head as a pending field while preserving the existing ordering checks.

Both `debug_assert!`s were bare, so a failure printed only the truncated
expression (`assertion failed: (field_idx as usize) <`) with neither the
slot that was written nor the field-list length it exceeded. That is the
whole diagnosis when the failure reproduces only on a machine the
developer cannot attach to.

Carry `field_idx`, `all_fielddescrs().len()` and the descr index into the
message on both the `Virtual` and `VirtualStruct` arms.

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: 141146307f

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

// handler) leaves `MIFrame.pc`.
if !std::mem::replace(&mut ctx.abort_after_panic, false) {
if let Some(top) = standalone.frames.frames.last_mut() {
top.pc = top.code_cursor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resume pre-execution recursive-call aborts at the opcode

When the production recursive decision returns ResidualCall for an uncompiled green key, exec_recursive_call decodes the operands but returns Abort without executing or pushing the callee. Overwriting top.pc with the post-decode code_cursor therefore stages the conversion after BC_RECURSIVE_CALL_*; once this pending conversion is consumed, the blackhole skips the call entirely, leaving a typed result uninitialized and omitting the callee's side effects. Preserve the opcode-start position, or decline conversion for abort paths that did not complete the current instruction.

AGENTS.md reference: AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

`test_guard_forces_size_store_paired_with_virtual_head_store` builds the
virtual with `virtual_size_descr(1, 4)` — four fields — and stored into it
through `descr(10)`. `optimize_setfield_gc` indexes
`PtrInfo::VirtualStruct.fields` by the field descr's slot, and
`virtualize.rs` asserts the slot stays inside the struct's own
`all_fielddescrs()`.

A `TestDescr` reports no `index_in_parent`, so it reads back as the trait
default 0 where the descr is used raw and as `offset() / field_size()` —
its index — wherever it is reconciled. On the CI runners the store
resolved to slot 10 of a 4-field struct and the assertion fired; the
combination cannot arise in production, where the field descr comes from
the struct type being stored into.

Use `descr(0)`. `d_head` / `d_size` are stored into the non-virtual `p0`
and keep their indices.

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: 88c5a36d42

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

// included), and `None` from one that needs no barrier
// (`gc.py:156 GcLLDescr_boehm`) keeps `rewrite.py:393` from
// emitting `COND_CALL_GC_WB*` the backend could not assemble.
wb_descr: gc.get_write_barrier_descr(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep barriers enabled for legacy GC implementations

When a custom generational GcAllocator overrides write_barrier but relies on the trait's existing default get_write_barrier_descr() (which returns None), this now passes None into GcRewriterImpl, so the new rewrite gates emit no COND_CALL_GC_WB*. Before this change both native backends constructed a generic descriptor for every allocator, so such implementations compile unchanged but silently lose old-to-young barriers and may reclaim reachable nursery objects; make the accessor required/default to the prior descriptor, or reserve None for collectors that explicitly opt out.

Useful? React with 👍 / 👎.

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