Skip to content

Commit 9ba2669

Browse files
committed
ZJIT: Check dynamic EP escape flag on recompilation
When a method's EP escapes to the heap (e.g. due to lambda creation), the NoEPEscape PatchPoint fires and invalidates the JIT code. On recompilation, iseq_to_hir() was only checking the static cruby::iseq_escapes_ep() (which only returns true for main/eval frames), missing the dynamic invariants::iseq_escapes_ep() that tracks runtime EP escapes. This caused recompiled code to keep using SSA locals, but since EP had moved to the heap, the stack-based spills and heap-based reads pointed to different memory, making locals appear nil.
1 parent 8dc2676 commit 9ba2669

File tree

3 files changed

+144
-17
lines changed

3 files changed

+144
-17
lines changed

test/ruby/test_zjit.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4838,6 +4838,38 @@ def test(x)
48384838
}
48394839
end
48404840

4841+
def test_ep_escape_with_optional_params
4842+
# Regression test: when a method with optional params creates a lambda
4843+
# (causing EP to escape), recompilation must use EP-based locals instead
4844+
# of SSA locals. Otherwise, the spill writes to the old stack location
4845+
# while reads go to the heap-allocated EP, causing locals to appear as nil.
4846+
assert_runs ':ok', %q{
4847+
CONST = {}.freeze
4848+
def test(list, sep=nil, iter_method=:each)
4849+
sep ||= lambda { }
4850+
first = true
4851+
kwsplat = CONST
4852+
list.__send__(iter_method) {|*v|
4853+
if first
4854+
first = false
4855+
else
4856+
sep.call
4857+
end
4858+
yield(*v)
4859+
}
4860+
end
4861+
4862+
test({a: 1}, nil, :each_pair) { |k, v|
4863+
test([1], lambda { }) { |x| }
4864+
}
4865+
4866+
test({a: 1}, nil, :each_pair) { |k, v|
4867+
test([1], lambda { }) { |x| }
4868+
}
4869+
:ok
4870+
}
4871+
end
4872+
48414873
def test_exit_tracing
48424874
# This is a very basic smoke test. The StackProf format
48434875
# this option generates is external to us.

zjit/src/hir.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6266,7 +6266,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
62666266

62676267
// Check if the EP is escaped for the ISEQ from the beginning. We give up
62686268
// optimizing locals in that case because they're shared with other frames.
6269-
let ep_escaped = iseq_escapes_ep(iseq);
6269+
let ep_escaped = iseq_escapes_ep(iseq) || crate::invariants::iseq_escapes_ep(iseq);
62706270

62716271
// Iteratively fill out basic blocks using a queue.
62726272
// TODO(max): Basic block arguments at edges

zjit/src/hir/opt_tests.rs

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10556,24 +10556,25 @@ mod hir_opt_tests {
1055610556
Jump bb2(v8, v9, v10, v11, v12)
1055710557
bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:BasicObject, v18:NilClass):
1055810558
CheckInterrupts
10559+
SetLocal :formatted, l0, EP@3, v15
1055910560
PatchPoint SingleRactorMode
10560-
v56:HeapBasicObject = GuardType v14, HeapBasicObject
10561-
v57:CShape = LoadField v56, :_shape_id@0x1000
10562-
v58:CShape[0x1001] = GuardBitEquals v57, CShape(0x1001)
10563-
StoreField v56, :@formatted@0x1002, v15
10564-
WriteBarrier v56, v15
10565-
v61:CShape[0x1003] = Const CShape(0x1003)
10566-
StoreField v56, :_shape_id@0x1000, v61
10567-
v45:Class[VMFrozenCore] = Const Value(VALUE(0x1008))
10561+
v57:HeapBasicObject = GuardType v14, HeapBasicObject
10562+
v58:CShape = LoadField v57, :_shape_id@0x1000
10563+
v59:CShape[0x1001] = GuardBitEquals v58, CShape(0x1001)
10564+
StoreField v57, :@formatted@0x1002, v15
10565+
WriteBarrier v57, v15
10566+
v62:CShape[0x1003] = Const CShape(0x1003)
10567+
StoreField v57, :_shape_id@0x1000, v62
10568+
v46:Class[VMFrozenCore] = Const Value(VALUE(0x1008))
1056810569
PatchPoint NoSingletonClass(Class@0x1010)
1056910570
PatchPoint MethodRedefined(Class@0x1010, lambda@0x1018, cme:0x1020)
10570-
v66:BasicObject = CCallWithFrame v45, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050
10571-
v48:BasicObject = GetLocal :a, l0, EP@6
10572-
v49:BasicObject = GetLocal :_b, l0, EP@5
10573-
v50:BasicObject = GetLocal :_c, l0, EP@4
10574-
v51:BasicObject = GetLocal :formatted, l0, EP@3
10571+
v67:BasicObject = CCallWithFrame v46, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050
10572+
v49:BasicObject = GetLocal :a, l0, EP@6
10573+
v50:BasicObject = GetLocal :_b, l0, EP@5
10574+
v51:BasicObject = GetLocal :_c, l0, EP@4
10575+
v52:BasicObject = GetLocal :formatted, l0, EP@3
1057510576
CheckInterrupts
10576-
Return v66
10577+
Return v67
1057710578
");
1057810579
}
1057910580

