Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a35b628
jit(fbw): decline the inline instead of aborting the trace at five mu…
youknowone Jul 28, 2026
6ba59c6
jit(fbw): screen an inline callee's body for abort_permanent at the c…
youknowone Jul 28, 2026
6e4443f
jit: residualize the try-block catch-marker inline decline instead of…
youknowone Jul 28, 2026
eff66df
jit: deny the callee named by the nested-residual hazard arm instead …
youknowone Jul 28, 2026
23b9418
Revert "jit: residualize the try-block catch-marker inline decline in…
youknowone Jul 28, 2026
f5cd058
jit: record the bridge-guard decline for the exc-edge cross-frame ret…
youknowone Jul 28, 2026
a514d7b
bench: promote five synth fixtures out of _pending
youknowone Jul 28, 2026
9eaaecd
jit: keep lowering the code object behind an abort_permanent opcode
youknowone Jul 28, 2026
9d36419
jit: close the abort_permanent block at the two sites that skip their…
youknowone Jul 28, 2026
456577e
jit: attach the exception edge before a branch closes its can-raise b…
youknowone Jul 28, 2026
ae456d1
gc: keep nursery objects out of the major marking worklist
youknowone Jul 28, 2026
a5d05ec
jit: let a residual call re-enter the JIT instead of forcing plain eval
youknowone Jul 28, 2026
e530069
jit: record why the hazardous-inline deny is keyed on the callee Code…
youknowone Jul 28, 2026
baedd4c
jit: park the residual call's exception cells across a nested JIT re-…
youknowone Jul 29, 2026
973f89b
jit: record the PyCode immortality both address-keyed inline memos re…
youknowone Jul 29, 2026
9cc4940
gc: forward PyCode.w_globals through a registry of stamped code objects
youknowone Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 62 additions & 31 deletions majit/majit-gc/src/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2053,40 +2053,65 @@ impl MiniMarkGC {
}

