summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Bernstein <rubybugs@bernsteinbear.com>2026-02-09 16:40:25 -0500
committerGitHub <noreply@github.com>2026-02-09 21:40:25 +0000
commit9efe557da5e17fc860f40956192e9df2e6bd1e6c (patch)
tree2288d6eb5d2c976b537d8880d7d9220d85eb6b08
parent51d4c2c465e3ce708dddcd65d93e6e29225b454d (diff)
ZJIT: Drop has_blockiseq guard on local variable access (#16113)
So `has_blockiseq` was masking two other bugs. First, that we had only a static EP escape check for an iseq. We need to check both that an iseq is not known to statically escape them *and that it has not escaped them before*. Only then can we use SSA locals. This popped up in a weird PP unit test that was reducible to the test cases added in the PR. So I fixed that. Second, that we didn't VM lock when code patching for EP escape. That showed up in the Ractor btests in my previous PRs #14600 and #15254 #15316, and I had no idea what was going on at the time, so I dropped it. But if we add this lock, like all the other invariant patching cases have, the problem goes away. Seems reasonable. Finally, we can use normal full SSA locals even in the presence of a blockiseq. We still reload all of them after each send-with-block, but we can in a later PR reduce that to just the locals that are (recursively) visibly written to.
-rw-r--r--zjit/src/hir.rs24
-rw-r--r--zjit/src/hir/opt_tests.rs120
-rw-r--r--zjit/src/invariants.rs24
3 files changed, 128 insertions, 40 deletions
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs
index 7812c6058e..3fd5fcc5cb 100644
--- a/zjit/src/hir.rs
+++ b/zjit/src/hir.rs
@@ -6087,14 +6087,12 @@ pub fn jit_entry_insns(iseq: IseqPtr) -> Vec<u32> {
struct BytecodeInfo {
jump_targets: Vec<u32>,
- has_blockiseq: bool,
}
fn compute_bytecode_info(iseq: *const rb_iseq_t, opt_table: &[u32]) -> BytecodeInfo {
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
let mut insn_idx = 0;
let mut jump_targets: HashSet<u32> = opt_table.iter().copied().collect();
- let mut has_blockiseq = false;
while insn_idx < iseq_size {
// Get the current pc and opcode
let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx) };
@@ -6118,18 +6116,12 @@ fn compute_bytecode_info(iseq: *const rb_iseq_t, opt_table: &[u32]) -> BytecodeI
jump_targets.insert(insn_idx);
}
}
- YARVINSN_send | YARVINSN_invokesuper => {
- let blockiseq: IseqPtr = get_arg(pc, 1).as_iseq();
- if !blockiseq.is_null() {
- has_blockiseq = true;
- }
- }
_ => {}
}
}
let mut result = jump_targets.into_iter().collect::<Vec<_>>();
result.sort();
- BytecodeInfo { jump_targets: result, has_blockiseq }
+ BytecodeInfo { jump_targets: result }
}
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -6244,7 +6236,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
// Compute a map of PC->Block by finding jump targets
let jit_entry_insns = jit_entry_insns(iseq);
- let BytecodeInfo { jump_targets, has_blockiseq } = compute_bytecode_info(iseq, &jit_entry_insns);
+ let BytecodeInfo { jump_targets } = compute_bytecode_info(iseq, &jit_entry_insns);
// Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps
// in good places, but it's not necessary for correctness. TODO: Higher quality scheduling during lowering.
@@ -6276,7 +6268,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
// Check if the EP is escaped for the ISEQ from the beginning. We give up
// optimizing locals in that case because they're shared with other frames.
- let ep_escaped = iseq_escapes_ep(iseq);
+ let ep_starts_escaped = iseq_escapes_ep(iseq);
+ // Check if the EP has been escaped at some point in the ISEQ. If it has, then we assume that
+ // its EP is shared with other frames.
+ let ep_has_been_escaped = crate::invariants::iseq_escapes_ep(iseq);
+ let ep_escaped = ep_starts_escaped || ep_has_been_escaped;
// Iteratively fill out basic blocks using a queue.
// TODO(max): Basic block arguments at edges
@@ -6620,7 +6616,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
// Use FrameState to get kw_bits when possible, just like getlocal_WC_0.
let val = if !local_inval {
state.getlocal(ep_offset)
- } else if ep_escaped || has_blockiseq {
+ } else if ep_escaped {
fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false })
} else {
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state.without_locals() });
@@ -6743,7 +6739,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
// In case of JIT-to-JIT send locals might never end up in EP memory.
let val = state.getlocal(ep_offset);
state.stack_push(val);
- } else if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
+ } else if ep_escaped {
// Read the local using EP
let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false });
state.setlocal(ep_offset, val); // remember the result to spill on side-exits
@@ -6764,7 +6760,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
YARVINSN_setlocal_WC_0 => {
let ep_offset = get_arg(pc, 0).as_u32();
let val = state.stack_pop()?;
- if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
+ if ep_escaped {
// Write the local using EP
fun.push_insn(block, Insn::SetLocal { val, ep_offset, level: 0 });
} else if local_inval {
diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs
index d0d2e19078..afbbc8bedc 100644
--- a/zjit/src/hir/opt_tests.rs
+++ b/zjit/src/hir/opt_tests.rs
@@ -2926,15 +2926,14 @@ mod hir_opt_tests {
Jump bb2(v5, v6)
bb2(v8:BasicObject, v9:NilClass):
v13:Fixnum[1] = Const Value(1)
- SetLocal :a, l0, EP@3, v13
PatchPoint NoSingletonClass(Object@0x1000)
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
v31:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v8, HeapObject[class_exact*:Object@VALUE(0x1000)]
IncrCounter inline_iseq_optimized_send_count
- v20:BasicObject = GetLocal :a, l0, EP@3
- v24:BasicObject = GetLocal :a, l0, EP@3
+ v19:BasicObject = GetLocal :a, l0, EP@3
+ PatchPoint NoEPEscape(test)
CheckInterrupts
- Return v24
+ Return v19
");
}
@@ -3415,15 +3414,14 @@ mod hir_opt_tests {
Jump bb2(v6, v7, v8)
bb2(v10:BasicObject, v11:BasicObject, v12:NilClass):
v16:ArrayExact = NewArray
- SetLocal :a, l0, EP@3, v16
- v22:TrueClass = Const Value(true)
+ v21:TrueClass = Const Value(true)
IncrCounter complex_arg_pass_caller_kwarg
- v24:BasicObject = Send v11, 0x1000, :each_line, v22 # SendFallbackReason: Complex argument passing
- v25:BasicObject = GetLocal :s, l0, EP@4
- v26:BasicObject = GetLocal :a, l0, EP@3
- v30:BasicObject = GetLocal :a, l0, EP@3
+ v23:BasicObject = Send v11, 0x1000, :each_line, v21 # SendFallbackReason: Complex argument passing
+ v24:BasicObject = GetLocal :s, l0, EP@4
+ v25:BasicObject = GetLocal :a, l0, EP@3
+ PatchPoint NoEPEscape(test)
CheckInterrupts
- Return v30
+ Return v25
");
}
@@ -6523,7 +6521,6 @@ mod hir_opt_tests {
Jump bb2(v5, v6)
bb2(v8:BasicObject, v9:NilClass):
v13:ArrayExact = NewArray
- SetLocal :result, l0, EP@3, v13
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, A)
v36:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
@@ -6533,10 +6530,10 @@ mod hir_opt_tests {
PatchPoint NoSingletonClass(Array@0x1020)
PatchPoint MethodRedefined(Array@0x1020, zip@0x1028, cme:0x1030)
v43:BasicObject = CCallVariadic v36, :zip@0x1058, v39
- v25:BasicObject = GetLocal :result, l0, EP@3
- v29:BasicObject = GetLocal :result, l0, EP@3
+ v24:BasicObject = GetLocal :result, l0, EP@3
+ PatchPoint NoEPEscape(test)
CheckInterrupts
- Return v29
+ Return v24
");
}
@@ -11887,4 +11884,97 @@ mod hir_opt_tests {
Return v31
");
}
+
+ #[test]
+ fn recompile_after_ep_escape_uses_ep_locals() {
+ // When a method creates a lambda, EP escapes to the heap. After
+ // invalidation and recompilation, the compiler must use EP-based
+ // locals (SetLocal/GetLocal) instead of SSA locals, because the
+ // spill target (stack) and the read target (heap EP) diverge.
+ eval("
+ CONST = {}.freeze
+ def test_ep_escape(list, sep=nil, iter_method=:each)
+ sep ||= lambda { }
+ kwsplat = CONST
+ list.__send__(iter_method) {|*v| yield(*v) }
+ end
+
+ test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
+ test_ep_escape([1], lambda { }) { |x| }
+ }
+ test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
+ test_ep_escape([1], lambda { }) { |x| }
+ }
+ ");
+ assert_snapshot!(hir_string("test_ep_escape"), @r"
+ fn test_ep_escape@<compiled>:3:
+ bb0():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ v2:BasicObject = GetLocal :list, l0, SP@7
+ v3:BasicObject = GetLocal :sep, l0, SP@6
+ v4:BasicObject = GetLocal :iter_method, l0, SP@5
+ v5:NilClass = Const Value(nil)
+ v6:CPtr = LoadPC
+ v7:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
+ v8:CBool = IsBitEqual v6, v7
+ IfTrue v8, bb2(v1, v2, v3, v4, v5)
+ v10:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
+ v11:CBool = IsBitEqual v6, v10
+ IfTrue v11, bb4(v1, v2, v3, v4, v5)
+ Jump bb6(v1, v2, v3, v4, v5)
+ bb1(v15:BasicObject, v16:BasicObject):
+ EntryPoint JIT(0)
+ v17:NilClass = Const Value(nil)
+ v18:NilClass = Const Value(nil)
+ v19:NilClass = Const Value(nil)
+ Jump bb2(v15, v16, v17, v18, v19)
+ bb2(v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass):
+ v42:NilClass = Const Value(nil)
+ SetLocal :sep, l0, EP@5, v42
+ Jump bb4(v35, v36, v42, v38, v39)
+ bb3(v22:BasicObject, v23:BasicObject, v24:BasicObject):
+ EntryPoint JIT(1)
+ v25:NilClass = Const Value(nil)
+ v26:NilClass = Const Value(nil)
+ Jump bb4(v22, v23, v24, v25, v26)
+ bb4(v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass):
+ v53:StaticSymbol[:each] = Const Value(VALUE(0x1010))
+ SetLocal :iter_method, l0, EP@4, v53
+ Jump bb6(v46, v47, v48, v53, v50)
+ bb5(v29:BasicObject, v30:BasicObject, v31:BasicObject, v32:BasicObject):
+ EntryPoint JIT(2)
+ v33:NilClass = Const Value(nil)
+ Jump bb6(v29, v30, v31, v32, v33)
+ bb6(v57:BasicObject, v58:BasicObject, v59:BasicObject, v60:BasicObject, v61:NilClass):
+ CheckInterrupts
+ v67:CBool = Test v59
+ v68:Truthy = RefineType v59, Truthy
+ IfTrue v67, bb7(v57, v58, v68, v60, v61)
+ v70:Falsy = RefineType v59, Falsy
+ PatchPoint NoSingletonClass(Object@0x1018)
+ PatchPoint MethodRedefined(Object@0x1018, lambda@0x1020, cme:0x1028)
+ v114:HeapObject[class_exact*:Object@VALUE(0x1018)] = GuardType v57, HeapObject[class_exact*:Object@VALUE(0x1018)]
+ v115:BasicObject = CCallWithFrame v114, :Kernel#lambda@0x1050, block=0x1058
+ v74:BasicObject = GetLocal :list, l0, EP@6
+ v76:BasicObject = GetLocal :iter_method, l0, EP@4
+ v77:BasicObject = GetLocal :kwsplat, l0, EP@3
+ SetLocal :sep, l0, EP@5, v115
+ Jump bb7(v57, v74, v115, v76, v77)
+ bb7(v81:BasicObject, v82:BasicObject, v83:BasicObject, v84:BasicObject, v85:BasicObject):
+ PatchPoint SingleRactorMode
+ PatchPoint StableConstantNames(0x1060, CONST)
+ v110:HashExact[VALUE(0x1068)] = Const Value(VALUE(0x1068))
+ SetLocal :kwsplat, l0, EP@3, v110
+ v95:BasicObject = GetLocal :list, l0, EP@6
+ v97:BasicObject = GetLocal :iter_method, l0, EP@4
+ v99:BasicObject = Send v95, 0x1070, :__send__, v97 # SendFallbackReason: Send: unsupported method type Optimized
+ v100:BasicObject = GetLocal :list, l0, EP@6
+ v101:BasicObject = GetLocal :sep, l0, EP@5
+ v102:BasicObject = GetLocal :iter_method, l0, EP@4
+ v103:BasicObject = GetLocal :kwsplat, l0, EP@3
+ CheckInterrupts
+ Return v99
+ ");
+ }
}
diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs
index f1180acf2a..7aa13cbfcb 100644
--- a/zjit/src/invariants.rs
+++ b/zjit/src/invariants.rs
@@ -206,20 +206,22 @@ pub extern "C" fn rb_zjit_invalidate_no_ep_escape(iseq: IseqPtr) {
return;
}
- // Remember that this ISEQ may escape EP
- let invariants = ZJITState::get_invariants();
- invariants.ep_escape_iseqs.insert(iseq);
+ with_vm_lock(src_loc!(), || {
+ // Remember that this ISEQ may escape EP
+ let invariants = ZJITState::get_invariants();
+ invariants.ep_escape_iseqs.insert(iseq);
- // If the ISEQ has been compiled assuming it doesn't escape EP, invalidate the JIT code.
- if let Some(patch_points) = invariants.no_ep_escape_iseq_patch_points.get(&iseq) {
- debug!("EP is escaped: {}", iseq_name(iseq));
+ // If the ISEQ has been compiled assuming it doesn't escape EP, invalidate the JIT code.
+ if let Some(patch_points) = invariants.no_ep_escape_iseq_patch_points.get(&iseq) {
+ debug!("EP is escaped: {}", iseq_name(iseq));
- // Invalidate the patch points for this ISEQ
- let cb = ZJITState::get_code_block();
- compile_patch_points!(cb, patch_points, "EP is escaped: {}", iseq_name(iseq));
+ // Invalidate the patch points for this ISEQ
+ let cb = ZJITState::get_code_block();
+ compile_patch_points!(cb, patch_points, "EP is escaped: {}", iseq_name(iseq));
- cb.mark_all_executable();
- }
+ cb.mark_all_executable();
+ }
+ });
}
/// Track that JIT code for a ISEQ will assume that base pointer is equal to environment pointer.