From 03562de9923a0aedf882d4faeef0fecdc6c12e28 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 14:41:59 +0900 Subject: [PATCH 1/6] jit: set the vstack reorder mask on every operand-stack store The mask records which slot the walk executed a store into, which the boundary restore then keeps in preference to its pc-derived snapshot. It was set only inside the method-form LOAD_ATTR branch, so a reorder window whose only executed store came from another opcode reported an empty mask and the restore replaced the whole mirror. Move the mask update out of that branch; the positional mirror write stays scoped to the method form. Measured over 444 bench programs: 28 slots where the virtualizable shadow agreed with the mirror and the restore overwrote it, of which 8 are covered by this change. Output is byte-identical across the corpus with a same-length-name decoy arm as the control. Assisted-by: Claude --- .../src/jitcode_dispatch/vable_ops.rs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs index 098bd1f109a..e6a51fc3367 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs @@ -870,22 +870,32 @@ pub(crate) fn setarrayitem_vable_via_metainterp( _ => false, }) }); + let stack_slot = (index_value - nlocals) as usize; if method_load { - let stack_slot = (index_value - nlocals) as usize; if ctx.vstack_boxes.len() <= stack_slot { ctx.vstack_boxes.resize(stack_slot + 1, OpRef::NONE); } ctx.vstack_boxes[stack_slot] = value; - // Mark the slot as execution-derived while an out-of-order - // region is armed, so the boundary restore does not put its - // pc-derived snapshot back over it. Outside a region the - // mask does not exist and this costs one `Option` test. - if let Some((_, _, _, mask)) = ctx.vstack_reorder_saved.as_mut() { - if mask.len() <= stack_slot { - mask.resize(stack_slot + 1, false); - } - mask[stack_slot] = true; + } + // Mark the slot as execution-derived while an out-of-order region + // is armed, so the boundary restore does not put its pc-derived + // snapshot back over it. Outside a region the mask does not exist + // and this costs one `Option` test. + // + // This is a property of the STORE, not of the opcode that issued + // it: the mask records which slot the walk demonstrably wrote, and + // that is the same fact whatever the surrounding opcode is. Only + // the positional mirror write above is method-form specific, and + // for the reason its own comment gives. Scoping the mask to that + // opcode too left every other push shape unprotected, so a window + // whose only executed store came from, say, `LOAD_NAME` or + // `BINARY_OP` reported an empty mask and the restore reinstated the + // pre-window mirror wholesale over it. + if let Some((_, _, _, mask)) = ctx.vstack_reorder_saved.as_mut() { + if mask.len() <= stack_slot { + mask.resize(stack_slot + 1, false); } + mask[stack_slot] = true; } ctx.vstack_last_ref = value; } From d750a7ef7df7a5609d2797db5fa1a31148ee0524 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 14:41:59 +0900 Subject: [PATCH 2/6] jit: decline the pcdep capture augment where the shadow holds a live Ref The per-PC color map fills operand-stack slots the walk mirror left as holes. It did not consult the virtualizable shadow, so it also replaced slots the shadow carried: omitting a slot leaves the shadow's own value standing, and every push and pop writes the shadow, including a NULL push and a pop's clear. Measured over 444 bench programs: 14 branch-guard captures published a box disagreeing with a live shadow Ref, all 14 sourced from this augment and none from the mirror. Comparing concrete values shows the two boxes are different objects in all 14, and the published ConstPtr is invariant across captures while the shadow box varies. Output is byte-identical across the corpus with a decoy arm. Assisted-by: Claude --- .../src/jitcode_dispatch/resume_snapshot.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs index 4bb42c59a6b..e811b76ed68 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs @@ -828,9 +828,30 @@ pub(crate) fn walker_capture_snapshot_for_last_guard_impl( // (`resume.py`). The orthodox fix is // push-time boxing plus optimizer virtualization, // beyond this capture hook. + // Decline wherever the shadow already carries a + // live Ref for this slot. Omitting a slot leaves + // the shadow's own value standing in the snapshot, + // and the shadow is the executed-store record — + // every push and pop writes it, including a NULL + // push (`emit_pushvalue_ref_const!`) and a pop's + // clear (`emit_popvalue_ref!`). The per-PC color + // map is a program-point LABEL rather than a + // binding, so it must fill a slot neither the + // mirror nor the shadow can source, not outrank a + // slot the walk demonstrably stored: the label is + // fixed per PC while the stored box varies per + // capture, so overriding published one invariant + // box over several different live ones. + let shadow_has_live_ref = + ctx.trace_ctx.virtualizable_box_at(vidx).is_some_and(|b| { + b != OpRef::NONE + && !opref_is_null_const_ptr(b) + && b.ty() == Some(majit_ir::Type::Ref) + }); if box_op != OpRef::NONE && !opref_is_null_const_ptr(box_op) && box_op.ty() == Some(majit_ir::Type::Ref) + && !shadow_has_live_ref { augmented.push((vidx, box_op)); covered.insert(vidx); From 81c89e3724637e5c6a9c2308b52a63dcc5d09ac7 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 14:42:11 +0900 Subject: [PATCH 3/6] jit: record the exact jitcode-offset to Python-PC segmentation The vstack mirror resolves which Python opcode owns a jitcode offset through py_floor_by_jit_pc, which is derived from the first-offset-per-PC table. A PC that emits in two disjoint regions keeps only the earlier one, so a floor segment's extent is not one opcode's byte range. Three emission shapes produce that: a can-raise trailing marker re-keyed to the call's fallthrough PC, a block opened at an already-merged PC and drained after later PCs, and a mid-opcode block split. Record the owning PC at every point the emitted stream's PC changes, translate it through the existing batched insn-index to byte-offset call, and ship it as py_exact_by_jit_pc. One entry per contiguous run, so it is the same order of size as the floor table. Nothing reads the new table outside PYRE_VSTACK_EXACT_AUDIT, which reports where it disagrees with the floor tier and asserts the table's own integrity at build time. Three places handle insn-index side tables and they guarantee different things: stream_positions_mut covers the shifts a splice pass applies, the assignment after the splice replaces the stream wholesale, and remove_repeated_live deletes ops and remaps. A table registered only with the first stays empty after the second and addresses past the shortened stream after the third. Comment each. Over 442 bench programs, 1075 jitcodes: no crashes, and the audit reports 303 disagreements at boundaries the mirror acts on. Assisted-by: Claude --- .../src/jitcode_dispatch/diag.rs | 11 ++++ .../src/jitcode_dispatch/vstack_mirror.rs | 24 +++++++++ pyre/pyre-jit-trace/src/pyjitcode.rs | 25 +++++++++ pyre/pyre-jit-trace/src/state.rs | 1 + pyre/pyre-jit/src/jit/codewriter.rs | 52 +++++++++++++++++++ pyre/pyre-jit/src/jit/flatten.rs | 29 +++++++++++ 6 files changed, 142 insertions(+) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs index 904e7c974fb..0bf0cac2d89 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs @@ -28,6 +28,17 @@ pub(crate) fn pcmap_containing_audit_enabled() -> bool { *ENABLED.get_or_init(|| std::env::var_os("PYRE_PCMAP_CONTAINING_AUDIT").is_some()) } +/// `PYRE_VSTACK_EXACT_AUDIT`: report where the per-emission exact segmentation +/// (`py_exact_by_jit_pc`) disagrees with the floor tier the vstack mirror +/// currently resolves its boundary from. The mirror INFERS whether a Python +/// opcode retired; the exact table states it. Every disagreement is a case the +/// inference must special-case, so this is the gate that has to be quiet +/// before any consumer switches over. Off in production. +pub(crate) fn vstack_exact_audit_enabled() -> bool { + static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); + *ENABLED.get_or_init(|| std::env::var_os("PYRE_VSTACK_EXACT_AUDIT").is_some()) +} + /// `PYRE_PCMAP_AFTERRESIDUAL_AUDIT`: assert the Slice-C after-residual depth /// twin (`depth_after_residual_for_jitcode_pc`) equals the raw /// `depth_at_py_pc[semantic_fallthrough_pc(containing_py_pc_for_jitcode_pc(jit_pc))]` diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index 318515775bf..50113f3a83b 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -1047,6 +1047,30 @@ pub(crate) fn step_vstack_mirror(ctx: &mut WalkContext<'_, '_, Sym } else { raw_depth() }; + // Stage gate for the per-emission exact segmentation. The + // boundary the mirror acts on is currently INFERRED — `py_pc` is + // the floor segment's owner, and whether an opcode retired is then + // guessed from (prev, new, depth). `py_exact_by_jit_pc` answers it + // directly. Compare the two at every step before any consumer + // trusts the new table: a disagreement is a boundary the floor tier + // reports differently from the emission record, and each one is a + // case the inference has to special-case. + if vstack_exact_audit_enabled() { + if let Some(exact) = crate::pyjitcode::exact_py_pc_for_jitcode_pc( + &jc.payload.metadata.py_exact_by_jit_pc, + jit_pc, + ) && exact != py_pc + { + let code = &*jc.payload.code_ptr; + eprintln!( + "[vstack-exact] code={} jit_pc={jit_pc} floor_py={py_pc} \ + exact_py={exact} cur_py={} boundary={}", + code.obj_name, + ctx.vstack_cur_pypc, + py_pc != ctx.vstack_cur_pypc, + ); + } + } (py_pc, jc.payload.code_ptr, depth) } }; diff --git a/pyre/pyre-jit-trace/src/pyjitcode.rs b/pyre/pyre-jit-trace/src/pyjitcode.rs index f3e824434a3..79dc495315c 100644 --- a/pyre/pyre-jit-trace/src/pyjitcode.rs +++ b/pyre/pyre-jit-trace/src/pyjitcode.rs @@ -89,6 +89,22 @@ pub struct PyJitCodeMetadata { /// predecessor op-start tier: exact block-head marker precedence remains in /// `block_head_py_by_jit_pc`. Empty for skeleton / fixture metadata. pub py_floor_by_jit_pc: Vec<(u32, u32)>, + /// JitCode byte-offset → owning Python PC, one entry per contiguous + /// emission run, sorted ascending by offset. + /// + /// `py_floor_by_jit_pc` above answers the same question from the + /// FIRST-offset-per-PC table, so it collapses a PC that emits in two + /// disjoint regions and its segment extent is not one opcode's byte range. + /// This table is recorded at emission instead, so consecutive entries + /// bracket exactly one opcode's bytes even when a block is drained out of + /// source order, a mid-opcode split reopens the same PC, or a can-raise + /// trailing marker is re-keyed to the call's fallthrough. + /// + /// That makes "did the walk leave the opcode it was in" answerable rather + /// than inferable: the PC owning `jit_pc` differing from the mirror's + /// current coordinate IS the opcode boundary. Empty for skeleton / fixture + /// metadata, in which case consumers must fall back to the floor tier. + pub py_exact_by_jit_pc: Vec<(u32, u32)>, /// Exact `abort_permanent` jitcode offset → the Python PC whose lowering /// emitted it, sorted ascending by offset for binary search. /// @@ -472,6 +488,14 @@ pub fn derive_resume_marker( /// Return the floor segment containing `jit_pc` in a codewriter-built JitCode /// PC pivot. An empty table is deliberately distinguishable from the `(0, 0)` /// fallback segment carried by every drained install. +/// Exact owning Python PC for a JitCode byte offset, from the per-emission +/// run table. `None` when the table is empty (skeleton / fixture metadata) so +/// the caller can fall back to the floor tier rather than read a bogus 0. +pub fn exact_py_pc_for_jitcode_pc(py_exact_by_jit_pc: &[(u32, u32)], jit_pc: usize) -> Option { + let end = py_exact_by_jit_pc.partition_point(|&(off, _)| (off as usize) <= jit_pc); + end.checked_sub(1).map(|idx| py_exact_by_jit_pc[idx].1) +} + pub fn floor_segment_for_jitcode_pc( py_floor_by_jit_pc: &[(u32, u32)], jit_pc: usize, @@ -1063,6 +1087,7 @@ impl PyJitCodeMetadata { n_py_instrs: 0, block_head_py_by_jit_pc: Vec::new(), py_floor_by_jit_pc: Vec::new(), + py_exact_by_jit_pc: Vec::new(), abort_permanent_py_pc_by_jit_pc: Vec::new(), merge_entry_by_green: Vec::new(), pcdep_by_jit_pc: Vec::new(), diff --git a/pyre/pyre-jit-trace/src/state.rs b/pyre/pyre-jit-trace/src/state.rs index 83a8072d682..9c9196d29ef 100644 --- a/pyre/pyre-jit-trace/src/state.rs +++ b/pyre/pyre-jit-trace/src/state.rs @@ -13655,6 +13655,7 @@ mod tests { n_py_instrs: 0, block_head_py_by_jit_pc: vec![(0, 0)], py_floor_by_jit_pc: Vec::new(), + py_exact_by_jit_pc: Vec::new(), abort_permanent_py_pc_by_jit_pc: Vec::new(), merge_entry_by_green: Vec::new(), pcdep_by_jit_pc: vec![(0, Vec::new())], diff --git a/pyre/pyre-jit/src/jit/codewriter.rs b/pyre/pyre-jit/src/jit/codewriter.rs index a5166dbf5f3..4321d822294 100644 --- a/pyre/pyre-jit/src/jit/codewriter.rs +++ b/pyre/pyre-jit/src/jit/codewriter.rs @@ -4929,6 +4929,14 @@ fn filter_liveness_in_place( for (pos, _) in ssarepr.abort_permanent_insn_pos.iter_mut() { *pos = remap[*pos]; } + // Same pre-merge stream, same rewrite. `compute_liveness` DELETES repeated + // `-live-` ops, so an unremapped position does not merely drift, it can + // address past the shortened stream and trip + // `insn_pos_to_byte_offset`'s range check. Collapsing two runs onto one + // position is expected here and is deduped when the byte table is built. + for (pos, _) in ssarepr.pc_run_insn_pos.iter_mut() { + *pos = remap[*pos]; + } let live_vars = pyre_jit_trace::state::liveness_for(code as *const _); let nlocals = code.varnames.len(); let live_markers_out = live_markers.clone(); @@ -14403,6 +14411,11 @@ impl CodeWriter { ssarepr.insns = spliced.insns; ssarepr.pc_first_insn_pos = spliced.pc_first_insn_pos; ssarepr.abort_permanent_insn_pos = spliced.abort_permanent_insn_pos; + // Every insn-index side table the splice rebuilt has to come back here. + // `stream_positions_mut` keeps a table in sync with the SHIFTS a pass + // applies, but this assignment replaces the stream wholesale, so a + // table left out silently keeps its pre-splice contents. + ssarepr.pc_run_insn_pos = spliced.pc_run_insn_pos; // Per-PC `-live-` marker indices feeding `filter_liveness_in_place` // (translated through its `remove_repeated_live` remap), and the // sparse after-residual-call resume anchors — both derived from the @@ -14750,10 +14763,13 @@ impl CodeWriter { // sparse group: `finish_with_positions_from` consumes `ssarepr`, so the // owning Python PCs are read off it here, before the call. let abort_permanent_some: Vec<(usize, i64)> = ssarepr.abort_permanent_insn_pos.clone(); + // PC-run starts ride the same translation as a fourth sparse group. + let pc_run_some: Vec<(usize, i64)> = ssarepr.pc_run_insn_pos.clone(); let mut combined_indices = pc_map.clone(); combined_indices.extend(after_call_some.iter().map(|(_, idx)| *idx)); combined_indices.extend(first_insn_some.iter().map(|(_, idx)| *idx)); combined_indices.extend(abort_permanent_some.iter().map(|(idx, _)| *idx)); + combined_indices.extend(pc_run_some.iter().map(|(idx, _)| *idx)); let (jitcode, combined_bytes) = { let mut asm = self.assembler.borrow_mut(); assembler.finish_with_positions_from(&mut *asm, ssarepr, &combined_indices, num_regs) @@ -14774,6 +14790,24 @@ impl CodeWriter { .collect(); abort_permanent_py_pc_by_jit_pc.sort_unstable_by_key(|&(off, _)| off); abort_permanent_py_pc_by_jit_pc.dedup_by_key(|&mut (off, _)| off); + // Exact jitcode-offset -> owning-Python-PC segmentation, one entry per + // contiguous emission run. Unlike `py_floor_by_jit_pc` below this is + // NOT derived from the first-offset-per-PC table, so a PC that emits in + // two disjoint regions keeps both, and the byte range between two + // consecutive entries is exactly one opcode's emission. + let pc_run_base = abort_permanent_base + abort_permanent_some.len(); + let mut py_exact_by_jit_pc: Vec<(u32, u32)> = pc_run_some + .iter() + .enumerate() + .filter_map(|(k, (_, py_pc))| { + let off = u32::try_from(combined_bytes[pc_run_base + k]).ok()?; + Some((off, u32::try_from(*py_pc).ok()?)) + }) + .collect(); + py_exact_by_jit_pc.sort_unstable_by_key(|&(off, _)| off); + // A marker splice can land two runs on one offset; the later-emitted + // one owns it, matching the floor tier's later-py-wins tie break. + py_exact_by_jit_pc.dedup_by_key(|&mut (off, _)| off); // `usize::MAX` = the PC emitted no jitcode of its own (trivia / // folded). This local build-time table seeds the floor and marker twins. let mut first_jit_pc_by_py_pc: Vec = vec![usize::MAX; pc_map.len()]; @@ -14839,6 +14873,23 @@ impl CodeWriter { if py_floor_by_jit_pc.first().is_none_or(|&(off, _)| off != 0) { py_floor_by_jit_pc.insert(0, (0, 0)); } + if std::env::var_os("PYRE_VSTACK_EXACT_AUDIT").is_some() { + eprintln!( + "[exact-build] runs_recorded={} translated={} floor_len={} n_py={} \ + first={:?} last={:?} maxoff_exact={:?} maxoff_floor={:?}", + pc_run_some.len(), + py_exact_by_jit_pc.len(), + py_floor_by_jit_pc.len(), + first_jit_pc_by_py_pc.len(), + py_exact_by_jit_pc.first(), + py_exact_by_jit_pc.last(), + py_exact_by_jit_pc.iter().map(|&(o, _)| o).max(), + first_jit_pc_by_py_pc + .iter() + .filter(|&&p| p != usize::MAX) + .max(), + ); + } // Floor-only depth twin of the containing-opcode resolution // (`vstack_containing_py_pc`). Shares `py_floor_by_jit_pc`'s keys @@ -15312,6 +15363,7 @@ impl CodeWriter { n_py_instrs, block_head_py_by_jit_pc, py_floor_by_jit_pc, + py_exact_by_jit_pc, abort_permanent_py_pc_by_jit_pc, merge_entry_by_green, pcdep_by_jit_pc, diff --git a/pyre/pyre-jit/src/jit/flatten.rs b/pyre/pyre-jit/src/jit/flatten.rs index dc9c9de09c2..ca70992d8cd 100644 --- a/pyre/pyre-jit/src/jit/flatten.rs +++ b/pyre/pyre-jit/src/jit/flatten.rs @@ -126,6 +126,23 @@ pub struct SSARepr { /// are a handful per jitcode, so recording them exactly costs nothing that /// a dense per-op table would. pub abort_permanent_insn_pos: Vec<(usize, i64)>, + /// Pyre-only side-table: `(insns index, py_pc)` at every point the + /// emitted stream's owning Python PC CHANGES, in emission order. + /// + /// `pc_first_insn_pos` keeps only the first position per Python PC, so it + /// collapses a PC that emits in two places; the floor tier derived from it + /// therefore reports a segment extent that is not one opcode's byte range. + /// Three emission shapes make that gap real: a can-raise trailing `-live-` + /// re-keyed to the call's fallthrough PC, a block opened at an + /// already-merged PC and drained after later PCs, and a mid-opcode block + /// split that leaves one PC owning two disjoint regions. + /// + /// Recording the changes rather than every op keeps this the same order of + /// size as the floor table — one entry per contiguous run — while making + /// "which opcode owns this byte" exact, including for a re-entered PC. + /// Synthetic ops (`offset < 0`) start no run: they belong to whichever PC + /// was last anchored, which is what leaving the run open expresses. + pub pc_run_insn_pos: Vec<(usize, i64)>, /// Per-kind fresh-Variable counter. RPython has no analog /// because RPython's `Variable()` constructor produces objects with /// implicit identity and `regalloc.py` numbers them densely after @@ -149,6 +166,7 @@ impl SSARepr { insns_pos: None, pc_first_insn_pos: Vec::new(), abort_permanent_insn_pos: Vec::new(), + pc_run_insn_pos: Vec::new(), next_var_idx: [0; 3], } } @@ -163,6 +181,7 @@ impl SSARepr { .iter_mut() .map(|(_, pos)| pos) .chain(self.abort_permanent_insn_pos.iter_mut().map(|(pos, _)| pos)) + .chain(self.pc_run_insn_pos.iter_mut().map(|(pos, _)| pos)) } /// Allocate a fresh `(kind, index)` Variable for this SSARepr. @@ -1387,6 +1406,16 @@ impl<'a> GraphFlattener<'a> { if op.opname == "abort_permanent" { self.ssarepr.abort_permanent_insn_pos.push((pos, py_pc)); } + // Open a new run only where the owning PC actually changes, so the + // table stays one entry per contiguous region. + if self + .ssarepr + .pc_run_insn_pos + .last() + .is_none_or(|&(_, last_py)| last_py != py_pc) + { + self.ssarepr.pc_run_insn_pos.push((pos, py_pc)); + } } let insn = self.flatten_space_operation(op); // `jtransform.py:467-482` appends a `-live-` AFTER a call op so the From 45d2dd54de3e05cde1557cd3f870f976107facb8 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 15:38:49 +0900 Subject: [PATCH 4/6] jit: resolve the vstack mirror's coordinate from the exact segmentation `vstack_step_py_pc` resolved which Python opcode owns a jitcode offset through the floor tier, which is derived from the first-offset-per-PC table and so collapses a PC that emits in two disjoint regions. The later region then reads as belonging to whichever PC last opened a segment, and the mirror replays that opcode's stack effect at a boundary the walk never crossed. Read `py_exact_by_jit_pc` instead, falling back to the floor tier when it is empty. The block-head marker rule still wins ahead of both: a control-flow marker is not the lowering of a Python opcode. Classified over the 13 programs with the most disagreements, of the boundaries the mirror acts on: 90 where the floor tier reports a boundary the emission record places inside the current opcode, 37 where both report one but name a different opcode, 0 where the floor tier misses one. Output is byte-identical across 444 bench programs, with a same-length-name decoy arm as the layout control and `PYRE_VSTACK_NO_EXACT` restoring the floor tier in the same binary. check.py dynasm 436/436, cranelift 436/436; cargo test -p pyre-jit-trace green. Assisted-by: Claude --- .../src/jitcode_dispatch/vstack_mirror.rs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index 50113f3a83b..e231be34f9e 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -958,10 +958,34 @@ pub(crate) fn vstack_step_py_pc( // block (for example its RETURN_VALUE); applying that opcode here corrupts // the live caller stack before the destination block is entered. if metadata_block_head_py_pc(metadata, jit_pc).is_some() { - current_py_pc - } else { - vstack_containing_py_pc(metadata, jit_pc) + return current_py_pc; + } + // Prefer the per-emission segmentation over the floor tier. The floor + // table keys each Python PC to its FIRST jitcode offset, so a PC that + // emits in two disjoint regions is collapsed onto the earlier one and the + // later region reads as belonging to whichever PC last opened a segment. + // The mirror then sees a boundary the walk never crossed and replays that + // opcode's stack effect against a stack it does not describe — the same + // shape as applying a SWAP at a boundary that did not retire it. + // + // `py_exact_by_jit_pc` records the owning PC at every point the emitted + // stream changes owner, so it answers directly. Empty for skeleton / + // fixture metadata, where the floor tier remains the only source. + if let Some(exact) = + crate::pyjitcode::exact_py_pc_for_jitcode_pc(&metadata.py_exact_by_jit_pc, jit_pc) + && !exact_segmentation_disabled() + { + return exact; } + vstack_containing_py_pc(metadata, jit_pc) +} + +/// `PYRE_VSTACK_NO_EXACT`: fall back to the floor tier for the mirror's +/// coordinate. The escape hatch for A/B-ing the segmentation switchover +/// against the tier it replaces. +fn exact_segmentation_disabled() -> bool { + static OFF: std::sync::OnceLock = std::sync::OnceLock::new(); + *OFF.get_or_init(|| std::env::var_os("PYRE_VSTACK_NO_EXACT").is_some()) } /// #73: step the walk-level operand-stack box mirror at From bfa6415620434fffeafe9006fa441d6fc3bfaaa9 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 19:54:26 +0900 Subject: [PATCH 5/6] gate-triage: register the vstack exact-segmentation gates `PYRE_VSTACK_EXACT_AUDIT` and `PYRE_VSTACK_NO_EXACT` are read from the environment but had no entry in pyre/gate-triage.md, which `gate_triage_complete::every_live_gate_has_a_triage_entry` asserts against. Assisted-by: Claude --- pyre/gate-triage.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyre/gate-triage.md b/pyre/gate-triage.md index c90baa63668..fb5a3aa27d7 100644 --- a/pyre/gate-triage.md +++ b/pyre/gate-triage.md @@ -974,7 +974,7 @@ the folds it selects, not before them. `PYRE_FBW_SPEC_CENSUS` in §6c is its read-only half: the per-fold consulted/fired tallies. -### §6c — Default-OFF diagnostics, censuses and probes (62): keep, cost nothing +### §6c — Default-OFF diagnostics, censuses and probes (64): keep, cost nothing Each is inert unless set, so none is a removal target by this file's already-ON criterion. They are listed so they cannot be missed again. @@ -1000,6 +1000,7 @@ already-ON criterion. They are listed so they cannot be missed again. `PYRE_PORTAL_RCA`, `PYRE_PROBE_BH_STARTUP`, `PYRE_PROBE_SNAPSHOT`, `PYRE_PROBE_SUBSCR`, `PYRE_PROFILE_PIPELINE`, `PYRE_QMUT_MAPDICT_FORCE`, `PYRE_RERAISE_DIAG`, `PYRE_SIZE_SHELL_OWNERS`, `PYRE_SNAPSHOT_DIAG`, +`PYRE_VSTACK_EXACT_AUDIT`, `PYRE_VSTACK_NO_EXACT`, `PYRE_WASM_DUMP_BAD_TRACE`, `PYRE_WASM_EXEC_TRACE`, `PYRE_WASM_FBW_CENSUS`, `PYRE_WASM_GUARD_CENSUS`, `PYRE_WASM_JIT_STATS`, `PYRE_WASM_CALL_HIST`, `PYRE_WASM_NO_CACHE`, `PYRE_WASM_STARTUP_TRACE`. @@ -1010,6 +1011,16 @@ value knobs bound the capture window, sampling rate, and report size. This is a diagnostic tool rather than a temporary runtime experiment, so it retires only if the example itself is removed. +`PYRE_VSTACK_NO_EXACT` is an A/B switch over the walk-level operand-stack +mirror, restoring the behaviour its default replaced: resolving the mirror's +Python-PC coordinate from the floor tier rather than from the per-emission +segmentation. It exists so the switchover stays measurable on one binary — any +env var of any name moves the allocation layout, so a knob and its control must +live in the same build. It goes when the floor tier has no plausible reader left +to compare against. `PYRE_VSTACK_EXACT_AUDIT` dumps that segmentation table at +build time and, on the walk side, the coordinate it yields; it is a report with +no ON behaviour. + `PYRE_CHECK_INHERIT_ENV` is the other odd one: an A/B switch, not a report. `check.py` starts a benchmark child from an allowlisted environment because the inherited one is startup allocation and moves `guard_failures`; setting the From f6c54f8af1573af52833b9560dc163f098c6fd37 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 16 Aug 2026 19:54:54 +0900 Subject: [PATCH 6/6] jit: drop an armed vstack reorder region when the mirror is re-seeded `vstack_reorder_saved` holds the `(py_pc, depth)` coordinate the walk left along with the mirror it left there, and `reconcile_vstack_at_boundary` restores that snapshot when the walk reports the same coordinate again. Three callers rewrite `vstack_boxes` wholesale and move that coordinate while leaving the region armed: the full-body and callee exception-handler re-seeds in `vstack_mirror.rs`, and the self-recursive call-assembler return in `inline_call.rs`. Disarm the region at all three. An armed region also forces every boundary inside it to `ShadowReseed`, which drops the walk-register-only boxes a handler body carries; disarming leaves those boundaries on the same per-op reconcile an unarmed walk uses. `PYRE_VSTACK_KEEP_REORDER` restores the previous behaviour. Over the 442-program bench corpus, a three-arm A/B (base / same-length-name decoy / knob) records 0 attributable stdout diffs and 0 exit-status changes. Assisted-by: Claude --- pyre/gate-triage.md | 23 ++++++++-------- .../src/jitcode_dispatch/inline_call.rs | 1 + .../src/jitcode_dispatch/vstack_mirror.rs | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/pyre/gate-triage.md b/pyre/gate-triage.md index fb5a3aa27d7..da3c52881a4 100644 --- a/pyre/gate-triage.md +++ b/pyre/gate-triage.md @@ -974,7 +974,7 @@ the folds it selects, not before them. `PYRE_FBW_SPEC_CENSUS` in §6c is its read-only half: the per-fold consulted/fired tallies. -### §6c — Default-OFF diagnostics, censuses and probes (64): keep, cost nothing +### §6c — Default-OFF diagnostics, censuses and probes (65): keep, cost nothing Each is inert unless set, so none is a removal target by this file's already-ON criterion. They are listed so they cannot be missed again. @@ -1000,7 +1000,7 @@ already-ON criterion. They are listed so they cannot be missed again. `PYRE_PORTAL_RCA`, `PYRE_PROBE_BH_STARTUP`, `PYRE_PROBE_SNAPSHOT`, `PYRE_PROBE_SUBSCR`, `PYRE_PROFILE_PIPELINE`, `PYRE_QMUT_MAPDICT_FORCE`, `PYRE_RERAISE_DIAG`, `PYRE_SIZE_SHELL_OWNERS`, `PYRE_SNAPSHOT_DIAG`, -`PYRE_VSTACK_EXACT_AUDIT`, `PYRE_VSTACK_NO_EXACT`, +`PYRE_VSTACK_EXACT_AUDIT`, `PYRE_VSTACK_KEEP_REORDER`, `PYRE_VSTACK_NO_EXACT`, `PYRE_WASM_DUMP_BAD_TRACE`, `PYRE_WASM_EXEC_TRACE`, `PYRE_WASM_FBW_CENSUS`, `PYRE_WASM_GUARD_CENSUS`, `PYRE_WASM_JIT_STATS`, `PYRE_WASM_CALL_HIST`, `PYRE_WASM_NO_CACHE`, `PYRE_WASM_STARTUP_TRACE`. @@ -1011,15 +1011,16 @@ value knobs bound the capture window, sampling rate, and report size. This is a diagnostic tool rather than a temporary runtime experiment, so it retires only if the example itself is removed. -`PYRE_VSTACK_NO_EXACT` is an A/B switch over the walk-level operand-stack -mirror, restoring the behaviour its default replaced: resolving the mirror's -Python-PC coordinate from the floor tier rather than from the per-emission -segmentation. It exists so the switchover stays measurable on one binary — any -env var of any name moves the allocation layout, so a knob and its control must -live in the same build. It goes when the floor tier has no plausible reader left -to compare against. `PYRE_VSTACK_EXACT_AUDIT` dumps that segmentation table at -build time and, on the walk side, the coordinate it yields; it is a report with -no ON behaviour. +`PYRE_VSTACK_NO_EXACT` and `PYRE_VSTACK_KEEP_REORDER` are A/B switches over the +walk-level operand-stack mirror, each restoring the behaviour its default +replaced: resolving the mirror's Python-PC coordinate from the floor tier rather +than from the per-emission segmentation, and leaving an armed out-of-order +region in place across a mirror re-seed. They exist so each switchover stays +measurable on one binary — any env var of any name moves the allocation layout, +so a knob and its control must live in the same build. Each goes when the +behaviour it restores has no plausible reader left to compare against. +`PYRE_VSTACK_EXACT_AUDIT` dumps that segmentation table at build time and, on +the walk side, the coordinate it yields; it is a report with no ON behaviour. `PYRE_CHECK_INHERIT_ENV` is the other odd one: an A/B switch, not a report. `check.py` starts a benchmark child from an allowlisted environment because the diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs index 211f3656ca5..71a55797d13 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -1142,6 +1142,7 @@ pub(crate) fn try_walker_call_assembler_self_recursive( ctx.vstack_cur_pypc = resume_py_pc; ctx.vstack_depth = resume_depth; ctx.vstack_last_ref = OpRef::NONE; + super::vstack_mirror::disarm_vstack_reorder_region(ctx); } // pyjitpl.py: GUARD_NOT_FORCED + resume snapshot advanced past diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index e231be34f9e..e4b6c92d347 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -1291,6 +1291,31 @@ fn py_stack_depth(code_ptr: *const pyre_interpreter::CodeObject, py: u32) -> usi .unwrap_or(0) as usize } +/// Drop any armed out-of-order region. +/// +/// The region's snapshot is keyed on the `(py_pc, depth)` coordinate the walk +/// left, and it is restored when the walk returns to that same coordinate on +/// the premise that no Python opcode retired in between. A caller that +/// rewrites `vstack_boxes` wholesale and moves the coordinate — an exception +/// handler entry, a call-assembler return — breaks that premise: the stack the +/// snapshot describes no longer exists, so a later boundary that happens to +/// land on the same `(py_pc, depth)` would restore it over the live mirror. +pub(crate) fn disarm_vstack_reorder_region(ctx: &mut WalkContext<'_, '_, Sym>) { + if keep_reorder_region_across_reseed() { + return; + } + ctx.vstack_reorder_ceiling = u32::MAX; + ctx.vstack_reorder_saved = None; +} + +/// `PYRE_VSTACK_KEEP_REORDER`: leave an armed region in place across a mirror +/// re-seed. The escape hatch for A/B-ing the disarm against the behaviour it +/// replaces. +fn keep_reorder_region_across_reseed() -> bool { + static KEEP: std::sync::OnceLock = std::sync::OnceLock::new(); + *KEEP.get_or_init(|| std::env::var_os("PYRE_VSTACK_KEEP_REORDER").is_some()) +} + /// The handler-entry coordinate the mirror adopted, under `PYRE_VSTACK_DIAG`. /// `floor_py` is reported next to it: the two disagreeing is what an /// out-of-line catch target looks like from the walk's side, and without both @@ -1391,6 +1416,7 @@ pub(crate) fn vstack_enter_exception_handler( }; ctx.vstack_boxes.clear(); ctx.vstack_boxes.resize(handler_depth, OpRef::NONE); + disarm_vstack_reorder_region(ctx); // The unwinder pushes the caught exception onto the new TOS. if handler_depth >= 1 && exc != OpRef::NONE { ctx.vstack_boxes[handler_depth - 1] = exc; @@ -1464,6 +1490,7 @@ fn vstack_enter_exception_handler_callee( // mirror shallower than the handler depth pads with NONE holes, which // `mirror_covers_kept` declines per slot rather than latching invalid. ctx.vstack_boxes.resize(handler_depth, OpRef::NONE); + disarm_vstack_reorder_region(ctx); // The unwinder pushes the caught exception onto the new TOS. if handler_depth >= 1 && exc != OpRef::NONE { ctx.vstack_boxes[handler_depth - 1] = exc;