From 85b88fbb1a83a28a8a159caa19a3d9a9bf3d490f Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 20:46:39 +0900 Subject: [PATCH 1/6] Fix fannkuch loop merge points --- .../majit-metainterp/src/jitcode/assembler.rs | 25 +++++---- .../src/optimizeopt/optimizer.rs | 19 +------ majit/majit-metainterp/src/resume.rs | 52 ++++++++++++++++++- majit/majit-metainterp/src/warmstate.rs | 10 ++-- pyre/pyre-jit-trace/src/jitcode_dispatch.rs | 3 ++ pyre/pyre-jit-trace/src/state.rs | 28 ++++++++++ pyre/pyre-jit-trace/src/trace_opcode.rs | 12 ++++- pyre/pyre-jit/src/jit/codewriter.rs | 28 +++------- 8 files changed, 119 insertions(+), 58 deletions(-) diff --git a/majit/majit-metainterp/src/jitcode/assembler.rs b/majit/majit-metainterp/src/jitcode/assembler.rs index 58b84242e36..d42c7670cc9 100644 --- a/majit/majit-metainterp/src/jitcode/assembler.rs +++ b/majit/majit-metainterp/src/jitcode/assembler.rs @@ -1350,19 +1350,18 @@ impl JitCodeBuilder { reds_r: &[u8], reds_f: &[u8], ) { - // Capture the bytecode offset of the OPCODE byte (before - // `write_insn` pushes it). jtransform.py:1690-1712 emits exactly - // one `jit_merge_point` per portal jitcode; assert the second - // call so a double-emit lowerer bug fails loud rather than - // overwriting the offset and leaving the validator on the wrong - // payload. - assert!( - self.jit_merge_point_offset.is_none(), - "JitCodeBuilder::jit_merge_point called twice; \ - jtransform.py:1690-1712 emits exactly one merge point per \ - portal jitcode", - ); - self.jit_merge_point_offset = Some(self.code.len()); + // Capture the bytecode offset of the first OPCODE byte (before + // `write_insn` pushes it). PyPy's portal dispatch loop executes + // `jit_merge_point()` at every bytecode dispatch, so a lowered + // portal jitcode can contain more than one merge point when pyre + // materializes several Python loop headers in one dispatch body. + // The offset is only used by `register_dispatch_jitcode` for + // schema validation, so keeping the first one preserves that + // validation without rejecting later structurally identical merge + // points. + if self.jit_merge_point_offset.is_none() { + self.jit_merge_point_offset = Some(self.code.len()); + } if (-128..=127).contains(&jdindex) { self.write_insn("jit_merge_point/cIRFIRF"); self.push_u8((jdindex & 0xFF) as u8); diff --git a/majit/majit-metainterp/src/optimizeopt/optimizer.rs b/majit/majit-metainterp/src/optimizeopt/optimizer.rs index 2a536af945f..2444777ce33 100644 --- a/majit/majit-metainterp/src/optimizeopt/optimizer.rs +++ b/majit/majit-metainterp/src/optimizeopt/optimizer.rs @@ -3238,14 +3238,6 @@ impl Optimizer { OptContext::with_inputarg_types(32, &types) }); - // RPython parity: export virtual state BEFORE flush() and - // force_at_the_end_of_preamble(). In RPython, get_virtual_state - // is called inside _jump_to_existing_trace on the original Box - // objects whose PtrInfo reflects the pre-flush optimizer state. - // flush() may force pending virtuals, changing their PtrInfo to - // non-virtual. Capture the virtual state before any forcing. - let pre_force_vs = crate::optimizeopt::virtualstate::export_state(&jump_args, &ctx); - self.flush(&mut ctx); // unroll.py:204-205: force_at_the_end_of_preamble for each jump arg @@ -3256,13 +3248,6 @@ impl Optimizer { } ctx.current_pass_idx = saved_pass_idx; - // RPython parity: jump_op.getarglist() returns the ORIGINAL Boxes. - // force_at_the_end_of_preamble recurses into virtual fields but - // does NOT force the top-level virtuals. So get_virtual_state on - // the original args still sees Virtual PtrInfo. - // DO NOT resolve jump_args here — pass originals so virtual_state - // is computed from the pre-force state. - // unroll.py:206-211: jump_to_existing_trace(force_boxes=False) // RPython iterates ALL target_tokens; preamble (virtual_state=None) // is skipped inside jump_to_existing_trace (unroll.py:327-328). @@ -3275,7 +3260,7 @@ impl Optimizer { &mut ctx, false, &pre_opt_jump_args, - Some(pre_force_vs.clone()), + None, ) { Ok(vs) => vs, // unroll.py:209-210: except InvalidLoop → jump_to_preamble @@ -3328,7 +3313,7 @@ impl Optimizer { &mut ctx, true, &pre_opt_jump_args, - Some(pre_force_vs), + None, ) { Ok(vs) => vs, // unroll.py:224-225: except InvalidLoop: pass diff --git a/majit/majit-metainterp/src/resume.rs b/majit/majit-metainterp/src/resume.rs index 93c55bd4aa4..42117e1ac29 100644 --- a/majit/majit-metainterp/src/resume.rs +++ b/majit/majit-metainterp/src/resume.rs @@ -4095,7 +4095,27 @@ impl ResumeDataLoopMemo { numb_state.append_short(tagged); continue; } - let box_type = snapshot_box.tp.unwrap_or_else(|| env.get_type(opref)); + // resume.py:201-212: + // + // box = iter.get(...) + // box = box.get_box_replacement() + // ... + // if box.type == 'r': + // + // The type used for virtual classification is the replacement + // box's type, not the original snapshot slot's fallback type. A + // snapshot slot can carry an Int fallback from tracing but forward + // to a Ref virtual after optimization; keeping the stale fallback + // would number that virtual as a TAGBOX and the subsequent + // optimizer.py:681 fail-arg force would materialize it. + let box_type = if opref == raw_opref { + opref + .ty() + .or(snapshot_box.tp) + .unwrap_or_else(|| env.get_type(opref)) + } else { + env.get_type(opref) + }; let is_virtual = match box_type { majit_ir::Type::Ref => env.is_virtual_ref(opref), majit_ir::Type::Int => env.is_virtual_raw(opref), @@ -5073,6 +5093,36 @@ mod tests { assert_eq!(val, 1); } + #[test] + fn test_number_boxes_uses_replacement_type_for_virtual_classification() { + use majit_ir::OpRef; + let mut memo = ResumeDataLoopMemo::new(); + let mut env = SimpleBoxEnv::new(); + + // RPython resume.py reads box.type after get_box_replacement(). + // Model a stale Int-typed snapshot slot that now forwards to a Ref + // virtual, the shape produced by optimized boxed-int locals. + let source = OpRef::int_op(1); + let target = OpRef::ref_op(2); + env.replacements.insert(source.raw(), target); + env.virtuals.insert(target.raw()); + env.types.insert(target.raw(), majit_ir::Type::Ref); + + let snapshot = Snapshot::single_frame_boxes( + 0, + 10, + vec![SnapshotBox::typed(source, majit_ir::Type::Int)], + ); + let numb_state = memo.number(&snapshot, &env, -1).unwrap(); + let items = crate::resumecode::unpack_all(&numb_state.create_numbering()); + + let (val, tagbits) = untag(items[6] as i16); + assert_eq!(tagbits, TAGVIRTUAL); + assert_eq!(val, 0); + assert_eq!(numb_state.num_boxes, 0); + assert_eq!(numb_state.num_virtuals, 1); + } + #[test] fn test_multi_frame_snapshot() { use majit_ir::OpRef; diff --git a/majit/majit-metainterp/src/warmstate.rs b/majit/majit-metainterp/src/warmstate.rs index 2ccdb65cab2..a655ba44a26 100644 --- a/majit/majit-metainterp/src/warmstate.rs +++ b/majit/majit-metainterp/src/warmstate.rs @@ -269,8 +269,8 @@ const DEFAULT_MAX_INLINE_DEPTH: u32 = 7; /// rlib/jit.py:592 trace_limit = 6000 const DEFAULT_TRACE_LIMIT: u32 = crate::trace_ctx::DEFAULT_TRACE_LIMIT as u32; -/// warmspot.py:93 retrace_limit=5 -const DEFAULT_RETRACE_LIMIT: u32 = 5; +/// rlib/jit.py:595 retrace_limit = 0. +const DEFAULT_RETRACE_LIMIT: u32 = 0; /// rlib/jit.py:598 max_unroll_loops = 0 const DEFAULT_MAX_UNROLL_LOOPS: u32 = 0; @@ -455,8 +455,7 @@ impl WarmEnterState { pureop_historylength: 16, memory_manager: { let mut m = crate::memmgr::MemoryManager::new(0); - // warmspot.py:93 test default retrace_limit=5 (rlib/jit.py:588 - // PARAMETERS is 0, applied in production via set_user_param). + // rlib/jit.py:595 PARAMETERS default retrace_limit=0. m.retrace_limit = DEFAULT_RETRACE_LIMIT; // rlib/jit.py:598 / pyjitpl.py:2946: default 0 means // the first cancelled unrolled compile immediately retries @@ -492,8 +491,7 @@ impl WarmEnterState { pureop_historylength: 16, memory_manager: { let mut m = crate::memmgr::MemoryManager::new(0); - // warmspot.py:93 test default retrace_limit=5 (rlib/jit.py:588 - // PARAMETERS is 0, applied in production via set_user_param). + // rlib/jit.py:595 PARAMETERS default retrace_limit=0. m.retrace_limit = DEFAULT_RETRACE_LIMIT; // rlib/jit.py:598 / pyjitpl.py:2946: default 0 means // the first cancelled unrolled compile immediately retries diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch.rs index 785b1b818dd..d1cc2886383 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch.rs @@ -9296,6 +9296,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let ret_byte = *insns_opname_to_byte() @@ -9368,6 +9369,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let raise_byte = *insns_opname_to_byte() @@ -9439,6 +9441,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let ret_byte = *insns_opname_to_byte() .get("ref_return/r") diff --git a/pyre/pyre-jit-trace/src/state.rs b/pyre/pyre-jit-trace/src/state.rs index e99950c0a97..1bff1720859 100644 --- a/pyre/pyre-jit-trace/src/state.rs +++ b/pyre/pyre-jit-trace/src/state.rs @@ -1593,6 +1593,11 @@ pub struct MIFrame { /// bank may include post-regalloc color slots above the semantic /// locals+stack prefix. pub(crate) pre_opcode_semantic_depth: Option, + /// When a Python bytecode normally classified as may-raise is handled by + /// a generated jtransform-style primitive lowering, the opcode is no + /// longer an `exc=True` residual call and must not emit the trailing + /// GUARD_NO_EXCEPTION for this one step. + pub(crate) suppress_guard_no_exception_for_opcode: bool, /// PyPy capture_resumedata: parent frame chain for multi-frame guards. /// Each entry points at one parent frame plus the resumepc that /// should be used when that parent is snapshotted. This stays much @@ -6426,6 +6431,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let err = OpcodeStepExecutor::reraise(&mut state, 0).expect_err("reraise should raise"); @@ -6549,6 +6555,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let err = @@ -6576,6 +6583,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6611,6 +6619,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6656,6 +6665,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6701,6 +6711,7 @@ mod tests { concrete_frame_addr: (&mut *frame) as *mut pyre_interpreter::PyFrame as usize, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6738,6 +6749,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6781,6 +6793,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6883,6 +6896,7 @@ mod tests { concrete_frame_addr: (&mut *frame) as *mut pyre_interpreter::PyFrame as usize, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; ::push_value( @@ -6928,6 +6942,7 @@ mod tests { concrete_frame_addr: (&mut *frame) as *mut pyre_interpreter::PyFrame as usize, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; frame.push(caught_exc); @@ -7200,6 +7215,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; state.with_ctx(|this, ctx| { @@ -7239,6 +7255,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let _ = state.with_ctx(|this, ctx| this.trace_guarded_int_payload(ctx, int_obj)); @@ -7286,6 +7303,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; trace_unbox_int_with_resume( &mut state, @@ -7342,6 +7360,7 @@ mod tests { concrete_frame_addr: (&mut *frame) as *mut pyre_interpreter::PyFrame as usize, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let instance_ref = ctx.const_ref(instance as i64); @@ -7720,6 +7739,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let loaded = @@ -7772,6 +7792,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; state @@ -7807,6 +7828,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let _ = ::trace_binary_value( @@ -7849,6 +7871,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let _ = state @@ -7916,6 +7939,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let concrete_lhs = w_int_new(10); @@ -8005,6 +8029,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let concrete_lhs = w_int_new(10); @@ -8100,6 +8125,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; assert!( @@ -8465,6 +8491,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let jump_args = state.with_ctx(|this, ctx| this.close_loop_args(ctx)); @@ -8543,6 +8570,7 @@ mod tests { concrete_frame_addr: frame_ptr, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let jump_args = state.with_ctx(|this, ctx| this.close_loop_args(ctx)); diff --git a/pyre/pyre-jit-trace/src/trace_opcode.rs b/pyre/pyre-jit-trace/src/trace_opcode.rs index fceaa1601f9..38480b3daa6 100644 --- a/pyre/pyre-jit-trace/src/trace_opcode.rs +++ b/pyre/pyre-jit-trace/src/trace_opcode.rs @@ -620,6 +620,7 @@ impl MIFrame { orgpc, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, parent_frames: Vec::new(), pending_result_stack_idx: None, pending_result_type: None, @@ -4521,6 +4522,12 @@ impl MIFrame { )) })?; if handled { + // RPython parity: once STORE_SUBSCR is lowered through + // jtransform's list strategy primitive path, the emitted ops are + // guard_class/check_index/setarrayitem, not an exc=True residual + // call. The generic bytecode-level may-raise classification must + // not append GUARD_NO_EXCEPTION to this primitive lowering. + self.suppress_guard_no_exception_for_opcode = true; return Ok(()); } self.trace_store_subscr(obj, key, value) @@ -5931,6 +5938,7 @@ impl MIFrame { self.set_orgpc(pc); self.prepare_fallthrough(); + self.suppress_guard_no_exception_for_opcode = false; // RPython pyjitpl.py captures resumedata at each guard site, not at // every opcode boundary. Pyre still needs an opcode-start snapshot // for stack-machine opcodes that can mutate stack/register state @@ -6048,7 +6056,7 @@ impl MIFrame { // RPython pyjitpl.py:1956-1957 execute_varargs: exc=True ops // always call handle_possible_exception, which internally decides // GUARD_EXCEPTION vs GUARD_NO_EXCEPTION. - if instruction_may_raise(instruction) { + if instruction_may_raise(instruction) && !self.suppress_guard_no_exception_for_opcode { let action = self.handle_possible_exception(code, pc); if !matches!(action, TraceAction::Continue) { return action; @@ -7557,6 +7565,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let active = frame.get_list_of_active_boxes(&mut ctx, false, false); @@ -7632,6 +7641,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: Some(vec![local0, local1, stack0]), pre_opcode_semantic_depth: Some(3), + suppress_guard_no_exception_for_opcode: false, }; let active = frame.get_list_of_active_boxes(&mut ctx, false, false); diff --git a/pyre/pyre-jit/src/jit/codewriter.rs b/pyre/pyre-jit/src/jit/codewriter.rs index 3790fa47ae3..35e96960c16 100644 --- a/pyre/pyre-jit/src/jit/codewriter.rs +++ b/pyre/pyre-jit/src/jit/codewriter.rs @@ -3146,25 +3146,13 @@ impl CodeWriter { // are live at each PC. let mut depth_at_pc: Vec = vec![0; num_instrs]; // RPython parity: every backward jump goes through dispatch() → - // jit_merge_point(). The blackhole's bhimpl_jit_merge_point raises - // ContinueRunningNormally at the bottommost level. Ideally all - // loop headers should emit BC_JIT_MERGE_POINT, but the - // CRN→interpreter→JIT-reentry cycle crashes in JIT compiled code - // because blackhole-modified frame locals can contain values - // incompatible with the compiled trace's assumptions. Until the - // full RPython CRN→portal_ptr restart is implemented, only the - // first loop header is a merge point. - // RPython jtransform.py:1690: jit_merge_point only in the portal - // graph. merge_point_pc is the trace entry PC (from bound_reached). - // Other loop headers use loop_header (no-op in the blackhole). - let merge_point_pc = if is_portal { - merge_point_pc.or_else(|| loop_header_pcs.iter().copied().min()) - } else { - // Callee — no jit_merge_point emit. RPython's jtransform.py:1690 - // `jit_merge_point only in the portal graph` is the matching - // statement. - None - }; + // jit_merge_point(). `merge_point_pc` is still threaded in from + // bound_reached as the trace-entry refinement hint, but portal + // jitcode emission must not restrict merge-point bytecodes to that + // single PC: PyPy's dispatch loop reaches a portal merge point for + // every bytecode dispatch, and nested Python loops rely on the + // blackhole CRN at those inner headers to compile and target their + // own loops instead of growing giant bridges. // pyframe.py:379-417 pushvalue/popvalue_maybe_none parity: // Each push/pop writes self.valuestackdepth = depth ± 1. @@ -4478,7 +4466,7 @@ impl CodeWriter { emit_live_placeholder!(ssarepr); if loop_header_pcs.contains(&py_pc) { - if merge_point_pc == Some(py_pc) { + if is_portal { // interp_jit.py:64 portal contract: // greens = ['next_instr', 'is_being_profiled', 'pycode'] // reds = ['frame', 'ec'] From ed4e19f226208b039fb68db09c8c6d7c0a10af4f Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 20:53:19 +0900 Subject: [PATCH 2/6] Suppress inline primitive store exception guard --- pyre/pyre-jit-trace/src/trace_opcode.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-jit-trace/src/trace_opcode.rs b/pyre/pyre-jit-trace/src/trace_opcode.rs index 38480b3daa6..fb4f5637586 100644 --- a/pyre/pyre-jit-trace/src/trace_opcode.rs +++ b/pyre/pyre-jit-trace/src/trace_opcode.rs @@ -6431,6 +6431,7 @@ impl MIFrame { self.set_orgpc(pc); self.prepare_fallthrough(); + self.suppress_guard_no_exception_for_opcode = false; // Keep inline-frame guard capture aligned with the root-frame path: // only opcodes that can actually reach a guard carry an opcode-start // snapshot, and specific guard paths may still suppress it. @@ -6555,7 +6556,7 @@ impl MIFrame { return InlineTraceStepAction::Trace(self.handle_possible_exception(code, pc)); } } - if instruction_may_raise(instruction) { + if instruction_may_raise(instruction) && !self.suppress_guard_no_exception_for_opcode { let exc_action = self.handle_possible_exception(code, pc); if !matches!(exc_action, TraceAction::Continue) { return InlineTraceStepAction::Trace(exc_action); From 7f0633eeaba5612b99b38e69d87c8c74b0c6da70 Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 21:06:34 +0900 Subject: [PATCH 3/6] Fix MIFrame test initializers --- pyre/pyre-jit-trace/src/state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyre/pyre-jit-trace/src/state.rs b/pyre/pyre-jit-trace/src/state.rs index 1bff1720859..021172739c6 100644 --- a/pyre/pyre-jit-trace/src/state.rs +++ b/pyre/pyre-jit-trace/src/state.rs @@ -6481,6 +6481,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let err = OpcodeStepExecutor::reraise(&mut state, 1).expect_err("reraise should raise"); @@ -6523,6 +6524,7 @@ mod tests { concrete_frame_addr: 0, pre_opcode_registers_r: None, pre_opcode_semantic_depth: None, + suppress_guard_no_exception_for_opcode: false, }; let err = OpcodeStepExecutor::reraise(&mut state, 1).expect_err("reraise should raise"); From 3fe0e4d6f837cf17d28c9ec2ffbaae55c4a751c0 Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 21:09:29 +0900 Subject: [PATCH 4/6] Remove unreachable portal loop header fallback --- pyre/pyre-jit/src/jit/codewriter.rs | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/pyre/pyre-jit/src/jit/codewriter.rs b/pyre/pyre-jit/src/jit/codewriter.rs index 35e96960c16..b6a0f9cbacb 100644 --- a/pyre/pyre-jit/src/jit/codewriter.rs +++ b/pyre/pyre-jit/src/jit/codewriter.rs @@ -4587,20 +4587,6 @@ impl CodeWriter { }, ) .emit_space_operation(&graph_op); - } else if let Some(jdindex) = portal_jd_index { - // jtransform.py:1714 handle_jit_marker__loop_header - // asserts `jd is not None` — only emit when this - // compilation belongs to a registered jitdriver. - let loop_header_op = emit_graph_op_void( - ¤t_block.block(), - "loop_header", - vec![super::flow::Constant::signed(jdindex as i64).into()], - py_pc as i64, - ); - GraphFlattener::new(&mut ssarepr, |_variable| { - unreachable!("loop_header graph op does not carry Variables") - }) - .emit_space_operation(&loop_header_op); } } @@ -9962,11 +9948,11 @@ mod tests { .callcontrol() .find_compiled_jitcode_arc(code_ptr) .expect("make_jitcodes must populate the registered portal"); + assert_eq!(pyjit.jitcode.jitdriver_sd(), Some(0)); assert!( - pyjit.merge_point_pc.is_some(), - "registered portal with no hint should infer its first loop header" + pyjit.jitcode.exec.jit_merge_point_offset.is_some(), + "registered portal with no hint should still emit portal jit_merge_point bytecode" ); - assert_eq!(pyjit.jitcode.jitdriver_sd(), Some(0)); } #[test] From b4764051f6e646a9009fc58b7b6acbcdb376c24d Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 21:12:14 +0900 Subject: [PATCH 5/6] Prefer intrinsic replacement type in resume numbering --- majit/majit-metainterp/src/resume.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/majit/majit-metainterp/src/resume.rs b/majit/majit-metainterp/src/resume.rs index 42117e1ac29..01c9658b5cb 100644 --- a/majit/majit-metainterp/src/resume.rs +++ b/majit/majit-metainterp/src/resume.rs @@ -4108,14 +4108,10 @@ impl ResumeDataLoopMemo { // to a Ref virtual after optimization; keeping the stale fallback // would number that virtual as a TAGBOX and the subsequent // optimizer.py:681 fail-arg force would materialize it. - let box_type = if opref == raw_opref { - opref - .ty() - .or(snapshot_box.tp) - .unwrap_or_else(|| env.get_type(opref)) - } else { - env.get_type(opref) - }; + let box_type = opref + .ty() + .or(snapshot_box.tp) + .unwrap_or_else(|| env.get_type(opref)); let is_virtual = match box_type { majit_ir::Type::Ref => env.is_virtual_ref(opref), majit_ir::Type::Int => env.is_virtual_raw(opref), From a03950cbbec371fd30a44454c83b90e0b8cab16a Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 13 May 2026 21:15:49 +0900 Subject: [PATCH 6/6] Strengthen resume replacement type test --- majit/majit-metainterp/src/resume.rs | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/majit/majit-metainterp/src/resume.rs b/majit/majit-metainterp/src/resume.rs index 01c9658b5cb..de069d144b9 100644 --- a/majit/majit-metainterp/src/resume.rs +++ b/majit/majit-metainterp/src/resume.rs @@ -5092,8 +5092,74 @@ mod tests { #[test] fn test_number_boxes_uses_replacement_type_for_virtual_classification() { use majit_ir::OpRef; + struct RefOnlyVirtualEnv { + constants: HashMap, + replacements: HashMap, + types: HashMap, + virtuals: std::collections::HashSet, + virtual_fields: HashMap, + } + + impl RefOnlyVirtualEnv { + fn new() -> Self { + Self { + constants: HashMap::new(), + replacements: HashMap::new(), + types: HashMap::new(), + virtuals: std::collections::HashSet::new(), + virtual_fields: HashMap::new(), + } + } + } + + impl BoxEnv for RefOnlyVirtualEnv { + fn get_box_replacement(&self, opref: majit_ir::OpRef) -> majit_ir::OpRef { + self.replacements + .get(&opref.raw()) + .copied() + .unwrap_or(opref) + } + + fn get_box_replacement_not_const(&self, opref: majit_ir::OpRef) -> majit_ir::OpRef { + self.get_box_replacement(opref) + } + + fn is_const(&self, opref: majit_ir::OpRef) -> bool { + self.constants.contains_key(&opref.raw()) + } + + fn get_const(&self, opref: majit_ir::OpRef) -> (i64, majit_ir::Type) { + self.constants + .get(&opref.raw()) + .copied() + .unwrap_or((0, majit_ir::Type::Int)) + } + + fn get_type(&self, opref: majit_ir::OpRef) -> majit_ir::Type { + self.types + .get(&opref.raw()) + .copied() + .unwrap_or(majit_ir::Type::Int) + } + + fn is_virtual_ref(&self, opref: majit_ir::OpRef) -> bool { + self.virtuals.contains(&opref.raw()) + } + + fn is_virtual_raw(&self, _opref: majit_ir::OpRef) -> bool { + false + } + + fn get_virtual_fields( + &self, + opref: majit_ir::OpRef, + ) -> Option { + self.virtual_fields.get(&opref.raw()).cloned() + } + } + let mut memo = ResumeDataLoopMemo::new(); - let mut env = SimpleBoxEnv::new(); + let mut env = RefOnlyVirtualEnv::new(); // RPython resume.py reads box.type after get_box_replacement(). // Model a stale Int-typed snapshot slot that now forwards to a Ref