diff options
Diffstat (limited to 'zjit/src/hir.rs')
-rw-r--r-- | zjit/src/hir.rs | 704 |
1 files changed, 655 insertions, 49 deletions
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c67f25451a..2bd7174f2d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9,7 +9,7 @@ use crate::{ use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, - ffi::{c_int, c_void}, + ffi::{c_int, c_void, CStr}, mem::{align_of, size_of}, ptr, slice::Iter @@ -185,7 +185,9 @@ impl<'a> std::fmt::Display for InvariantPrinter<'a> { write!(f, "BOPRedefined(")?; match klass { INTEGER_REDEFINED_OP_FLAG => write!(f, "INTEGER_REDEFINED_OP_FLAG")?, + STRING_REDEFINED_OP_FLAG => write!(f, "STRING_REDEFINED_OP_FLAG")?, ARRAY_REDEFINED_OP_FLAG => write!(f, "ARRAY_REDEFINED_OP_FLAG")?, + HASH_REDEFINED_OP_FLAG => write!(f, "HASH_REDEFINED_OP_FLAG")?, _ => write!(f, "{klass}")?, } write!(f, ", ")?; @@ -201,6 +203,8 @@ impl<'a> std::fmt::Display for InvariantPrinter<'a> { BOP_LE => write!(f, "BOP_LE")?, BOP_GT => write!(f, "BOP_GT")?, BOP_GE => write!(f, "BOP_GE")?, + BOP_FREEZE => write!(f, "BOP_FREEZE")?, + BOP_UMINUS => write!(f, "BOP_UMINUS")?, BOP_MAX => write!(f, "BOP_MAX")?, _ => write!(f, "{bop}")?, } @@ -427,7 +431,7 @@ pub enum Insn { /// Return C `true` if `val` is `Qnil`, else `false`. IsNil { val: InsnId }, Defined { op_type: usize, obj: VALUE, pushval: VALUE, v: InsnId }, - GetConstantPath { ic: *const iseq_inline_constant_cache }, + GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId }, /// Get a global variable named `id` GetGlobal { id: ID, state: InsnId }, @@ -473,6 +477,9 @@ pub enum Insn { state: InsnId, }, + // Invoke a builtin function + InvokeBuiltin { bf: rb_builtin_function, args: Vec<InsnId>, state: InsnId }, + /// Control flow instructions Return { val: InsnId }, @@ -489,6 +496,9 @@ pub enum Insn { FixnumGt { left: InsnId, right: InsnId }, FixnumGe { left: InsnId, right: InsnId }, + // Distinct from `SendWithoutBlock` with `mid:to_s` because does not have a patch point for String to_s being redefined + ObjToString { val: InsnId, call_info: CallInfo, cd: *const rb_call_data, state: InsnId }, + /// Side-exit if val doesn't have the expected type. GuardType { val: InsnId, guard_type: Type, state: InsnId }, /// Side-exit if val is not the expected VALUE. @@ -632,6 +642,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) } + Insn::InvokeBuiltin { bf, args, .. } => { + write!(f, "InvokeBuiltin {}", unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap())?; + for arg in args { + write!(f, ", {arg}")?; + } + Ok(()) + } Insn::Return { val } => { write!(f, "Return {val}") } Insn::FixnumAdd { left, right, .. } => { write!(f, "FixnumAdd {left}, {right}") }, Insn::FixnumSub { left, right, .. } => { write!(f, "FixnumSub {left}, {right}") }, @@ -647,7 +664,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::GuardType { val, guard_type, .. } => { write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map)) }, Insn::GuardBitEquals { val, expected, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map)) }, Insn::PatchPoint(invariant) => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) }, - Insn::GetConstantPath { ic } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) }, + Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) }, Insn::CCall { cfun, args, name, return_type: _, elidable: _ } => { write!(f, "CCall {}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfun))?; for arg in args { @@ -681,6 +698,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::ToNewArray { val, .. } => write!(f, "ToNewArray {val}"), Insn::ArrayExtend { left, right, .. } => write!(f, "ArrayExtend {left}, {right}"), Insn::ArrayPush { array, val, .. } => write!(f, "ArrayPush {array}, {val}"), + Insn::ObjToString { val, .. } => { write!(f, "ObjToString {val}") }, Insn::SideExit { .. } => write!(f, "SideExit"), Insn::PutSpecialObject { value_type } => { write!(f, "PutSpecialObject {}", value_type) @@ -999,6 +1017,12 @@ impl Function { FixnumLt { left, right } => FixnumLt { left: find!(*left), right: find!(*right) }, FixnumLe { left, right } => FixnumLe { left: find!(*left), right: find!(*right) }, PutSpecialObject { value_type } => PutSpecialObject { value_type: *value_type }, + ObjToString { val, call_info, cd, state } => ObjToString { + val: find!(*val), + call_info: call_info.clone(), + cd: *cd, + state: *state, + }, SendWithoutBlock { self_val, call_info, cd, args, state } => SendWithoutBlock { self_val: find!(*self_val), call_info: call_info.clone(), @@ -1023,6 +1047,7 @@ impl Function { args: args.iter().map(|arg| find!(*arg)).collect(), state: *state, }, + InvokeBuiltin { bf, args, state } => InvokeBuiltin { bf: *bf, args: find_vec!(*args), state: *state }, ArraySet { array, idx, val } => ArraySet { array: find!(*array), idx: *idx, val: find!(*val) }, ArrayDup { val , state } => ArrayDup { val: find!(*val), state: *state }, &HashDup { val , state } => HashDup { val: find!(val), state }, @@ -1119,6 +1144,7 @@ impl Function { Insn::SendWithoutBlock { .. } => types::BasicObject, Insn::SendWithoutBlockDirect { .. } => types::BasicObject, Insn::Send { .. } => types::BasicObject, + Insn::InvokeBuiltin { .. } => types::BasicObject, Insn::Defined { .. } => types::BasicObject, Insn::DefinedIvar { .. } => types::BasicObject, Insn::GetConstantPath { .. } => types::BasicObject, @@ -1127,6 +1153,7 @@ impl Function { Insn::GetIvar { .. } => types::BasicObject, Insn::ToNewArray { .. } => types::ArrayExact, Insn::ToArray { .. } => types::ArrayExact, + Insn::ObjToString { .. } => types::BasicObject, } } @@ -1250,6 +1277,38 @@ impl Function { } } + fn rewrite_if_frozen(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId, klass: u32, bop: u32) { + let self_type = self.type_of(self_val); + if let Some(obj) = self_type.ruby_object() { + if obj.is_frozen() { + self.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass, bop })); + self.make_equal_to(orig_insn_id, self_val); + return; + } + } + self.push_insn_id(block, orig_insn_id); + } + + fn try_rewrite_freeze(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId) { + if self.is_a(self_val, types::StringExact) { + self.rewrite_if_frozen(block, orig_insn_id, self_val, STRING_REDEFINED_OP_FLAG, BOP_FREEZE); + } else if self.is_a(self_val, types::ArrayExact) { + self.rewrite_if_frozen(block, orig_insn_id, self_val, ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE); + } else if self.is_a(self_val, types::HashExact) { + self.rewrite_if_frozen(block, orig_insn_id, self_val, HASH_REDEFINED_OP_FLAG, BOP_FREEZE); + } else { + self.push_insn_id(block, orig_insn_id); + } + } + + fn try_rewrite_uminus(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId) { + if self.is_a(self_val, types::StringExact) { + self.rewrite_if_frozen(block, orig_insn_id, self_val, STRING_REDEFINED_OP_FLAG, BOP_UMINUS); + } else { + self.push_insn_id(block, orig_insn_id); + } + } + /// Rewrite SendWithoutBlock opcodes into SendWithoutBlockDirect opcodes if we know the target /// ISEQ statically. This removes run-time method lookups and opens the door for inlining. fn optimize_direct_sends(&mut self) { @@ -1280,6 +1339,10 @@ impl Function { self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGt { left, right }, BOP_GT, self_val, args[0], state), Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == ">=" && args.len() == 1 => self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGe { left, right }, BOP_GE, self_val, args[0], state), + Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, .. } if method_name == "freeze" && args.len() == 0 => + self.try_rewrite_freeze(block, insn_id, self_val), + Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, .. } if method_name == "-@" && args.len() == 0 => + self.try_rewrite_uminus(block, insn_id, self_val), Insn::SendWithoutBlock { mut self_val, call_info, cd, args, state } => { let frame_state = self.frame_state(state); let (klass, guard_equal_to) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() { @@ -1315,7 +1378,7 @@ impl Function { let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, call_info, cd, cme, iseq, args, state }); self.make_equal_to(insn_id, send_direct); } - Insn::GetConstantPath { ic } => { + Insn::GetConstantPath { ic, .. } => { let idlist: *const ID = unsafe { (*ic).segments }; let ice = unsafe { (*ic).entry }; if ice.is_null() { @@ -1334,6 +1397,15 @@ impl Function { let replacement = self.push_insn(block, Insn::Const { val: Const::Value(unsafe { (*ice).value }) }); self.make_equal_to(insn_id, replacement); } + Insn::ObjToString { val, call_info, cd, state, .. } => { + if self.is_a(val, types::StringExact) { + // behaves differently from `SendWithoutBlock` with `mid:to_s` because ObjToString should not have a patch point for String to_s being redefined + self.make_equal_to(insn_id, val); + } else { + let replacement = self.push_insn(block, Insn::SendWithoutBlock { self_val: val, call_info, cd, args: vec![], state }); + self.make_equal_to(insn_id, replacement) + } + } _ => { self.push_insn_id(block, insn_id); } } } @@ -1602,10 +1674,14 @@ impl Function { if necessary[insn_id.0] { continue; } necessary[insn_id.0] = true; match self.find(insn_id) { - Insn::Const { .. } | Insn::Param { .. } - | Insn::PatchPoint(..) | Insn::GetConstantPath { .. } + Insn::Const { .. } + | Insn::Param { .. } + | Insn::PatchPoint(..) | Insn::PutSpecialObject { .. } => {} + Insn::GetConstantPath { ic: _, state } => { + worklist.push_back(state); + } Insn::ArrayMax { elements, state } | Insn::NewArray { elements, state } => { worklist.extend(elements); @@ -1683,6 +1759,10 @@ impl Function { worklist.extend(args); worklist.push_back(state); } + Insn::InvokeBuiltin { args, state, .. } => { + worklist.extend(args); + worklist.push_back(state) + } Insn::CCall { args, .. } => worklist.extend(args), Insn::GetIvar { self_val, state, .. } | Insn::DefinedIvar { self_val, state, .. } => { worklist.push_back(self_val); @@ -1698,6 +1778,10 @@ impl Function { worklist.push_back(val); worklist.push_back(state); } + Insn::ObjToString { val, state, .. } => { + worklist.push_back(val); + worklist.push_back(state); + } Insn::GetGlobal { state, .. } | Insn::SideExit { state } => worklist.push_back(state), } @@ -2269,7 +2353,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { } YARVINSN_opt_getconstant_path => { let ic = get_arg(pc, 0).as_ptr(); - state.stack_push(fun.push_insn(block, Insn::GetConstantPath { ic })); + let snapshot = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + state.stack_push(fun.push_insn(block, Insn::GetConstantPath { ic, state: snapshot })); } YARVINSN_branchunless => { let offset = get_arg(pc, 0).as_i64(); @@ -2344,6 +2429,14 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { } YARVINSN_pop => { state.stack_pop()?; } YARVINSN_dup => { state.stack_push(state.stack_top()?); } + YARVINSN_dupn => { + // Duplicate the top N element of the stack. As we push, n-1 naturally + // points higher in the original stack. + let n = get_arg(pc, 0).as_usize(); + for _ in 0..n { + state.stack_push(state.stack_topn(n-1)?); + } + } YARVINSN_swap => { let right = state.stack_pop()?; let left = state.stack_pop()?; @@ -2421,6 +2514,34 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id }); state.stack_push(send); } + YARVINSN_opt_hash_freeze | + YARVINSN_opt_ary_freeze | + YARVINSN_opt_str_freeze | + YARVINSN_opt_str_uminus => { + // NB: these instructions have the recv for the call at get_arg(0) + let cd: *const rb_call_data = get_arg(pc, 1).as_ptr(); + let call_info = unsafe { rb_get_call_data_ci(cd) }; + if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { + // Unknown call type; side-exit into the interpreter + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + fun.push_insn(block, Insn::SideExit { state: exit_id }); + break; // End the block + } + let argc = unsafe { vm_ci_argc((*cd).ci) }; + let name = insn_name(opcode as usize); + assert_eq!(0, argc, "{name} should not have args"); + let args = vec![]; + + let method_name = unsafe { + let mid = rb_vm_ci_mid(call_info); + mid.contents_lossy().into_owned() + }; + + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); + let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id }); + state.stack_push(send); + } YARVINSN_leave => { fun.push_insn(block, Insn::Return { val: state.stack_pop()? }); @@ -2533,6 +2654,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { let val = state.stack_pop()?; fun.push_insn(block, Insn::SetIvar { self_val: self_param, id, val, state: exit_id }); } + YARVINSN_opt_reverse => { + // Reverse the order of the top N stack items. + let n = get_arg(pc, 0).as_usize(); + for i in 0..n/2 { + let bottom = state.stack_topn(n - 1 - i)?; + let top = state.stack_topn(i)?; + state.stack_setn(i, bottom); + state.stack_setn(n - 1 - i, top); + } + } YARVINSN_newrange => { let flag = RangeType::from(get_arg(pc, 0).as_u32()); let high = state.stack_pop()?; @@ -2541,6 +2672,55 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { let insn_id = fun.push_insn(block, Insn::NewRange { low, high, flag, state: exit_id }); state.stack_push(insn_id); } + YARVINSN_invokebuiltin => { + let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() }; + + let mut args = vec![]; + for _ in 0..bf.argc { + args.push(state.stack_pop()?); + } + args.push(self_param); + args.reverse(); + + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id }); + state.stack_push(insn_id); + } + YARVINSN_opt_invokebuiltin_delegate | + YARVINSN_opt_invokebuiltin_delegate_leave => { + let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() }; + let index = get_arg(pc, 1).as_usize(); + let argc = bf.argc as usize; + + let mut args = vec![self_param]; + for &local in state.locals().skip(index).take(argc) { + args.push(local); + } + + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id }); + state.stack_push(insn_id); + } + YARVINSN_objtostring => { + let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); + let call_info = unsafe { rb_get_call_data_ci(cd) }; + + if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { + assert!(false, "objtostring should not have unknown call type"); + } + let argc = unsafe { vm_ci_argc((*cd).ci) }; + assert_eq!(0, argc, "objtostring should not have args"); + + let method_name: String = unsafe { + let mid = rb_vm_ci_mid(call_info); + mid.contents_lossy().into_owned() + }; + + let recv = state.stack_pop()?; + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let objtostring = fun.push_insn(block, Insn::ObjToString { val: recv, call_info: CallInfo { method_name }, cd, state: exit_id }); + state.stack_push(objtostring) + } _ => { // Unknown opcode; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); @@ -2834,7 +3014,7 @@ mod tests { #[track_caller] fn assert_method_hir(method: &str, hir: Expect) { - let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method)); + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, hir); @@ -2861,7 +3041,7 @@ mod tests { #[track_caller] fn assert_method_hir_with_opcodes(method: &str, opcodes: &[u32], hir: Expect) { - let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method)); + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); for &opcode in opcodes { assert!(iseq_contains_opcode(iseq, opcode), "iseq {method} does not contain {}", insn_name(opcode as usize)); } @@ -2883,7 +3063,7 @@ mod tests { #[track_caller] fn assert_compile_fails(method: &str, reason: ParseError) { - let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method)); + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; let result = iseq_to_hir(iseq); assert!(result.is_err(), "Expected an error but succesfully compiled to HIR: {}", FunctionPrinter::without_snapshot(&result.unwrap())); @@ -3099,6 +3279,62 @@ mod tests { } #[test] + fn test_opt_hash_freeze() { + eval(" + def test = {}.freeze + "); + assert_method_hir_with_opcode("test", YARVINSN_opt_hash_freeze, expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v4:BasicObject = SendWithoutBlock v3, :freeze + Return v4 + "#]]); + } + + #[test] + fn test_opt_ary_freeze() { + eval(" + def test = [].freeze + "); + assert_method_hir_with_opcode("test", YARVINSN_opt_ary_freeze, expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v4:BasicObject = SendWithoutBlock v3, :freeze + Return v4 + "#]]); + } + + #[test] + fn test_opt_str_freeze() { + eval(" + def test = ''.freeze + "); + assert_method_hir_with_opcode("test", YARVINSN_opt_str_freeze, expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v4:BasicObject = SendWithoutBlock v3, :freeze + Return v4 + "#]]); + } + + #[test] + fn test_opt_str_uminus() { + eval(" + def test = -'' + "); + assert_method_hir_with_opcode("test", YARVINSN_opt_str_uminus, expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v4:BasicObject = SendWithoutBlock v3, :-@ + Return v4 + "#]]); + } + + #[test] fn test_setlocal_getlocal() { eval(" def test @@ -3621,14 +3857,14 @@ mod tests { assert_method_hir_with_opcode("test", YARVINSN_opt_new, expect![[r#" fn test: bb0(v0:BasicObject): - v2:BasicObject = GetConstantPath 0x1000 - v3:NilClassExact = Const Value(nil) - Jump bb1(v0, v3, v2) - bb1(v5:BasicObject, v6:NilClassExact, v7:BasicObject): - v10:BasicObject = SendWithoutBlock v7, :new - Jump bb2(v5, v10, v6) - bb2(v12:BasicObject, v13:BasicObject, v14:NilClassExact): - Return v13 + v3:BasicObject = GetConstantPath 0x1000 + v4:NilClassExact = Const Value(nil) + Jump bb1(v0, v4, v3) + bb1(v6:BasicObject, v7:NilClassExact, v8:BasicObject): + v11:BasicObject = SendWithoutBlock v8, :new + Jump bb2(v6, v11, v7) + bb2(v13:BasicObject, v14:BasicObject, v15:NilClassExact): + Return v14 "#]]); } @@ -4036,6 +4272,47 @@ mod tests { } #[test] + fn opt_reverse() { + eval(" + def reverse_odd + a, b, c = @a, @b, @c + [a, b, c] + end + + def reverse_even + a, b, c, d = @a, @b, @c, @d + [a, b, c, d] + end + "); + assert_method_hir_with_opcode("reverse_odd", YARVINSN_opt_reverse, expect![[r#" + fn reverse_odd: + bb0(v0:BasicObject): + v1:NilClassExact = Const Value(nil) + v2:NilClassExact = Const Value(nil) + v3:NilClassExact = Const Value(nil) + v6:BasicObject = GetIvar v0, :@a + v8:BasicObject = GetIvar v0, :@b + v10:BasicObject = GetIvar v0, :@c + v12:ArrayExact = NewArray v6, v8, v10 + Return v12 + "#]]); + assert_method_hir_with_opcode("reverse_even", YARVINSN_opt_reverse, expect![[r#" + fn reverse_even: + bb0(v0:BasicObject): + v1:NilClassExact = Const Value(nil) + v2:NilClassExact = Const Value(nil) + v3:NilClassExact = Const Value(nil) + v4:NilClassExact = Const Value(nil) + v7:BasicObject = GetIvar v0, :@a + v9:BasicObject = GetIvar v0, :@b + v11:BasicObject = GetIvar v0, :@c + v13:BasicObject = GetIvar v0, :@d + v15:ArrayExact = NewArray v7, v9, v11, v13 + Return v15 + "#]]); + } + + #[test] fn test_branchnil() { eval(" def test(x) = x&.itself @@ -4051,6 +4328,81 @@ mod tests { Return v10 "#]]); } + + #[test] + fn test_invokebuiltin_delegate_with_args() { + assert_method_hir_with_opcode("Float", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#" + fn Float: + bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject): + v6:BasicObject = InvokeBuiltin rb_f_float, v0, v1, v2 + Jump bb1(v0, v1, v2, v3, v6) + bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject, v12:BasicObject): + Return v12 + "#]]); + } + + #[test] + fn test_invokebuiltin_delegate_without_args() { + assert_method_hir_with_opcode("class", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#" + fn class: + bb0(v0:BasicObject): + v3:BasicObject = InvokeBuiltin _bi20, v0 + Jump bb1(v0, v3) + bb1(v5:BasicObject, v6:BasicObject): + Return v6 + "#]]); + } + + #[test] + fn test_invokebuiltin_with_args() { + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("GC", "start")); + assert!(iseq_contains_opcode(iseq, YARVINSN_invokebuiltin), "iseq GC.start does not contain invokebuiltin"); + let function = iseq_to_hir(iseq).unwrap(); + assert_function_hir(function, expect![[r#" + fn start: + bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject): + v6:FalseClassExact = Const Value(false) + v8:BasicObject = InvokeBuiltin gc_start_internal, v0, v1, v2, v3, v6 + Return v8 + "#]]); + } + + #[test] + fn dupn() { + eval(" + def test(x) = (x[0, 1] ||= 2) + "); + assert_method_hir_with_opcode("test", YARVINSN_dupn, expect![[r#" + fn test: + bb0(v0:BasicObject, v1:BasicObject): + v3:NilClassExact = Const Value(nil) + v4:Fixnum[0] = Const Value(0) + v5:Fixnum[1] = Const Value(1) + v7:BasicObject = SendWithoutBlock v1, :[], v4, v5 + v8:CBool = Test v7 + IfTrue v8, bb1(v0, v1, v3, v1, v4, v5, v7) + v10:Fixnum[2] = Const Value(2) + v12:BasicObject = SendWithoutBlock v1, :[]=, v4, v5, v10 + Return v10 + bb1(v14:BasicObject, v15:BasicObject, v16:NilClassExact, v17:BasicObject, v18:Fixnum[0], v19:Fixnum[1], v20:BasicObject): + Return v20 + "#]]); + } + + #[test] + fn test_objtostring() { + eval(" + def test = \"#{1}\" + "); + assert_method_hir_with_opcode("test", YARVINSN_objtostring, expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:Fixnum[1] = Const Value(1) + v5:BasicObject = ObjToString v3 + SideExit + "#]]); + } } #[cfg(test)] @@ -4061,7 +4413,7 @@ mod opt_tests { #[track_caller] fn assert_optimized_method_hir(method: &str, hir: Expect) { - let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method)); + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; let mut function = iseq_to_hir(iseq).unwrap(); function.optimize(); @@ -5031,9 +5383,9 @@ mod opt_tests { assert_optimized_method_hir("test", expect![[r#" fn test: bb0(v0:BasicObject): - v2:BasicObject = GetConstantPath 0x1000 - v3:Fixnum[5] = Const Value(5) - Return v3 + v3:BasicObject = GetConstantPath 0x1000 + v4:Fixnum[5] = Const Value(5) + Return v4 "#]]); } @@ -5102,8 +5454,8 @@ mod opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, M) PatchPoint MethodRedefined(Module@0x1008, name@0x1010) - v6:Fixnum[1] = Const Value(1) - Return v6 + v7:Fixnum[1] = Const Value(1) + Return v7 "#]]); } @@ -5220,8 +5572,8 @@ mod opt_tests { assert_optimized_method_hir("test", expect![[r#" fn test: bb0(v0:BasicObject): - v2:BasicObject = GetConstantPath 0x1000 - Return v2 + v3:BasicObject = GetConstantPath 0x1000 + Return v3 "#]]); } @@ -5235,8 +5587,8 @@ mod opt_tests { assert_optimized_method_hir("test", expect![[r#" fn test: bb0(v0:BasicObject): - v2:BasicObject = GetConstantPath 0x1000 - Return v2 + v3:BasicObject = GetConstantPath 0x1000 + Return v3 "#]]); } @@ -5251,8 +5603,8 @@ mod opt_tests { bb0(v0:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Kernel) - v6:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - Return v6 + v7:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Return v7 "#]]); } @@ -5273,8 +5625,8 @@ mod opt_tests { bb0(v0:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Foo::Bar::C) - v6:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - Return v6 + v7:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Return v7 "#]]); } @@ -5290,14 +5642,14 @@ mod opt_tests { bb0(v0:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v19:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v3:NilClassExact = Const Value(nil) - Jump bb1(v0, v3, v19) - bb1(v5:BasicObject, v6:NilClassExact, v7:BasicObject[VALUE(0x1008)]): - v10:BasicObject = SendWithoutBlock v7, :new - Jump bb2(v5, v10, v6) - bb2(v12:BasicObject, v13:BasicObject, v14:NilClassExact): - Return v13 + v20:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v4:NilClassExact = Const Value(nil) + Jump bb1(v0, v4, v20) + bb1(v6:BasicObject, v7:NilClassExact, v8:BasicObject[VALUE(0x1008)]): + v11:BasicObject = SendWithoutBlock v8, :new + Jump bb2(v6, v11, v7) + bb2(v13:BasicObject, v14:BasicObject, v15:NilClassExact): + Return v14 "#]]); } @@ -5317,15 +5669,15 @@ mod opt_tests { bb0(v0:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v21:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v3:NilClassExact = Const Value(nil) - v4:Fixnum[1] = Const Value(1) - Jump bb1(v0, v3, v21, v4) - bb1(v6:BasicObject, v7:NilClassExact, v8:BasicObject[VALUE(0x1008)], v9:Fixnum[1]): - v12:BasicObject = SendWithoutBlock v8, :new, v9 - Jump bb2(v6, v12, v7) - bb2(v14:BasicObject, v15:BasicObject, v16:NilClassExact): - Return v15 + v22:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v4:NilClassExact = Const Value(nil) + v5:Fixnum[1] = Const Value(1) + Jump bb1(v0, v4, v22, v5) + bb1(v7:BasicObject, v8:NilClassExact, v9:BasicObject[VALUE(0x1008)], v10:Fixnum[1]): + v13:BasicObject = SendWithoutBlock v9, :new, v10 + Jump bb2(v7, v13, v8) + bb2(v15:BasicObject, v16:BasicObject, v17:NilClassExact): + Return v16 "#]]); } @@ -5385,4 +5737,258 @@ mod opt_tests { Return v2 "#]]); } + + #[test] + fn test_elide_freeze_with_frozen_hash() { + eval(" + def test = {}.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_elide_freeze_with_refrozen_hash() { + eval(" + def test = {}.freeze.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE) + PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_no_elide_freeze_with_unfrozen_hash() { + eval(" + def test = {}.dup.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:HashExact = NewHash + v5:BasicObject = SendWithoutBlock v3, :dup + v7:BasicObject = SendWithoutBlock v5, :freeze + Return v7 + "#]]); + } + + #[test] + fn test_no_elide_freeze_hash_with_args() { + eval(" + def test = {}.freeze(nil) + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:HashExact = NewHash + v4:NilClassExact = Const Value(nil) + v6:BasicObject = SendWithoutBlock v3, :freeze, v4 + Return v6 + "#]]); + } + + #[test] + fn test_elide_freeze_with_frozen_ary() { + eval(" + def test = [].freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_elide_freeze_with_refrozen_ary() { + eval(" + def test = [].freeze.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_no_elide_freeze_with_unfrozen_ary() { + eval(" + def test = [].dup.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:ArrayExact = NewArray + v5:BasicObject = SendWithoutBlock v3, :dup + v7:BasicObject = SendWithoutBlock v5, :freeze + Return v7 + "#]]); + } + + #[test] + fn test_no_elide_freeze_ary_with_args() { + eval(" + def test = [].freeze(nil) + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:ArrayExact = NewArray + v4:NilClassExact = Const Value(nil) + v6:BasicObject = SendWithoutBlock v3, :freeze, v4 + Return v6 + "#]]); + } + + #[test] + fn test_elide_freeze_with_frozen_str() { + eval(" + def test = ''.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_elide_freeze_with_refrozen_str() { + eval(" + def test = ''.freeze.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE) + Return v3 + "#]]); + } + + #[test] + fn test_no_elide_freeze_with_unfrozen_str() { + eval(" + def test = ''.dup.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:StringExact = StringCopy v2 + v5:BasicObject = SendWithoutBlock v3, :dup + v7:BasicObject = SendWithoutBlock v5, :freeze + Return v7 + "#]]); + } + + #[test] + fn test_no_elide_freeze_str_with_args() { + eval(" + def test = ''.freeze(nil) + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:StringExact = StringCopy v2 + v4:NilClassExact = Const Value(nil) + v6:BasicObject = SendWithoutBlock v3, :freeze, v4 + Return v6 + "#]]); + } + + #[test] + fn test_elide_uminus_with_frozen_str() { + eval(" + def test = -'' + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_UMINUS) + Return v3 + "#]]); + } + + #[test] + fn test_elide_uminus_with_refrozen_str() { + eval(" + def test = -''.freeze + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE) + PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_UMINUS) + Return v3 + "#]]); + } + + #[test] + fn test_no_elide_uminus_with_unfrozen_str() { + eval(" + def test = -''.dup + "); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:StringExact = StringCopy v2 + v5:BasicObject = SendWithoutBlock v3, :dup + v7:BasicObject = SendWithoutBlock v5, :-@ + Return v7 + "#]]); + } + + #[test] + fn test_objtostring_string() { + eval(r##" + def test = "#{('foo')}" + "##); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v4:StringExact = StringCopy v3 + SideExit + "#]]); + } + + #[test] + fn test_objtostring_with_non_string() { + eval(r##" + def test = "#{1}" + "##); + assert_optimized_method_hir("test", expect![[r#" + fn test: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:Fixnum[1] = Const Value(1) + v8:BasicObject = SendWithoutBlock v3, :to_s + SideExit + "#]]); + } } |