Skip to content

Commit 71782cf

Browse files
committed
jit: close gh#152 deep-kept operand-stack resume gap (RPython register/liveness channel) (#436)
* majit: route successful-compile trace teardown to abort_trace_live Four trace-close success edges in JitDriver called abort_trace(false), which runs both the live history teardown and aborted_tracing accounting. On a successful compile the accounting must not fire: raise_if_successful() raises ContinueRunningNormally (pyjitpl.py:3095-3123), bypassing the `except SwitchToBlackhole` handler that reaches aborted_tracing, and the loop/bridge counter was already bumped at backend-compile time. Calling the accounting half here bumped stats.loops_aborted on success and double-counted the re-observe edge. Change the four success edges (CompileTrace arm, the bridge Compiled close, the CloseLoopWithArgs bridge Compiled close, and the take_compile_trace_success re-observe) to abort_trace_live(false) so only the live teardown runs. loops_aborted now reads 0 on nbody/fannkuch/ spectral_norm/nested_loop; loops_compiled, bridges_compiled, and guard_failures are unchanged. Assisted-by: Claude * synth: correct mutation-count comment in kept_stack_deep_var_shortcircuit_mutate The loop appends three times per iteration (g, h, and one conditional g/h), so len(log) for f(40000) is 120000, not the 80000 the "2 mutations per iteration" comment implied. State the 3x invariant. Assisted-by: Claude * jit: deep-kept operand-stack resume via register/liveness channel; delete FOR_ITER hazard gate Restore deep kept operand-stack slots at a branch-guard resume from the guard-PC register file instead of the pop-cleared vable positional shadow, porting get_list_of_active_boxes (rpython/jit/metainterp/pyjitpl.py:177-234), which captures resume boxes from registers_r[index] via the per-PC -live- set. In walker_capture_snapshot_for_last_guard_impl (jitcode_dispatch.rs), a kept operand slot the walk mirror (ctx.vstack_boxes) leaves NULL across a not-taken branch merge is recovered from ctx.registers_r[color], where color is the pcdep_color_slots[guard_py_pc] inverse (new state::semantic_slot_color_for_ref_slot). The recovery is capture-only (transient snapshot overlay, never the live shadow) and fills only Type::Ref slots; an unboxed-int kept temp is left a hole. Delete the foriter_deep_kept_call_hazard gate (codewriter.rs) that withheld the FOR_ITER per-iteration getarrayitem_vable_r reload for loop bodies with a CALL across an in-body conditional branch. The reload is now unconditional for portals, matching w_iterator = self.peekvalue() (pypy/interpreter/pyopcode.py:1303). kept_stack_deep_var_{condexpr,nested_call,shortcircuit} and the shortcircuit mutate canary run resident without the deep-guard SIGSEGV; check.py 155/155 on dynasm and cranelift. Assisted-by: Claude * jit: rustfmt the FOR_ITER iterator-reload block Reformat the `iter_value` binding after the `foriter_deep_kept_call_hazard` gate removal collapsed the condition to a single `if is_portal`. No logic change; `cargo fmt --check` clean. Assisted-by: Claude * majit: clear pending_abort payload on successful trace teardown The four successful-compile teardown edges (CompileTrace, both bridge-Compiled paths, and the re-observed-success path) call abort_trace_live(false) for live cleanup but fire no aborted_tracing, so the pending_abort_green_key/permanent that abort_trace_live stages was never consumed. A later unrelated abort would then take() that stale key and fire on_trace_abort for the earlier successfully-compiled greenkey. Add MetaInterp::clear_pending_abort() and call it on each success edge, matching the existing bridge-FINISH success path (pyjitpl.rs) which already cleared these fields inline for the same reason. No aborted_tracing follows on success (raise_if_successful raises ContinueRunningNormally, pyjitpl.py:3095-3123). check.py 155/155 dynasm and cranelift; aborted_tracing tests pass. Assisted-by: Claude * interp: type(x) one-arg __init__ skip + null-mro subtype base-chain fallback type.__call__ suppresses __init__ when self is the `type` builtin with no keyword arguments and exactly one positional argument (type(x) returns the class of x from __new__). Add `type_call_type_x_shortcut` and gate the three plain-instantiation __init__ dispatch sites in call.rs on it (typeobject.py:735-736). w_type_issubtype falls back to a find_best_base base-chain walk when mro_w is null instead of returning false, matching _issubtype_slow_and_wrong for a partially initialised type (typeobject.py:1640-1655, find_best_base 1335-1354; the "incomplete type" raise is omitted since the fn returns bool). Assisted-by: Claude * jit: deep-kept Int-bank recovery channel (PYRE_FBW_DEEPKEPT_INT, default OFF) Port the i-bank half of get_list_of_active_boxes (pyjitpl.py:206-210, add_box_to_storage(registers_i[index])) to the deep-kept operand-stack recovery in walker_capture_snapshot_for_last_guard_impl. A bank-0 (Int) stack pcdep entry names the Int-bank color owning the slot; registers_i[color] holds the raw int, boxed into a W_IntObject via wrapint so the uniformly Ref-typed vable array carries a Ref. Generalize semantic_slot_color_for_ref_slot into a bank-generic semantic_slot_color_for_slot core; add semantic_slot_color_for_int_slot (bank=0) alongside the ref (bank=1) delegate. Gated default-OFF: flag-off is byte-identical to leaving the int a hole (resume re-materializes it from its defining IR). On the current frontend the operand stack is uniformly Ref-banked so no bank-0 stack entry exists and the block fires nowhere; the real int-hole (a Ref-bank color whose OpRef is Int-typed) cannot be boxed at capture time — wrapint emits into a settled trace and trips store_final_boxes_in_guard (resume.py:397) — so it must be synthesized at operand-stack push time, a frontend change tracked separately. This is the RPython-parity read channel for when bank-0 stack entries exist. Assisted-by: Claude
1 parent b7b559b commit 71782cf

