diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d5d381acfa6a87..1b20018130bfed 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -159,6 +159,17 @@ define_split_jumps! { jz => Jz, } +/// Record on the ISEQ payload whether `self` is guaranteed to be a heap object, +/// derived from the owning class of the method entry on `cfp`. Called from compile +/// triggers before the HIR is built so the `self`-producing instructions can be +/// typed precisely. Must be called while holding the VM lock (it writes the payload). +fn update_self_is_heap_object(iseq: IseqPtr, cfp: CfpPtr) { + let cme = unsafe { rb_vm_frame_method_entry(cfp) }; + let self_is_heap_object = !cme.is_null() + && iseq_self_is_heap_object(iseq, unsafe { (*cme).owner }); + get_or_create_iseq_payload(iseq).self_is_heap_object = self_is_heap_object; +} + /// CRuby API to compile a given ISEQ. /// If jit_exception is true, compile JIT code for handling exceptions. /// See jit_compile_exception() for details. @@ -173,6 +184,10 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. with_vm_lock(src_loc!(), || { + // The current frame is this ISEQ's method frame, so its method entry tells + // us the owning class and thus whether `self` is always a heap object. + update_self_is_heap_object(iseq, unsafe { get_ec_cfp(ec) }); + let cb = ZJITState::get_code_block(); let mut code_ptr = with_time_stat(compile_time_ns, || gen_iseq_entry_point(cb, iseq, jit_exception)); @@ -3152,6 +3167,11 @@ c_callable! { let cb = ZJITState::get_code_block(); let native_stack_full = unsafe { rb_ec_stack_check(ec as _) } != 0; let payload = get_or_create_iseq_payload(iseq); + // cfp is the callee's (this ISEQ's) frame here, so its method entry gives + // the owning class and thus whether `self` is always a heap object. + let cme = unsafe { rb_vm_frame_method_entry(cfp) }; + payload.self_is_heap_object = !cme.is_null() + && iseq_self_is_heap_object(iseq, unsafe { (*cme).owner }); let last_status = payload.versions.last().map(|version| &unsafe { version.as_ref() }.status); let compile_error = match last_status { Some(IseqStatus::CantCompile(err)) => Some(err), diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index b0993717187d83..698212241e503d 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2285,6 +2285,39 @@ fn test_fixnum_floor() { assert_snapshot!(assert_compiles("test(4)"), @"0"); } +#[test] +fn test_fixnum_div_min_by_neg_one() { + // FIXNUM_MIN / -1 overflows to a Bignum: the JIT must side exit, not return a mistyped Fixnum. + eval(" + def test(a, b) = a / b + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles_allowing_exits("test(-4611686018427387904, -1)"), @"4611686018427387904"); +} + +#[test] +fn test_fixnum_div_overflow_propagation() { + // The div must side exit before its Bignum result reaches the specialized (a / b) & 1 op. + eval(" + def test(a, b) = (a / b) & 1 + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles_allowing_exits("test(-4611686018427387904, -1)"), @"0"); +} + +#[test] +fn test_fixnum_div_by_neg_one_is_fine() { + // x / -1 (x != FIXNUM_MIN) is a normal Fixnum and must NOT trip the overflow guard. + eval(" + def test(a, b) = a / b + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles("test(10, -1)"), @"-10"); +} + #[test] fn test_opt_not() { eval(" diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index eb3241b0a2fb08..4e4a246b8a1f72 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1547,6 +1547,38 @@ pub fn class_has_leaf_allocator(class: VALUE) -> bool { unsafe { rb_zjit_class_has_default_allocator(class) } } +/// Whether a method ISEQ defined on `owner` is guaranteed to run with a `self` +/// that is a heap (non-immediate) object. +/// +/// True only for plain `def` methods (`ISEQ_TYPE_METHOD`) defined on a normal, +/// initialized, non-singleton class that uses the default allocator +/// (`rb_class_allocate_instance`). The receiver of such a method is always +/// `kind_of?` the owner, and no user class with the default allocator can be +/// inserted into the ancestry of an immediate, so `self` cannot be an immediate. +/// +/// The default-allocator check alone is not sufficient: `Object`, `BasicObject`, +/// and `Numeric` use the default allocator yet are ancestors of immediates (e.g. +/// `Integer`). Every such class is also an ancestor of `Integer`, so a single +/// `rb_obj_is_kind_of(, owner)` check rules all of them out. +/// +/// Returns `false` conservatively for anything that doesn't clearly qualify +/// (modules, singleton classes, custom allocators, non-`def` ISEQs, etc.). +pub fn iseq_self_is_heap_object(iseq: IseqPtr, owner: VALUE) -> bool { + if unsafe { rb_get_iseq_body_type(iseq) } != ISEQ_TYPE_METHOD { return false; } + if !unsafe { RB_TYPE_P(owner, RUBY_T_CLASS) } { return false; } + // Check initialized + non-singleton before reading the allocator (reading it otherwise + // aborts). + // TODO(max): Determine if we can loosen this to allow methods defined on singleton classes. + if !unsafe { rb_zjit_class_initialized_p(owner) } { return false; } + if unsafe { rb_zjit_singleton_class_p(owner) } { return false; } + if !unsafe { rb_zjit_class_has_default_allocator(owner) } { return false; } + // Exclude Object/BasicObject/Numeric and friends: classes that use the default + // allocator but sit above an immediate class in the ancestry chain. They are + // all ancestors of Integer, so this single check covers every immediate type. + if unsafe { rb_obj_is_kind_of(VALUE::fixnum_from_usize(0), owner) }.test() { return false; } + true +} + /// Interned ID values for Ruby symbols and method names. /// See [type@crate::cruby::ID] and usages outside of ZJIT. pub(crate) mod ids { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c2ae897d9aa9cb..cfa3c01d475658 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2539,6 +2539,11 @@ pub struct Function { /// Whether previously, a function for this ISEQ was invalidated due to /// singleton class creation (violation of NoSingletonClass invariant). was_invalidated_for_singleton_class_creation: bool, + /// Whether `self` is guaranteed to be a heap (non-immediate) object. When set, + /// the `self`-producing instructions (`LoadSelf` and the `SelfParam` `LoadArg`) + /// are typed `HeapBasicObject` instead of `BasicObject`. Sourced from + /// `IseqPayload::self_is_heap_object`. + self_is_heap_object: bool, /// Controls code generation strategy for optimization passes. policy: CompilePolicy, /// The types for the parameters of this function. They are copied to the type @@ -2648,6 +2653,7 @@ impl Function { Function { iseq, was_invalidated_for_singleton_class_creation: false, + self_is_heap_object: false, policy: CompilePolicy::new(iseq), insns: vec![], insn_types: vec![], @@ -2955,7 +2961,9 @@ impl Function { Insn::FixnumAdd { .. } => types::Fixnum, Insn::FixnumSub { .. } => types::Fixnum, Insn::FixnumMult { .. } => types::Fixnum, - Insn::FixnumDiv { .. } => types::Fixnum, + // FIXNUM_MIN / -1 overflows to a Bignum, so the result is Integer, not Fixnum. + // Downstream Fixnum ops insert their own GuardType(Fixnum) + Insn::FixnumDiv { .. } => types::Integer, Insn::FixnumMod { .. } => types::Fixnum, Insn::FloatAdd { .. } => types::Float, Insn::FloatSub { .. } => types::Float, @@ -3003,7 +3011,7 @@ impl Function { Insn::LoadSP => types::CPtr, Insn::LoadEC => types::CPtr, Insn::GetEP { .. } => types::CPtr, - Insn::LoadSelf => types::BasicObject, + Insn::LoadSelf => if self.self_is_heap_object { types::HeapBasicObject } else { types::BasicObject }, &Insn::LoadField { return_type, .. } => return_type, Insn::GetSpecialSymbol { .. } => types::StringExact.union(types::NilClass), Insn::GetSpecialNumber { .. } => types::StringExact.union(types::NilClass), @@ -6687,6 +6695,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let mut profiles = ProfileOracle::new(payload); let mut fun = Function::new(iseq); fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; + fun.self_is_heap_object = payload.self_is_heap_object; // Compute a map of PC->Block by finding jump targets let jit_entry_insns = unsafe { iseq.params() }.opt_table_slice().iter().copied().map(VALUE::as_u32).collect::>(); @@ -8594,7 +8603,10 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent }; let mut arg_idx: u32 = 0; - let self_param = fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: FieldName::SelfParam, val_type: types::BasicObject }); + // For `def` methods on classes that can only produce heap (non-immediate) + // instances, `self` is a HeapBasicObject. See `iseq_self_is_heap_object`. + let self_type = if fun.self_is_heap_object { types::HeapBasicObject } else { types::BasicObject }; + let self_param = fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: FieldName::SelfParam, val_type: self_type }); arg_idx += 1; let mut entry_state = FrameState::new(iseq); let mut ep: Option = None; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 53f65db4ccd502..149c36fb840bb9 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -371,7 +371,7 @@ mod hir_opt_tests { v10:Fixnum[7] = Const Value(7) v12:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1000, /@0x1008, cme:0x1010) - v23:Fixnum = FixnumDiv v10, v12 + v23:Integer = FixnumDiv v10, v12 CheckInterrupts Return v23 "); @@ -425,7 +425,7 @@ mod hir_opt_tests { v10:Fixnum[-4611686018427387904] = Const Value(-4611686018427387904) v12:Fixnum[-1] = Const Value(-1) PatchPoint MethodRedefined(Integer@0x1000, /@0x1008, cme:0x1010) - v23:Fixnum = FixnumDiv v10, v12 + v23:Integer = FixnumDiv v10, v12 CheckInterrupts Return v23 "); @@ -2916,7 +2916,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) v32:Fixnum = GuardType v12, Fixnum v33:Fixnum = GuardType v13, Fixnum - v34:Fixnum = FixnumDiv v32, v33 + v34:Integer = FixnumDiv v32, v33 v24:Fixnum[5] = Const Value(5) CheckInterrupts Return v24 @@ -7696,16 +7696,15 @@ mod hir_opt_tests { fn foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): PatchPoint SingleRactorMode - v11:HeapBasicObject = GuardType v6, HeapBasicObject - v12:CUInt64 = LoadField v11, :RBASIC_FLAGS@0x1000 + v12:CUInt64 = LoadField v6, :RBASIC_FLAGS@0x1000 v14:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) v15:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) v16 = RefineType v15, CUInt64 @@ -7713,7 +7712,7 @@ mod hir_opt_tests { v18:CBool = IsBitEqual v17, v16 CondBranch v18, bb5(), bb6() bb5(): - v20:BasicObject = LoadField v11, :@foo@0x1002 + v20:BasicObject = LoadField v6, :@foo@0x1002 Jump bb4(v20) bb6(): v22:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) @@ -7723,10 +7722,10 @@ mod hir_opt_tests { v26:CBool = IsBitEqual v25, v24 CondBranch v26, bb7(), bb8() bb7(): - v28:BasicObject = LoadField v11, :@foo@0x1004 + v28:BasicObject = LoadField v6, :@foo@0x1004 Jump bb4(v28) bb8(): - v30:BasicObject = GetIvar v11, :@foo + v30:BasicObject = GetIvar v6, :@foo Jump bb4(v30) bb4(v13:BasicObject): CheckInterrupts @@ -7734,6 +7733,263 @@ mod hir_opt_tests { "); } + // The following tests pin down the soundness boundary of the `self: + // HeapBasicObject` inference (see `iseq_self_is_heap_object`). A `def` method + // gets `self: HeapBasicObject` only when its owning class can never produce an + // immediate receiver. For each class below, `self` must stay `BasicObject`: + // the six immediate classes have no default allocator, and Object/BasicObject/ + // Numeric use the default allocator but are ancestors of immediates (caught by + // the Integer kind_of check). Each test reopens the class, compiles the method + // (call threshold is 30), then checks the resulting `self` type. + + #[test] + fn test_self_not_heap_object_owner_integer() { + eval(" + class Integer + def probe = @foo + end + 100.times { 5.probe } + "); + assert_snapshot!(hir_string_proc("5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_symbol() { + eval(" + class Symbol + def probe = @foo + end + 100.times { :sym.probe } + "); + assert_snapshot!(hir_string_proc(":sym.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_float() { + eval(" + class Float + def probe = @foo + end + 100.times { 1.5.probe } + "); + assert_snapshot!(hir_string_proc("1.5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_nil_class() { + eval(" + class NilClass + def probe = @foo + end + 100.times { nil.probe } + "); + assert_snapshot!(hir_string_proc("nil.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_true_class() { + eval(" + class TrueClass + def probe = @foo + end + 100.times { true.probe } + "); + assert_snapshot!(hir_string_proc("true.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_false_class() { + eval(" + class FalseClass + def probe = @foo + end + 100.times { false.probe } + "); + assert_snapshot!(hir_string_proc("false.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_object() { + // Object uses the default allocator, but Integer (and every other immediate) + // descends from it, so a method on Object can run with an immediate self. + eval(" + class Object + def probe = @foo + end + o = Object.new + 100.times { o.probe } + "); + assert_snapshot!(hir_string_proc("Object.new.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v6, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1000 + v19:CShape[0x1001] = GuardBitEquals v18, CShape(0x1001) recompile + v20:NilClass = Const Value(nil) + CheckInterrupts + Return v20 + "); + } + + #[test] + fn test_self_not_heap_object_owner_basic_object() { + // Same as Object: BasicObject has the default allocator but is the root of + // the immediate classes' ancestry. + eval(" + class BasicObject + def probe = @foo + end + o = Object.new + 100.times { o.probe } + "); + assert_snapshot!(hir_string_proc("Object.new.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v6, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1000 + v19:CShape[0x1001] = GuardBitEquals v18, CShape(0x1001) recompile + v20:NilClass = Const Value(nil) + CheckInterrupts + Return v20 + "); + } + + #[test] + fn test_self_not_heap_object_owner_numeric() { + // Numeric has the default allocator but Integer/Float descend from it, so a + // method on Numeric can run with an immediate self. + eval(" + class Numeric + def probe = @foo + end + 100.times { 5.probe } + "); + assert_snapshot!(hir_string_proc("5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + #[test] fn test_definedivar_shape_guard_recompile() { // Call with one shape to compile, then call with a different shape to @@ -7759,13 +8015,13 @@ mod hir_opt_tests { fn has_foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v10:StringExact|NilClass = DefinedIvar v6, :@foo CheckInterrupts Return v10 @@ -7797,13 +8053,13 @@ mod hir_opt_tests { fn foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v10:Fixnum[5] = Const Value(5) PatchPoint SingleRactorMode SetIvar v6, :@foo, v10 @@ -7842,16 +8098,16 @@ mod hir_opt_tests { fn write@:12: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :obj@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :obj@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] @@ -14574,13 +14830,13 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): PatchPoint MethodRedefined(A@0x1000, foo@0x1008, cme:0x1010) v18:CPtr = GetEP 0 v19:RubyValue = LoadField v18, :VM_ENV_DATA_INDEX_ME_CREF@0x1038 @@ -14643,16 +14899,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :x@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): PatchPoint MethodRedefined(A@0x1008, foo@0x1010, cme:0x1018) v28:CPtr = GetEP 0 v29:RubyValue = LoadField v28, :VM_ENV_DATA_INDEX_ME_CREF@0x1040 @@ -14695,16 +14951,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:ArrayExact = LoadField v2, :x@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v16:ArrayExact = ToArray v10 v18:BasicObject = InvokeSuper v9, 0x1008, v16 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts @@ -14739,13 +14995,13 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v11:BasicObject = InvokeSuper v6, 0x1000 # SendFallbackReason: super: call made with a block CheckInterrupts Return v11 @@ -14909,18 +15165,18 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :blk@0x1000 v4:NilClass = Const Value(nil) Jump bb3(v1, v3, v4) bb2(): EntryPoint JIT(0) - v7:BasicObject = LoadArg :self@0 + v7:HeapBasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :blk@1 v9:NilClass = Const Value(nil) Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): PatchPoint NoSingletonClass(B@0x1008) PatchPoint MethodRedefined(B@0x1008, proc@0x1010, cme:0x1018) v39:ObjectSubclass[class_exact:B] = GuardType v11, ObjectSubclass[class_exact:B] @@ -14962,16 +15218,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :items@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :items@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v16:StaticSymbol[:succ] = Const Value(VALUE(0x1008)) v18:BasicObject = InvokeSuper v9, 0x1010, v10, v16 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts @@ -15004,7 +15260,7 @@ mod hir_opt_tests { fn foo@:9: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :content@0x1000 v4:CPtr = LoadPC @@ -15015,19 +15271,19 @@ mod hir_opt_tests { Jump bb5(v1, v3) bb2(): EntryPoint JIT(0) - v10:BasicObject = LoadArg :self@0 + v10:HeapBasicObject = LoadArg :self@0 v11:NilClass = Const Value(nil) Jump bb3(v10, v11) - bb3(v17:BasicObject, v18:BasicObject): + bb3(v17:HeapBasicObject, v18:BasicObject): v21:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v22:StringExact = StringCopy v21 Jump bb5(v17, v22) bb4(): EntryPoint JIT(1) - v14:BasicObject = LoadArg :self@0 + v14:HeapBasicObject = LoadArg :self@0 v15:BasicObject = LoadArg :content@1 Jump bb5(v14, v15) - bb5(v25:BasicObject, v26:BasicObject): + bb5(v25:HeapBasicObject, v26:BasicObject): v32:BasicObject = InvokeSuper v25, 0x1010, v26 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts Return v32 @@ -16116,29 +16372,28 @@ mod hir_opt_tests { fn set_value_loop@:4: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:NilClass = Const Value(nil) Jump bb3(v1, v2) bb2(): EntryPoint JIT(0) - v5:BasicObject = LoadArg :self@0 + v5:HeapBasicObject = LoadArg :self@0 v6:NilClass = Const Value(nil) Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + bb3(v8:HeapBasicObject, v9:NilClass): v13:Fixnum[0] = Const Value(0) CheckInterrupts Jump bb6(v8, v13) - bb6(v19:BasicObject, v20:Fixnum): + bb6(v19:HeapBasicObject, v20:Fixnum): v24:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) v110:BoolExact = FixnumLt v20, v24 CheckInterrupts v30:CBool = Test v110 CondBranch v30, bb4(v19, v20), bb7() - bb4(v40:BasicObject, v41:Fixnum): + bb4(v40:HeapBasicObject, v41:Fixnum): PatchPoint SingleRactorMode - v46:HeapBasicObject = GuardType v40, HeapBasicObject - v47:CUInt64 = LoadField v46, :RBASIC_FLAGS@0x1038 + v47:CUInt64 = LoadField v40, :RBASIC_FLAGS@0x1038 v49:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) v50:CPtr[CPtr(0x1039)] = Const CPtr(0x1039) v51 = RefineType v50, CUInt64 @@ -16146,7 +16401,7 @@ mod hir_opt_tests { v53:CBool = IsBitEqual v52, v51 CondBranch v53, bb9(), bb10() bb9(): - v55:BasicObject = LoadField v46, :@levar@0x103a + v55:BasicObject = LoadField v40, :@levar@0x103a Jump bb8(v55) bb10(): v57:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) @@ -16159,24 +16414,24 @@ mod hir_opt_tests { v63:NilClass = Const Value(nil) Jump bb8(v63) bb12(): - v97:CShape = LoadField v46, :shape_id@0x103c + v97:CShape = LoadField v40, :shape_id@0x103c v98:CShape[0x103d] = GuardBitEquals v97, CShape(0x103d) recompile - v99:BasicObject = LoadField v46, :@levar@0x103a + v99:BasicObject = LoadField v40, :@levar@0x103a Jump bb8(v99) bb8(v48:BasicObject): CheckInterrupts v69:CBool = Test v48 - CondBranch v69, bb5(v46, v41), bb13() + CondBranch v69, bb5(v40, v41), bb13() bb13(): PatchPoint NoEPEscape(set_value_loop) PatchPoint SingleRactorMode - v101:CShape = LoadField v46, :shape_id@0x103c + v101:CShape = LoadField v40, :shape_id@0x103c v102:CShape[0x103e] = GuardBitEquals v101, CShape(0x103e) recompile - StoreField v46, :@levar@0x103a, v41 - WriteBarrier v46, v41 + StoreField v40, :@levar@0x103a, v41 + WriteBarrier v40, v41 v105:CShape[0x103d] = Const CShape(0x103d) - StoreField v46, :shape_id@0x103c, v105 - v79:HeapBasicObject = RefineType v46, HeapBasicObject + StoreField v40, :shape_id@0x103c, v105 + v79:HeapBasicObject = RefineType v40, HeapBasicObject Jump bb5(v79, v41) bb5(v81:HeapBasicObject, v82:Fixnum): PatchPoint NoEPEscape(set_value_loop) diff --git a/zjit/src/payload.rs b/zjit/src/payload.rs index 010972bdae19a9..807c33b8b76fe2 100644 --- a/zjit/src/payload.rs +++ b/zjit/src/payload.rs @@ -16,6 +16,14 @@ pub struct IseqPayload { /// Whether a previous compilation of this ISEQ was invalidated due to /// singleton class creation (violation of [`crate::hir::Invariant::NoSingletonClass`]). pub was_invalidated_for_singleton_class_creation: bool, + /// Whether `self` is guaranteed to be a heap (non-immediate) object for this + /// ISEQ. Set at compile triggers (entry point / function stub hit) where the + /// owning class is known via the method entry, and consumed in `iseq_to_hir` + /// to type the `self`-producing instructions (`LoadSelf` / `SelfParam` + /// `LoadArg`) as `HeapBasicObject`. Defaults to `false` (the conservative + /// `BasicObject`) when the owner is unknown. + /// See [`crate::cruby::iseq_self_is_heap_object`]. + pub self_is_heap_object: bool, } impl IseqPayload { @@ -24,6 +32,7 @@ impl IseqPayload { profile: IseqProfile::new(), versions: vec![], was_invalidated_for_singleton_class_creation: false, + self_is_heap_object: false, } } }