fn seed_major_root(&mut self, gcref: GcRef) {
if !gcref.is_null() && self.is_managed_heap_object(gcref.0) {
let hdr = unsafe { header_of(gcref.0) };
// SAFETY: header_of returns a raw pointer; keep each access
// short-lived to avoid creating overlapping exclusive borrows.
let newly_marked = unsafe {
if !(*hdr).has_flag(flags::VISITED) {
(*hdr).set_flag(flags::VISITED);
true
} else {
false
}
};
if newly_marked {
self.incr_state.gray_stack.push(gcref.0);
self.note_nonmoving_nursery_mark(gcref.0);

// incminimark.py:1322-1340 requires marking worklists to
// contain no nursery objects after a minor. Pyre JITFRAME
// gcmap spills are collector metadata, not SETFIELD_GC
// writes: a frame first grayed in STATE_SCANNING can leave
// the active shadow stack before the next minor without a
// mutator barrier. Arm this newly seeded old root in the
// existing old_objects_pointing_to_young shape once, so that
// minor forwards any such spill before resetting nursery.
if !self.is_in_nursery(gcref.0)
&& unsafe { (*hdr).has_flag(flags::TRACK_YOUNG_PTRS) }
{
unsafe { (*hdr).clear_flag(flags::TRACK_YOUNG_PTRS) };
self.remembered_set.push(gcref.0);
}
if gcref.is_null()
|| !self.is_managed_heap_object(gcref.0)
|| !self.may_enter_marking_worklist(gcref.0)
{
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
let hdr = unsafe { header_of(gcref.0) };
// SAFETY: header_of returns a raw pointer; keep each access
// short-lived to avoid creating overlapping exclusive borrows.
let newly_marked = unsafe {
if !(*hdr).has_flag(flags::VISITED) {
(*hdr).set_flag(flags::VISITED);
true
} else {
false
}
};
if newly_marked {
self.incr_state.gray_stack.push(gcref.0);
self.note_nonmoving_nursery_mark(gcref.0);

// incminimark.py:1322-1340 requires marking worklists to
// contain no nursery objects after a minor. Pyre JITFRAME
// gcmap spills are collector metadata, not SETFIELD_GC
// writes: a frame first grayed in STATE_SCANNING can leave
// the active shadow stack before the next minor without a
// mutator barrier. Arm this newly seeded old root in the
// existing old_objects_pointing_to_young shape once, so that
// minor forwards any such spill before resetting nursery.
if !self.is_in_nursery(gcref.0) && unsafe { (*hdr).has_flag(flags::TRACK_YOUNG_PTRS) } {
unsafe { (*hdr).clear_flag(flags::TRACK_YOUNG_PTRS) };
self.remembered_set.push(gcref.0);
}
}
}

/// `incminimark.py:2739-2753 _collect_obj`: an object in the nursery is
/// never appended to `objects_to_trace` — "such an object is handled by
/// minor collections and shouldn't be specially handled by major
/// collections" — and `visit` (:2797-2799) asserts
/// `not self.is_in_nursery(obj)` on every popped entry. The worklist
/// outlives the mutator resuming, so a nursery address left on it is read
/// back after the next `reset_nursery` has recycled those bytes: the
/// header decodes to a garbage `type_id`.
///
/// A young object stays reachable without the entry. When the minor
/// promotes it, `drag_out_root` greys the promoted copy
/// (`_trace_drag_out1_marking_phase`); when it is only reachable from an
/// old parent, that parent is in the remembered set and gets requeued
/// while MARKING (`_add_to_more_objects_to_trace`).
///
/// The non-moving oldgen major is the one mode that marks the nursery in
/// place — it leaves those bytes untouched by contract, and
/// [`Self::note_nonmoving_nursery_mark`] clears the marks afterwards.
#[inline]
fn may_enter_marking_worklist(&self, addr: usize) -> bool {
self.oldgen_nonmoving_active || !self.is_in_nursery(addr)
}

/// Record a nursery object greyed during a non-moving major so its
/// stale `flags::VISITED` is cleared as the strictly-last collection step.
/// No-op (just a range check) outside a non-moving major; the normal
Expand Down Expand Up @@ -2552,7 +2577,7 @@ impl MiniMarkGC {
/// type system guarantees every `Ptr(GcStruct)` is GC-managed; it converges
/// away once every `gc_ptr_offsets` target is a real GC allocation.
fn grey_child(&mut self, addr: usize, holder_addr: usize, slot_addr: usize, site: &str) {
if self.is_managed_heap_object(addr) {
if self.is_managed_heap_object(addr) && self.may_enter_marking_worklist(addr) {

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 Trace old children before skipping nursery objects

When marking finishes through the standalone gc_step() between minor collections, a black old object may have been updated to point to a young container that contains a still-white old object. rescan_remembered_black_and_drain requeues the old parent, but this new condition skips the young container entirely, so its old child remains white and is swept while still reachable, leaving a dangling pointer in the live nursery object. Upstream's nursery-worklist exclusion relies on major progress being driven after a minor; preserve that scheduling here or synchronously trace nursery objects without retaining their addresses on the worklist.

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

Useful? React with 👍 / 👎.

let hdr = unsafe { header_of(addr) };
let type_id = unsafe { (*hdr).type_id() };
if type_id as usize >= self.types.len() {
Expand Down Expand Up @@ -2586,6 +2611,12 @@ impl MiniMarkGC {
// varsize GC-pointer array is never buffered (the gray stack already
// retains the live children); only the bounded fixed-field offsets are
// copied into the reused `mark_offsets` buffer.
// incminimark.py:2797-2799 `visit`: `ll_assert(not
// self.is_in_nursery(obj), "nursery object in 'objects_to_trace'")`.
debug_assert!(
self.may_enter_marking_worklist(obj_addr),
"nursery object {obj_addr:#x} in the marking worklist",
);
let type_id = unsafe { (*header_of(obj_addr)).type_id() };
// A non-moving major can trace a live nursery object without first
// copying it into its reserved old-gen shadow. Keep that unoccupied
Expand Down
54 changes: 0 additions & 54 deletions pyre/bench/synth/_pending/exception_nested_exc_info_restore.py

This file was deleted.

48 changes: 48 additions & 0 deletions pyre/bench/synth/exception_nested_exc_info_restore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Regression oracle: nested try/except where each handler reads
# `sys.exc_info()`. After an inner handler unwinds, POP_EXCEPT must restore the
# slot to the prev its matching PUSH_EXC_INFO saved (the outer ValueError), and

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 | 🟡 Minor | ⚡ Quick win

Clarify the PUSH_EXC_INFO comment.

Line 3 contains the malformed phrase “the prev its matching,” which makes the documented restoration behavior difficult to understand. Rewrite it to state that POP_EXCEPT restores the value saved by the matching PUSH_EXC_INFO.

🤖 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/bench/synth/exception_nested_exc_info_restore.py` at line 3, Rewrite the
comment near the nested exception handling example to clearly state that
POP_EXCEPT restores the value saved by its matching PUSH_EXC_INFO, removing the
malformed “the prev its matching” wording.

# after the outer handler to None. Expected per-iteration signature
# 2*1 + 1*10 + 0*100 = 12 → 360000.
#
# Was `_pending/`: the JIT answered 3320000 (trait) / 3360000 (FBW walker)
# because the nested handlers' POP_EXCEPT restores were not lowered to the EC
# `sys_exc_value` slot — the in-handler `sys.exc_info()` may-force ended the
# authoritative walk, so the slot kept the inner exception after the handler
# exited. Both tracers now answer 360000, matching the interpreter and CPython.
import sys

N = 30000


def classify(t):
if t is ValueError:
return 1
if t is KeyError:
return 2
if t is None:
return 0
return 9


def run(n):
acc = 0
i = 0
while i < n:
try:
raise ValueError("outer")
except ValueError:
try:
raise KeyError("inner")
except KeyError:
acc += classify(sys.exc_info()[0]) * 1
acc += classify(sys.exc_info()[0]) * 10
acc += classify(sys.exc_info()[0]) * 100
i += 1
return acc


def main():
print(run(N))


main()
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# KNOWN FAILING on cranelift: aborts with
# Regression oracle. Was `_pending/`: cranelift aborted 8/10 to 9/10 runs with
# GC BUG: invalid type_id=... site=object_total_size
# from `gc_alloc_nursery_shim` -> `alloc_with_type` -> `do_collect_nursery` ->
# `incremental_mark_step`, i.e. a dangling nursery pointer on the MAJOR gray
# stack. dynasm and PYRE_JIT=0 are clean, which matches the known
# cranelift/wasm/Windows-only shape of that signature.
#
# Intermittent but high-rate - 8/10 to 9/10 runs - and needs no GC stress build.
# stack, while dynasm and the no-JIT run stayed clean. Now 20/20 clean on
# cranelift with the pinned output below.
#
# REFUTED lead, do not re-attempt: the unbarriered `(*frame).f_backref =
# saved_topframeref` in ResidualFrameChainGuard::enter looks exactly like the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@
# loop is JIT-compiled: the callee's traced iteration is committed concretely
# during recording AND re-applied at the trace->compile boundary.
#
# Expected: len(acc) == 2 * N. Under the bug the JIT prints 2*N + 3 (a constant
# Expected: len(acc) == 2 * N. Under the bug the JIT printed 2*N + 3 (a constant
# over-count, independent of N, present only once N crosses the compile
# threshold). Both backends share the trace/resume layer, so both diverge.
#
# Kept under _pending/ (excluded by check.py's non-recursive `*.py` glob) so it
# does not fail the gate while #14 is open. Run explicitly with:
# python3 pyre/check.py --synthetic-only --synthetic-pattern '_pending/loop_callee_shared_mutation.py'
# threshold), on both backends, which share the trace/resume layer.
N = 20000


Expand Down
24 changes: 24 additions & 0 deletions pyre/pyre-interpreter/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,30 @@ pub fn call_user_function(
call_user_function_with_eval(frame, callable, args, eval_fn)
}

/// Residual-call sibling of [`call_user_function_plain`] that keeps the
/// JIT-aware eval function.
///
/// `blackhole.py:1225 bhimpl_residual_call_r_i` is `cpu.bh_call_i(func, ...)`
/// — it invokes the *translated function*, and when that function's graph
/// reaches a `jit_merge_point` (`execute_frame` does) the JIT is entered
/// normally. "Opaque to the trace" does not mean "the JIT is off inside":
/// upstream has no flag that disables it for the extent of a residual call.
/// `bhimpl_recursive_call_*` (`blackhole.py:1095-1132`) is not the only way
/// to reach the portal — it is the path the codewriter emits when the callee
/// is *statically* the portal graph.
///
/// Re-entrant tracing is prevented where upstream prevents it, on the green
/// key: `warmstate.py:473-477` skips a hot back-edge while `JC_TRACING` is
/// set, which pyre mirrors with the `driver.is_tracing()` guard in
/// `maybe_compile_and_run`.
pub fn call_user_function_residual(
frame: &PyFrame,
callable: PyObjectRef,
args: &[PyObjectRef],
) -> PyResult {
call_user_function_with_eval(frame, callable, args, get_eval_fn())

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 Increment depth before entering the residual callee

When a compiled CALL residualizes a user function, this new entry bypasses the increment_call_depth() guard used by call_user_function; consequently eval_with_jit performs its entry stack_check() using the caller's depth. Residual or indirect recursion can therefore run past the configured Python recursion limit, and sys.setrecursionlimit() observes too few active frames. Wrap this call with the same depth guard as the ordinary user-function entry.

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

}

/// Plain interpreter-only user-function call.
///
/// JIT residual helpers should use this instead of the injected eval override.
Expand Down
15 changes: 15 additions & 0 deletions pyre/pyre-interpreter/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ thread_local! {
last_exec_ctx: crate::call::capture_last_exec_ctx_cell(),
import_roots: crate::importing::capture_import_root_area(),
mapdict_method_cache: crate::pycode::capture_mapdict_method_cache_root_area(),
w_globals_stamped_codes: crate::pycode::capture_w_globals_stamped_code_root_area(),
in_flight_exception: IN_FLIGHT_EXCEPTION.with(|cell| cell as *const _),
bh_last_exception: majit_metainterp::blackhole::BH_LAST_EXC_VALUE
.with(|cell| cell as *const _),
Expand All @@ -64,6 +65,7 @@ struct PyFrameRootArea {
last_exec_ctx: *const (),
import_roots: *const (),
mapdict_method_cache: *const (),
w_globals_stamped_codes: *const (),
in_flight_exception: *const Cell<PyObjectRef>,
bh_last_exception: *const Cell<i64>,
guard_exception: *const Cell<i64>,
Expand Down Expand Up @@ -944,6 +946,19 @@ pub unsafe fn walk_pyframe_roots_area(
area.mapdict_method_cache,
&mut forward,
);
// `pycode.py:159-165 frame_stores_global` stamps `w_globals`
// permanently, and upstream traces it through the GC-managed
// `PyCode`. Box-immortal code objects are reached only when
// `walk_raw_code_roots` runs on a `frame.pycode` / `func.code`
// that happens to be walked, so a stamped code object sitting
// off the frame chain keeps a pre-move address once its
// globals dict is promoted. Forward every stamped slot.
crate::pycode::walk_w_globals_stamped_code_root_area(
area.w_globals_stamped_codes,
&mut |slot| {
visitor(&mut *(slot as *mut PyObjectRef as *mut majit_ir::GcRef));
},
);
}
}
}
Expand Down
Loading
Loading