8 files changed

Lines changed: 366 additions & 111 deletions

File tree

majit/majit-metainterp/src/jitdriver.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,7 +1783,18 @@ impl<S: JitState> JitDriver<S> {
17831783
match action {
17841784
TraceAction::Continue => {}
17851785
TraceAction::CompileTrace => {
1786-
self.meta.abort_trace(false);
1786+
// Successful compile-into-existing-target: raise_if_successful()
1787+
// raises ContinueRunningNormally (pyjitpl.py:3095-3123), which
1788+
// bypasses the `except SwitchToBlackhole` handler, so only the
1789+
// live history teardown runs — `aborted_tracing` accounting is
1790+
// NOT reached on success (the loop/bridge counter was already
1791+
// bumped at backend-compile time). Call the live-teardown half
1792+
// only, not the accounting half.
1793+
self.meta.abort_trace_live(false);
1794+
// No aborted_tracing follows on success, so drop the
1795+
// pending_abort_* payload abort_trace_live staged (else it
1796+
// attaches this key to a later, unrelated abort's hook).
1797+
self.meta.clear_pending_abort();
17871798
self.sym = None;
17881799
self.meta.clear_trace_session();
17891800
self.compile_trace_success = true;
@@ -1906,7 +1917,11 @@ impl<S: JitState> JitDriver<S> {
19061917
continue_running_normally_values,
19071918
None,
19081919
);
1909-
self.meta.abort_trace(false);
1920+
// Success edge: live teardown only, no
1921+
// aborted_tracing accounting (see the
1922+
// CompileTrace arm above).
1923+
self.meta.abort_trace_live(false);
1924+
self.meta.clear_pending_abort();
19101925
return;
19111926
}
19121927
// pyjitpl.py:2993-3007: after retrace_needed(),
@@ -2060,7 +2075,11 @@ impl<S: JitState> JitDriver<S> {
20602075
continue_running_normally_values,
20612076
loop_header_pc,
20622077
);
2063-
self.meta.abort_trace(false);
2078+
// Success edge: live teardown only, no
2079+
// aborted_tracing accounting (see the
2080+
// CompileTrace arm above).
2081+
self.meta.abort_trace_live(false);
2082+
self.meta.clear_pending_abort();
20642083
return;
20652084
}
20662085
// pyjitpl.py:2993-3007: after retrace_needed(),
@@ -2440,7 +2459,12 @@ impl<S: JitState> JitDriver<S> {
24402459
let (continue_running_normally_values, continue_running_normally_pc) = self
24412460
.take_continue_running_normally_payload()
24422461
.map_or((None, None), |(values, pc)| (Some(values), pc));
2443-
self.meta.abort_trace(false);
2462+
// Re-observing a success the CompileTrace arm already tore down
2463+
// (compile_trace_success flag). Live teardown only — the
2464+
// accounting must not fire (it would double-count a single
2465+
// successful compile as two aborts).
2466+
self.meta.abort_trace_live(false);
2467+
self.meta.clear_pending_abort();
24442468
self.sym = None;
24452469
self.meta.clear_trace_session();
24462470
return Some(DetailedDriverRunOutcome::Jump {

majit/majit-metainterp/src/pyjitpl.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6982,6 +6982,20 @@ impl<M: Clone> MetaInterp<M> {
69826982
self.clear_trace_session();
69836983
}
69846984

6985+
/// Drop the `pending_abort_*` payload staged by `abort_trace_live`.
6986+
///
6987+
/// `abort_trace_live` always stages `(green_key, permanent)` for the
6988+
/// `aborted_tracing` call that normally follows on the `SwitchToBlackhole`
6989+
/// unwind. A *successful* compile teardown runs `abort_trace_live` for its
6990+
/// live cleanup but fires no `aborted_tracing` (raise_if_successful raises
6991+
/// `ContinueRunningNormally`, pyjitpl.py:3095-3123), so the staged key must
6992+
/// be dropped here — leaving it would attach this successfully-compiled
6993+
/// key to a later, unrelated abort's `on_trace_abort` hook.
6994+
pub fn clear_pending_abort(&mut self) {
6995+
self.pending_abort_green_key = None;
6996+
self.pending_abort_permanent = false;
6997+
}
6998+
69856999
/// Finish the current trace with a terminal `FINISH`, then optimize and compile it.
69867000
///
69877001
/// `exit_with_exception` selects the FINISH descr per `pyjitpl.py`:
@@ -12575,8 +12589,7 @@ impl<M: Clone> MetaInterp<M> {
1257512589
// fires, so letting stale greenkey linger would
1257612590
// attach this successfully-compiled bridge's key
1257712591
// to a later, unrelated abort.
12578-
self.pending_abort_green_key = None;
12579-
self.pending_abort_permanent = false;
12592+
self.clear_pending_abort();
1258012593
Ok(())
1258112594
}
1258212595
// pyjitpl.py:3220/:3245 `compile.giveup()` per

pyre/bench/synth/kept_stack_deep_var_shortcircuit_mutate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# (a non-journaled STORE_SUBSCR-class heap effect committed inside a user
55
# frame). A FOR_ITER trace that consumes the iterator, aborts on the deep
66
# kept-stack guard, and then DELIVERS the in-flight item would re-run the body
7-
# and DOUBLE the mutation. The `log` length must equal the iteration count
8-
# exactly (2 mutations per iteration): a doubled delivery over-counts, a
9-
# dropped iteration under-counts.
7+
# and DOUBLE the mutation. The `log` length must equal 3x the iteration count
8+
# exactly (g, h, and one conditional g/h append per iteration): a doubled
9+
# delivery over-counts, a dropped iteration under-counts.
1010
log = []
1111

1212

pyre/pyre-interpreter/src/call.rs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,20 +2041,20 @@ pub fn call_with_kwargs(
20412041
let instance_slot = pyre_object::gc_roots::shadow_stack_len();
20422042
pyre_object::gc_roots::pin_root(instance);
20432043
// Step 2: __init__(self, *args, **kwargs) with full kwargs support.
2044-
if let Some(w_insttype) = type_call_init_type(instance, callable) {
2045-
if let Some(init_fn) =
2044+
if let Some(w_insttype) = type_call_init_type(instance, callable)
2045+
&& !type_call_type_x_shortcut(callable, pos_args.len(), kwargs.is_empty())
2046+
&& let Some(init_fn) =
20462047
unsafe { crate::baseobjspace::lookup_in_type(w_insttype, "__init__") }
2047-
{
2048-
let mut init_args = Vec::with_capacity(1 + pos_args.len());
2049-
init_args.push(instance);
2050-
init_args.extend_from_slice(pos_args);
2051-
let init_result = if unsafe { crate::is_function(init_fn) } && !kwargs.is_empty() {
2052-
call_with_kwargs(frame, init_fn, &init_args, kwargs)?
2053-
} else {
2054-
call_callable(frame, init_fn, &init_args)?
2055-
};
2056-
check_init_returned_none(init_result)?;
2057-
}
2048+
{
2049+
let mut init_args = Vec::with_capacity(1 + pos_args.len());
2050+
init_args.push(instance);
2051+
init_args.extend_from_slice(pos_args);
2052+
let init_result = if unsafe { crate::is_function(init_fn) } && !kwargs.is_empty() {
2053+
call_with_kwargs(frame, init_fn, &init_args, kwargs)?
2054+
} else {
2055+
call_callable(frame, init_fn, &init_args)?
2056+
};
2057+
check_init_returned_none(init_result)?;
20582058
}
20592059
return Ok(pyre_object::gc_roots::shadow_stack_get(instance_slot));
20602060
}
@@ -2363,7 +2363,9 @@ fn type_descr_call_impl(w_type: PyObjectRef, args: &[PyObjectRef]) -> PyObjectRe
23632363
// Step 2: __init__ — only if __new__ returned an instance of w_type.
23642364
// PyPy checks the Python-level type(instance), so builtin-layout subtypes
23652365
// like set subclasses still run __init__.
2366-
if let Some(w_insttype) = type_call_init_type(instance, w_type) {
2366+
if let Some(w_insttype) = type_call_init_type(instance, w_type)
2367+
&& !type_call_type_x_shortcut(w_type, args.len(), true)
2368+
{
23672369
if let Some(init_fn) =
23682370
unsafe { crate::baseobjspace::lookup_in_type(w_insttype, "__init__") }
23692371
{
@@ -2415,6 +2417,14 @@ fn type_call_init_type(instance: PyObjectRef, w_type: PyObjectRef) -> Option<PyO
24152417
}
24162418
}
24172419

2420+
/// typeobject.py:735-736 — the `type(x)` shortcut: `type.__call__` skips
2421+
/// __init__ when self is the `type` builtin, there are no keyword
2422+
/// arguments, and exactly one positional argument (`type(x)` returns the
2423+
/// class of x, already produced by __new__).
2424+
fn type_call_type_x_shortcut(w_type: PyObjectRef, nargs: usize, no_kwargs: bool) -> bool {
2425+
no_kwargs && nargs == 1 && std::ptr::eq(w_type, crate::typedef::w_type())
2426+
}
2427+
24182428
/// Pointer-based subtype check for descr_call __init__ guard — the MRO
24192429
/// membership scan lives in `pyre_object::w_type_issubtype`.
24202430
fn issubtype_ptr(w_type: PyObjectRef, cls: PyObjectRef) -> bool {
@@ -3627,7 +3637,9 @@ fn type_descr_call_with_mode(
36273637

36283638
// Step 2: __init__ — only if __new__ returned an instance of w_type.
36293639
// PyPy: descr_call — skips __init__ when __new__ returns a foreign type.
3630-
if let Some(w_insttype) = type_call_init_type(instance, w_type) {
3640+
if let Some(w_insttype) = type_call_init_type(instance, w_type)
3641+
&& !type_call_type_x_shortcut(w_type, args.len(), true)
3642+
{
36313643
if let Some(init_fn) =
36323644
unsafe { crate::baseobjspace::lookup_in_type(w_insttype, "__init__") }
36333645
{

pyre/pyre-jit-trace/src/jitcode_dispatch.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7329,6 +7329,28 @@ pub(crate) fn fbw_vable_scalar_ca_enabled() -> bool {
73297329
})
73307330
}
73317331

7332+
/// `PYRE_FBW_DEEPKEPT_INT` (default OFF) — extend the deep-kept operand-stack
7333+
/// recovery in [`walker_capture_snapshot_for_last_guard_impl`] to fill an
7334+
/// UNBOXED-INT hole from the Int register bank. A Ref-typed stack slot
7335+
/// semantically holding an Int (e.g. condexpr's `a+i` `IntAdd` result) is left
7336+
/// a hole by the Ref-only fill; when on, the capture reads `registers_i[color]`
7337+
/// (the color named by `semantic_slot_color_for_int_slot`) and boxes the raw
7338+
/// int into a `W_IntObject` (`wrapint`) so the vable array carries a Ref.
7339+
/// Ports the `if length_i:` i-bank section of `get_list_of_active_boxes`
7340+
/// (`pyjitpl.py:206-210`). Default OFF: flag-off is byte-identical to the
7341+
/// int-as-hole behavior (resume re-materializes the int from its defining IR),
7342+
/// mirroring how the S1 mirror extension staged behind `PYRE_FBW_DEEPKEPT_MIRROR`.
7343+
pub(crate) fn deepkept_int_recovery_enabled() -> bool {
7344+
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7345+
*ENABLED.get_or_init(|| match std::env::var_os("PYRE_FBW_DEEPKEPT_INT") {
7346+
Some(v) => {
7347+
let v = v.to_string_lossy();
7348+
v != "0" && !v.eq_ignore_ascii_case("false")
7349+
}
7350+
None => false,
7351+
})
7352+
}
7353+
73327354
/// `PYRE_FBW_RAISE` (default ON) — the FBW walker owns the Python raise/except
73337355
/// loop. The twin NULL-ref guards exempt the trailing `cause` sentinel of a
73347356
/// [`PyreHelperKind::RaiseVarargs`] residual so the walker records the raise.
@@ -10166,6 +10188,145 @@ fn walker_capture_snapshot_for_last_guard_impl(
1016610188
} else {
1016710189
Vec::new()
1016810190
};
10191+
// Recover a kept operand-stack slot the walk mirror lost across a
10192+
// not-taken branch merge. At a branch guard whose not-taken arm
10193+
// keeps a CALL result deep on the operand stack
10194+
// (`t=(g(i),h(i),(g(i) if p or q else h(i)))`), the walk took the
10195+
// OTHER arm, so `ctx.vstack_boxes[s]` is a `PY_NULL` hole for the
10196+
// deep slot and the `stack_sync` overlay above omits it — the
10197+
// resumed vable array then reads that slot NULL and the interpreted
10198+
// body dereferences it (SIGSEGV). The trampoline edge-move recovery
10199+
// (`resolved_recovered`) is empty for this shape, so the mirror is
10200+
// the only kept-stack source and it has a hole.
10201+
// Fill the hole from the guard-PC register file: the per-PC
10202+
// `pcdep_color_slots` map at `guard_py_pc` names the Ref-bank color
10203+
// that holds operand-stack slot `nlocals + s` at THIS guard PC (the
10204+
// same authoritative inversion `collect_outer_active_boxes` reads),
10205+
// and `ctx.registers_r[color]` holds the live guard-state box. This
10206+
// is the guard-PC color read (as `resolved_recovered` does for
10207+
// `registers_r[src]`), NOT the retired stale merge-color read.
10208+
// This ports `get_list_of_active_boxes`
10209+
// (rpython/jit/metainterp/pyjitpl.py:177-234), which captures guard
10210+
// resume boxes from `registers_r[index]` via the per-PC `-live-`
10211+
// set.
10212+
// Capture-only: writes the transient snapshot overlay, never the live
10213+
// shadow (the trace_opcode.rs:2218 bridge-NULL constraint holds).
10214+
let stack_sync: Vec<(usize, OpRef)> = if guard_py_pc.is_some()
10215+
&& sym.owns_virtualizable_shadow()
10216+
&& !sym.jitcode.is_null()
10217+
{
10218+
let gpc = guard_py_pc.unwrap() as usize;
10219+
let nlocals = sym.nlocals;
10220+
let nvs = crate::virtualizable_gen::NUM_VABLE_SCALARS;
10221+
let depth = unsafe {
10222+
let jc = &*sym.jitcode;
10223+
if jc.payload.code_ptr.is_null() {
10224+
0usize
10225+
} else {
10226+
crate::liveness::liveness_for(jc.payload.code_ptr)
10227+
.depth_at_py_pc()
10228+
.get(gpc)
10229+
.copied()
10230+
.unwrap_or(0) as usize
10231+
}
10232+
};
10233+
let pcdep: Vec<(u8, u16, u16)> = unsafe {
10234+
let jc = &*sym.jitcode;
10235+
jc.payload
10236+
.metadata
10237+
.pcdep_color_slots
10238+
.get(gpc)
10239+
.cloned()
10240+
.unwrap_or_default()
10241+
};
10242+
let mut covered: std::collections::HashSet<usize> =
10243+
stack_sync.iter().map(|&(idx, _)| idx).collect();
10244+
let mut augmented = stack_sync;
10245+
for s in 0..depth {
10246+
let vidx = nvs + nlocals + s;
10247+
if covered.contains(&vidx) {
10248+
continue;
10249+
}
10250+
// Guard-PC Ref color that owns operand-stack slot
10251+
// `nlocals + s` (`get_list_of_active_boxes` `if length_r:`
10252+
// section, `pyjitpl.py:211-215`).
10253+
if let Some(color) =
10254+
crate::state::semantic_slot_color_for_ref_slot(&pcdep, nlocals + s)
10255+
{
10256+
if let Some(&box_op) = ctx.registers_r.get(color) {
10257+
// Only a genuine Ref box may fill an operand-stack
10258+
// slot: the vable array is uniformly Ref-typed, and
10259+
// `build_vable_snapshot_boxes` reads each entry's
10260+
// `OpRef::ty()`. An unboxed-int kept temp (a `Ref`-
10261+
// bank color holding an `IntAdd` result, e.g.
10262+
// condexpr's `a+i`) is Int-typed and would decode as
10263+
// `Box type Int != expected Ref`; it needs a
10264+
// `NEW_W_INT` box the capture cannot synthesize
10265+
// (boxing here emits into a settled trace →
10266+
// `store_final_boxes_in_guard` panic), so leave it a
10267+
// hole (the mirror already sourced every restorable
10268+
// slot). The int-bank recovery below handles the
10269+
// bank-0 channel where a raw int lives in the Int
10270+
// register file instead.
10271+
if box_op != OpRef::NONE
10272+
&& !opref_is_null_const_ptr(box_op)
10273+
&& box_op.ty() == Some(majit_ir::Type::Ref)
10274+
{
10275+
augmented.push((vidx, box_op));
10276+
covered.insert(vidx);
10277+
}
10278+
}
10279+
}
10280+
// Int-bank fill (`get_list_of_active_boxes` `if length_i:`
10281+
// section, `pyjitpl.py:206-210` —
10282+
// `add_box_to_storage(self.registers_i[index])`), gated
10283+
// `PYRE_FBW_DEEPKEPT_INT` (default OFF). Ref precedence:
10284+
// only fill a slot the Ref bank left a hole. A bank-0 (Int)
10285+
// pcdep entry names the Int-bank color owning slot
10286+
// `nlocals + s`, and `registers_i[color]` holds the raw
10287+
// unboxed int; box it into a `W_IntObject` (`wrapint`, the
10288+
// `NewWithVtable` + `SetfieldGc(intval)` pair
10289+
// `materialize_loop_carried_value` emits for a Ref-typed
10290+
// loop-carried slot) so the uniformly Ref-typed vable array
10291+
// carries a Ref, not a raw Int the resume decode would
10292+
// reject as `Box type Int != expected Ref`. Default OFF:
10293+
// flag-off leaves the int a hole, byte-identical to today
10294+
// (resume re-materializes it from its defining IR).
10295+
//
10296+
// Scaffolding on the current frontend: pyre's operand stack
10297+
// is uniformly Ref-banked in the pcdep map
10298+
// (`locals_cells_stack_w` is a `W_Root[]` array), so no
10299+
// bank-0 stack entry exists yet and this fires nowhere. The
10300+
// real int-hole source — a Ref-bank color whose
10301+
// `registers_r[color]` OpRef is itself Int-typed — CANNOT be
10302+
// boxed here: `wrapint` emits `NewWithVtable` + `SetfieldGc`
10303+
// into an already-settled trace at capture time, which trips
10304+
// `store_final_boxes_in_guard` (`resume.py:397`
10305+
// `resume_position >= 0`). The box must be synthesized at
10306+
// operand-stack PUSH time, not lazily at snapshot capture —
10307+
// that is a frontend change beyond this capture hook (a
10308+
// bank-0 channel, e.g. the tagged-int epic, or a push-time
10309+
// NEW_W_INT), tracked as a follow-up. This literal i-bank
10310+
// read stays as the RPython-parity channel for when bank-0
10311+
// stack entries do exist.
10312+
if deepkept_int_recovery_enabled() && !covered.contains(&vidx) {
10313+
if let Some(color) =
10314+
crate::state::semantic_slot_color_for_int_slot(&pcdep, nlocals + s)
10315+
{
10316+
if let Some(&raw) = ctx.registers_i.get(color) {
10317+
if raw != OpRef::NONE && raw.ty() == Some(majit_ir::Type::Int) {
10318+
let boxed = crate::state::wrapint(ctx.trace_ctx, raw);
10319+
augmented.push((vidx, boxed));
10320+
covered.insert(vidx);
10321+
}
10322+
}
10323+
}
10324+
}
10325+
}
10326+
augmented
10327+
} else {
10328+
stack_sync
10329+
};
1016910330
let saved_shadow: Vec<(usize, Option<OpRef>)> = stack_sync
1017010331
.iter()
1017110332
.map(|&(idx, _)| (idx, ctx.trace_ctx.virtualizable_box_at(idx)))

0 commit comments

Comments
 (0)