diff --git a/majit/examples/tiny2/src/jit_interp.rs b/majit/examples/tiny2/src/jit_interp.rs index 52298ab5545..164980a8342 100644 --- a/majit/examples/tiny2/src/jit_interp.rs +++ b/majit/examples/tiny2/src/jit_interp.rs @@ -1,7 +1,7 @@ //! JIT-enabled tiny2 interpreter via `#[jit_interp]` proc macro with `state_fields`. //! -//! TODO: `rpython/jit/tl/tiny2_hotpath.py:90` models the -//! operand stack as a linked-list `Stack(value, next)`; each push allocates +//! Representation difference: `tiny2_hotpath.Stack` represents the operand +//! stack as a linked list of `Stack(value, next)` nodes. Each push allocates //! one cons cell that RPython's JIT peels as a chain of virtuals. pyre's //! `state_fields = { stackpos, stack: [int; virt] }` does not express //! linked-list stacks — it requires a contiguous virtualizable array. The diff --git a/majit/examples/tiny3/src/jit_interp.rs b/majit/examples/tiny3/src/jit_interp.rs index d83956ebfbd..91e86291202 100644 --- a/majit/examples/tiny3/src/jit_interp.rs +++ b/majit/examples/tiny3/src/jit_interp.rs @@ -1,10 +1,12 @@ //! JIT-enabled tiny3 interpreter via `#[jit_interp]` proc macro with `state_fields`. //! -//! TODO: `rpython/jit/tl/tiny3_hotpath.py:96` models the -//! operand stack as a linked-list `Stack(value, next)`, identical shape to -//! tiny2_hotpath.py. pyre's `state_fields = { stackpos, stack: [int; virt] }` -//! does not express linked-list stacks — see the same adaptation note on -//! `majit/examples/tiny2/src/jit_interp.rs`. +//! Representation difference: `tiny3_hotpath.Stack` represents +//! the operand stack as a linked list of `Stack(value, next)` nodes. Each push +//! allocates one cons cell that RPython's JIT peels as a chain of virtuals. +//! pyre's `state_fields = { stackpos, stack: [int; virt] }` requires a +//! contiguous virtualizable array and cannot express that linked-list shape. +//! The array backing is a source-shape deviation, although its optimized trace +//! is equivalent for the shallow, constant-height stacks used by tiny3. //! //! Greens: [pc] //! Reds: [stackpos, stack] (args at bottom, computation stack on top) diff --git a/majit/majit-backend-dynasm/src/aarch64/assembler.rs b/majit/majit-backend-dynasm/src/aarch64/assembler.rs index 53ffef1c43c..522452083e8 100644 --- a/majit/majit-backend-dynasm/src/aarch64/assembler.rs +++ b/majit/majit-backend-dynasm/src/aarch64/assembler.rs @@ -210,8 +210,8 @@ const CC_GE: u8 = 11; // signed >= const CC_LE: u8 = 12; // signed <= const CC_G: u8 = 13; // signed > -/// Invert a condition code. -/// Widest value `cmp Xn, #imm` encodes — `codebuilder.py:389 CMP_ri`. +/// Widest value accepted by `codebuilder.py`'s `CMP_ri` encoding of +/// `cmp Xn, #imm`. const MAX_CMP_IMM12: u32 = 4095; /// Forward reach of `b.cond`: a signed 19-bit displacement in 4-byte words. @@ -1077,8 +1077,8 @@ impl<'a> AssemblerARM64<'a> { let l = self.load_loc_to_reg(lhs, 17); let d = dst_reg; // Rd/Rn take the SP-form register family for the `#imm12` - // encoding; `XSP(r)` encodes identically to `X(r)` for the - // non-SP registers the allocator hands out here. + // encoding; `XSP(r)` encodes identically to `X(r)` for every + // non-SP register allocated to this instruction. match opcode { OpCode::IntSub => { dynasm!(self.mc ; .arch aarch64 ; sub XSP(d), XSP(l), im as u32) @@ -1178,16 +1178,16 @@ impl<'a> AssemblerARM64<'a> { } _ => return, }; - // `opassembler.py:129 emit_int_comp_op` takes `CMP_ri` when the - // right-hand side is an immediate; `codebuilder.py:389 CMP_ri` holds + // `opassembler.py`'s `emit_int_comp_op` takes `CMP_ri` when the + // right-hand side is an immediate; `codebuilder.py`'s `CMP_ri` holds // a 12-bit unsigned field, so anything wider still needs a register. if let Loc::Immed(i) = loc1 && let Ok(imm) = u32::try_from(i.value) && imm <= MAX_CMP_IMM12 { - // dynasm's `cmp Xn|SP, #uimm` form cannot take a dynamic - // register operand, so encode it the way - // `codebuilder.py:389 CMP_ri` does: SUBS with Rd = xzr. + // dynasm's `cmp Xn|SP, #uimm` form cannot take a dynamic register + // operand. `codebuilder.py`'s `CMP_ri` encodes the equivalent + // instruction as SUBS with Rd = xzr. let word: u32 = (0b1111000100u32 << 22) | (imm << 10) | ((r0 as u32) << 5) | 0b11111; dynasm!(self.mc ; .arch aarch64 ; .u32 word); return; diff --git a/majit/majit-backend-dynasm/src/runner.rs b/majit/majit-backend-dynasm/src/runner.rs index ab489a85248..2977a222e7b 100644 --- a/majit/majit-backend-dynasm/src/runner.rs +++ b/majit/majit-backend-dynasm/src/runner.rs @@ -2023,32 +2023,31 @@ impl DynasmBackend { } } - /// llsupport/regalloc.py:861-871 `_set_initial_bindings` parity: + /// Parity with `BaseRegalloc._set_initial_bindings`: /// `_ll_initial_locs` stores `loc.value - base_ofs`, measured in bytes /// from `FIRST_ITEM_OFFSET`, not input-order slot numbers. fn input_initial_loc(position: usize) -> i32 { (Self::input_slot(position) * crate::jitframe::SIZEOFSIGNED) as i32 } - /// llmodel.py:412 get_latest_descr parity: resolve a raw jf_descr - /// pointer to its `DescrRef`. Searches root loop fail_descrs + /// Resolve a raw `jf_descr` pointer to its `DescrRef`, as + /// `LLGraphCPU.get_latest_descr` does. Searches root loop fail descriptors /// first, then all bridge fail_descrs stored in asmmemmgr_blocks. /// RPython does this via AbstractDescr.show() which works for any /// descr from any loop/bridge. /// - /// `compile.py:618-671` parity: the four `DoneWithThisFrame*` + /// `compile.py`'s four `DoneWithThisFrame*` descriptors /// + `ExitFrameWithExceptionDescrRef` singletons attached to /// `self.cpu` are compared by pointer identity against the raw /// `jf_descr` value — same as RPython - /// `llgraph/runner.py:1478-1484` (`faildescr == self.cpu.done_with_this_frame_descr_*`). + /// `llgraph/runner.py`'s `LLGraphCPU.execute_token` + /// (`faildescr == self.cpu.done_with_this_frame_descr_*`). /// /// Panics if not found — RPython uses object identity, so lookup /// failure is impossible in well-formed execution. /// - /// `frame_ptr` is required so the `propagate_exception_descr` arm - /// can run the equivalent of `compile.py:1092-1098`'s - /// `cpu.grab_exc_value(deadframe)` — read `jf_guard_exc` (the grab is - /// read-only, `llmodel.py:240-242`; the clear alongside it is pyre's) + /// `frame_ptr` lets the `propagate_exception_descr` arm implement + /// `PropagateExceptionDescr.handle_fail`: read `jf_guard_exc`, clear it, /// and stage the value into `jf_frame[0]` before synthesizing the /// exit-frame-with-exception descr the toplevel consumer expects. fn find_descr_by_ptr( @@ -2058,7 +2057,7 @@ impl DynasmBackend { frame_ptr: *mut JitFrame, ) -> majit_ir::DescrRef { let attached = self.attached_descr_ptrs(); - // compile.py:618-669 done_with_this_frame_descr — check all 4 variants. + // Check all four `DoneWithThisFrameDescr` variants. // Forward through `meta_descr` so the metainterp class hierarchy // (DoneWithThisFrameDescr{Void,Int,Ref,Float}) answers // `is_finish` / `fail_arg_types` etc. via `compile.py:624 final_descr=True`. @@ -2069,9 +2068,9 @@ impl DynasmBackend { || ptr == attached.done_with_this_frame_descr_float) { // Return the metainterp `DoneWithThisFrameDescr*` Arc directly. - // `compile.py:618-672` class hierarchy answers + // The `DoneWithThisFrameDescr` class hierarchy answers // `is_finish`/`fail_arg_types` via its own FailDescr impl — - // no backend wrapper needed (Phase C-1 cascade endpoint). + // no backend wrapper is needed. let att = self.descr_attachments.read().unwrap(); let meta = if ptr == attached.done_with_this_frame_descr_void { att.done_with_this_frame_descr_void.clone() diff --git a/majit/majit-backend-wasm/tests/codegen_test.rs b/majit/majit-backend-wasm/tests/codegen_test.rs index 44e751ece24..9ccddcf06d2 100644 --- a/majit/majit-backend-wasm/tests/codegen_test.rs +++ b/majit/majit-backend-wasm/tests/codegen_test.rs @@ -25,6 +25,17 @@ fn workspace_root() -> PathBuf { .to_path_buf() } +/// The wasm-host module these runtime tests measure. +/// +/// Only the snapshot path, never the raw `pyre_wasm.wasm` cargo output: the +/// `web` and `wasm-host` features of `pyre-wasm` build to that one filename and +/// overwrite each other, so a tree that last built `web` leaves a module there +/// which loads and runs but is not the one whose counters these tests pin. +/// `check.py` copies the wasm-host build here for exactly that reason. +fn wasm_host_module(root: &Path) -> PathBuf { + root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm") +} + fn run_runtime_program( binary: &Path, script: &Path, @@ -56,13 +67,7 @@ fn global_reassign_retraces_non_last_label_backedge_at_runtime() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); for artifact in [&dynasm, &wasm_runner, &wasm_module] { assert!( @@ -118,13 +123,7 @@ fn raise_catch_clear_root_does_not_cross_the_host_per_exception() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); let script = root.join("pyre/bench/raise_catch_loop.py"); for artifact in [&dynasm, &wasm_runner, &wasm_module] { @@ -170,13 +169,7 @@ fn recursive_call_assembler_does_not_refill_zeroed_nursery_frames() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); let script = root.join("pyre/bench/fib_recursive.py"); for artifact in [&dynasm, &wasm_runner, &wasm_module] { @@ -209,8 +202,14 @@ fn recursive_call_assembler_does_not_refill_zeroed_nursery_frames() { wasm_run.stdout, dynasm_run.stdout, "wasm recursive fib output diverged from dynasm:\n{stderr}" ); - assert_eq!(stat_value(&stderr, "compiles"), 4); - assert_eq!(stat_value(&stderr, "BRIDGE_OK"), 3); + // `compiles` is the host's module-compile tally, one per loop and one per + // bridge, and `BRIDGE_OK` counts the bridges the backend accepted — the + // same event `bridges_compiled` counts, since both are bumped only on the + // `Ok` side of `compile_bridge`. So both follow from the committed + // `pyre/bench/fib_recursive.wasm.jitstats`: `loops_compiled=1` + + // `bridges_compiled=8`. Re-record these two alongside that baseline. + assert_eq!(stat_value(&stderr, "compiles"), 9); + assert_eq!(stat_value(&stderr, "BRIDGE_OK"), 8); assert!( !stderr.contains("memory.fill"), "recursive CA still refills a nursery that is already zeroed:\n{stderr}" @@ -224,13 +223,7 @@ fn fannkuch_blackhole_helpers_do_not_reflect_through_the_host() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); let script = root.join("pyre/bench/fannkuch.py"); for artifact in [&dynasm, &wasm_runner, &wasm_module] { assert!( @@ -272,13 +265,7 @@ fn terminal_declined_call_assembler_matches_dynasm_at_runtime() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); let script = root.join("pyre/bench/ca_terminal_decline.py"); for artifact in [&dynasm, &wasm_runner, &wasm_module] { @@ -329,13 +316,7 @@ fn wasm_outlier_bridges_stay_compiled_at_runtime() { let root = workspace_root(); let dynasm = root.join("target/release/pyre-dynasm"); let wasm_runner = root.join("target/release/pyre-wasm-runner"); - let host_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm-host.wasm"); - let plain_module = root.join("target/wasm32-unknown-unknown/release/pyre_wasm.wasm"); - let wasm_module = if host_module.exists() { - host_module - } else { - plain_module - }; + let wasm_module = wasm_host_module(&root); for artifact in [&dynasm, &wasm_runner, &wasm_module] { assert!( diff --git a/majit/majit-metainterp/src/blackhole.rs b/majit/majit-metainterp/src/blackhole.rs index 07bb9c0d906..ca15aee78aa 100644 --- a/majit/majit-metainterp/src/blackhole.rs +++ b/majit/majit-metainterp/src/blackhole.rs @@ -5029,29 +5029,29 @@ fn bhimpl_int_add(a: i64, b: i64) -> i64 { a.wrapping_add(b) } -/// blackhole.py:462-464 `bhimpl_int_sub(a, b): return intmask(a - b)`. +/// Port of `blackhole.bhimpl_int_sub`: `return intmask(a - b)`. fn bhimpl_int_sub(a: i64, b: i64) -> i64 { a.wrapping_sub(b) } -/// blackhole.py:466-468 `bhimpl_int_mul(a, b): return intmask(a * b)`. +/// Port of `blackhole.bhimpl_int_mul`: `return intmask(a * b)`. fn bhimpl_int_mul(a: i64, b: i64) -> i64 { a.wrapping_mul(b) } -/// RPython `rint.py:399-408 ll_int_py_div` (oopspec `int.py_div`). +/// Port of `rint.ll_int_py_div` (oopspec `int.py_div`). /// The OS_INT_PY_DIV residual call lands here at runtime. The JIT /// trace contains two explicit guards upstream of this call, -/// produced by the inlined `_ovf_zer` wrapper (`rint.py:429 -/// ll_int_py_div_ovf_zer`): +/// produced by the inlined `rint.ll_int_py_div_ovf_zer` wrapper: /// * `int_eq(rhs, 0) -> guard_false` (zero divisor), /// * `int_and(int_eq(lhs, INT_MIN), int_eq(rhs, -1)) -> /// guard_false` (overflow corner — `INT_MIN // -1` would /// overflow to `INT_MIN` in two's-complement; PyPy -/// `intobject.py:316/491/804` routes this case through +/// `intobject.py`'s `_floordiv`, `descr_floordiv`, and `descr_rfloordiv` +/// route this case through /// `ovf2long` to long arithmetic). /// Other negative operand combinations are valid: PyPy -/// `intobject.py:316 _floordiv` only handles `ZeroDivisionError`, +/// `intobject.py`'s `_floordiv` only handles `ZeroDivisionError`, /// and the no-branch floor correction /// (`(a ^ b) < 0 && d * b != a -> d - 1`) yields Python-floor /// semantics for every legal sign combination of `(a, b)`. @@ -5063,8 +5063,8 @@ fn bhimpl_int_mul(a: i64, b: i64) -> i64 { /// path: the `_ovf_zer` wrapper's `int_eq(rhs, 0) -> guard_false` and /// `(lhs == INT_MIN) & (rhs == -1) -> guard_false` runtime guards /// (emitted at `codegen.rs::generated_binary_int_value`) bail out the -/// trace before this helper is invoked, matching RPython's -/// `rint.py:429 ll_int_py_div_ovf_zer` shape. Direct (non-traced) +/// trace before this helper is invoked, matching +/// `rint.ll_int_py_div_ovf_zer`. Direct non-traced /// callers must respect the same precondition. /// /// `extern "C"`: the residual-call path @@ -5084,7 +5084,7 @@ pub extern "C" fn ll_int_py_div(a: i64, b: i64) -> i64 { } } -/// RPython `rint.py:496-500 ll_int_py_mod` (oopspec `int.py_mod`). +/// Port of `rint.ll_int_py_mod` (oopspec `int.py_mod`). /// See [`ll_int_py_div`] for the JIT-side runtime guard /// rationale (`int_eq(rhs, 0)` + `(lhs == INT_MIN) & (rhs == -1)`). /// Uses `wrapping_rem` for the C-style remainder step, then applies diff --git a/majit/majit-metainterp/src/compile.rs b/majit/majit-metainterp/src/compile.rs index 65dc4111734..20defcc6ef0 100644 --- a/majit/majit-metainterp/src/compile.rs +++ b/majit/majit-metainterp/src/compile.rs @@ -2891,22 +2891,10 @@ mod tests { ); } } -/// `compile.py:855` ResumeGuardDescr `_attrs_ = ('rd_numb', 'rd_consts', -/// 'rd_virtuals', 'rd_pendingfields', 'status')` — the per-guard -/// resume payload shared by every concrete `AbstractResumeGuardDescr` -/// subclass. Pyre stores them in `UnsafeCell` so the optimizer can -/// mutate the descr in place via `FailDescr::set_rd_*` without -/// breaking the `Arc` identity stamped on the op. -/// -/// Each slot wraps `Arc<[T]>` so `copy_all_attributes_from` -/// (compile.py:861-867) — `self.rd_consts = other.rd_consts` etc. — -/// can mirror RPython's reference-share semantics with a single -/// `Arc::clone()` rather than a `Vec::clone()` that would deep-copy -/// the bytes. External setters still accept `Option>`; the -/// conversion to `Arc<[T]>` is one move per (rare) write. -// RdPayload moved to majit-backend::rd_payload (Phase C-1 -// preparatory step toward backend struct deletion). Re-export from -// here so existing `compile::RdPayload` references stay resolvable. +/// Re-export the backend-owned resume payload under its historical +/// `compile::RdPayload` path. [`copy_all_attributes_from`] shares the payload's +/// `Arc`-backed sections, matching `ResumeGuardDescr.copy_all_attributes_from` +/// without changing the descriptor identity stored on a guard operation. pub use majit_backend::RdPayload; fn push_vector_info(head: &mut Option>, mut info: AccumInfo) { @@ -5172,11 +5160,10 @@ pub fn make_compile_loop_version_descr_from(source_op: &majit_ir::Op) -> DescrRe make_compile_loop_version_descr_with_payload(types, payload) } -// Resume data for a guard now lives on `StoredExitLayout.resume_layout` -// (per-guard `ResumeLayoutSummary`) rather than a separate trace-side -// `HashMap`. See `pyjitpl.rs CompiledTrace` and the -// producers/readers below. This mirrors RPython's single guard-owned -// `ResumeGuardDescr` container (`compile.py:855`). +// `StoredExitLayout.resume_layout` is the canonical per-guard +// `ResumeLayoutSummary`: `build_guard_metadata` derives it from the guard +// descriptor, and backend exit recovery consumes it. This preserves RPython's +// single guard-owned `compile.ResumeGuardDescr` container. // // These are the **compile role** of `TraceCtx`, mirroring RPython's diff --git a/majit/majit-metainterp/src/executor.rs b/majit/majit-metainterp/src/executor.rs index fedb7f17dc4..b957b6c9096 100644 --- a/majit/majit-metainterp/src/executor.rs +++ b/majit/majit-metainterp/src/executor.rs @@ -377,29 +377,28 @@ pub fn execute_varargs( result } -/// executor.py:555 `execute_nonspec_const` for binary integer opcodes. +/// `executor.py::execute_nonspec_const` for binary integer opcodes. /// /// Returns the folded `i64` result when the operation is recognized and /// the result is well-defined; returns `None` to abort folding when: /// * the opcode is not a recognized binary int op /// * an OVF arithmetic op (IntAddOvf/SubOvf/MulOvf) overflows — -/// RPython's `do_int_add_ovf` then hits -/// `assert metainterp is not None` (executor.py:287) which +/// RPython's `do_int_add_ovf` then hits its +/// `assert metainterp is not None`, which /// AssertionErrors in the `constant_fold` path (metainterp=None); /// pyre prefers the softer `None` skip so the op stays in the /// trace and the runtime guard fires -/// * a shift count is outside `0..64` (mirrors -/// `blackhole.py:258 check_shift_count`) +/// * a shift count is outside `0..64`, as rejected by +/// `blackhole.check_shift_count` /// * IntFloorDiv / IntMod with a zero divisor /// /// Non-OVF IntAdd/IntSub/IntMul match `bhimpl_int_add/_sub/_mul` -/// (`blackhole.py:459-468`) which compute `intmask(a + b)` — i.e. -/// wrapping i64 arithmetic. Earlier `checked_*` use here would have -/// aborted the fold on a representable wrapping result. +/// compute `intmask(a + b)`, i.e. wrapping i64 arithmetic. Using +/// `checked_*` for these non-overflowing opcodes would abort the fold on a +/// representable wrapping result. /// -/// Mirrors the `do_int_*` entries at executor.py:279-309 (OVF) + -/// `EXECUTE_BY_NUM_ARGS` binary-int rows (the unrolled dispatch table -/// generated at executor.py:495-498). +/// Mirrors the overflow helpers and binary-int rows generated in +/// `executor.EXECUTE_BY_NUM_ARGS`. pub fn execute_binary_int_const(opcode: OpCode, a: i64, b: i64) -> Option { let result = match opcode { OpCode::IntAdd => a.wrapping_add(b), @@ -433,7 +432,7 @@ pub fn execute_binary_int_const(opcode: OpCode, a: i64, b: i64) -> Option { if (r != 0) && ((r ^ b) < 0) { r + b } else { r } } OpCode::IntSignext if (1..=8).contains(&b) => { - // blackhole.py:568 bhimpl_int_signext → support.py:30 int_signext. + // `blackhole.bhimpl_int_signext` delegates to `support.int_signext`. crate::support::int_signext(a, b) } OpCode::UintMulHigh => { @@ -445,8 +444,8 @@ pub fn execute_binary_int_const(opcode: OpCode, a: i64, b: i64) -> Option { Some(result) } -/// executor.py:495-498 ptr-compare row of EXECUTE_BY_NUM_ARGS. -/// Mirrors blackhole.py bhimpl_ptr_eq/_ne and instance_ptr_eq/_ne — +/// Pointer-comparison row of `executor.EXECUTE_BY_NUM_ARGS`. +/// Mirrors `blackhole.bhimpl_ptr_eq`, `bhimpl_ptr_ne`, and their instance forms: /// straight pointer identity once both args are constant references. pub fn execute_ptr_compare_const(opcode: OpCode, a: usize, b: usize) -> Option { let result = match opcode { @@ -457,13 +456,12 @@ pub fn execute_ptr_compare_const(opcode: OpCode, a: usize, b: usize) -> Option Option { let result = match opcode { OpCode::IntNeg => a.wrapping_neg(), @@ -476,8 +474,8 @@ pub fn execute_unary_int_const(opcode: OpCode, a: i64) -> Option { Some(result) } -/// executor.py:495-498 unary-float row mirrors blackhole.py float -/// unops: bhimpl_float_neg / _abs. +/// Unary-float row of `executor.EXECUTE_BY_NUM_ARGS`, mirroring +/// `blackhole.bhimpl_float_neg` and `bhimpl_float_abs`. pub fn execute_unary_float_const(opcode: OpCode, a: f64) -> Option { let result = match opcode { OpCode::FloatNeg => -a, @@ -487,14 +485,13 @@ pub fn execute_unary_float_const(opcode: OpCode, a: f64) -> Option { Some(result) } -/// executor.py:495-498 binary-float row. Float arithmetic + comparisons -/// (comparisons return bool wrapped as 0/1 in the caller). Mirrors -/// blackhole.py bhimpl_float_add/_sub/_mul/_truediv (`:697-718`). +/// Binary-float row of `executor.EXECUTE_BY_NUM_ARGS`, mirroring the +/// corresponding `blackhole.bhimpl_float_*` helpers. /// FLOAT_TRUEDIV with `b == 0.0` is NOT folded — see upstream /// `test_optimizebasic.test_float_division_by_multiplication` which /// preserves `float_truediv(f, 0.0)` in the optimized loop rather than /// freezing the IEEE inf/nan constant. The runtime executor still -/// performs `a / b` per `blackhole.py:717` (translated C semantics); +/// performs `a / b` through `blackhole.bhimpl_float_truediv`; /// only trace-time folding is suppressed. pub fn execute_binary_float_const(opcode: OpCode, a: f64, b: f64) -> Option { let result = match opcode { @@ -507,8 +504,8 @@ pub fn execute_binary_float_const(opcode: OpCode, a: f64, b: f64) -> Option Some(result) } -/// executor.py:495-498 float→bool row. Mirrors blackhole.py -/// bhimpl_float_lt/_le/_eq/_ne/_gt/_ge. +/// Float-comparison row of `executor.EXECUTE_BY_NUM_ARGS`, mirroring the +/// corresponding `blackhole.bhimpl_float_*` comparison helpers. pub fn execute_float_compare_const(opcode: OpCode, a: f64, b: f64) -> Option { let result = match opcode { OpCode::FloatLt => a < b, @@ -522,40 +519,14 @@ pub fn execute_float_compare_const(opcode: OpCode, a: f64, b: f64) -> Option JitDriver { self.meta.single_pass_outcome.take() } - /// `pyjitpl.py:2949 run_blackhole_interp_to_cancel_tracing` → - /// `blackhole.py:1799 convert_and_run_from_pyjitpl`. + /// `MetaInterp.run_blackhole_interp_to_cancel_tracing` delegates to + /// `blackhole.convert_and_run_from_pyjitpl`. /// /// A tracing abort can land anywhere in the portal jitcode, including in /// the middle of a source opcode whose remaining effects have not run. The @@ -1916,32 +1916,31 @@ impl JitDriver { /// read one: it converts `metainterp.framestack` into a blackhole chain, /// runs it forward until the bottommost frame reaches its /// `jit_merge_point`, and resumes the interpreter from the greens that - /// merge point reports (`blackhole.py:1068-1069` raises - /// `ContinueRunningNormally`, caught by `warmspot.py:961 - /// handle_jitexception`). Every opcode the walk left half-executed + /// merge point reports through `ContinueRunningNormally`, which + /// `WarmRunnerDesc.handle_jitexception` catches. Every opcode the walk left half-executed /// therefore *finishes* instead of being skipped. /// /// Returns the resume pc when the conversion ran and reached a merge point, /// having already written the blackhole's register banks back into `state`. /// `None` leaves the abort on the pre-existing source-pc handoff — the walk - /// did not abort, or the state shape has no seed path (see below). + /// did not abort, or the state shape has no seed path. /// /// **`None` means "declined before the chain ran", and only that.** Once /// `drive_multi_frame_blackhole` returns, the chain has executed the rest /// of the half-finished opcodes against the real heap; the caller's - /// source-pc handoff resumes at a pc dispatch advanced *before* those + /// source-pc handoff resumes at a pc dispatch advanced before those /// opcodes' arms, so answering `None` there runs them a second time — /// the very double-application this conversion exists to prevent. Every /// post-chain path therefore returns `Some`, using the `single_pass_finish` /// + `usize::MAX` "no forward pc" outcome when it has no merge point to - /// resume at. Upstream has no such split: `blackhole.py:1799 - /// convert_and_run_from_pyjitpl` never returns to its caller at all. + /// resume at. Upstream has no such split: + /// `blackhole.convert_and_run_from_pyjitpl` never returns to its caller. /// /// **Seeding.** The walk keeps state fields on the sym; the blackhole reads /// them out of the identity registers `StateFieldLayout` names. The abort /// arm captured the sym's image, and this places it into the root frame's /// banks before the conversion. A `[.. ; virt]` array needs two more - /// things, both done here: its elements pushed into native `state` (they + /// things: its elements are pushed into native `state` (they /// live on the trace-ctx shadow during the walk, and the chain's /// `getarrayitem_vable_*` read the real object), and the virtualizable /// identity seeded into its own slot plus handed to the chain alongside the diff --git a/majit/majit-metainterp/src/optimizeopt/bridgeopt.rs b/majit/majit-metainterp/src/optimizeopt/bridgeopt.rs index 7c70d1ea0b5..67e14b34bdb 100644 --- a/majit/majit-metainterp/src/optimizeopt/bridgeopt.rs +++ b/majit/majit-metainterp/src/optimizeopt/bridgeopt.rs @@ -86,17 +86,16 @@ pub fn serialize_optimizer_knowledge( }) .collect(); - // bridgeopt.py:74-88: known classes bitfield - // RPython: for each livebox, call getptrinfo(box).get_known_class(cpu). + // `serialize_optimizer_knowledge` records a known-class bit for each Ref + // livebox by calling `getptrinfo(box).get_known_class(cpu)`. // The actual class pointer is recovered at deserialization time // via cpu.cls_of_box(frontend_boxes[i]). // - // RPython Box.type parity: bridgeopt.py:77 uses `box.type != "r"`, + // RPython uses the box's intrinsic `type`, // where `box.type` is intrinsic/immutable. Pyre reads the same // type that `finish()` stores in `numb_state.livebox_types` (this - // map feeds `fail_arg_types` / `livebox_types` on the deserialize - // side — see bridgeopt.rs below). If we queried `env.get_type()` - // here instead, a livebox whose OptContext-side type differs from + // map feeds `fail_arg_types` and deserialization). Querying + // `env.get_type()` instead could let an OptContext-side type differ from // its numbering-time type would cause serialize/deserialize to // disagree on which Ref-typed slots get a bitfield bit, producing // an out-of-bounds rd_numb read in `deserialize_optimizer_knowledge` @@ -113,7 +112,8 @@ pub fn serialize_optimizer_knowledge( continue; } bitfield <<= 1; - // bridgeopt.py:79-80: info = getptrinfo(box) + // `bridgeopt.serialize_optimizer_knowledge` obtains `info` with + // `getptrinfo(box)` and records whether it has a known class. // known_class = info is not None and info.get_known_class(cpu) is not None if env.has_known_class(*opref) { bitfield |= 1; @@ -129,9 +129,9 @@ pub fn serialize_optimizer_knowledge( numb_state.append_int((bitfield << (6 - shifts)) as i64); } - // bridgeopt.py:92-122: heap knowledge + // Serialize heap and loop-invariant knowledge after the class bitfield. let Some(knowledge) = optimizer_knowledge else { - // bridgeopt.py:109-111,121-122: no optheap/optrewrite → zeros + // No optimizer knowledge means three empty sections. numb_state.append_int(0); // struct fields count numb_state.append_int(0); // array items count numb_state.append_int(0); // loopinvariant count diff --git a/majit/majit-metainterp/src/optimizeopt/heap.rs b/majit/majit-metainterp/src/optimizeopt/heap.rs index 3220e66cb91..1c6ba0133fe 100644 --- a/majit/majit-metainterp/src/optimizeopt/heap.rs +++ b/majit/majit-metainterp/src/optimizeopt/heap.rs @@ -122,7 +122,7 @@ impl DictArgKey { /// it indexes `PtrInfo._fields`. type FieldKey = (OpRef, usize); -/// heap.py:20-165 AbstractCachedEntry +/// Rust representation of `heap.AbstractCachedEntry` and `CachedField`. /// /// PyPy uses Python inheritance to share `do_setfield`, /// `force_lazy_set`, `getfield_from_cache`, `possible_aliasing` and @@ -142,14 +142,13 @@ type FieldKey = (OpRef, usize); /// field-cache identity, with a separate `field_idx` / `descr_idx` /// (u32) only where the RPython source indexes `PtrInfo` slots or /// EffectInfo bitsets. -/// heap.py:168-226 CachedField(AbstractCachedEntry) struct CachedField { - /// heap.py:39 cached_structs — struct boxes with a cached value + /// `AbstractCachedEntry.cached_structs`: struct boxes with a cached value /// for this descr. Replaces RPython's parallel `cached_infos`; /// the PtrInfo itself is read on-demand from /// `ctx.get_ptr_info(opref)` / `ctx.get_const_info(opref)`. cached_structs: Vec, - /// heap.py:40 _lazy_set — at most one pending SetfieldGc per descr. + /// `AbstractCachedEntry._lazy_set`: at most one pending `SetfieldGc` per descr. /// Stores only the pending `Op` (`_lazy_set = op`); the struct base /// is `op.getarg(0)`, resolved on demand by the consumers. lazy_set: Option, @@ -1450,21 +1449,18 @@ impl OptHeap { /// Invalidate caches on calls and other side-effecting operations. /// - /// Caches that survive: + /// `OptHeap.force_from_effectinfo` invalidates non-pure field and array + /// caches. Caches that survive are: /// - Immutable (green) field caches: values never change. - /// - Unescaped object caches: calls cannot access objects that haven't + /// - Unescaped object caches: calls cannot access objects that have not /// been passed to a call or stored into the heap. - /// heap.py:379-391: invalidate non-pure field/array caches. - /// Only `is_always_pure` (immutable) fields survive. /// - /// heap.py:189-196 `CachedField.invalidate(descr)` clears - /// `opinfo._fields[idx]` for every cached_info BEFORE clearing the - /// `cached_infos`/`cached_structs` lists. The Rust port routes that + /// `CachedField.invalidate` clears `opinfo._fields[idx]` for every cached + /// info before clearing `cached_infos` and `cached_structs`. The Rust port routes that /// PtrInfo cleanup through `invalidate_with_ctx` so the per-pass /// "single source of truth" stays in sync after a clean. fn clean_caches(&mut self, ctx: &mut OptContext) { - // heap.py:380-381 `if not we_are_translated(): items.sort(key=str, - // reverse=True)` — the ordering exists only untranslated, so walk the + // `OptHeap.clean_caches` sorts descriptors only when untranslated, so walk the // cache in place through a permuted index list. Materializing the // entries instead cost a `DescrRef` clone per cached field on every // residual call, which is where clean_caches runs. @@ -1472,8 +1468,7 @@ impl OptHeap { sort_descr_entry_indices_untranslated(&self.cached_fields, &mut order); for i in order { let (_field_idx, descr, cf) = &mut self.cached_fields[i]; - // heap.py:384: `cf.invalidate(descr)` — purity self-gate - // inside the method (heap.py:189-194). `_field_idx` unused + // `CachedField.invalidate` applies its own purity gate. `_field_idx` is unused // post-purity-lift; index now recomputed from `descr`. cf.invalidate(descr, ctx); } diff --git a/majit/majit-metainterp/src/optimizeopt/rewrite.rs b/majit/majit-metainterp/src/optimizeopt/rewrite.rs index b957556a5be..2a55908d6a1 100644 --- a/majit/majit-metainterp/src/optimizeopt/rewrite.rs +++ b/majit/majit-metainterp/src/optimizeopt/rewrite.rs @@ -658,18 +658,19 @@ impl OptRewrite { // ── Guards ── - /// Optimize GUARD_TRUE following RPython rewrite.py: optimize_guard(op, CONST_1). + /// Optimize `GUARD_TRUE` through `OptRewrite.optimize_guard(op, CONST_1)`. /// If the condition is a known constant 0, the trace is impossible and must abort. /// - /// rewrite.py:163-184 `optimize_guard` proper (the contradiction check - /// + emit) is the call-time half. The `make_constant(box, CONST_1)` half + /// `OptRewrite.optimize_guard` performs the contradiction check and emission + /// at call time. Its `make_constant(box, CONST_1)` step /// of the upstream `optimize_guard` is split into - /// `propagate_postprocess` (rewrite.py:352-371) per RPython's - /// `have_postprocess` model — see the bottom of this file. + /// `OptRewrite::propagate_postprocess` per RPython's + /// `have_postprocess` model. fn optimize_guard_true(&self, op: &Op, ctx: &mut OptContext) -> OptimizationResult { let arg0 = op.arg(0); - // rewrite.py:165-168: box.type=='i' checks intbound.is_constant(), + // The integer path in `OptRewrite.optimize_guard` checks + // `intbound.is_constant()`, // which catches values narrowed to a single point by bounds analysis, // not just the constant pool. if let Some(val) = ctx @@ -685,7 +686,7 @@ impl OptRewrite { OptimizationResult::PassOn } - /// Optimize GUARD_FALSE following RPython rewrite.py: optimize_guard(op, CONST_0). + /// Optimize `GUARD_FALSE` through `OptRewrite.optimize_guard(op, CONST_0)`. fn optimize_guard_false(&self, op: &Op, ctx: &mut OptContext) -> OptimizationResult { let arg0 = op.arg(0); diff --git a/majit/majit-metainterp/src/optimizeopt/shortpreamble.rs b/majit/majit-metainterp/src/optimizeopt/shortpreamble.rs index c0f57ad571d..408f68266a6 100644 --- a/majit/majit-metainterp/src/optimizeopt/shortpreamble.rs +++ b/majit/majit-metainterp/src/optimizeopt/shortpreamble.rs @@ -2095,16 +2095,15 @@ impl AbstractShortPreambleBuilderState { preamble_op } - /// shortpreamble.py:382-407: use_box(box, preamble_op, optimizer) - /// Non-recursive: iterates preamble_op's args (adding non-input deps - /// + guards to short), then appends preamble_op + result guards. - /// Called by force_op_from_preamble (unroll.py:32). + /// Non-recursive port of `AbstractShortPreambleBuilderState.use_box`: adds + /// non-input dependencies and guards for `preamble_op`, then appends the + /// operation and its result guards. + /// Called by `OptUnroll.force_op_from_preamble`. /// /// Dependency args carry the dep's replay op object (produce_arg /// object-carry); a non-input, non-const arg whose bound op still /// holds the builder's `set_forwarded` marker IS a short-box replay - /// op — append it and consume the marker (upstream - /// `arg.set_forwarded(None)`, shortpreamble.py:391-396). + /// op; append it and consume its marker with `arg.set_forwarded(None)`. fn use_box( &mut self, preamble_op: &majit_ir::OpRc, @@ -2337,20 +2336,20 @@ impl ShortPreambleBuilder { Some(self.state.append_to_short(result.to_opref(), &produced)) } - /// shortpreamble.py:310: add_op_to_short — recursive, used during + /// Recursive `ShortPreambleBuilder.add_op_to_short`, used during /// export-time create_short_boxes to resolve transitive dependencies. pub fn add_op_to_short(&mut self, result: &majit_ir::operand::Operand) -> Option { self.use_box_recursive(result, &mut IndexSet::new()) .map(|op| (*op).clone()) } - /// shortpreamble.py:382-407: use_box(box, preamble_op, optimizer) - /// Non-recursive. Called by force_op_from_preamble (unroll.py:32). + /// Port of `AbstractShortPreambleBuilderState.use_box`. + /// Non-recursive. Called by `OptUnroll.force_op_from_preamble`. /// /// RPython passes `preamble_op.preamble_op` directly — the replay op /// IS the carried object, so there is no entry-selection lookup. The /// pop's replay Rc is the builder's own object (threaded by the - /// produce_op family), verified by the debug probe below. + /// `produce_op` family), verified against the builder entry. pub fn use_box( &mut self, source: OpRef, @@ -3115,8 +3114,9 @@ impl ExtendedShortPreambleBuilder { remapped } - /// shortpreamble.py:478-481: use_box — pop JUMP, add deps, re-append JUMP. - /// Called by force_op_from_preamble (unroll.py:32). + /// Port of `ExtendedShortPreambleBuilder.use_box`: pop `JUMP`, add its + /// dependencies, and append `JUMP` again. + /// Called by `OptUnroll.force_op_from_preamble`. /// /// RPython passes `preamble_op.preamble_op` directly — the pop's /// replay Rc is the carried object (threaded by the produce_op diff --git a/majit/majit-metainterp/src/optimizeopt/virtualstate.rs b/majit/majit-metainterp/src/optimizeopt/virtualstate.rs index 0ae08d8b268..85595cd55d9 100644 --- a/majit/majit-metainterp/src/optimizeopt/virtualstate.rs +++ b/majit/majit-metainterp/src/optimizeopt/virtualstate.rs @@ -1333,55 +1333,21 @@ impl VirtualState { Ok(guards) } - /// virtualstate.py per-entry generate_guards parity, recursive form. + /// Recursive port of `AbstractVirtualStateInfo.generate_guards` and each + /// subclass's `_generate_guards` implementation. Every recursive call + /// performs the alias-consistency check in the shared `renum` namespace. /// - /// Mirrors `AbstractVirtualStateInfo.generate_guards` (virtualstate.py:72-101) - /// + the per-subclass `_generate_guards` dispatch. The alias-consistency - /// check (virtualstate.py:84-94) lives at the entry of every recursive - /// call so nested virtual fields/items participate in the same renum - /// namespace. + /// `runtime_box` supplies RPython's concrete "educated guess" for emitting + /// non-permanent guards. Nested virtuals obtain their concrete fields and + /// items through `OptContext::get_runtime_field`, `get_runtime_item`, and + /// `get_runtime_interiorfield`, corresponding to the methods of the same + /// names on `GenerateGuardState`. Those helpers return fresh const-pool + /// operands so compile-time virtual fields remain distinct from runtime + /// values. A missing placeholder or unreadable descriptor propagates + /// `None`, causing guards that require runtime evidence to reject the match. /// - /// `runtime_box`: when Some, non-permanent guard emission is possible. - /// When None (generalization_of path, or no runtime guidance), only - /// structurally compatible pairs are accepted. RPython uses the - /// concrete runtime value as an "educated guess" (virtualstate.py:551-555). - /// - /// For nested struct/array recursion (virtualstate.py:148-176/241-261/292-326) - /// pyre threads the inner `fieldbox`/`fieldbox_runtime` through, and - /// the runtime class read for KnownClass branches is performed by - /// `cpu.cls_of_box(runtime_box)` (virtualstate.py:601/:608/:620), - /// not by the optimizer-tracked PtrInfo. - /// - /// Nested struct/array recursion: RPython's - /// `GenerateGuardState.get_runtime_field` / `get_runtime_item` / - /// `get_runtime_interiorfield` (virtualstate.py:39-67) call - /// `cpu.bh_getfield_gc_*` / `bh_getarrayitem_gc_*` / - /// `bh_getinteriorfield_gc_*` to read the *concrete* value off the - /// runtime object and wrap it in a fresh `InputArg*`. The pyre port - /// (`OptContext::get_runtime_field`, mod.rs) walks `runtime_box` to - /// its `Value::Ref(gcref)` payload and reads at - /// `gcref.raw() + descr.offset()` using the FieldDescr's - /// size/sign/type triple — direct ptr arithmetic matching the - /// backend `Cpu::bh_getfield_gc_*` implementation - /// (compiler.rs:14570). The read is wrapped in a freshly allocated - /// const-pool OpRef so the recursive `runtime_box` parameter carries - /// a concrete value distinct from the compile-time `fieldbox`. - /// - /// NONE-placeholder slots (`info.rs:755`) propagate as - /// `runtime_box=None` so downstream NonNull / IntBounded arms - /// (:1474, :1500) reject the case, matching RPython's - /// `if fieldbox is None` skip at virtualstate.py:174. - /// - /// `get_runtime_field` returns `None` when the parent's - /// `runtime_box` is not a concrete Ref or when the descr is not a - /// FieldDescr — the recursive `runtime_box` is then `None` and - /// downstream guards that need a concrete pointer fail-fast. - /// - /// See `peek_parent_field_oprefs` and the per-variant - /// Virtual/VStruct/VArray/VArrayStruct match arms. - /// - /// `force_boxes`: when true, Virtual incoming can be accepted by - /// non-virtual targets (virtualstate.py:523-524 _generate_virtual_guards). + /// With `force_boxes`, a non-virtual target may accept a compatible virtual + /// incoming value through `NotVirtualStateInfoPtr._generate_virtual_guards`. fn generate_guards_for_entry_recursive( arg_idx: usize, expected: &VirtualStateInfoNode, @@ -1390,7 +1356,8 @@ impl VirtualState { runtime_box: Option, state: &mut GenerateGuardState, ) -> Result<(), VirtualStatesCantMatch> { - // virtualstate.py:83 `assert self.position != -1`. Pyre assigns + // `AbstractVirtualStateInfo.generate_guards` requires an assigned + // position. Pyre assigns // positions in `enum_top_level`; sentinel -1 means a node was // never enumerated, which is a constructor bug. RPython's // `assert` is always-on (no debug gate); pyre matches with @@ -1469,40 +1436,16 @@ impl VirtualState { _ => Ok(()), }; } - // There is no force_boxes relaxation for an expected (target) - // Virtual against a non-virtual incoming. When `self` is a - // VirtualStateInfo the dispatch is - // `AbstractVirtualStructStateInfo._generate_guards` - // (virtualstate.py:141), which has no force-box branch and requires - // the incoming to be a matching virtual struct. The sole force_boxes - // relaxation lives in `NotVirtualStateInfoPtr._generate_guards` - // (virtualstate.py:522-524) — the expected-non-virtual, incoming- - // virtual branch handled directly above. A Virtual target with a - // non-virtual incoming therefore falls through to the structural - // match below and is rejected by its `_ => Err(VirtualStatesCantMatch::default())` arm. - - // virtualstate.py:392-394 NotVirtualStateInfo._generate_guards: - // - // if not isinstance(other, NotVirtualStateInfo): - // raise VirtualStatesCantMatch( - // 'comparing a constant against something that is a virtual') - // - // This isinstance check lives in NotVirtualStateInfo(Int/Ptr) - // subclasses, NOT in VirtualStateInfo. When `expected` is itself - // a Virtual/VArray/VStruct, the comparison is handled by the - // VirtualStateInfo._generate_guards path (the main match below), - // which does struct-level field comparison — not type-tag matching. - // - // This gate is ALSO what rejects a Virtual / VArray / VStruct / - // VArrayStruct incoming against a NotVirtual `expected`, the - // LEVEL_CONSTANT one included: `info_type_matches` - // (virtualstate.rs:103-145) has no arm accepting a Virtual* incoming - // for any expected type — Int, Float and Ref all fall to their - // `_ => false` catch-alls. That is the port of virtualstate.py:392-394 - // for the Int/Float constant leaves and of virtualstate.py:525-529 for - // the Ptr leaf. The `(Constant(_), _)` arm below is therefore never - // entered with a virtual incoming; do NOT add a separate - // `other.is_virtual()` arm there — it would be dead code. + // `AbstractVirtualStructStateInfo._generate_guards` has no force-box + // relaxation: a virtual target requires a matching virtual incoming. + // The converse relaxation belongs to + // `NotVirtualStateInfoPtr._generate_virtual_guards` and is handled by + // the `state.force_boxes` branch for a non-virtual target. + + // `NotVirtualStateInfo._generate_guards` rejects virtual incoming + // values before constant or pointer-specific matching. Keeping that + // check in `info_type_matches` also ensures the `Constant` match arm is + // reached only for non-virtual incoming state. if !expected_info.is_virtual() && let Some(expected_type) = expected_info.info_type() && !info_type_matches(expected_type, incoming_info) @@ -1549,8 +1492,9 @@ impl VirtualState { // ... // raise e // - // pyre returns Err(VirtualStatesCantMatch::default()) instead of raising; the wrapper below - // populates `state.bad` on Err before propagating. Per-arm + // Pyre returns `Err(VirtualStatesCantMatch::default())` instead of + // raising; `VirtualStateInfo::generate_guards` populates `state.bad` + // before propagating the error. Per-arm // pointer-identity keys (`expected as *const _`) match Python // object-identity dict keying. let result = match (expected_info, incoming_info) { diff --git a/majit/majit-metainterp/src/pyjitpl/frame.rs b/majit/majit-metainterp/src/pyjitpl/frame.rs index 3e06f788b28..cca1f68940d 100644 --- a/majit/majit-metainterp/src/pyjitpl/frame.rs +++ b/majit/majit-metainterp/src/pyjitpl/frame.rs @@ -302,11 +302,12 @@ impl MIFrame { } /// Decode a `getfield_vable_/rd>X` operand triple, returning - /// `(vable_reg, field_idx, dest_reg)` per `assembler.py:165-167` + - /// `:197-207`. Canonical layout: 1B vable_reg + 2B descr_pool_idx - /// + 1B dest_reg. The leading `r` operand carries the live struct - /// register consumed as the `struct` argument by RPython - /// `pyjitpl.py:1166 _opimpl_setfield_vable_*`. + /// `(vable_reg, field_idx, dest_reg)` per + /// `assembler.py::Assembler.write_insn` and `Assembler.write_op_live`. + /// Canonical layout: 1B vable_reg + 2B descr_pool_idx + /// + 1B dest_reg. The leading `r` operand carries the live struct register + /// consumed as the `struct` argument by the `_opimpl_setfield_vable_*` + /// family in `pyjitpl.py`. pub fn read_vable_getfield(&mut self) -> (usize, usize, usize) { let field_idx = self.vable_field_index_at(self.code_cursor + 1); let base = self.next_u8() as usize; diff --git a/majit/majit-metainterp/src/resume.rs b/majit/majit-metainterp/src/resume.rs index 6ff4e89830a..3f7260a5ac3 100644 --- a/majit/majit-metainterp/src/resume.rs +++ b/majit/majit-metainterp/src/resume.rs @@ -3816,18 +3816,11 @@ impl ResumeDataLoopMemo { UNASSIGNED } - /// resume.py:192-226 _number_boxes — tag each box in a snapshot section. + /// `ResumeDataLoopMemo._number_boxes` tags each box in a snapshot section. /// - /// Exact port of RPython's `_number_boxes(self, iter, iterator, numb_state)`. - /// - /// `env` provides box access matching RPython's box operations: - /// - `get_box_replacement(opref)` → forwarded OpRef (resume.py:202) - /// - `is_const(opref)` → isinstance(box, Const) (resume.py:204) - /// - `get_const(opref)` → (value, type) for constants - /// - `get_type(opref)` → box.type ('i', 'r', 'f') (resume.py:211,214) - /// - `is_virtual_ref(opref)` → getptrinfo(box).is_virtual() (resume.py:212-213) - /// - `is_virtual_raw(opref)` → getrawptrinfo(box).is_virtual() (resume.py:215-216) - /// resume.py:192-226 `_number_boxes` — tag each box in a snapshot section. + /// `env` supplies the operations used by the upstream method: replacement + /// lookup, constant lookup, box type lookup, and virtual Ref/raw-pointer + /// classification. pub fn _number_boxes( &mut self, boxes: &[SnapshotBox], diff --git a/majit/majit-metainterp/src/trace_ctx.rs b/majit/majit-metainterp/src/trace_ctx.rs index b3a368a16f6..60a1fe01057 100644 --- a/majit/majit-metainterp/src/trace_ctx.rs +++ b/majit/majit-metainterp/src/trace_ctx.rs @@ -3063,38 +3063,15 @@ impl TraceCtx { } } - /// Best-effort concrete (runtime) value associated with an OpRef, from - /// TraceCtx-local state. Parallels upstream `box.getref_base()` / - /// `box.getint()` / `box.getfloatstorage()` — in RPython each Box - /// carries its own runtime concrete via the Box subclass; pyre's - /// `OpRef` is opaque, so concrete is reconstructed from the - /// available trace-time state. + /// Recover a concrete Ref value from trace-local state. /// - /// Resolution order, mirroring the subclass dispatch upstream performs - /// implicitly: - /// 1. Constant OpRefs — read inline off the OpRef variant (value + - /// type). Mirrors `history.py:220/261/307 ConstInt/ConstFloat/ - /// ConstPtr` Box.value intrinsic field. - /// 2. `standard_virtualizable_box()` — use the runtime shadow held in - /// `virtualizable_values[-1]`. Standard vable identity check. - /// 3. `opref_concrete` — Box.value stamp populated at every record - /// site that has the runtime result in scope (HEAP loads, - /// register reads, resume-data materialization). Covers - /// non-Const result OpRefs whose runtime concrete is known. - /// 4. Fallback — `None`: no concrete is known. Consumers treat - /// `None` as "never matches a real heap pointer", so PTR_EQ - /// comparisons with the standard vable resolve to "different" at - /// trace time. - /// Reconstruct a ref value whose recorder box carries no stamped concrete, - /// by re-executing its recorded producer against now-resolvable operands. - /// history.py:948 `resbox = execute_with_descr(...)` deferred to - /// resume-image build time: an inlined sub-walk that reads a loop-invariant - /// OUTER input arg records a `getfield_gc_r` while the obj is still symbolic - /// (its concrete lives only in the outer frame's register shadow), so - /// `concrete_of_opref` stays `None` until the seeded input arg lets the load - /// re-run here. Follows a chain of `getfield_gc_r` ops down to a resolvable - /// root; `depth` bounds the walk. Returns `None` when the chain roots at an - /// op that is neither stamped nor a ref getfield, or the obj is null. + /// [`TraceCtx::concrete_of_opref`] handles constants, the standard + /// virtualizable, and operations whose result was stamped while recording. + /// For an unstamped `GetfieldGcR`, this method recursively resolves the + /// object and rereads the described field. That case occurs when an inlined + /// sub-walk records the load before its outer-frame input acquires a concrete + /// resume value. `depth` bounds recursive field chains; unsupported + /// producers, null objects, and exhausted depth return `None`. pub fn recover_ref_value(&self, opref: OpRef, depth: u32) -> Option { if let Some(v) = self.concrete_of_opref(opref) { return Some(v); diff --git a/majit/majit-metainterp/src/warmstate.rs b/majit/majit-metainterp/src/warmstate.rs index 0cb93079c63..a63739b5501 100644 --- a/majit/majit-metainterp/src/warmstate.rs +++ b/majit/majit-metainterp/src/warmstate.rs @@ -273,7 +273,7 @@ pub use crate::memmgr::MemoryManager; // Warm state manager — the orchestrator of the JIT lifecycle. It keeps track // of per-greenkey cells and the global hot counter. -// rlib/jit.py:588-605 PARAMETERS defaults. +// Defaults from `rpython.rlib.jit.PARAMETERS`. // DEFAULT_ constants must match RPython exactly. /// rlib/jit.py:588 threshold = 1039 (just above 1024, prime) @@ -1871,7 +1871,7 @@ impl WarmEnterState { // ── set_param / get_stats API ── - /// Set a JIT parameter by name, mirroring warmstate.py set_param_*(). + /// Set a JIT parameter by its RPython name. /// /// Supported parameters: /// - "threshold": compilation threshold @@ -1879,13 +1879,12 @@ impl WarmEnterState { /// - "trace_eagerness": guard fail count before bridge compilation /// - "function_threshold": calls before inlining /// - "max_inline_depth": maximum inlining depth - /// warmstate.py: set_param() — set a JIT parameter by name. - /// Negative values for thresholds mean "disabled/off" (rpython/rlib/jit.py:843). - /// counter.py:124 — compute_threshold(threshold<=0) returns 0.0 (JIT off). - /// Parameter names match RPython exactly: vec, vec_all, vec_cost. + /// + /// `JitDriver.set_param` defines negative thresholds as disabled, and + /// `JitCounter.compute_threshold` maps a disabled threshold to `0.0`. pub fn set_param(&mut self, name: &str, value: i64) { - // counter.py:124 — threshold <= 0 → compute_threshold returns 0.0 - // (JIT off). Negative i64 must clamp to 0, not wrap to u32::MAX. + // Clamp disabled thresholds to zero instead of wrapping a negative + // value to `u32::MAX`. let as_u32 = if value < 0 { 0u32 } else { value as u32 }; match name { "threshold" => self.set_threshold(as_u32), diff --git a/majit/majit-translate/src/annotator/builtin.rs b/majit/majit-translate/src/annotator/builtin.rs index 035f8f78240..64e7d875dd6 100644 --- a/majit/majit-translate/src/annotator/builtin.rs +++ b/majit/majit-translate/src/annotator/builtin.rs @@ -84,7 +84,7 @@ use crate::flowspace::model::ConstValue; /// * `kwds_s` — keyword argument annotations, already `s_` prefixed to /// match upstream's `kwds_s['s_'+key] = s_value`. Most analysers do /// not consume keywords. -/// `unaryop.py:944-946 SomeBuiltin.call`: `args_s, kwds = args.unpack(); +/// `SomeBuiltin.call` in `unaryop.py`: `args_s, kwds = args.unpack(); /// return self.analyser(*args_s, **kwds_s)` — args_s passes through as a /// Python list possibly carrying `None` for unbound caller args. The /// analyser body raises AttributeError on the first @@ -93,7 +93,7 @@ use crate::flowspace::model::ConstValue; /// `&[Option]`: each analyser body unwraps via [`arg_at`] /// at the touch site, panicking with the analyser name and slot /// index — observationally equivalent to upstream's first-attribute- -/// touch failure, matching `unaryop.py:940 simple_call_SomeBuiltin`'s +/// touch failure, matching `simple_call_SomeBuiltin`'s /// bind-then-body sequence. pub type BuiltinAnalyzer = fn( bk: &Rc, diff --git a/majit/majit-translate/src/codewriter/codewriter.rs b/majit/majit-translate/src/codewriter/codewriter.rs index f29f9169337..6277805f58b 100644 --- a/majit/majit-translate/src/codewriter/codewriter.rs +++ b/majit/majit-translate/src/codewriter/codewriter.rs @@ -223,7 +223,7 @@ impl CodeWriter { /// the fused per-graph path. The whole /// portal closure is annotated to a fixpoint before any block is rtyped, /// matching upstream `translator.annotate()` → `rtyper.specialize()` → - /// `codewriter.make_jitcodes()` (driver.py:306/345/361) rather than the + /// `codewriter.make_jitcodes()` rather than the /// fused per-graph annotate+rtype. /// Must run AFTER `grab_initial_jitcodes` (so `candidate_graphs` is the /// portal closure) and BEFORE any `drain_pending_graphs` that publishes a @@ -238,9 +238,11 @@ impl CodeWriter { ); } - /// RPython: `CodeWriter.transform_graph_to_jitcode()` (codewriter.py:33-72). + /// Port of `CodeWriter.transform_graph_to_jitcode`. /// - /// Transforms a FunctionGraph into a JitCode through the 4-step pipeline. + /// Transforms a `FunctionGraph` into a `JitCode` through the upstream + /// transform, register-allocation, flattening, liveness, and assembly + /// pipeline. /// Upstream signature `(self, graph, jitcode, verbose, index)`. Pyre adds /// `path` / `callcontrol` / `config` as pyre-specific additions: /// - `path`: graph identity surrogate (upstream uses `graph` object @@ -251,27 +253,21 @@ impl CodeWriter { /// - `config`: pyre's `GraphTransformConfig` carries options that /// upstream keeps in globals / command-line flags. /// - /// Steps: - /// 0. annotate + rtype (majit-specific; RPython does this before codewriter) - /// 1. jtransform — `transform_graph()` (codewriter.py:42) - /// 2. regalloc — `perform_register_allocation()` per kind (codewriter.py:45-47) - /// 3. flatten — `flatten_graph()` (codewriter.py:53) - /// 3b. liveness — `compute_liveness()` (codewriter.py:56, called inside assemble) - /// 4. assemble — `assembler.assemble()` (codewriter.py:67) - /// 5. `jitcode.index = index` (codewriter.py:68) - /// 6. `if self.debug: self.print_ssa_repr(ssarepr, portal_jd, verbose)` - /// (codewriter.py:71-72) + /// Pyre performs annotation and rtyping before the upstream stages, calls + /// `compute_liveness` from its assembler, and fills the call descriptors + /// before committing the assembled body. The resulting stage order remains + /// the one implemented by `CodeWriter.transform_graph_to_jitcode`. /// /// **Type-source contract (post graph-side concretetype migration)** /// /// `regalloc`/`flatten`/`assemble`/`liveness`/`format` all read /// kinds via `FunctionGraph::concretetype_of(&v)`, which routes /// straight to the backing `Variable.concretetype` cell carried - /// inline by the `Variable` objects the IR references — RPython's - /// `Variable.concretetype` (`flowspace/model.py:280`) is the + /// inline by the `Variable` objects the IR references. RPython's + /// `flowspace.model.Variable.concretetype` is the /// single source of truth for every value. No type side-table /// parameter survives across stages: the post-rtyper merge - /// below (`merge_synth_kinds_into_graph`) stamps each synth + /// `merge_synth_kinds_into_graph` stamps each synthetic /// Variable's `.concretetype` cell via /// `set_concretetype_of_inline`, then `apply_from_flowspace_variables` /// copies lltypes from typed Variables in the `value_to_var` map so the @@ -288,31 +284,15 @@ impl CodeWriter { /// kind). What still ties the codewriter to the legacy IR is that /// it consumes [`crate::model::FunctionGraph`] and bridges to the /// rtyper's typed Variables through the identity-keyed - /// `value_to_var` map. Migrating to the `Variable`-based IR - /// throughout would let pyre drop the `value_to_var` bridge and - /// consume the rtyper's Variable graph directly — multi-week - /// scope tracked separately. - /// Shared dual-gate type-resolve entry. + /// `value_to_var` map. A graph-native typed IR would remove that bridge and + /// let the codewriter consume the rtyper's `Variable` graph directly. /// - /// Runs [`dual_gate_check_with_registry`] against the - /// program-wide `PyreCallRegistry`; on Match the real path's - /// `LegacyToTyped` map (with each `Variable.concretetype` - /// cell populated by `RPythonTyper::specialize`) is returned - /// directly, on Skip the legacy walker (`legacy_annotator::annotate` + - /// `legacy_resolve::resolve_types`) commits kinds to - /// `graph.concretetype` cells so non-portal jitcodes that didn't - /// pass the real path still get sound kinds. - /// - /// `diag_label` is appended to the optional Skip log line and the - /// real-path panic message; production callers pass - /// `path.canonical_key()`-style identification, the lib.rs - /// debug-snapshot path passes `graph.name`. - /// Run the dual-gate type resolver and commit every resolved kind - /// to each backing `Variable.concretetype` cell on `graph` (RPython - /// `rtyper.py:258 v.concretetype = ...`). Returns the - /// `LegacyToTyped` map produced by the Match arm so the - /// post-jtransform path can rebind operand Variables to the - /// upstream-typed ones; Skip arm returns `None`. + /// Run the dual-gate type resolver against the program-wide + /// `PyreCallRegistry` and commit every resolved kind to the graph's backing + /// `Variable.concretetype` cells. A Match returns the `LegacyToTyped` map so + /// post-jtransform operands can be rebound to typed variables. A Skip runs + /// the legacy annotator and resolver, commits their kinds, and returns + /// `None`. `diag_label` identifies either outcome in diagnostics. pub fn dual_gate_publish_concretetypes( &mut self, graph: &FunctionGraph, @@ -736,26 +716,22 @@ impl CodeWriter { || crate::regalloc::perform_all_register_allocations(rewritten_graph), ); - // Step 3: flatten (codewriter.py:53) - // RPython: ssarepr = flatten_graph(graph, regallocs, cpu=cpu) + // Flatten the graph after register allocation. // Each Variable's `.concretetype` cell is the kind source - // after the merge/hydration steps above; flatten reads it via + // after type merge and hydration; flatten reads it via // `FunctionGraph::concretetype_of(&var)`. `flatten_graph` - // itself runs `enforce_input_args` (flatten.py:88-100) so the + // itself runs `enforce_input_args` so the // startblock inputarg colors land in the dense `0..N` prefix // of each kind, and the rotation persists into the assembler - // call below — matching upstream `flatten.py:63-66` - // invocation order verbatim. + // assembly, matching the upstream invocation order. let mut ssarepr = crate::codewriter::transform_profile::time_phase("step3_flatten_graph", || { crate::flatten::flatten_graph(rewritten_graph, &mut regallocs) }); - // Step 3b + 4: liveness + assemble (codewriter.py:56,67) - // RPython: compute_liveness(ssarepr) then assembler.assemble(ssarepr, jitcode, num_regs) - // In majit, assemble() calls compute_liveness() internally and now - // returns the body so the codewriter can fill calldescr before - // committing the shell via `set_body`. + // `assemble_with_callcontrol` computes liveness internally and returns + // the body so call descriptors can be filled before `set_body` commits + // the JitCode shell. let mut body = crate::codewriter::transform_profile::time_phase("step4_assemble", || { self.assembler .assemble_with_callcontrol(&mut ssarepr, ®allocs, Some(callcontrol)) diff --git a/majit/majit-translate/src/codewriter/insns.rs b/majit/majit-translate/src/codewriter/insns.rs index 22fee82e2fc..6737b4c1282 100644 --- a/majit/majit-translate/src/codewriter/insns.rs +++ b/majit/majit-translate/src/codewriter/insns.rs @@ -1174,20 +1174,12 @@ pub fn wellknown_bh_insns() -> IndexMap<&'static str, u8> { /// JIT machine from a single function definition. RPython's /// metaprogramming is runtime / annotator-driven and has no /// proc-macro counterpart. The generated machine carries a -/// `JitCodeSym` whose state-field slots are accessed by flat slot -/// index (`d` argcode) without an explicit virtualizable-pointer -/// register — the canonical `setfield_vable_*` shape (`r` vable-ptr -/// + `d` FieldDescr) does not apply because the entire machine IS -/// the vable, accessed via the implicit `self` of the proc-macro- -/// generated handler functions. Migration to canonical `*_vable_*` -/// is feasible (see `epic_e_task94b_prereq_audit_2026_05_04.md`) but -/// requires materializing `state` as a vable-ptr register + -/// synthesizing FieldDescr/ArrayDescr/LenDescr objects for every -/// state slot — a 4-6 session proc-macro refactor with non-obvious -/// failure modes. Per CLAUDE.md, the proc-macro bridge is itself -/// a permitted Rust adaptation, so quarantining the 6 keys is the -/// orthodox shape: keep `wellknown_bh_insns()` strictly canonical, -/// keep the proc-macro state addressing here. +/// `JitCodeSym` whose state fields use flat slot indices (`d` argcode) +/// without an explicit virtualizable-pointer register. The canonical +/// `setfield_vable_*` shape requires an `r` vable pointer plus a `d` +/// `FieldDescr`, so it cannot address these proc-macro-owned slots. Keeping +/// the six state opcodes in `pyre_local_bh_insns()` isolates that adaptation +/// while `wellknown_bh_insns()` remains canonical. /// /// All extension keys retain their fixed `BC_*` byte values in the same /// number-space as the canonical opcodes; only the catalogue is split diff --git a/majit/majit-translate/src/inline.rs b/majit/majit-translate/src/inline.rs index b29f6c00362..834d971ee13 100644 --- a/majit/majit-translate/src/inline.rs +++ b/majit/majit-translate/src/inline.rs @@ -1212,7 +1212,7 @@ pub fn is_pure_op(kind: &OpKind) -> bool { // pure. | OpKind::LoadStatic { .. } => true, // Per-opname classification for `OpKind::BinOp` mirrors - // `simplify.CanRemove` (`simplify.py:405-417`) + + // `simplify.CanRemove` and // `enum_ops_without_sideeffects()` for binary ops. Pyre's // `OpKind::BinOp` carries the opname as a string field, so // the parity-correct classification is opname-keyed rather @@ -1223,16 +1223,16 @@ pub fn is_pure_op(kind: &OpKind) -> bool { OpKind::BinOp { op, .. } => is_pure_binop_opname(op), // Per-opname classification mirrors `enum_ops_without_sideeffects()`'s // `LL_OPERATIONS[opname].sideeffects` lookup - // (`rpython/rtyper/lltypesystem/lloperation.py:128-134`). + // (`lloperation.enum_ops_without_sideeffects`). // Pyre's `OpKind::UnaryOp` carries the opname as a string // field, so the parity-correct classification is opname-keyed // rather than enum-blanket. OpKind::UnaryOp { op, .. } => is_pure_unary_opname(op), // Side-effecting writes / calls / guards / markers / aborts. // `direct_call`-family ops are routed here even when the - // callee is elidable: `simplify.py:441-445`'s `canremove` + // callee is elidable: `simplify.transform_dead_op_vars`'s `canremove` // split treats `direct_call` as side-effecting (args go - // straight to `read_vars`), and `simplify.py:500` performs a + // straight to `read_vars`), and the same function performs a // separate elidable-graph removal that requires `translator` // to be supplied (pyre's call site passes `translator=None`, // so the removal arm is unreachable). The post-jtransform @@ -1272,7 +1272,7 @@ pub fn is_pure_op(kind: &OpKind) -> bool { // `LoweredBlackholeOp` carries register-shaped blackhole insns whose // side-effect class is opname-dependent — the same split `op_can_raise` - // (`call.rs`) and upstream `lloperation.py:382-383` (sideeffects / + // (`call.rs`) and upstream `lloperation.LL_OPERATIONS` (sideeffects / // canfold) make. The read family // (`strlen`/`unicodelen`/`strgetitem`/`unicodegetitem`) is // pure/removable; the alloc/store/copy family @@ -1306,37 +1306,34 @@ pub fn can_remove_op(kind: &OpKind) -> bool { /// Whitelist of `OpKind::UnaryOp` opnames that are side-effect-free /// upstream — direct port of the unary entries in -/// `simplify.CanRemove` (`rpython/translator/simplify.py:405-417`) +/// `simplify.CanRemove` /// + `enum_ops_without_sideeffects()` -/// (`rpython/rtyper/lltypesystem/lloperation.py:128-134`). +/// (`lloperation.enum_ops_without_sideeffects`). /// /// Any opname not in this list is treated as side-effecting so the /// dead-op DCE pass does not silently remove it. Notably absent: -/// `not` — Python's `not` is control flow, `operation.py:465-474` -/// does not register it; pyre's adapter -/// (`translator/rtyper/flowspace_adapter.rs:344-353`) requires the +/// `not` — Python's `not` is control flow and `flowspace.operation` does not +/// register it; pyre's `flowspace_adapter` requires the /// frontend to desugar `!x` away before reaching the rtyper, so DCE /// must surface a live `not` op to the rtyper rather than silently /// dropping a dead one. `front::mir` collapses Rust deref `*x` /// (no flowspace peer) when it lowers `UnaryOp`/`Deref`, so the -/// frontend never emits it; when the frontend stops emitting `not`, -/// this whitelist will only retain post-jtransform / rtyper-emitted -/// opnames. +/// frontend never emits it. /// Whitelist of `OpKind::BinOp` opnames that are side-effect-free /// upstream — direct port of the binary entries in -/// `simplify.CanRemove` (`simplify.py:405-417`) + +/// `simplify.CanRemove` plus /// `enum_ops_without_sideeffects()`. /// /// Notable omissions: /// - `and` / `or` (no trailing underscore): Rust `&&` / `||` -/// short-circuit operators; `operation.py:475-510` does not +/// short-circuit operators; `flowspace.operation` does not /// register them as binary operators (they are control flow), /// and `translator/rtyper/flowspace_adapter.rs:392-400` requires /// the frontend to desugar them before reaching the rtyper. DCE /// must keep dead occurrences alive so the rtyper surfaces the /// fail-loud TyperError instead of silently dropping them. The -/// trailing-underscore canonical names `and_` / `or_` (PyPy's -/// bitwise AND/OR registered at `operation.py:485-486`) ARE +/// trailing-underscore canonical names `and_` / `or_` are PyPy's +/// registered bitwise AND/OR operations and are /// pure and listed below. /// - `*_assign` (Rust compound assignments): `inplace_*` upstream; /// `simplify.py:CanRemove` does not include `inplace_*`, and @@ -1351,14 +1348,13 @@ pub fn can_remove_op(kind: &OpKind) -> bool { /// surfaced by frontend lowering passes, not via BinOp/UnaryOp. /// - `getattr` — variable arity (`getattr(o, name [, default])`), /// pyre lowers as `OpKind::FieldRead` / Call rather than BinOp. -/// - `get` (`operation.py:514`) — 3-arg descriptor `__get__`; pyre +/// - `get` — three-argument descriptor `__get__`; pyre /// has no TernaryOp surface. Drop is the call-DCE's concern. fn is_pure_binop_opname(opname: &str) -> bool { if matches!( opname, - // `simplify.py:405-417` CanRemove — every entry registered as - // a 2-arg `add_operator(...)` at `flowspace/operation.py`. - // Arithmetic — `operation.py:475-484`. + // `simplify.CanRemove` entries registered through + // `flowspace.operation.add_operator`. "add" | "sub" | "mul" @@ -1370,8 +1366,7 @@ fn is_pure_binop_opname(opname: &str) -> bool { | "pow" | "lshift" | "rshift" - // Canonical PyPy bitwise binops — `simplify.py:410-411` - // `and_ or_ xor` (`operation.py:485-487`). These are the + // Canonical PyPy bitwise binops `and_`, `or_`, and `xor`. These are the // trailing-underscore forms that surface after // `flowspace_adapter.rs:379-381 normalize_binop_name` rewrites // pyre's `bitand`/`bitor`/`bitxor`; both forms are pure so the @@ -1382,21 +1377,15 @@ fn is_pure_binop_opname(opname: &str) -> bool { | "bitand" | "bitor" | "bitxor" - // Comparisons — `simplify.py:411 lt le eq ne gt ge`. + // Comparisons from `simplify.CanRemove`. | "lt" | "le" | "eq" | "ne" | "gt" | "ge" - // Remaining 2-arg CanRemove entries: - // is_ operation.py:445 (identity test) - // issubtype operation.py:448 (issubclass for new-style classes) - // isinstance operation.py:449 - // getitem operation.py:457 - // cmp operation.py:511 (`simplify.py:411`) - // coerce operation.py:512 (`simplify.py:411`) - // contains operation.py:513 (`simplify.py:411`) + // Remaining two-argument `CanRemove` entries: identity/type tests, + // indexing, comparison, coercion, and containment. | "is_" | "issubtype" | "isinstance" @@ -1408,7 +1397,7 @@ fn is_pure_binop_opname(opname: &str) -> bool { return true; } // Post-rtyper / lltype binops — `enum_ops_without_sideeffects()` - // (`lloperation.py:128-134`) registers `int_add int_sub int_mul + // `lloperation.enum_ops_without_sideeffects` registers `int_add int_sub int_mul // int_lt ... uint_add ... float_add ...` with sideeffects=False. // Pyre's BinOp arrives here pre-rtyper (frontend names like // `add`); the post-rtyper shape is also accepted so a future diff --git a/majit/majit-translate/src/model.rs b/majit/majit-translate/src/model.rs index 1f9625c4644..9fcafea6086 100644 --- a/majit/majit-translate/src/model.rs +++ b/majit/majit-translate/src/model.rs @@ -3795,7 +3795,8 @@ pub(crate) fn prune_dead_boxing_remnants(graph: &mut FunctionGraph) -> usize { } Some(ExitSwitch::LastException) | None => {} } - // Terminal blocks implicitly read every inputarg (`simplify.py:459-462`). + // `simplify.transform_dead_op_vars` treats every terminal-block + // input argument as read. if block.exits.is_empty() { read_vars.extend(block.inputargs.iter().cloned()); } @@ -3874,14 +3875,16 @@ pub(crate) fn prune_dead_boxing_remnants(graph: &mut FunctionGraph) -> usize { removed } -/// Remove dead operations and dead inputargs from `graph` per -/// backward dataflow over operation operands + exitswitches + -/// `Link.args`-as-dependencies. Line-by-line port of -/// `simplify.transform_dead_op_vars_in_blocks(blocks, graphs, -/// translator=None)` (`rpython/translator/simplify.py:422-524`). +/// Remove dead operations and input arguments using the backward dataflow from +/// `simplify.transform_dead_op_vars_in_blocks`. /// -/// `blocks` is the BFS-reachable closure of every entry block (mirrors -/// `flowspace/model.py:66 iterblocks()`). +/// Removable operations contribute operand dependencies from their result; +/// side-effecting and potentially raising operations contribute immediate +/// reads. Exit switches and terminal-block inputs are reads, while each link +/// argument depends on the target input it supplies. Start-block parameters are +/// pinned as a calling-convention contract, then liveness flows backward to a +/// fixpoint before unread removable producers and their link arguments are +/// dropped. /// /// TODO: `start_blocks` is `{graph.startblock} ∪ /// {blocks with no incoming link}` rather than the strict single-graph @@ -3921,7 +3924,7 @@ pub(crate) fn prune_dead_boxing_remnants(graph: &mut FunctionGraph) -> usize { /// each block with no exits (`returnblock` / `exceptblock` / /// otherwise terminal), the inputargs are also added — return /// and except blocks implicitly use their inputs -/// (`simplify.py:459-462`). +/// (`simplify.transform_dead_op_vars`). /// 2. For every Link, record `dependencies[targetarg].add(linkarg)` /// mapping (`simplify.py:457-458`). This is the key /// difference from a naïve forward pass — link args are NOT diff --git a/majit/majit-translate/src/tool/algo/regalloc.rs b/majit/majit-translate/src/tool/algo/regalloc.rs index 5065548cdb6..c9a492ddb52 100644 --- a/majit/majit-translate/src/tool/algo/regalloc.rs +++ b/majit/majit-translate/src/tool/algo/regalloc.rs @@ -393,11 +393,10 @@ impl RegAllocator { /// `FunctionGraph::new` with `Unknown` placeholders; this helper /// stamps the canonical Signed / GcRef kinds whenever the rtyper /// hand-off (`apply_to_graph` / `apply_from_flowspace_variables`) -/// did not — equivalent to the previous `augment_value_kinds_*` -/// helper but written directly through to each backing -/// `Variable.concretetype` cell via -/// `FunctionGraph::set_concretetype_of_inline` instead of returning -/// a transitional HashMap. +/// did not. This function writes directly to each backing +/// `Variable.concretetype` cell through +/// `FunctionGraph::set_concretetype_of_inline`, preserving the graph as the +/// sole kind owner. pub(crate) fn augment_canonical_exceptblock_on_graph(graph: &mut FunctionGraph) { let except_args = &graph.block(graph.exceptblock).inputargs; if except_args.len() == 2 { diff --git a/majit/majit-translate/src/translator/backendopt/all.rs b/majit/majit-translate/src/translator/backendopt/all.rs index adc3d12435a..f43b591a02c 100644 --- a/majit/majit-translate/src/translator/backendopt/all.rs +++ b/majit/majit-translate/src/translator/backendopt/all.rs @@ -473,21 +473,20 @@ fn task_error(error: impl std::fmt::Debug) -> TaskError { } } -/// RPython `get_function(dottedname)` at `all.py:19-33`. +/// Closed-world port of RPython's `all.get_function(dottedname)`. /// /// Upstream resolves an arbitrary dotted name through `__import__` /// + `getattr`. Pyre has no Python-style import resolver, so this /// helper carries the closed-world equivalent: a registry mapping /// the dotted names that upstream config defaults ship into the -/// already-ported Rust callable. Misses surface as `TaskError` -/// (the same shape upstream's `Exception("Function %s not found")` -/// at `:31` produces); future heuristic ports register an entry -/// alongside their landing commit. +/// already-ported Rust callable. A missing entry becomes `TaskError`, matching +/// the failure produced by upstream `get_function` when `getattr` cannot find +/// the requested callable. /// -/// The two production callers live at `:83 inline_heuristic` and -/// `:101 profile_based_inline_heuristic`. Both default to +/// The two production callers are `inline_heuristic` and +/// `profile_based_inline_heuristic`. Both default to /// `"rpython.translator.backendopt.inline.inlining_heuristic"` -/// (`translationoption.py:216`, `:239`) which maps to +/// as configured by `translationoption`, and that dotted name maps to /// [`inline::inlining_heuristic`]. #[expect( clippy::type_complexity, diff --git a/majit/majit-translate/src/translator/rtyper/lltypesystem/lltype.rs b/majit/majit-translate/src/translator/rtyper/lltypesystem/lltype.rs index d6a596084a6..20d759b50b3 100644 --- a/majit/majit-translate/src/translator/rtyper/lltypesystem/lltype.rs +++ b/majit/majit-translate/src/translator/rtyper/lltypesystem/lltype.rs @@ -4348,10 +4348,10 @@ impl Array { } /// Raw `Array(OF, hints={...})`. Upstream - /// `rpython/rtyper/lltypesystem/lltype.py:428-439 Array.__init__` + /// `rpython.rtyper.lltypesystem.lltype.Array.__init__` /// + `_install_extras` forwards the `hints` kwarg to /// `self._hints`. Used by - /// `SmallFunctionSetPBCRepr._setup_repr` (rpbc.py:416-418) which + /// `SmallFunctionSetPBCRepr._setup_repr`, which /// builds `Array(self.pointer_repr.lowleveltype, /// hints={'nolength': True, 'immutable': True, /// 'static_immutable': True})`. diff --git a/majit/majit-translate/src/translator/rtyper/rpbc.rs b/majit/majit-translate/src/translator/rtyper/rpbc.rs index 3019e64d971..5ef9d1ebd80 100644 --- a/majit/majit-translate/src/translator/rtyper/rpbc.rs +++ b/majit/majit-translate/src/translator/rtyper/rpbc.rs @@ -3423,7 +3423,7 @@ impl Repr for SmallFunctionSetPBCRepr { } *self.descriptions.borrow_mut() = descriptions.clone(); - // upstream rpbc.py:416-418 — `POINTER_TABLE = Array( + // `SmallFunctionSetPBCRepr._setup_repr` defines `POINTER_TABLE = Array( // self.pointer_repr.lowleveltype, hints={...})`. let item_type = self.pointer_repr.lowleveltype().clone(); let array_type = Array::with_hints( @@ -6165,7 +6165,7 @@ impl Repr for ClassesPBCRepr { /// `convert_desc` walks `getuniqueclassdef` → `getclassrepr_arc` /// → `getruntime(self.lowleveltype)` to materialise the vtable /// pointer constant. - /// RPython `ClassesPBCRepr.rtype_getattr(self, hop)` (rpbc.py:970-987): + /// RPython `ClassesPBCRepr.rtype_getattr(self, hop)`: /// /// ```python /// def rtype_getattr(self, hop): diff --git a/majit/majit-translate/src/translator/transform.rs b/majit/majit-translate/src/translator/transform.rs index b277c6dcaf9..bc184a94ab9 100644 --- a/majit/majit-translate/src/translator/transform.rs +++ b/majit/majit-translate/src/translator/transform.rs @@ -21,9 +21,8 @@ //! - `transform_list_contains` (transform.py:115-134) //! - `transform_dead_op_vars` (transform.py:137-143) — forwards to //! [`crate::translator::simplify::transform_dead_op_vars_in_blocks`]. -//! - `transform_dead_code` + `cutoff_alwaysraising_block` -//! (transform.py:145-198). -//! - `default_extra_passes` + `transform_graph` (transform.py:246-272). +//! - `transform_dead_code` + `cutoff_alwaysraising_block`. +//! - `default_extra_passes` + `transform_graph`. //! //! `insert_ll_stackcheck` (transform.py:200-243) is rtyper-phase and //! therefore out of scope for the annotator-phase port. @@ -211,7 +210,7 @@ fn dedupe_blocks(blocks: &[BlockRef]) -> Vec { out } -/// RPython `transform.py:145-165` — `transform_dead_code(self, block_subset)`. +/// RPython `transform_dead_code(self, block_subset)` from `transform.py`. /// /// ```python /// def transform_dead_code(self, block_subset): diff --git a/pyre/cpython_tests/run.py b/pyre/cpython_tests/run.py index 6bfae050b01..b4a69c6dd67 100644 --- a/pyre/cpython_tests/run.py +++ b/pyre/cpython_tests/run.py @@ -208,6 +208,45 @@ def last_stderr_line(err: str) -> str: return "" +def traceback_verdict(lines: list[str], header: int) -> str: + """The exception line closing the traceback unittest printed at `header`. + + A block runs from the `FAIL:`/`ERROR:` header to the `====` rule opening + the next one (or unittest's closing `Ran N tests`). Every frame inside a + traceback is indented, so the exception is the first unindented line after + the `Traceback` banner. The banner is what arms the search rather than the + exception simply being the block's last unindented line, because an + assertion failure prints its diff below the `AssertionError` and those + lines are unindented too. + + A chained exception prints one traceback per link, joined by `The above + exception ...` / `During handling ...`. The last link is the one the test + actually failed with, so each banner overwrites the previous answer. + """ + verdict = "(no traceback)" + armed = False + for line in lines[header + 1:]: + stripped = line.strip() + if stripped.startswith("====") or stripped.startswith("Ran "): + break + if stripped.startswith("Traceback ("): + armed = True + continue + # Continuation lines of a traceback are indented, its separator rules + # are punctuation, and blank lines end nothing. + if not stripped or line[:1].isspace() or set(stripped) <= {"-", "="}: + continue + if stripped.startswith("File "): + continue + if armed: + verdict = stripped[:160] + armed = False + elif verdict == "(no traceback)": + # No banner opened this block — a bare `SkipTest` reason, say. + verdict = stripped[:160] + return verdict + + def failure_digest(out: str, err: str) -> str: """Which cases unittest reported against, and its closing verdict. @@ -220,14 +259,21 @@ def failure_digest(out: str, err: str) -> str: unittest writes `FAIL: ` / `ERROR: ` headers and one closing `FAILED (...)`; those are the runner's own account of what went wrong, and they are what a CI log needs to carry. + + The header names the case but not the cause, and a case that only fails on + the CI host cannot be re-run locally to find out — so each header carries + the closing line of its traceback (the exception type and message), which + is the one line that says what actually went wrong. """ lines = f"{out}\n{err}".splitlines() cases: list[str] = [] verdict = "" - for line in lines: + for idx, line in enumerate(lines): line = line.strip() - if line.startswith(("FAIL: ", "ERROR: ")) and line not in cases: - cases.append(line) + if line.startswith(("FAIL: ", "ERROR: ")): + entry = f"{line} -> {traceback_verdict(lines, idx)}" + if entry not in cases: + cases.append(entry) elif line.startswith("FAILED ("): verdict = line shown = cases[:4] @@ -282,7 +328,9 @@ def classify(rc: int, out: str, err: str) -> tuple[str, str]: if ran: # An IMPORTERROR never reached unittest, so its tail is all there is; # a FAIL has unittest's own account, and only that names a test. - return "FAIL", f"rc={rc} {failure_digest(out, err) or last}"[:300] + # Wide enough for four cases that each now carry their exception line; + # at 300 the digest was cut off inside the first case's name. + return "FAIL", f"rc={rc} {failure_digest(out, err) or last}"[:900] return "IMPORTERROR", f"rc={rc} {last}"[:120] diff --git a/pyre/extra_tests/parity_tests/module_name_lone_surrogate.py b/pyre/extra_tests/parity_tests/module_name_lone_surrogate.py new file mode 100644 index 00000000000..7a9ff0fd02c --- /dev/null +++ b/pyre/extra_tests/parity_tests/module_name_lone_surrogate.py @@ -0,0 +1,34 @@ +# CPython-suite gap: test_module never names a module with a lone surrogate. +# parity-tests reason: PyPy keeps the name object, so no encoding step can reject it. + +"""A module name carrying a lone surrogate survives construction and lookup. + +The import machinery reaches such a name whenever a filename was decoded with +surrogateescape, which is how `test_import.test_unencodable_filename` imports +`TESTFN_UNENCODABLE`. Reading it as UTF-8 anywhere on the way in aborts the +interpreter rather than raising. +""" + +import types + +name = "mod-\udcff" + +module = types.ModuleType(name) +assert module.__name__ is name +assert module.__dict__["__name__"] is name + +# `module.__init__` re-seeds the name on an already-built module, the path +# `module.__new__` leaves for the import machinery to fill in. +reseeded = types.ModuleType("anonymous") +reseeded.__init__(name) +assert reseeded.__name__ is name + +# `repr` formats the name rather than storing it, so it takes its own route +# out — through `repr` of the name, which escapes the surrogate. +assert repr(name)[1:-1] in repr(module) + +# A surrogate name is an ordinary dict key, so the module resolves under it. +registry = {module.__name__: module} +assert registry[name] is module + +print("OK") diff --git a/pyre/pyre-interpreter/src/module/unicodedata/mod.rs b/pyre/pyre-interpreter/src/module/unicodedata/mod.rs index 36590c3d2a9..65c0a330128 100644 --- a/pyre/pyre-interpreter/src/module/unicodedata/mod.rs +++ b/pyre/pyre-interpreter/src/module/unicodedata/mod.rs @@ -107,14 +107,14 @@ fn char_and_default( // `LEGACY` for the `ucd_3_2_0` instance. fn category_impl(db: &ucd_core::Ucd, args: &[PyObjectRef]) -> PyResult { - Ok(w_str_new(db.category(one_char("category", args)?))) + Ok(w_str_new_managed(db.category(one_char("category", args)?))) } fn category(args: &[PyObjectRef]) -> PyResult { category_impl(&MODERN, args) } fn bidirectional_impl(db: &ucd_core::Ucd, args: &[PyObjectRef]) -> PyResult { - Ok(w_str_new( + Ok(w_str_new_managed( db.bidirectional(one_char("bidirectional", args)?), )) } @@ -123,7 +123,7 @@ fn bidirectional(args: &[PyObjectRef]) -> PyResult { } fn east_asian_width_impl(db: &ucd_core::Ucd, args: &[PyObjectRef]) -> PyResult { - Ok(w_str_new( + Ok(w_str_new_managed( db.east_asian_width(one_char("east_asian_width", args)?), )) } @@ -146,7 +146,7 @@ fn mirrored(args: &[PyObjectRef]) -> PyResult { } fn decomposition_impl(db: &ucd_core::Ucd, args: &[PyObjectRef]) -> PyResult { - Ok(w_str_new( + Ok(w_str_new_managed( &db.decomposition(one_char("decomposition", args)?), )) } @@ -198,7 +198,7 @@ fn name_impl(db: &ucd_core::Ucd, args: &[PyObjectRef]) -> PyResult { if db.category(cp) != UNASSIGNED_CATEGORY && let Some(name) = cp.to_char().and_then(ucd_core::character_name) { - return Ok(w_str_new(&name)); + return Ok(w_str_new_managed(&name)); } default.ok_or_else(|| PyError::value_error("no such name")) } @@ -230,7 +230,7 @@ fn lookup(args: &[PyObjectRef]) -> PyResult { { let mut buf = String::with_capacity(ch.len_utf8()); buf.push(ch); - return Ok(w_str_new(&buf)); + return Ok(w_str_new_managed(&buf)); } let mut msg = Wtf8Buf::from_string("undefined character name '".to_string()); msg.push_wtf8(name); diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index e47edd098f2..79deaf8e586 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -19883,6 +19883,10 @@ fn init_object_type(ns: PyObjectRef) { unsafe { pyre_object::w_tuple_len(args[0]) as i64 } } else if unsafe { pyre_object::is_bytes(args[0]) } { unsafe { pyre_object::w_bytes_len(args[0]) as i64 } + } else if unsafe { pyre_object::is_type(args[0]) } { + // A type's variable tail is its `__slots__` member + // table, so `Py_SIZE` is the slot count. + unsafe { pyre_object::w_type_get_nslots(args[0]) as i64 } } else if std::ptr::eq( unsafe { pyre_object::w_type_get_layout(w_type) }, &pyre_object::memoryview::MEMORYVIEW_TYPE, diff --git a/pyre/pyre-jit/src/jit/codewriter.rs b/pyre/pyre-jit/src/jit/codewriter.rs index a591fd16415..334ec5eed1c 100644 --- a/pyre/pyre-jit/src/jit/codewriter.rs +++ b/pyre/pyre-jit/src/jit/codewriter.rs @@ -1062,14 +1062,18 @@ fn derive_pc_live_indices_from_sparse( .collect() } -/// Per-`py_pc` pre-merge index of the post-`residual_call` `-live-` that -/// immediately precedes a `catch_exception`, derived from a SPLICED -/// (canonical) SSARepr. These after-residual-call resume anchors feed the -/// runtime's post-call catch-marker twin once -/// `compute_liveness_with_pc_anchors` remaps them (`liveness.rs:78-81`) into -/// the spliced bytes. For each `catch_exception`, the bare `-live-` directly -/// before it is the anchor, keyed to the canraise opcode that owns the call -/// (the py_pc whose `pc_first_insn_pos` range contains the marker). +/// Derive the pre-merge `-live-` anchor that immediately precedes each +/// `catch_exception` in the canonical SSA representation. The anchor is keyed +/// by the Python PC whose `pc_first_insn_pos` range owns it, then +/// `compute_liveness_with_pc_anchors` remaps it into the spliced instruction +/// stream used by the runtime's post-call catch marker. +/// +/// `derive_after_call_indices_from_sparse` stores one anchor per Python PC. +/// Multiple `catch_exception` sites owned by one PC would overwrite that entry. +/// The representation is sound because `catch_exception` is emitted +/// once for each can-raise block exit, while additional catch links from a +/// multi-exit block lower through `make_exception_link`, which emits no +/// `catch_exception`. fn derive_after_call_indices_from_sparse( ssarepr: &super::flatten::SSARepr, n_pcs: usize, @@ -4442,6 +4446,26 @@ fn catch_live_census_enabled() -> bool { *ENABLED.get_or_init(|| std::env::var_os("PYRE_CATCH_LIVE_CENSUS").is_some()) } +/// Return the Ref-register colors live at every landing named by a +/// `catch_exception` instruction's `TLabel` operands. +fn catch_landing_ref_colors( + args: &[super::flatten::Operand], + label2alive: &std::collections::HashMap< + String, + std::collections::HashSet, + >, +) -> std::collections::BTreeSet { + use super::flatten::{Kind as SsaKind, Operand as SsaOperand}; + args.iter() + .filter_map(|op| match op { + SsaOperand::TLabel(label) => label2alive.get(&label.name), + _ => None, + }) + .flat_map(|alive| alive.iter()) + .filter_map(|reg| (reg.kind == SsaKind::Ref).then_some(reg.index)) + .collect() +} + #[derive(Default)] struct CatchLiveCensus { catch_sites: usize, @@ -4488,7 +4512,6 @@ fn catch_target_extra_ref_colors( >, code: &CodeObject, ) -> std::collections::BTreeMap> { - use super::flatten::{Kind as SsaKind, Operand as SsaOperand}; let mut out: std::collections::BTreeMap> = std::collections::BTreeMap::new(); let census_enabled = catch_live_census_enabled(); @@ -4513,16 +4536,6 @@ fn catch_target_extra_ref_colors( // Folded ties resolve to the higher PC; both candidates share the folded // marker anyway, so widening lands on the same `out` key. let pre_merge_pc_pos = census_enabled.then(|| sparse_pc_owner_table(ssarepr)); - let catch_ref_colors_from_args = |args: &[SsaOperand]| -> std::collections::BTreeSet { - args.iter() - .filter_map(|op| match op { - SsaOperand::TLabel(label) => label2alive.get(&label.name), - _ => None, - }) - .flat_map(|alive| alive.iter()) - .filter_map(|reg| (reg.kind == SsaKind::Ref).then_some(reg.index)) - .collect() - }; let catch_ref_colors_at = |catch_idx: usize| -> std::collections::BTreeSet { let Some(super::flatten::Insn::Op { opname, args, .. }) = ssarepr.insns.get(catch_idx) else { @@ -4531,7 +4544,7 @@ fn catch_target_extra_ref_colors( if opname != "catch_exception" { return std::collections::BTreeSet::new(); } - catch_ref_colors_from_args(args) + catch_landing_ref_colors(args, label2alive) }; let census = { let mut census = CatchLiveCensus::default(); @@ -4668,6 +4681,121 @@ fn catch_target_extra_ref_colors( out } +/// When `PYRE_CATCH_LIVE_CENSUS` is enabled, verify after marker finalization +/// that every `catch_exception` landing's live Ref colors occur in the resume +/// marker owned by the same Python PC. +/// +/// `catch_target_extra_ref_colors` has two widening routes. The anchored route +/// reads the single `after_call_markers[pc]` entry and therefore cannot +/// represent multiple anchored sites owned by one PC; `multi_site_*` counters +/// report that population. The anchorless route reads each site directly and +/// has no one-entry limit. Independently, intersecting marker liveness with SSA +/// liveness can remove a color carried by dataflow unless one of those routes +/// adds it back. +/// +/// Unreachable PCs are skipped because `filter_liveness_in_place` clears their +/// markers and no execution can resume from them; an empty marker for such a PC +/// is therefore valid. +fn catch_live_coverage_census( + ssarepr: &super::flatten::SSARepr, + live_markers: &[usize], + first_insn_post_merge: &[Option], + label2alive: &std::collections::HashMap< + String, + std::collections::HashSet, + >, + is_reachable: impl Fn(usize) -> bool, + code: &CodeObject, +) { + use super::flatten::{Kind as SsaKind, Operand as SsaOperand}; + let mut pc_pos: Vec<(usize, usize)> = first_insn_post_merge + .iter() + .enumerate() + .filter_map(|(pc, entry)| entry.map(|pos| (pos, pc))) + .collect(); + pc_pos.sort_unstable(); + + let mut sites: Vec<(usize, Option, std::collections::BTreeSet)> = Vec::new(); + let mut sites_per_pc: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for (q, insn) in ssarepr.insns.iter().enumerate() { + let super::flatten::Insn::Op { opname, args, .. } = insn else { + continue; + }; + if opname != "catch_exception" { + continue; + } + let owner = sparse_owner_pc(&pc_pos, q); + if let Some(pc) = owner { + *sites_per_pc.entry(pc).or_default() += 1; + } + sites.push((q, owner, catch_landing_ref_colors(args, label2alive))); + } + + let marker_ref_colors = |idx: usize| -> std::collections::BTreeSet { + ssarepr + .insns + .get(idx) + .and_then(|insn| insn.live_args()) + .map(|args| { + args.iter() + .filter_map(|op| match op { + SsaOperand::Register(reg) if reg.kind == SsaKind::Ref => Some(reg.index), + _ => None, + }) + .collect() + }) + .unwrap_or_default() + }; + + let multi_site_pcs = sites_per_pc.values().filter(|&&n| n > 1).count(); + let mut unowned_sites = 0usize; + let mut skipped_unreachable = 0usize; + let mut uncovered_sites = 0usize; + let mut uncovered_colors = 0usize; + let mut multi_site_uncovered = 0usize; + for (q, owner, landing) in &sites { + let Some(pc) = *owner else { + unowned_sites += 1; + continue; + }; + if !is_reachable(pc) { + skipped_unreachable += 1; + continue; + } + let Some(&marker) = live_markers.get(pc) else { + unowned_sites += 1; + continue; + }; + let covered = marker_ref_colors(marker); + let missing: Vec = landing.difference(&covered).copied().collect(); + if missing.is_empty() { + continue; + } + uncovered_sites += 1; + uncovered_colors += missing.len(); + let sites_at_pc = sites_per_pc.get(&pc).copied().unwrap_or(0); + if sites_at_pc > 1 { + multi_site_uncovered += 1; + } + eprintln!( + "[catch-live-uncovered] code={} q={q} owning_py_pc={pc} marker={marker} \ + sites_at_pc={sites_at_pc} landing_ref_colors={landing:?} \ + missing_ref_colors={missing:?}", + code.obj_name + ); + } + eprintln!( + "[catch-live-coverage] code={} sites={} owned_pcs={} multi_site_pcs={multi_site_pcs} \ + unowned_sites={unowned_sites} skipped_unreachable={skipped_unreachable} \ + uncovered_sites={uncovered_sites} uncovered_colors={uncovered_colors} \ + multi_site_uncovered={multi_site_uncovered}", + code.obj_name, + sites.len(), + sites_per_pc.len(), + ); +} + /// RPython: `liveness.py:19-80` `compute_liveness(ssarepr)` — /// backward dataflow over the populated `SSARepr` that fills each /// `-live-` marker with the set of registers alive across it. @@ -5246,6 +5374,16 @@ fn filter_liveness_in_place( } existing.extend(non_register); } + if catch_live_census_enabled() { + catch_live_coverage_census( + ssarepr, + &live_markers, + &first_insn_post_merge, + &label2alive, + |pc| live_vars.is_reachable(pc), + code, + ); + } ( live_markers_out, after_call_post_merge,