Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));

Expand Down Expand Up @@ -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),
Expand Down
33 changes: 33 additions & 0 deletions zjit/src/codegen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("
Expand Down
32 changes: 32 additions & 0 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(<a fixnum>, 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 {
Expand Down
18 changes: 15 additions & 3 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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![],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -6687,6 +6695,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
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::<Vec<_>>();
Expand Down Expand Up @@ -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<InsnId> = None;
Expand Down
Loading