Skip to content

gc: put the per-thread GC box behind a gc_box feature and gate its remaining dynasm probes - #1098

Merged
youknowone merged 8 commits into
mainfrom
gc-decouple
Aug 7, 2026
Merged

gc: put the per-thread GC box behind a gc_box feature and gate its remaining dynasm probes#1098
youknowone merged 8 commits into
mainfrom
gc-decouple

Conversation

@youknowone

@youknowone youknowone commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Rebased onto d936eb4be42 (#1102). Two of the original commits — the Windows
st_ino/st_dev path fix and the _random Mersenne Twister allocation — were
dropped: both landed under us in #1085, and re-applying them produced an empty
diff.

What is here

GC box (the epic's #396 R4 endpoint). Every backend trampoline used to open
with majit_gc::gc_box_installed() && DYNASM_ACTIVE_GC.with(|c| …) — the same
borrow-and-map preamble at 22 dynasm, 14 cranelift and 6 wasm sites. Each
backend now reaches the box through one private gc_box module exposing
with_ref / with_mut / with_reentrant_ref / with_mut_or_busy / present /
store / clear.

The runtime gate stays inside those accessors rather than becoming a #[cfg].
That is not a preference: a Cargo feature is per-crate, so a backend crate
cannot #[cfg(feature = "gc_box")] on a feature of majit-gc — rustc answers
expected values for 'feature' are: 'dynasm'. Without majit-gc/gc_box the call
is a constant false, so each accessor folds to None and nm -a target/release/pyre-dynasm | rg ACTIVE_GC finds only the three
majit_gc::ACTIVE_GC_* globals — no DYNASM_ACTIVE_GC. The box is eliminated by
the optimizer instead of by conditional compilation, and the R4 endpoint holds
either way.

vstack mirror. reconcile_vstack_at_boundary now restores the saved operand
boxes when a layout excursion returns to the (py_pc, depth) its reorder region
armed at, instead of reseeding from the virtualizable shadow — mid-expression
that shadow's stack region holds the NULLs the in-flight opcode's
popvalue_maybe_none wrote. New fixture
bench/synth/getframe_method_call_residual_body_once.py with its three backend
baselines; it prints (20000, 20000, 20005) without the change.

Windows os.fstat. It answered from std::fs::Metadata, which carries no
file index or volume serial, so st_ino/st_dev came back zero and st_nlink
one while the same file reached by path reported its real identity. Now goes
through rustpython_host_env::fileutils::fstat — the StatStruct producer the
path forms already use.

A defect this branch introduced, and fixed

The vstack restore above originally returned as soon as it replayed the saved
boxes. That skipped the function's shared tail, which is not only the three
position-update lines it duplicated but also the reseed_vstack_from_shadow
hole-fill. A slot the arm point already carried as NONE stayed NONE,
stack_sync omitted that operand, and the resumed frame read the slot below it:
bench/synth/getframe_stored_fback_walk.py raised
TypeError: 'range_iterator' object is not subscriptable on all three backends.
Fixed by keeping the restore as the boundary's reconcile (_ if restored) and
letting the shared fill run over it.

Verification

  • cargo test --all --no-default-features --features dynasm7504 passed, 0 failed
  • cargo fmt --all -- --check — clean
  • pyre/check.pydynasm 405/406, cranelift 405/406, wasm 401/402

The one remaining row is synth/pypy_type_surface
(bridges_compiled 5 -> 102, guard_failures 1011 -> 20497), and it is not
this branch's
:

  • the fixture and all three baselines arrived in stdlib: stabilize collections JIT and heapq fallback #999 (dad2a722907); this
    branch touches neither,
  • reverting every file this branch changes to origin/main and re-extracting
    the LLBC reproduces the row with byte-identical numbers,
  • it reds identically on all three backends.

It is an all-platform base regression, already root-caused elsewhere: origin/main's
own CI run 31179140700 at d936eb4be42 reds it on ubuntu-24.04, windows-latest
and macos-latest alike, and #999's own run is already red with the same numbers,
so the recorded 5 / 1011 reproduced on no host including the one that wrote
it. The cause is the Cls.__name__ fold guarding a null metaclass slot; the fix
is 95ec7f2d474 (PR #1106), which is not on main yet. This row goes green when
that lands. Not re-recorded — 5 -> 102 bridges is a regression signature, not
benign drift.

authored by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows file metadata reporting for fstat, including file identity, timestamps, and file-type information.
    • Improved JIT handling of complex stack-reordering scenarios, frame forcing, residual loops, and caught exceptions.
    • Increased reliability of garbage-collection behavior across supported execution backends, including reentrant and standalone operation.
  • Tests

    • Added benchmark coverage and runtime statistics for method-call residual loops and related JIT outcomes.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This change adds conditional GC-box support and centralizes GC access in three JIT backends. It also restores vstack mirror state across reorder excursions, updates Windows descriptor stat conversion, and adds a residual-loop benchmark with backend statistics.

Changes

Conditional GC-box runtime

Layer / File(s) Summary
GC-box feature contract
majit/majit-gc/Cargo.toml, majit/majit-gc/src/lib.rs, majit/majit-backend-*/Cargo.toml
The gc_box feature gates per-thread GC-box state. Non-feature builds return false for presence checks and panic on installation. Backend development dependencies enable the feature.
Cranelift and Wasm GC-box access
majit/majit-backend-cranelift/src/compiler.rs, majit/majit-backend-wasm/src/lib.rs
The backends centralize allocator storage, mutable access, reentrant queries, ownership checks, and teardown cleanup.
Dynasm GC dispatch and standalone installation
majit/majit-backend-dynasm/src/runner.rs, pyre/pyre-jit/src/eval.rs, pyre/pyre-jit/tests/gc_stress.rs
Dynasm routes GC operations through guarded helpers, preserves busy and reentrant behavior, and adds standalone installation through gc_sync. Related test comments describe the updated initialization path.

Vstack reorder restoration

Layer / File(s) Summary
Saved vstack reorder state
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs, bridge_subwalk.rs, inline_call.rs, tests.rs
WalkContext stores an optional Python coordinate, depth, and operand-box snapshot. Dispatch, sub-walk, and fixture initializers set the state to None.
Reorder-region restoration
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
The mirror saves state when entering a reorder region and restores it when the same coordinate and depth recur.

Windows stat conversion

Layer / File(s) Summary
Windows descriptor metadata conversion
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
Windows fstat uses fileutils::fstat and shared StatStruct conversion for identity, timestamps, and file-type metadata.

Residual-loop benchmark

Layer / File(s) Summary
Residual-loop reproduction
pyre/bench/synth/getframe_method_call_residual_body_once.py
The benchmark forces caller frames, raises and catches ValueError, tracks loop results and side effects, and prints the result tuple.
Backend JIT statistics
pyre/bench/synth/getframe_method_call_residual_body_once.*.jitstats
Cranelift, Dynasm, and Wasm statistics files define counters for compilation, frame handling, guards, panics, and loop outcomes.

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

Sequence Diagram(s)

sequenceDiagram
  participant JITBackend
  participant GCBox
  participant GCSync
  JITBackend->>GCBox: Check installed allocator
  alt GC box installed
    JITBackend->>GCBox: Perform guarded or reentrant GC operation
  else No GC box installed
    JITBackend->>GCSync: Perform singleton GC operation
  end
Loading

Possibly related PRs

Poem

A rabbit hops through GC’s box,
With guarded steps and reentrant locks.
The stack recalls its former place,
Windows stats now share one face.
Benchmarks count each looping beat—
Three JIT paths make results neat.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the PR's primary changes: gating the per-thread GC box behind a feature and gating dynasm probes.
✨ 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 gc-decouple

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/a8e9544867c6a7677e9d4ec10a862a7153d8b207/pyre-interpreter/src/module/posix/interp_posix.rs#L3615-L3616
P1 Badge Route Windows fstat through the identity-preserving path

On non-sandbox Windows builds with host_env, this branch makes os.stat(path) return the real file index and device, but fstat_fd still passes std::fs::Metadata to make_stat_result, whose Windows arm hardcodes st_ino and st_dev to zero. Consequently os.stat(path) and os.fstat(open(path).fileno()) now report different identities for every ordinary file, breaking callers that compare the two; the descriptor path needs to populate the same StatFields representation.

ℹ️ 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
Comment on lines +1647 to +1649
pub fn note_gc_box_installed() {
panic!(
"a per-thread GC box was installed in a build without the `gc_box` feature; \

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 public custom-GC installation path

In a normal downstream build, calling the still-public JitDriverPair::set_gc_allocator or any backend's set_gc_allocator reaches this unconditional panic because none of those crates exposes or enables majit-gc/gc_box; the backend crates' new dev-dependencies only affect their own tests and do not participate when the crates are consumed as dependencies. Thus the advertised custom-allocator API now always aborts outside those test targets unless consumers discover that they must add a direct feature-unifying majit-gc dependency; propagate an explicit feature through the public crate or restrict/remove the API rather than leaving it operational but panicking.

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 (1)
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs (1)

447-521: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset vstack_reorder_saved at mirror revalidation entry points.

seed_vstack_mirror, seed_callee_vstack_mirror, and the outer exception-handler reseeding path rebuild the vstack mirror and can revive vstack_valid, but they leave any prior vstack_reorder_saved intact. A stale saved (py_pc, depth, boxes) can later match a coincidental (new_pypc, new_depth) pair and restore boxes from the old walk. Clear vstack_reorder_saved together with vstack_reorder_ceiling at these revalidation points.

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

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs` around lines 447 -
521, Reset vstack_reorder_saved alongside vstack_reorder_ceiling in
seed_vstack_mirror, seed_callee_vstack_mirror, and the outer exception-handler
reseeding path. Ensure each mirror revalidation clears any prior saved boxes
before rebuilding the vstack mirror and reviving vstack_valid, preventing stale
state from being consumed by returned_to_arm_point.
🤖 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-gc/src/lib.rs`:
- Around line 1642-1653: Update install_gc_box() to call note_gc_box_installed()
before disarm_published_nursery(), ensuring unsupported gc_box builds panic
before mutating nursery publishing state. Preserve the existing installation
flow for builds with gc_box support.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 3382-3420: Update the Windows `fstat_fd` path to obtain a
`rustpython_host_env::fileutils::StatStruct` through the host API instead of
converting `std::fs::Metadata` with `make_stat_result`. Pass that result through
`stat_fields_from_statstruct`, matching `win_stat_fields` so `os.stat(path)` and
`os.fstat(fd)` expose the same `st_ino` and `st_dev`; add or preserve the
regression covering path/descriptor identity and `os.path.samestat`.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs`:
- Line 444: Add a focused test in the existing jitcode-dispatch test helpers
that exercises the `vstack_reorder_saved` restore path: build a synthetic
jitcode fixture with a non-`cfg_successor` boundary that arms the reorder
region, performs an out-of-order excursion, then returns to the same `(py_pc,
depth)`. Assert that the restored `vstack_boxes` exactly matches the
pre-excursion snapshot, while initializing the new field to `None` remains
unchanged in existing fixtures.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs`:
- Around line 447-521: Reset vstack_reorder_saved alongside
vstack_reorder_ceiling in seed_vstack_mirror, seed_callee_vstack_mirror, and the
outer exception-handler reseeding path. Ensure each mirror revalidation clears
any prior saved boxes before rebuilding the vstack mirror and reviving
vstack_valid, preventing stale state from being consumed by
returned_to_arm_point.
🪄 Autofix

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: 01970bbd-c2b2-48ec-b690-8d9e7e53c612

📥 Commits

Reviewing files that changed from the base of the PR and between 779da08 and a8e9544.

📒 Files selected for processing (19)
  • majit/majit-backend-cranelift/Cargo.toml
  • majit/majit-backend-dynasm/Cargo.toml
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/Cargo.toml
  • majit/majit-gc/Cargo.toml
  • majit/majit-gc/src/lib.rs
  • pyre/bench/synth/getframe_method_call_residual_body_once.cranelift.jitstats
  • pyre/bench/synth/getframe_method_call_residual_body_once.dynasm.jitstats
  • pyre/bench/synth/getframe_method_call_residual_body_once.py
  • pyre/bench/synth/getframe_method_call_residual_body_once.wasm.jitstats
  • pyre/pyre-interpreter/src/module/_random/mod.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/tests/gc_stress.rs

Comment thread majit/majit-gc/src/lib.rs
Comment thread pyre/pyre-interpreter/src/module/posix/interp_posix.rs
vstack_valid: false,
vstack_last_ref: OpRef::NONE,
vstack_reorder_ceiling: u32::MAX,
vstack_reorder_saved: 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.

📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add a dedicated test for the vstack_reorder_saved restore path.

All changes in this file only initialize the new vstack_reorder_saved field to None in existing fixtures. No test drives the walk through an actual out-of-order excursion (the documented LOAD_ATTR + CALL reorder case) and asserts that the saved (py_pc, depth, boxes) triple is restored verbatim when the walk returns to the arm coordinate.

This restore logic sits on a JIT-correctness-critical path: a wrong restore silently produces a corrupted operand-stack mirror that only surfaces as a miscompiled bridge resume at runtime. Add a synthetic jitcode fixture that arms the reorder region (a non-cfg_successor boundary), then returns to the same (py_pc, depth), and assert vstack_boxes equals the pre-excursion snapshot.

Also applies to: 667-667, 1054-1054

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

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs` at line 444, Add a focused
test in the existing jitcode-dispatch test helpers that exercises the
`vstack_reorder_saved` restore path: build a synthetic jitcode fixture with a
non-`cfg_successor` boundary that arms the reorder region, performs an
out-of-order excursion, then returns to the same `(py_pc, depth)`. Assert that
the restored `vstack_boxes` exactly matches the pre-excursion snapshot, while
initializing the new field to `None` remains unchanged in existing fixtures.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit be1f980).
Updated: 2026-08-07T15:28:32.847Z

Files in the reviewed diff
majit/majit-backend-cranelift/Cargo.toml
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/Cargo.toml
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/Cargo.toml
majit/majit-backend-wasm/src/lib.rs
majit/majit-gc/Cargo.toml
majit/majit-gc/src/lib.rs
pyre/bench/synth/getframe_method_call_residual_body_once.py
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/tests/gc_stress.rs

Codex did not produce a report (exit 1). Last log lines:

3. Mismatches that already existed before this patch
4. Structural adaptations

Exceptions: some differences cannot be ported 1:1 because of Python 3.11 vs
3.14 differences, opcode mismatches caused by using a CPython-compatible
compiler, GIL/free-threading differences, and fundamental implementation-
language differences between RPython and Rust. Mark those separately under
"Structural adaptations".

Scope discipline: before writing the report, run
`git diff upstream/main --name-only -- . ':(exclude)*.jitstats'` and treat that
file list as the authoritative definition of "this patch" (when an authoritative
changed-file list is appended below, use that instead of re-deriving it). The
excluded `*.jitstats` files are `pyre/check.py`'s recorded jit-stats baselines —
generated golden data with no RPython/PyPy counterpart, so no parity finding can
cite one, and a bulk re-record of them is not a change to review. Findings under
sections 1 and 2 MUST cite our-side files from that list; a divergence in any
file NOT in the list is by definition not introduced by this patch — report
it under section 3 instead, or omit it. Verify every section-1/2 citation
against the list before finalizing the report.

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.

Authoritative changed-file list for this patch (git diff upstream/main --name-only,
minus 3 generated `*.jitstats` baseline file(s)):
majit/majit-backend-cranelift/Cargo.toml
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/Cargo.toml
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/Cargo.toml
majit/majit-backend-wasm/src/lib.rs
majit/majit-gc/Cargo.toml
majit/majit-gc/src/lib.rs
pyre/bench/synth/getframe_method_call_residual_body_once.py
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/tests/gc_stress.rs
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Aug 8th, 2026 3:32 AM.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Aug 8th, 2026 3:32 AM.

… to its arm point

`reconcile_vstack_at_boundary` armed `vstack_reorder_ceiling` on a boundary
whose `new_pypc` is not a CFG successor of `prev_pypc`, and every following
boundary was then forced to `ShadowReseed`, which clears the operand mirror and
refills it from the virtualizable shadow. Mid-expression that shadow's stack
region holds the NULLs the in-flight opcode's `popvalue_maybe_none` wrote, so
the reseed dropped those slots and shifted the surviving boxes down: for a
method-form `LOAD_ATTR` + `CALL` pair the callable landed in the `self_or_null`
slot with the TOS left a hole, `flush_escape_state_with_latched_stack`
declined, and the walk kept the legacy replay.

Record `(py_pc, depth, boxes)` in the new `WalkContext::vstack_reorder_saved`
when the region arms, and when a boundary returns to that exact `(py_pc,
depth)` restore the boxes verbatim and close the region. Such an excursion
retires no Python opcode, so the operand stack it left is the one it resumes
with.

Add `bench/synth/getframe_method_call_residual_body_once.py` with its three
backend baselines (`fbw_blackhole_adopted_single_frame=10`,
`loops_aborted=10`, `loops_compiled=0` on all three); it prints
`(20000, 20000, 20005)` without this change. Re-record
`exception_reused_object_tb_not_doubled.wasm.jitstats`, whose
`fbw_blackhole_adopted_single_frame` rises 0 -> 3 with
`loops_compiled`/`bridges_compiled`/`guard_failures` unchanged.

Assisted-by: Claude
…stalled()

`with_dynasm_active_gc` and the six pyre-object-facing allocation trampolines
already skip the `DYNASM_ACTIVE_GC` thread-local when the process-global
`GC_BOX_INSTALLED` flag is clear. `with_dynasm_active_gc_mut` and eleven
further trampolines still read the thread-local unconditionally. That flag is
set only by `install_gc_box`, which no production path calls — `pyre-jit`'s
`eval.rs` installs each backend through `install_gc_standalone` — so the reads
always miss, and on Mach-O each is a `_tlv_get_addr` call plus a `RefCell`
borrow.

Gate the remaining twelve sites the same way. The cranelift and wasm
forwarders already carry the check; dynasm was the one backend without it.

Reached through `with_dynasm_active_gc_mut`: the four JIT malloc slow paths and
the three JIT write-barrier trampolines. Gated directly:
`dynasm_gc_write_barrier`, `dynasm_gc_write_barrier_managed`,
`dynasm_gc_add_root`, `dynasm_gc_remove_root`, `dynasm_heap_stats`,
`dynasm_collect_full`, `dynasm_collect_oldgen_nonmoving`, `dynasm_get_objects`,
`dynasm_get_referents`, `dynasm_is_tracked`, and the oldgen-freelist diagnostic.

On a list-ref-store loop, `sample` over five interleaved runs per arm charges
410 of 3988 `dynasm_gc_write_barrier` samples to `_tlv_get_addr` before and 265
of 3768 after. The remainder is `gc_sync`'s own thread-local, which this does
not touch.

Assisted-by: Claude
…aptations

The parity review surfaced three places where this branch has no upstream
counterpart and the code did not record why.

`DYNASM_ACTIVE_GC`: `gc.py:30` `GcLLDescription.__init__` holds `self.gcdescr`
as a plain field on the backend descriptor, so the per-thread cell is
scaffolding rather than a ported structure; note that only `install_gc_box`
fills it, that only tests reach that path, and that every read is consequently
gated on `majit_gc::gc_box_installed()`.

`WalkContext::vstack_reorder_saved`: `pyjitpl.py:1892` `MIFrame.run_one_step`
steps a live frame whose `registers_r` survive the step, so upstream has
nothing to snapshot; the mirror is reconstructed from source pcs instead.

The mirror-restore block in `reconcile_vstack_at_boundary`: `pyopcode.py:1037`
`LOAD_ATTR` pops and pushes the live value stack in place, so the interpreter
has no walk position to leave and return to.

Assisted-by: Claude
`gc_box_installed()` gated every backend's box read at run time. Without the
new `majit-gc/gc_box` feature it is a constant `false`, so each
`gc_box_installed() && ...` branch and the `GC_BOX_INSTALLED` load become dead
code; `note_gc_box_installed()` panics in such a build rather than leaving an
installed box invisible to every probe.

The three backend crates carry the feature in `dev-dependencies`, which is the
only place a box is installed from: every `set_gc_allocator` /
`with_gc_allocator` call site sits inside a `#[cfg(test)]` module or under
`tests/`, and `JitDriverPair::set_gc_allocator` has no caller at all.

Assisted-by: Claude
…zer runs

Three comments said `driver_pair` reaches `set_gc_allocator`. It reaches
`init_gc_subsystem` -> `install_gc_into_backend` -> `install_gc_standalone`,
which registers the hooks against the `gc_sync` singleton and stores no
per-thread box.

Assisted-by: Claude
`reconcile_vstack_at_boundary` returned as soon as it replayed the saved
boxes, which skipped the `reseed_vstack_from_shadow` hole-fill and the
`vstack_valid` check that guards it. A hole the arm point already carried
stayed NONE in the restored mirror, so `stack_sync` omitted that operand
slot and the resumed frame read the slot below it.

`bench/synth/getframe_stored_fback_walk.py` raised
`TypeError: 'range_iterator' object is not subscriptable` at
`box[0].f_back.f_locals['total']` on all three backends; it prints
`199990000 0` again.

Keep the restore as this boundary's reconcile (a new `_ if restored`
match arm) and let the shared fill and position update at the end of the
function run over it.

Assisted-by: Claude
…backend

Each trampoline opened with `majit_gc::gc_box_installed() && DYNASM_ACTIVE_GC.with(...)`
(and the cranelift / wasm equivalents), repeating the same borrow-and-map
preamble at 22, 14 and 6 sites. Move the `thread_local!` and that preamble
into a private `gc_box` module per backend, exposing `with_ref` / `with_mut` /
`with_reentrant_ref` / `with_mut_or_busy` / `present` / `store` / `clear`, and
call those instead.

The runtime `gc_box_installed()` gate stays where it was — inside the
accessors. It cannot become a `#[cfg]` here: a Cargo feature is per-crate, so
a backend crate cannot `#[cfg(feature = "gc_box")]` on a feature of
`majit-gc` (`expected values for 'feature' are: 'dynasm'`). Without
`majit-gc/gc_box` the call is a constant `false`, so each accessor folds to
`None` and `nm` finds no `*_ACTIVE_GC` symbol in the release binary.

Assisted-by: Claude
`os.fstat` on Windows built its result from `File::metadata`, which carries
no file index and no volume serial, so `make_stat_result` filled st_ino and
st_dev with zero and st_nlink with one. The same file reached by path
answered with its real identity, and a character device or pipe answered
with a disk file's format bits.

Call `rustpython_host_env::fileutils::fstat` — the `StatStruct` producer the
path forms already go through — and render it with
`stat_fields_from_statstruct` / `stat_result_from_fields`. That function
already reports an invalid fd as ERROR_INVALID_HANDLE, so the local
`invalid_handle()` now only covers an error with no raw OS code.

Not built locally: `cargo check --target x86_64-pc-windows-msvc` needs an
MSVC toolchain for `stacker`'s C build.

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

🤖 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-dynasm/src/runner.rs`:
- Around line 134-139: Update majit/majit-backend-dynasm/src/runner.rs:134-139
so with_ref delegates to the existing with_reentrant_ref accessor. In
majit/majit-backend-dynasm/src/runner.rs:204-206,
majit/majit-backend-cranelift/src/compiler.rs:1519-1521, and
majit/majit-backend-wasm/src/lib.rs:360-362, change each present accessor to use
try_borrow and return true when borrowing fails because a mutable borrow is
active.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs`:
- Around line 449-454: Clear vstack_reorder_saved and reset
vstack_reorder_ceiling when re-seeding in seed_callee_vstack_mirror,
seed_vstack_mirror, and the full-body vstack_enter_exception_handler path before
setting vstack_valid = true. Ensure stale reorder state cannot be restored
across unrelated seed boundaries.
🪄 Autofix

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: f09369e9-f55a-4c2a-8a2b-b2de1c547764

📥 Commits

Reviewing files that changed from the base of the PR and between a8e9544 and be1f980.

📒 Files selected for processing (9)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/lib.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/tests/gc_stress.rs

Comment on lines +134 to +139
pub(super) fn with_ref<R>(f: impl FnOnce(&dyn majit_gc::GcAllocator) -> R) -> Option<R> {
if !majit_gc::gc_box_installed() {
return None;
}
DYNASM_ACTIVE_GC.with(|cell| cell.borrow().as_deref().map(f))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Non-reentrant borrow() in the gc_box presence and read accessors. All three gc_box modules added reentrancy-tolerant accessors (with_reentrant_ref, with_mut_or_busy) because a GC query can fire while with_mut still holds the RefCell mutable borrow. The presence and plain-read accessors kept cell.borrow(), so those same reentrant paths panic with BorrowMutError instead of answering. Replace borrow() with try_borrow() and treat a held mutable borrow as "box present".

  • majit/majit-backend-dynasm/src/runner.rs#L134-L139: route with_ref through with_reentrant_ref, and change present at Lines 204-206 to try_borrow, reporting true on Err.
  • majit/majit-backend-cranelift/src/compiler.rs#L1519-L1521: change present to try_borrow, reporting true on Err.
  • majit/majit-backend-wasm/src/lib.rs#L360-L362: change present to try_borrow, reporting true on Err.
📍 Affects 3 files
  • majit/majit-backend-dynasm/src/runner.rs#L134-L139 (this comment)
  • majit/majit-backend-cranelift/src/compiler.rs#L1519-L1521
  • majit/majit-backend-wasm/src/lib.rs#L360-L362
🤖 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/runner.rs` around lines 134 - 139, Update
majit/majit-backend-dynasm/src/runner.rs:134-139 so with_ref delegates to the
existing with_reentrant_ref accessor. In
majit/majit-backend-dynasm/src/runner.rs:204-206,
majit/majit-backend-cranelift/src/compiler.rs:1519-1521, and
majit/majit-backend-wasm/src/lib.rs:360-362, change each present accessor to use
try_borrow and return true when borrowing fails because a mutable borrow is
active.

Comment on lines +449 to +454
ctx.vstack_reorder_saved = None;
}
if !cfg_successor && ctx.vstack_reorder_ceiling == u32::MAX {
ctx.vstack_reorder_ceiling = (new_pypc as usize).max(prev_pypc) as u32;
ctx.vstack_reorder_saved =
Some((prev_pypc as u32, ctx.vstack_depth, ctx.vstack_boxes.clone()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Expect every mirror re-seed and every path that sets vstack_valid=true
# to clear vstack_reorder_saved and reset vstack_reorder_ceiling.
rg -n -C 10 \
  'vstack_valid\s*=|vstack_reorder_saved|vstack_reorder_ceiling|vstack_enter_exception_handler|seed_callee_vstack_mirror' \
  pyre/pyre-jit-trace/src/jitcode_dispatch

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd pyre/pyre-jit-trace/src/jitcode_dispatch

fd -a '.*\.rs$' . | sed 's#^\./##' | grep -E '(^|/)vstack_mirror\.rs$|(^|/)exception|handler|residual|mirror|walk|i' | head -200

echo '--- vstack_mirror size ---'
wc -l vstack_mirror.rs

echo '--- exact occurrences in relevant source only (excluding tests.rs) ---'
rg -n -C 3 \
  'vstack_valid\s*=|vstack_reorder_saved|vstack_reorder_ceiling|vstack_enter_exception_handler|seed_callee_vstack_mirror|fbw_seed|invalid|invalidate|restart|resume|outer_jitcode_index|outer_resume_marker_jit_pc' \
  $(fd -a '.*\.rs$' . | sed 's#^\./##' | grep -v '/tests.rs$' | tr '\n' ' ')

echo '--- vstack_mirror relevant ranges ---'
sed -n '1,180p' vstack_mirror.rs
sed -n '180,240p' vstack_mirror.rs
sed -n '430,560p' vstack_mirror.rs

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd pyre/pyre-jit-trace/src/jitcode_dispatch

echo '--- vstack_mirror exact occurrences in file only ---'
rg -n -C 4 \
  'vstack_valid\s*=|vstack_reorder_saved|vstack_reorder_ceiling|vstack_enter_exception_handler|fbw_seed|invalid|invalidate|restart|resume|outer_jitcode_index|outer_resume_marker_jit_pc|reconcile_vstack_at_boundary' \
  vstack_mirror.rs || true

echo '--- walk seed/restart helper occurrences in non-test core files ---'
rg -n -C 3 \
  'fn fbw_seed|fbw_seed\(|fn .*seed|seed_callee_vstack_mirror|vstack_valid\s*=\s*(true|false|!)|outer_jitcode_index|outer_resume_marker_jit_pc|valid.*mirror|mirror' \
  $(fd -a '.*\.rs$' . | sed 's#^\./##' | grep -v '/tests.rs$' | tr '\n' ' ') | head -n 300

echo '--- vstack_mirror key ranges ---'
sed -n '1,230p' vstack_mirror.rs
echo '--- exit points ---'
sed -n '420,560p' vstack_mirror.rs

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd pyre/pyre-jit-trace/src/jitcode_dispatch

echo '--- seed callee vstack mirror ---'
sed -n '1003,1021p' vstack_mirror.rs

echo '--- seed full-body vstack mirror ---'
sed -n '1023,1131p' vstack_mirror.rs

echo '--- full-body exception handler vstack enter ---'
sed -n '1190,1293p' vstack_mirror.rs

echo '--- callee exception handler vstack enter ---'
sed -n '1295,1365p' vstack_mirror.rs

echo '--- other vstack_valid assignment sites excluding vstack_mirror.rs ---'
rg -n -C 3 \
  '\bvstack_valid\s*=|seed_callee_vstack_mirror\(|seed_vstack_mirror\(|vstack_enter_exception_handler\(' \
  $(git ls-files 'pyre/pyre-jit-trace/src/jitcode_dispatch/*.rs' | grep -v 'vstack_mirror.rs' | tr '\n' ' ')

echo '--- vstack_mirror remaining vstack_valid assignment sites ---'
rg -n -C 3 '\bvstack_valid\s*=' vstack_mirror.rs

Repository: youknowone/pyre

Length of output: 13590


Clear the saved reorder state in re-seed paths.

seed_callee_vstack_mirror, seed_vstack_mirror, and full-body vstack_enter_exception_handler all set vstack_valid = true without clearing vstack_reorder_saved and vstack_reorder_ceiling. Clear both fields in these re-seed paths, or guard the reorder restore so it cannot apply across unrelated seed boundaries.

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

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs` around lines 449 -
454, Clear vstack_reorder_saved and reset vstack_reorder_ceiling when re-seeding
in seed_callee_vstack_mirror, seed_vstack_mirror, and the full-body
vstack_enter_exception_handler path before setting vstack_valid = true. Ensure
stale reorder state cannot be restored across unrelated seed boundaries.

@youknowone
youknowone merged commit 444e153 into main Aug 7, 2026
13 of 17 checks passed
@youknowone
youknowone deleted the gc-decouple branch August 7, 2026 21:37
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