@@ -11575,8 +11576,9 @@ mod hir_opt_tests {
1157511576
v35:HeapObject[class_exact:B] = GuardType v10, HeapObject[class_exact:B]
1157611577
v36:BasicObject = CCallWithFrame v35, :Kernel#proc@0x1038, block=0x1040
1157711578
v18:BasicObject = GetLocal :blk, l0, EP@4
11578-
PatchPoint NoEPEscape(foo)
11579-
v27:BasicObject = InvokeSuper v10, 0x1048, v36 # SendFallbackReason: super: complex argument passing to `super` call
11579+
SetLocal :other_block, l0, EP@3, v36
11580+
v25:BasicObject = GetLocal :other_block, l0, EP@3
11581+
v27:BasicObject = InvokeSuper v10, 0x1048, v25 # SendFallbackReason: super: complex argument passing to `super` call
1158011582
CheckInterrupts
1158111583
Return v27
1158211584
");
@@ -11882,4 +11884,97 @@ mod hir_opt_tests {
1188211884
Return v31
1188311885
");
1188411886
}
11887+
11888+
#[test]
11889+
fn recompile_after_ep_escape_uses_ep_locals() {
11890+
// When a method creates a lambda, EP escapes to the heap. After
11891+
// invalidation and recompilation, the compiler must use EP-based
11892+
// locals (SetLocal/GetLocal) instead of SSA locals, because the
11893+
// spill target (stack) and the read target (heap EP) diverge.
11894+
eval("
11895+
CONST = {}.freeze
11896+
def test_ep_escape(list, sep=nil, iter_method=:each)
11897+
sep ||= lambda { }
11898+
kwsplat = CONST
11899+
list.__send__(iter_method) {|*v| yield(*v) }
11900+
end
11901+
11902+
test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
11903+
test_ep_escape([1], lambda { }) { |x| }
11904+
}
11905+
test_ep_escape({a: 1}, nil, :each_pair) { |k, v|
11906+
test_ep_escape([1], lambda { }) { |x| }
11907+
}
11908+
");
11909+
assert_snapshot!(hir_string("test_ep_escape"), @r"
11910+
fn test_ep_escape@<compiled>:3:
11911+
bb0():
11912+
EntryPoint interpreter
11913+
v1:BasicObject = LoadSelf
11914+
v2:BasicObject = GetLocal :list, l0, SP@7
11915+
v3:BasicObject = GetLocal :sep, l0, SP@6
11916+
v4:BasicObject = GetLocal :iter_method, l0, SP@5
11917+
v5:NilClass = Const Value(nil)
11918+
v6:CPtr = LoadPC
11919+
v7:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
11920+
v8:CBool = IsBitEqual v6, v7
11921+
IfTrue v8, bb2(v1, v2, v3, v4, v5)
11922+
v10:CPtr[CPtr(0x1000)] = Const CPtr(0x1008)
11923+
v11:CBool = IsBitEqual v6, v10
11924+
IfTrue v11, bb4(v1, v2, v3, v4, v5)
11925+
Jump bb6(v1, v2, v3, v4, v5)
11926+
bb1(v15:BasicObject, v16:BasicObject):
11927+
EntryPoint JIT(0)
11928+
v17:NilClass = Const Value(nil)
11929+
v18:NilClass = Const Value(nil)
11930+
v19:NilClass = Const Value(nil)
11931+
Jump bb2(v15, v16, v17, v18, v19)
11932+
bb2(v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass):
11933+
v42:NilClass = Const Value(nil)
11934+
SetLocal :sep, l0, EP@5, v42
11935+
Jump bb4(v35, v36, v42, v38, v39)
11936+
bb3(v22:BasicObject, v23:BasicObject, v24:BasicObject):
11937+
EntryPoint JIT(1)
11938+
v25:NilClass = Const Value(nil)
11939+
v26:NilClass = Const Value(nil)
11940+
Jump bb4(v22, v23, v24, v25, v26)
11941+
bb4(v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass):
11942+
v53:StaticSymbol[:each] = Const Value(VALUE(0x1010))
11943+
SetLocal :iter_method, l0, EP@4, v53
11944+
Jump bb6(v46, v47, v48, v53, v50)
11945+
bb5(v29:BasicObject, v30:BasicObject, v31:BasicObject, v32:BasicObject):
11946+
EntryPoint JIT(2)
11947+
v33:NilClass = Const Value(nil)
11948+
Jump bb6(v29, v30, v31, v32, v33)
11949+
bb6(v57:BasicObject, v58:BasicObject, v59:BasicObject, v60:BasicObject, v61:NilClass):
11950+
CheckInterrupts
11951+
v67:CBool = Test v59
11952+
v68:Truthy = RefineType v59, Truthy
11953+
IfTrue v67, bb7(v57, v58, v68, v60, v61)
11954+
v70:Falsy = RefineType v59, Falsy
11955+
PatchPoint NoSingletonClass(Object@0x1018)
11956+
PatchPoint MethodRedefined(Object@0x1018, lambda@0x1020, cme:0x1028)
11957+
v114:HeapObject[class_exact*:Object@VALUE(0x1018)] = GuardType v57, HeapObject[class_exact*:Object@VALUE(0x1018)]
11958+
v115:BasicObject = CCallWithFrame v114, :Kernel#lambda@0x1050, block=0x1058
11959+
v74:BasicObject = GetLocal :list, l0, EP@6
11960+
v76:BasicObject = GetLocal :iter_method, l0, EP@4
11961+
v77:BasicObject = GetLocal :kwsplat, l0, EP@3
11962+
SetLocal :sep, l0, EP@5, v115
11963+
Jump bb7(v57, v74, v115, v76, v77)
11964+
bb7(v81:BasicObject, v82:BasicObject, v83:BasicObject, v84:BasicObject, v85:BasicObject):
11965+
PatchPoint SingleRactorMode
11966+
PatchPoint StableConstantNames(0x1060, CONST)
11967+
v110:HashExact[VALUE(0x1068)] = Const Value(VALUE(0x1068))
11968+
SetLocal :kwsplat, l0, EP@3, v110
11969+
v95:BasicObject = GetLocal :list, l0, EP@6
11970+
v97:BasicObject = GetLocal :iter_method, l0, EP@4
11971+
v99:BasicObject = Send v95, 0x1070, :__send__, v97 # SendFallbackReason: Send: unsupported method type Optimized
11972+
v100:BasicObject = GetLocal :list, l0, EP@6
11973+
v101:BasicObject = GetLocal :sep, l0, EP@5
11974+
v102:BasicObject = GetLocal :iter_method, l0, EP@4
11975+
v103:BasicObject = GetLocal :kwsplat, l0, EP@3
11976+
CheckInterrupts
11977+
Return v99
11978+
");
11979+
}
1188511980
}

0 commit comments

Comments
 (0)