diff --git a/pyre/gate-triage.md b/pyre/gate-triage.md index c90baa63668..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 (62): 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,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_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`. @@ -1010,6 +1011,17 @@ 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` 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 inherited one is startup allocation and moves `guard_failures`; setting the 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/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/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); 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; } 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..e4b6c92d347 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 @@ -1047,6 +1071,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) } }; @@ -1243,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 @@ -1343,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; @@ -1416,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; 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