From cf93b84bb988c7e9aab9ac11181f47d27ac1c1e6 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 26 Jun 2026 18:29:03 +0900 Subject: [PATCH 01/15] Make `is_bigendian` for DYNAMIC_ENDIAN a compile-time invariant --- pack.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pack.c b/pack.c index 24221bc3d6429c..b05bc88caead7a 100644 --- a/pack.c +++ b/pack.c @@ -60,14 +60,8 @@ static const char endstr[] = "sSiIlLqQjJ"; static int is_bigendian(void) { - static int init = 0; - static int endian_value; - const char *p; - - if (init) return endian_value; - init = 1; - p = (char*)&init; - return endian_value = p[0]?0:1; + static const union {int i; char b[1];} endian_value = {1}; + return !endian_value.b[0]; } # define BIGENDIAN_P() (is_bigendian()) #elif defined(WORDS_BIGENDIAN) From 0a40bb7d0f51569054bc698dab1ac9abbd09e370 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 26 Jun 2026 09:41:30 -0500 Subject: [PATCH 02/15] [DOC] Update Set#<=> documentation --- set.c | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/set.c b/set.c index e6b758fed3ae73..094f39bff2eacc 100644 --- a/set.c +++ b/set.c @@ -1802,11 +1802,32 @@ set_i_disjoint(VALUE set, VALUE other) /* * call-seq: - * set <=> other -> -1, 0, 1, or nil + * self <=> object -> -1, 0, 1, or nil * - * Returns 0 if the set are equal, -1 / 1 if the set is a - * proper subset / superset of the given set, or nil if - * they both have unique elements. + * Compares +self+ and +object+. + * + * If +object+ is another set, returns: + * + * - +-1+, if +self+ is a proper subset of +object+. + * - +0+, if +self+ and +object+ have the same elements. + * - +1+, if +self+ is a proper superset of +object+. + * - +nil+, if none of the above; + * that is, if +self+ and +object+ each have one or more elements + * not included in the other. + * + * Examples: + * + * set = Set[0, 1, 2] + * set <=> Set[3, 2, 1, 0] # => -1 + * set <=> Set[2, 1, 0] # => 0 + * set <=> Set[1, 0] # => 1 + * set <=> Set[1, 0, 3] # => nil + * + * Returns +nil+ if +object+ is not a set: + * + * set <=> [2, 1, 0] # => nil # Array, not Set. + * + * Related: see {Methods for Comparing}[rdoc-ref:Set@Methods+for+Comparing]. */ static VALUE set_i_compare(VALUE set, VALUE other) From 680335473282a1f8c7851efb37b15ca5c94b6738 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 26 Jun 2026 09:57:58 -0700 Subject: [PATCH 03/15] ZJIT: Move objtostring specialization to HIR build (#17499) Emitting a Send in `type_specialize` makes the compiler require more phases to go and optimize that Send. Instead, in HIR build, emit either a) a guard or b) a run-time type check that might get specialized away. Also delete `ObjToString`. --- zjit/src/codegen.rs | 56 +++++++--- zjit/src/hir.rs | 68 ++++++------ zjit/src/hir/opt_tests.rs | 42 +++---- zjit/src/hir/tests.rs | 179 +++++++++++++++++++++++------- zjit/src/hir_type/gen_hir_type.rb | 2 + zjit/src/hir_type/hir_type.inc.rs | 5 +- zjit/src/stats.rs | 2 - 7 files changed, 238 insertions(+), 116 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index e422ca1acafbd1..b5697047a3a918 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -742,7 +742,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::GetSpecialNumber { nth, state } => gen_getspecial_number(asm, *nth, &function.frame_state(*state)), &Insn::IncrCounter(counter) => no_output!(gen_incr_counter(asm, counter)), Insn::IncrCounterPtr { counter_ptr } => no_output!(gen_incr_counter_ptr(asm, *counter_ptr)), - Insn::ObjToString { val, cd, state, .. } => gen_objtostring(jit, asm, opnd!(val), *cd, &function.frame_state(*state)), &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), Insn::BreakPoint => no_output!(asm.breakpoint()), Insn::Unreachable => no_output!(asm.abort()), @@ -802,21 +801,6 @@ fn gen_get_ep(asm: &mut Assembler, level: u32) -> Opnd { ep_opnd } -fn gen_objtostring(jit: &mut JITState, asm: &mut Assembler, val: Opnd, cd: *const rb_call_data, state: &FrameState) -> Opnd { - gen_prepare_non_leaf_call(jit, asm, state); - // TODO: Specialize for immediate types - // Call rb_vm_objtostring(cfp, recv, cd) - let ret = asm_ccall!(asm, rb_vm_objtostring, CFP, val, Opnd::const_ptr(cd)); - - // TODO: Call `to_s` on the receiver if rb_vm_objtostring returns Qundef - // Need to replicate what CALL_SIMPLE_METHOD does - asm_comment!(asm, "side-exit if rb_vm_objtostring returns Qundef"); - asm.cmp(ret, Qundef.into()); - asm.je(jit, side_exit(jit, state, ObjToStringFallback)); - - ret -} - fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, obj: VALUE, pushval: VALUE, tested_value: Opnd, lep_level: u32, state: &FrameState) -> Opnd { match op_type as defined_type { DEFINED_YIELD => { @@ -2648,6 +2632,46 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_typ let result = asm.csel_e(Opnd::UImm(1), Opnd::Imm(0)); asm.jmp(result_edge(result)); + // Result block -- receives the value via block parameter (phi node) + asm.set_current_block(result_block); + let label = jit.get_label(asm, result_block, hir_block_id); + asm.write_label(label); + let param = asm.new_block_param(VALUE_BITS); + asm.current_block().add_parameter(param); + param + } else if let Some(builtin_type) = ty.builtin_type_equivalent() { + let hir_block_id = asm.current_block().hir_block_id; + let rpo_idx = asm.current_block().rpo_index; + + // Create a result block that all paths converge to + let result_block = asm.new_block(hir_block_id, false, rpo_idx); + let result_edge = |v| Target::Block(lir::BranchEdge { + target: result_block, + args: vec![v], + }); + + // If val isn't in a register, load it to use it as the base of Opnd::mem later. + let val = asm.load_mem(val); + + let is_known_heap_basic_object = val_type.is_subtype(types::HeapBasicObject); + if !is_known_heap_basic_object { + // Immediate -> definitely not the class + asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); + asm.jnz(jit, result_edge(Opnd::Imm(0))); + + // Qfalse -> definitely not the class + asm.cmp(val, Qfalse.into()); + asm.je(jit, result_edge(Opnd::Imm(0))); + } + + // Heap object + // Mask and check the builtin type + let flags = asm.load(Opnd::mem(VALUE_BITS, val, RUBY_OFFSET_RBASIC_FLAGS)); + let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64)); + asm.cmp(tag, Opnd::UImm(builtin_type as u64)); + let result = asm.csel_e(Opnd::UImm(1), Opnd::Imm(0)); + asm.jmp(result_edge(result)); + // Result block -- receives the value via block parameter (phi node) asm.set_current_block(result_block); let label = jit.get_label(asm, result_block, hir_block_id); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1f9f473c75f3c0..e6fc1d74cebb85 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -547,7 +547,6 @@ pub enum SideExitReason { GuardSuperMethodEntry, PatchPoint(Invariant), CalleeSideExit, - ObjToStringFallback, Interrupt, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, @@ -1204,8 +1203,6 @@ pub enum Insn { /// Float#to_i: truncate float to integer via rb_jit_flo_to_i FloatToInt { recv: InsnId, state: InsnId }, - // Distinct from `Send` with `mid:to_s` because does not have a patch point for String to_s being redefined - ObjToString { val: InsnId, cd: *const rb_call_data, state: InsnId }, AnyToString { val: InsnId, str: InsnId, state: InsnId }, /// Refine the known type information of with additional type information. @@ -1528,10 +1525,6 @@ macro_rules! for_each_operand_impl { $visit_one!(val); $visit_one!(state); } - Insn::ObjToString { val, state, .. } => { - $visit_one!(val); - $visit_one!(state); - } Insn::AnyToString { val, str, state, .. } => { $visit_one!(val); $visit_one!(str); @@ -1798,7 +1791,6 @@ impl Insn { Insn::IntOr { .. } => effects::Empty, Insn::FixnumLShift { .. } => effects::Empty, Insn::FixnumRShift { .. } => effects::Empty, - Insn::ObjToString { .. } => effects::Any, Insn::AnyToString { .. } => effects::Any, Insn::GuardType { guard_type, .. } => Effect::read_write( @@ -2295,7 +2287,6 @@ 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::StringIntern { val, .. } => { write!(f, "StringIntern {val}") }, Insn::AnyToString { val, str, .. } => { write!(f, "AnyToString {val}, str: {str}") }, Insn::SideExit { reason, recompile, .. } => { @@ -3141,7 +3132,6 @@ impl Function { Insn::GetClassVar { .. } => types::BasicObject, Insn::ToNewArray { .. } => types::ArrayExact, Insn::ToArray { .. } => types::ArrayExact, - Insn::ObjToString { .. } => types::BasicObject, Insn::AnyToString { .. } => types::String, Insn::IsBlockParamModified { .. } => types::CBool, Insn::GetBlockParam { .. } => types::BasicObject, @@ -4081,28 +4071,6 @@ impl Function { self.push_insn_id(block, insn_id); continue; } } - Insn::ObjToString { val, cd, state, .. } => { - if self.is_a(val, types::String) { - // behaves differently from `Send` 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); continue; - } - - let Some(recv_type) = self.profiled_type_of_at(val, state) else { - self.push_insn_id(block, insn_id); continue - }; - - if recv_type.is_string() { - self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_type.class() }, state }); - let guard = self.push_insn(block, Insn::GuardType { val, guard_type: types::String, state, recompile: None }); - // Infer type so AnyToString can fold off this - self.insn_types[guard.0] = self.infer_type(guard); - self.make_equal_to(insn_id, guard); - } else { - let recv = self.push_insn(block, Insn::GuardType { val, guard_type: Type::from_profiled_type(recv_type), state, recompile: None }); - let send_to_s = self.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], state, reason: ObjToStringNotString }); - self.make_equal_to(insn_id, send_to_s); - } - } Insn::AnyToString { str, .. } => { if self.is_a(str, types::String) { self.make_equal_to(insn_id, str); @@ -6479,7 +6447,6 @@ impl Function { | Insn::SetClassVar { val, .. } | Insn::Return { val } | Insn::Throw { val, .. } - | Insn::ObjToString { val, .. } | Insn::GuardType { val, .. } | Insn::ToArray { val, .. } | Insn::ToNewArray { val, .. } @@ -9168,10 +9135,39 @@ fn add_iseq_to_hir( let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); let argc = crate::profile::num_arguments_on_stack(cd); assert_eq!(0, argc, "objtostring should not have args"); - let recv = state.stack_pop()?; - let objtostring = fun.push_insn(block, Insn::ObjToString { val: recv, cd, state: exit_id }); - state.stack_push(objtostring) + // TODO(max): Handle polymorphic profiles + let result = if let Some(profiled_type) = fun.monomorphic_summary(&profiles, recv, exit_id) { + if profiled_type.is_string() { + // TODO(max): Do we need PatchPoint? We are checking T_STRING-ness. + fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: profiled_type.class() }, state: exit_id }); + fun.push_insn(block, Insn::GuardType { val: recv, guard_type: types::String, state: exit_id, recompile: None }) + } else { + let recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state: exit_id, recompile: None }); + fun.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], state: exit_id, reason: ObjToStringNotString }) + } + } else { + let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected: types::String }); + let iftrue_block = fun.new_block(insn_idx); + let iffalse_block = fun.new_block(insn_idx); + let join_block = fun.new_block(insn_idx); + fun.push_insn(block, Insn::CondBranch { + val: has_type, + if_true: BranchEdge { target: iftrue_block, args: vec![] }, + if_false: BranchEdge { target: iffalse_block, args: vec![] } + }); + // true block + let refined = fun.push_insn(iftrue_block, Insn::RefineType { val: recv, new_type: types::String }); + fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![refined] })); + // false block + let refined = fun.push_insn(iffalse_block, Insn::RefineType { val: recv, new_type: types::NotString }); + let send = fun.push_insn(iffalse_block, Insn::Send { recv: refined, cd, block: None, args: vec![], state: exit_id, reason: ObjToStringNotString }); + fun.push_insn(iffalse_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); + // join block + block = join_block; + fun.push_insn(join_block, Insn::Param) + }; + state.stack_push(result); } YARVINSN_anytostring => { let str = state.stack_pop()?; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 19b7e8c1966034..72a0744e982eb6 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6928,9 +6928,9 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v14:StringExact = StringCopy v13 - v21:StringExact = StringConcat v10, v14 + v28:StringExact = StringConcat v10, v14 CheckInterrupts - Return v21 + Return v28 "); } @@ -6952,11 +6952,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) - v15:BasicObject = ObjToString v12 - v17:String = AnyToString v12, str: v15 - v19:StringExact = StringConcat v10, v17 + PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) + v34:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 + v26:StringExact = StringConcat v10, v34 CheckInterrupts - Return v19 + Return v26 "); } @@ -6985,10 +6985,10 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) - v29:String = GuardType v10, String - v22:StringExact = StringConcat v14, v29 + v19:String = GuardType v10, String + v23:StringExact = StringConcat v14, v19 CheckInterrupts - Return v22 + Return v23 "); } @@ -7020,10 +7020,10 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(MyString@0x1010) - v29:String = GuardType v10, String - v22:StringExact = StringConcat v14, v29 + v19:String = GuardType v10, String + v23:StringExact = StringConcat v14, v19 CheckInterrupts - Return v22 + Return v23 "); } @@ -7051,14 +7051,14 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v28:ArrayExact = GuardType v10, ArrayExact + v18:ArrayExact = GuardType v10, ArrayExact PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, to_s@0x1018, cme:0x1020) - v34:BasicObject = CCallWithFrame v28, :Array#to_s@0x1048 - v20:String = AnyToString v28, str: v34 - v22:StringExact = StringConcat v14, v20 + v33:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 + v21:String = AnyToString v18, str: v33 + v23:StringExact = StringConcat v14, v21 CheckInterrupts - Return v22 + Return v23 "); } @@ -10277,12 +10277,12 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v28:Fixnum = GuardType v10, Fixnum + v18:Fixnum = GuardType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1010, to_s@0x1018, cme:0x1020) - v33:StringExact = CCallVariadic v28, :Integer#to_s@0x1048 - v22:StringExact = StringConcat v14, v33 + v32:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 + v23:StringExact = StringConcat v14, v32 CheckInterrupts - Return v22 + Return v23 "); } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 91243bde0ebd73..0d44650486b4d6 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2037,12 +2037,21 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[123] = Const Value(123) - v15:BasicObject = ObjToString v12 - v17:String = AnyToString v12, str: v15 - v19:StringExact = StringConcat v10, v17 - v21:Symbol = StringIntern v19 + v15:CBool[false] = HasType v12, String + CondBranch v15, bb4(), bb5() + bb4(): + v17 = RefineType v12, String + Jump bb6(v17) + bb5(): + v19:Fixnum[123] = RefineType v12, NotString + v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v20) + bb6(v22:BasicObject): + v24:String = AnyToString v12, str: v22 + v26:StringExact = StringConcat v10, v24 + v28:Symbol = StringIntern v26 CheckInterrupts - Return v21 + Return v28 "); } @@ -5010,11 +5019,20 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) - v15:BasicObject = ObjToString v12 - v17:String = AnyToString v12, str: v15 - v19:StringExact = StringConcat v10, v17 + v15:CBool[false] = HasType v12, String + CondBranch v15, bb4(), bb5() + bb4(): + v17 = RefineType v12, String + Jump bb6(v17) + bb5(): + v19:Fixnum[1] = RefineType v12, NotString + v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v20) + bb6(v22:BasicObject): + v24:String = AnyToString v12, str: v22 + v26:StringExact = StringConcat v10, v24 CheckInterrupts - Return v19 + Return v26 "); } @@ -5036,17 +5054,44 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:BasicObject = ObjToString v10 - v15:String = AnyToString v10, str: v13 - v17:Fixnum[2] = Const Value(2) - v20:BasicObject = ObjToString v17 - v22:String = AnyToString v17, str: v20 - v24:Fixnum[3] = Const Value(3) - v27:BasicObject = ObjToString v24 - v29:String = AnyToString v24, str: v27 - v31:StringExact = StringConcat v15, v22, v29 + v13:CBool[false] = HasType v10, String + CondBranch v13, bb4(), bb5() + bb4(): + v15 = RefineType v10, String + Jump bb6(v15) + bb5(): + v17:Fixnum[1] = RefineType v10, NotString + v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v18) + bb6(v20:BasicObject): + v22:String = AnyToString v10, str: v20 + v24:Fixnum[2] = Const Value(2) + v27:CBool[false] = HasType v24, String + CondBranch v27, bb7(), bb8() + bb7(): + v29 = RefineType v24, String + Jump bb9(v29) + bb8(): + v31:Fixnum[2] = RefineType v24, NotString + v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb9(v32) + bb9(v34:BasicObject): + v36:String = AnyToString v24, str: v34 + v38:Fixnum[3] = Const Value(3) + v41:CBool[false] = HasType v38, String + CondBranch v41, bb10(), bb11() + bb10(): + v43 = RefineType v38, String + Jump bb12(v43) + bb11(): + v45:Fixnum[3] = RefineType v38, NotString + v46:BasicObject = Send v45, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v46) + bb12(v48:BasicObject): + v50:String = AnyToString v38, str: v48 + v52:StringExact = StringConcat v22, v36, v50 CheckInterrupts - Return v31 + Return v52 "); } @@ -5069,11 +5114,20 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:NilClass = Const Value(nil) - v15:BasicObject = ObjToString v12 - v17:String = AnyToString v12, str: v15 - v19:StringExact = StringConcat v10, v17 + v15:CBool[false] = HasType v12, String + CondBranch v15, bb4(), bb5() + bb4(): + v17 = RefineType v12, String + Jump bb6(v17) + bb5(): + v19:NilClass = RefineType v12, NotString + v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v20) + bb6(v22:BasicObject): + v24:String = AnyToString v12, str: v22 + v26:StringExact = StringConcat v10, v24 CheckInterrupts - Return v19 + Return v26 "); } @@ -5095,17 +5149,44 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:BasicObject = ObjToString v10 - v15:String = AnyToString v10, str: v13 - v17:Fixnum[2] = Const Value(2) - v20:BasicObject = ObjToString v17 - v22:String = AnyToString v17, str: v20 - v24:Fixnum[3] = Const Value(3) - v27:BasicObject = ObjToString v24 - v29:String = AnyToString v24, str: v27 - v31:RegexpExact = ToRegexp v15, v22, v29 + v13:CBool[false] = HasType v10, String + CondBranch v13, bb4(), bb5() + bb4(): + v15 = RefineType v10, String + Jump bb6(v15) + bb5(): + v17:Fixnum[1] = RefineType v10, NotString + v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v18) + bb6(v20:BasicObject): + v22:String = AnyToString v10, str: v20 + v24:Fixnum[2] = Const Value(2) + v27:CBool[false] = HasType v24, String + CondBranch v27, bb7(), bb8() + bb7(): + v29 = RefineType v24, String + Jump bb9(v29) + bb8(): + v31:Fixnum[2] = RefineType v24, NotString + v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb9(v32) + bb9(v34:BasicObject): + v36:String = AnyToString v24, str: v34 + v38:Fixnum[3] = Const Value(3) + v41:CBool[false] = HasType v38, String + CondBranch v41, bb10(), bb11() + bb10(): + v43 = RefineType v38, String + Jump bb12(v43) + bb11(): + v45:Fixnum[3] = RefineType v38, NotString + v46:BasicObject = Send v45, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v46) + bb12(v48:BasicObject): + v50:String = AnyToString v38, str: v48 + v52:RegexpExact = ToRegexp v22, v36, v50 CheckInterrupts - Return v31 + Return v52 "); } @@ -5127,14 +5208,32 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:BasicObject = ObjToString v10 - v15:String = AnyToString v10, str: v13 - v17:Fixnum[2] = Const Value(2) - v20:BasicObject = ObjToString v17 - v22:String = AnyToString v17, str: v20 - v24:RegexpExact = ToRegexp v15, v22, MULTILINE|IGNORECASE|EXTENDED|NOENCODING + v13:CBool[false] = HasType v10, String + CondBranch v13, bb4(), bb5() + bb4(): + v15 = RefineType v10, String + Jump bb6(v15) + bb5(): + v17:Fixnum[1] = RefineType v10, NotString + v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v18) + bb6(v20:BasicObject): + v22:String = AnyToString v10, str: v20 + v24:Fixnum[2] = Const Value(2) + v27:CBool[false] = HasType v24, String + CondBranch v27, bb7(), bb8() + bb7(): + v29 = RefineType v24, String + Jump bb9(v29) + bb8(): + v31:Fixnum[2] = RefineType v24, NotString + v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb9(v32) + bb9(v34:BasicObject): + v36:String = AnyToString v24, str: v34 + v38:RegexpExact = ToRegexp v22, v36, MULTILINE|IGNORECASE|EXTENDED|NOENCODING CheckInterrupts - Return v24 + Return v38 "); } diff --git a/zjit/src/hir_type/gen_hir_type.rb b/zjit/src/hir_type/gen_hir_type.rb index 951d623ab9399a..8db620123b72d7 100644 --- a/zjit/src/hir_type/gen_hir_type.rb +++ b/zjit/src/hir_type/gen_hir_type.rb @@ -198,6 +198,8 @@ def add_union name, type_names $numeric_bits["Truthy"] = $numeric_bits["BasicObject"] & ~$numeric_bits["Falsy"] $bits["NotNil"] = ["BasicObject & !NilClass"] $numeric_bits["NotNil"] = $numeric_bits["BasicObject"] & ~$numeric_bits["NilClass"] +$bits["NotString"] = ["BasicObject & !String"] +$numeric_bits["NotString"] = $numeric_bits["BasicObject"] & ~$numeric_bits["String"] # ===== Finished generating the DAG; write Rust code ===== diff --git a/zjit/src/hir_type/hir_type.inc.rs b/zjit/src/hir_type/hir_type.inc.rs index 7a9cceac9f72d0..ca3ddf6a9ec00e 100644 --- a/zjit/src/hir_type/hir_type.inc.rs +++ b/zjit/src/hir_type/hir_type.inc.rs @@ -52,6 +52,7 @@ mod bits { pub const ModuleSubclass: u64 = 1u64 << 30; pub const NilClass: u64 = 1u64 << 31; pub const NotNil: u64 = BasicObject & !NilClass; + pub const NotString: u64 = BasicObject & !String; pub const Numeric: u64 = Float | Integer | NumericExact | NumericSubclass; pub const NumericExact: u64 = 1u64 << 32; pub const NumericSubclass: u64 = 1u64 << 33; @@ -77,7 +78,7 @@ mod bits { pub const TrueClass: u64 = 1u64 << 45; pub const Truthy: u64 = BasicObject & !Falsy; pub const Undef: u64 = 1u64 << 46; - pub const AllBitPatterns: [(&str, u64); 77] = [ + pub const AllBitPatterns: [(&str, u64); 78] = [ ("Any", Any), ("RubyValue", RubyValue), ("Immediate", Immediate), @@ -87,6 +88,7 @@ mod bits { ("NotNil", NotNil), ("Truthy", Truthy), ("BuiltinExact", BuiltinExact), + ("NotString", NotString), ("BoolExact", BoolExact), ("TrueClass", TrueClass), ("HeapBasicObject", HeapBasicObject), @@ -212,6 +214,7 @@ pub mod types { pub const ModuleSubclass: Type = Type::from_bits(bits::ModuleSubclass); pub const NilClass: Type = Type::from_bits(bits::NilClass); pub const NotNil: Type = Type::from_bits(bits::NotNil); + pub const NotString: Type = Type::from_bits(bits::NotString); pub const Numeric: Type = Type::from_bits(bits::Numeric); pub const NumericExact: Type = Type::from_bits(bits::NumericExact); pub const NumericSubclass: Type = Type::from_bits(bits::NumericSubclass); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index f8ed7ae028d70d..f6457dbf2a6547 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -224,7 +224,6 @@ make_counters! { exit_patchpoint_no_singleton_class, exit_patchpoint_root_box_only, exit_callee_side_exit, - exit_obj_to_string_fallback, exit_interrupt, exit_stackoverflow, exit_block_param_proxy_not_iseq_or_ifunc, @@ -628,7 +627,6 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardGreaterEq => exit_guard_greater_eq_failure, GuardSuperMethodEntry => exit_guard_super_method_entry, CalleeSideExit => exit_callee_side_exit, - ObjToStringFallback => exit_obj_to_string_fallback, Interrupt => exit_interrupt, StackOverflow => exit_stackoverflow, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, From 2155057082e05bd7a0b85fd50d86dd8dba326b35 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Fri, 26 Jun 2026 19:37:29 +0200 Subject: [PATCH 04/15] Reduce `Array#include?` allocations for literals --- compile.c | 4 +++- test/ruby/test_optimization.rb | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/compile.c b/compile.c index c71a2e79cbb967..b5654bf3d30fbb 100644 --- a/compile.c +++ b/compile.c @@ -4357,7 +4357,9 @@ iseq_specialized_instruction(rb_iseq_t *iseq, INSN *iobj) INSN *niobj = (INSN *)iobj->link.next; if ((IS_INSN_ID(niobj, getlocal) || IS_INSN_ID(niobj, getinstancevariable) || - IS_INSN_ID(niobj, putself)) && + IS_INSN_ID(niobj, putself) || + IS_INSN_ID(niobj, putobject) || + IS_INSN_ID(niobj, putnil)) && IS_NEXT_INSN_ID(&niobj->link, send)) { LINK_ELEMENT *sendobj = &(niobj->link); // Below we call ->next; diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index 1554b43f18c8e5..d34e490d589126 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -1119,6 +1119,13 @@ def test_opt_duparray_send_include_p '@c = :b; [:a, :b].include?(@c)', '@c = "b"; %i[a b].include?(@c.to_sym)', '[:a, :b].include?(self) == false', + "[:a, :b].include?(:a)", + "[true, false].include?(false)", + "[1, nil].include?(nil)", + "# frozen_string_literal: true\n['a', 'b'].include?('a')", + "# frozen_string_literal: true\n['a', 'b'].include?('c') == false", + "# frozen_string_literal: true\n['a', 'b'].include?(2) == false", + "# frozen_string_literal: true\n['a', 'b'].include?(/a/) == false", ].each do |code| iseq = RubyVM::InstructionSequence.compile(code) insn = iseq.disasm From 1cd2c5db5f955b9d7b452f4a91bc13019568331f Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Fri, 26 Jun 2026 10:43:21 -0400 Subject: [PATCH 05/15] ZJIT: Replace guards we know can't pass with an unconditional side-exit The resulting type of a `GuardType` we prove cannot pass is `Empty`. Any HIR instructions using the type information from the `GuardType` will also see `Empty` and propagate that type through the compiler. Any generated code would not be executable at run time due to the always failing guard, so there's no point in generating it. --- zjit/src/hir.rs | 13 +++++++++++++ zjit/src/hir/opt_tests.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e6fc1d74cebb85..7bfc61bf033de0 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5509,6 +5509,19 @@ impl Function { let mut new_insns = vec![]; for insn_id in old_insns { let replacement_id = match self.find(insn_id) { + // TODO (nirvdrum 2026-06-26): Folding the guard to a SideExit is a workaround, + // not a proper fix. It relies on constant folding to keep an Empty-typed value + // (see below) from reaching codegen; disabling this pass would let that value + // through and the program would fail to compile on x86-64. Compilation correctness + // should not depend on an optimization pass, so this should be replaced by a + // comprehensive fix. + Insn::GuardType { val, guard_type, state, recompile } if !self.type_of(val).could_be(guard_type) => { + // The value's type is disjoint from the guard type, so the guard can never + // pass. Every execution would side-exit here, so we replace the guard with an + // unconditional exit. The terminator handling below then drops the rest of + // the block, which is now unreachable. + self.new_insn(Insn::SideExit { state, reason: SideExitReason::GuardType(guard_type), recompile }) + } Insn::GuardType { val, guard_type, .. } if self.is_a(val, guard_type) => { self.make_equal_to(insn_id, val); // Don't bother re-inferring the type of val; we already know it. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 72a0744e982eb6..88355ed70143ef 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2512,6 +2512,41 @@ mod hir_opt_tests { assert!(!insns.contains(&dead_const)); } + // A GuardType whose value type is disjoint from the guard type can never pass, so every + // execution side-exits there. fold_constants should replace the guard with an unconditional + // SideExit and drop the now-unreachable instructions that follow. + #[test] + fn test_fold_guard_type_that_can_never_pass_into_side_exit() { + let mut function = Function::new(std::ptr::null()); + let entry = function.entry_block; + + let state = function.push_insn(entry, Insn::Snapshot { state: FrameState::new(std::ptr::null()) }); + // A nil constant is a NilClass, which is disjoint from Fixnum, so the guard below can + // never pass and the optimizer infers its result as Empty. + let nil = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) }); + let guard = function.push_insn(entry, Insn::GuardType { val: nil, guard_type: types::Fixnum, state, recompile: None }); + function.push_insn(entry, Insn::StoreField { recv: nil, id: FieldName::len, offset: 0, val: guard }); + function.push_insn(entry, Insn::Return { val: guard }); + function.seal_entries(); + + function.infer_types(); + function.fold_constants(); + + let insns: Vec = function.blocks[entry.0].insns.iter().map(|&id| function.find(id)).collect(); + assert!( + insns.iter().any(|insn| matches!(insn, Insn::SideExit { .. })), + "expected the always-failing guard to be folded into a SideExit, got {insns:?}", + ); + assert!( + !insns.iter().any(|insn| matches!(insn, Insn::GuardType { .. })), + "the always-failing GuardType should have been removed, got {insns:?}", + ); + assert!( + !insns.iter().any(|insn| matches!(insn, Insn::StoreField { .. } | Insn::Return { .. })), + "instructions after the unconditional SideExit are unreachable and should have been dropped, got {insns:?}", + ); + } + #[test] fn test_eliminate_new_array() { eval(" From 02365e1736904c0dffc2acc19c6bf0671a90bf3f Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Fri, 26 Jun 2026 11:17:38 -0400 Subject: [PATCH 06/15] ZJIT: Assert we never try to get the byte size of an `Empty` value An `Empty` value has no representable size. The caller trying to get the size is operating on an unreachable instruction that should have already been removed. --- zjit/src/hir_type/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 6fb0483e4487c3..ef67de616e0c34 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -593,6 +593,9 @@ impl Type { } pub fn num_bytes(&self) -> u8 { + assert!(!self.bit_equal(types::Empty), + "a value of type Empty is unreachable and should have been eliminated before codegen"); + if self.is_subtype(types::CUInt8) || self.is_subtype(types::CInt8) { return 1; } if self.is_subtype(types::CUInt16) || self.is_subtype(types::CInt16) { return 2; } if self.is_subtype(types::CUInt32) || self.is_subtype(types::CInt32) { return 4; } From 5948386e040b99f1e7e585e53048676a2b972a22 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 26 Jun 2026 15:43:07 -0700 Subject: [PATCH 07/15] ZJIT: Add ZJIT perf symbols for trampolines (#17515) --- zjit/src/codegen.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b5697047a3a918..c8450ed72f4371 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -290,6 +290,16 @@ fn register_with_perf(symbol_name: String, start_ptr: usize, code_size: usize) { }; } +/// Register the code emitted from `start` through the current write pointer +/// under `symbol_name` in the perf map, if perf output is enabled. +fn register_current_code_range_with_perf(cb: &CodeBlock, symbol_name: &str, start: CodePtr) { + if get_option!(perf).is_some() { + let start_ptr = start.raw_addr(cb); + let end_ptr = cb.get_write_ptr().raw_addr(cb); + register_with_perf(symbol_name.to_string(), start_ptr, end_ptr - start_ptr); + } +} + /// Compile a shared JIT entry trampoline pub fn gen_entry_trampoline(cb: &mut CodeBlock) -> Result { // Set up registers for CFP, EC, SP, and basic block arguments @@ -309,12 +319,7 @@ pub fn gen_entry_trampoline(cb: &mut CodeBlock) -> Result let (code_ptr, gc_offsets) = asm.compile(cb)?; assert!(gc_offsets.is_empty()); - if get_option!(perf).is_some() { - let start_ptr = code_ptr.raw_addr(cb); - let end_ptr = cb.get_write_ptr().raw_addr(cb); - let code_size = end_ptr - start_ptr; - register_with_perf("entry trampoline".into(), start_ptr, code_size); - } + register_current_code_range_with_perf(cb, "entry trampoline", code_ptr); Ok(code_ptr) } @@ -3578,6 +3583,7 @@ pub fn gen_function_stub_hit_trampoline(cb: &mut CodeBlock) -> Result Result asm.compile(cb).map(|(code_ptr, gc_offsets)| { assert_eq!(gc_offsets.len(), 0); + register_current_code_range_with_perf(cb, "exit trampoline", code_ptr); code_ptr }) } @@ -3615,6 +3622,7 @@ pub fn gen_materialize_exit_trampoline(cb: &mut CodeBlock, exit_trampoline: Code asm.compile(cb).map(|(code_ptr, gc_offsets)| { assert_eq!(gc_offsets.len(), 0); + register_current_code_range_with_perf(cb, "materialize_exit trampoline", code_ptr); code_ptr }) } @@ -3630,6 +3638,7 @@ pub fn gen_materialize_exit_trampoline_with_counter(cb: &mut CodeBlock, material asm.compile(cb).map(|(code_ptr, gc_offsets)| { assert_eq!(gc_offsets.len(), 0); + register_current_code_range_with_perf(cb, "materialize_exit_with_counter trampoline", code_ptr); code_ptr }) } From 88c8877ac40f9697da2edcee99d78de0a0472b87 Mon Sep 17 00:00:00 2001 From: XrXr Date: Fri, 19 Jun 2026 20:43:31 -0400 Subject: [PATCH 08/15] ZJIT: Comment that we basically never add to test_zjit.rb now [DOC] Good for the humans and agents. --- test/ruby/test_zjit.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 198301d9332e30..18baf3832abb72 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -2,6 +2,11 @@ # # This set of tests can be run with: # make test-all TESTS=test/ruby/test_zjit.rb +# +# Instead of adding new tests here, you should probably +# be adding tests that run under the Rust test harness, +# say, in `codegen_tests.rs`. It parallelizes better and +# allows for easy inspection of VM internal states. require 'test/unit' require 'envutil' From 6ccae2b332fdf1841e888e8f63e0699dacfcee75 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 26 Jun 2026 17:58:33 -0500 Subject: [PATCH 09/15] [DOC] Update Set#| documentation Co-authored-by: Jeremy Evans --- set.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/set.c b/set.c index 094f39bff2eacc..4b23c341de1544 100644 --- a/set.c +++ b/set.c @@ -1340,13 +1340,16 @@ set_i_xor(VALUE set, VALUE other) /* * call-seq: - * set | enum -> new_set + * self | enumerable -> new_set * - * Returns a new set built by merging the set and the elements of the - * given enumerable object. + * Returns a new \Set object containing the elements of both +self+ + * and the given +enumerable+. * - * Set[1, 2, 3] | Set[2, 4, 5] #=> Set[1, 2, 3, 4, 5] - * Set[1, 5, 'z'] | (1..6) #=> Set[1, 5, "z", 2, 3, 4, 6] + * set = Set[0, 1, 2] + * set | Set[2, 1, 'a'] # => Set[0, 1, 2, "a"] + * set | set # => Set[0, 1, 2] + * + * Related: see {Methods for Set Operations}[rdoc-ref:Set@Methods+for+Set+Operations]. */ static VALUE set_i_union(VALUE set, VALUE other) From 5e03921cc69c970fe1eb9bc90cdce373be446b7e Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 26 Jun 2026 17:59:35 -0500 Subject: [PATCH 10/15] [DOC] Update Set#add documentation Co-authored-by: Jeremy Evans --- set.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/set.c b/set.c index 4b23c341de1544..09eeee112237e6 100644 --- a/set.c +++ b/set.c @@ -723,14 +723,15 @@ set_i_join(int argc, VALUE *argv, VALUE set) /* * call-seq: - * add(obj) -> self + * add(object) -> self * - * Adds the given object to the set and returns self. Use Set#merge to - * add many elements at once. + * Adds the given +object+ to +self+, returns +self+: * - * Set[1, 2].add(3) #=> Set[1, 2, 3] - * Set[1, 2].add([3, 4]) #=> Set[1, 2, [3, 4]] - * Set[1, 2].add(2) #=> Set[1, 2] + * set = Set[0, 1, 2] + * set.add(%w[a b c]) # => Set[0, 1, 2, ["a", "b", "c"]] + * set.add(0) # => Set[0, 1, 2, ["a", "b", "c"]] + * + * Related: see {Methods for Assigning}[rdoc-ref:Set@Methods+for+Assigning]. */ static VALUE set_i_add(VALUE set, VALUE item) From baa3b26af5b34d34b74916bc0edd7a9b26c1f839 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Sat, 27 Jun 2026 08:13:40 +0900 Subject: [PATCH 11/15] ZJIT: Reuse instruction profiling for recompile exits (#17457) * ZJIT: Reuse instruction profiling for recompile exits Deduplicate recompile-exit profiling by sharing the per-instruction profiling logic used by zjit_* instructions. Recompile exits now use the materialized CFP state to recover the current instruction and collect profiles through profile.rs, instead of passing kind-specific profiling payloads across the C ABI. * ZJIT: Restore HIR Recompile struct Keep HIR recompilation metadata as Option instead of a bool. This preserves a typed extension point for future recompilation policy without changing the shared recompile-exit profiling path. --- zjit/src/backend/lir.rs | 17 ++---- zjit/src/codegen.rs | 49 +++------------- zjit/src/hir.rs | 79 +++++++------------------- zjit/src/hir/opt_tests.rs | 68 ++++++++++++++++++++++ zjit/src/profile.rs | 115 ++++++++++++++++---------------------- 5 files changed, 145 insertions(+), 183 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 60f6cbb5805c67..67f43c1517da29 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -549,22 +549,18 @@ pub struct SideExit { pub stack: Vec, pub locals: Vec, pub iseq: IseqPtr, - /// If set, the side exit will call the recompile function with these arguments - /// to profile the send and invalidate the ISEQ for recompilation. + /// If set, the side exit will profile the current instruction and invalidate + /// the compiled ISEQ for recompilation. pub recompile: Option, } -/// Arguments for the recompile callback on side exit. +/// Metadata for the recompile callback on side exit. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct SideExitRecompile { - /// The frame's own iseq, where the runtime profile is recorded at `insn_idx`. - /// For an exit out of inlined code this is the inlined callee. - pub frame_iseq: Opnd, /// The compiled unit whose version must be invalidated to force a recompile. For inlined /// methods, this will be the outer function it was inlined into. pub compiled_iseq: Opnd, pub insn_idx: u32, - pub strategy: hir::Recompile, } /// Branch target (something that we can jump to) @@ -2588,15 +2584,10 @@ impl Assembler let payload = get_or_create_iseq_payload(exit.iseq); payload.reset_profiles_remaining(recompile.insn_idx as YarvInsnIdx); use crate::codegen::exit_recompile; - let (profile_kind, profile_payload) = recompile.strategy.to_c_args(); asm_comment!(asm, "profile and maybe recompile"); asm_ccall!(asm, exit_recompile, EC, - recompile.frame_iseq, - recompile.compiled_iseq, - recompile.insn_idx.into(), - profile_kind.into(), - profile_payload.into() + recompile.compiled_iseq ); } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index c8450ed72f4371..92a18e59f38cfe 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3182,11 +3182,9 @@ fn side_exit(jit: &JITState, state: &FrameState, reason: SideExitReason) -> Targ /// Build a Target::SideExit that optionally triggers exit_recompile on the exit path. fn side_exit_with_recompile(jit: &JITState, state: &FrameState, reason: SideExitReason, recompile: Option) -> Target { let mut exit = build_side_exit(jit, state); - exit.recompile = recompile.map(|strategy| SideExitRecompile { - frame_iseq: Opnd::Value(VALUE::from(state.iseq)), + exit.recompile = recompile.map(|_| SideExitRecompile { compiled_iseq: Opnd::Value(VALUE::from(jit.iseq())), insn_idx: state.insn_idx() as u32, - strategy, }); Target::SideExit { exit, reason } } @@ -3233,19 +3231,13 @@ pub(crate) use c_callable; c_callable! { /// Called from JIT side-exit code to profile operands and trigger recompilation. - /// `profile_kind` selects what to profile; `profile_payload` carries kind-specific data. /// Once enough profiles are gathered, invalidates the compiled unit for recompilation. /// - /// Two iseqs are passed because they diverge for inlined code. `frame_iseq_raw` is - /// the frame's own iseq, where the runtime profile is recorded; for an exit out of - /// an inlined callee this is the callee, which typically has no compiled version of - /// its own. `compiled_iseq_raw` is the function that was actually compiled (the - /// inliner folds the callee's body into it), so its version is the one holding the - /// failing guard and the one we must invalidate to force a recompile. For - /// non-inlined code the two are identical. - pub(crate) fn exit_recompile(ec: EcPtr, frame_iseq_raw: VALUE, compiled_iseq_raw: VALUE, insn_idx: u32, profile_kind: i32, profile_payload: i32) { - let recompile = Recompile::from_c_args(profile_kind, profile_payload); - + /// `compiled_iseq_raw` is the ISEQ that was actually compiled. For an exit out + /// of inlined code, the inliner folds the callee's body into the outer ISEQ, so + /// the outer ISEQ's version holds the failing guard and must be invalidated to + /// force a recompile. For non-inlined code, it is the same as the frame ISEQ. + pub(crate) fn exit_recompile(ec: EcPtr, compiled_iseq_raw: VALUE) { // Fast check before taking the VM lock: skip if the compiled unit is already // invalidated or at the version limit. This avoids expensive lock acquisition // on every shape guard exit after the recompile has already been triggered. @@ -3262,37 +3254,10 @@ c_callable! { } with_vm_lock(src_loc!(), || { - let frame_iseq: IseqPtr = frame_iseq_raw.as_iseq(); let compiled_iseq: IseqPtr = compiled_iseq_raw.as_iseq(); - // For no-profile sends, skip if already profiled at this insn_idx. - // For shape guard exits, always re-profile because the - // original YARV profiles were monomorphic but runtime showed new shapes. - if matches!(recompile, Recompile::ProfileSend { .. }) && - get_or_create_iseq_payload(frame_iseq).profile.done_profiling_at(insn_idx as usize) { - return; - } - let should_recompile = with_time_stat(Counter::profile_time_ns, || { - let cfp = unsafe { get_ec_cfp(ec) }; - let payload = get_or_create_iseq_payload(frame_iseq); - - match recompile { - Recompile::ProfileSend { argc } => { - let sp = unsafe { get_cfp_sp(cfp) }; - // Profile the receiver and arguments for this send instruction - payload.profile.profile_send_at(frame_iseq, insn_idx as usize, sp, argc as usize) - } - Recompile::ProfileSelf => { - // Profile self for shape guard exits - let self_val = unsafe { get_cfp_self(cfp) }; - payload.profile.profile_self_at(frame_iseq, insn_idx as usize, self_val) - } - Recompile::ProfileBlockHandler => { - // Profile the block handler for this getblockparamproxy instruction - payload.profile.profile_getblockparamproxy_at(frame_iseq, insn_idx as usize, cfp) - } - } + crate::profile::profile_recompile_insn(ec) }); // Once we have enough profiles, invalidate the compiled unit so it diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 7bfc61bf033de0..8a1f7b87980148 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -567,41 +567,9 @@ pub enum SideExitReason { InvokeBlockNotIfunc, } -/// Controls how a side exit triggers recompilation. -pub const RECOMPILE_PROFILE_SEND: i32 = 0; -pub const RECOMPILE_PROFILE_SELF: i32 = 1; -pub const RECOMPILE_PROFILE_BLOCK_HANDLER: i32 = 2; - +/// Marks a side exit as triggering profiling and recompilation. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Recompile { - /// Profile receiver + arguments from the stack (for sends without profile data). - ProfileSend { argc: i32 }, - /// Profile self from the CFP (for shape guard failures). - ProfileSelf, - /// Profile the block handler from the CFP (for getblockparamproxy guard failures). - ProfileBlockHandler, -} - -impl Recompile { - /// Convert to primitive arguments for passing across the C ABI. - pub fn to_c_args(self) -> (i32, i32) { - match self { - Recompile::ProfileSend { argc } => (RECOMPILE_PROFILE_SEND, argc), - Recompile::ProfileSelf => (RECOMPILE_PROFILE_SELF, 0), - Recompile::ProfileBlockHandler => (RECOMPILE_PROFILE_BLOCK_HANDLER, 0), - } - } - - /// Reconstruct from primitive arguments received across the C ABI. - pub fn from_c_args(kind: i32, payload: i32) -> Self { - match kind { - RECOMPILE_PROFILE_SEND => Recompile::ProfileSend { argc: payload }, - RECOMPILE_PROFILE_SELF => Recompile::ProfileSelf, - RECOMPILE_PROFILE_BLOCK_HANDLER => Recompile::ProfileBlockHandler, - _ => unreachable!("unknown recompile profile kind: {kind}"), - } - } -} +pub struct Recompile; #[derive(Debug, Clone, Copy)] pub enum MethodType { @@ -3865,8 +3833,7 @@ impl Function { // Add GuardType for profiled receiver if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); } let replacement = self.try_inline_send_direct(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: send_block }); @@ -3909,8 +3876,7 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); } let replacement = self.try_inline_send_direct(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: None }); @@ -3932,8 +3898,7 @@ impl Function { let id = unsafe { get_cme_def_body_attr_id(cme) }; if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); let replacement = self.try_emit_optimized_getivar(block, recv, id, profiled_type, state).unwrap_or_else(|counter| { self.count(block, counter); @@ -3959,12 +3924,11 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); let id = unsafe { get_cme_def_body_attr_id(cme) }; if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; // TODO: attr_writer SetIvar has a null inline cache and may target a receiver // operand other than CFP self. Support it with a reprofile strategy that // profiles the receiver operand even after the send insn has finished profiling. let recompile = None; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); self.try_emit_optimized_setivar(block, recv, id, val, profiled_type, state, recompile).unwrap_or_else(|counter| { self.count(block, counter); self.push_insn(block, Insn::SetIvar { self_val: recv, id, ic: std::ptr::null(), val, state }); @@ -3990,8 +3954,7 @@ impl Function { } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); } let kw_splat = flags & VM_CALL_KW_SPLAT != 0; let invoke_proc = self.push_insn(block, Insn::InvokeProc { recv, args: args.clone(), state, kw_splat }); @@ -4029,8 +3992,7 @@ impl Function { } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - let argc = unsafe { vm_ci_argc(ci) } as i32; - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); } // All structs from the same Struct class should have the same // length. So if our recv is embedded all runtime @@ -4902,7 +4864,7 @@ impl Function { } let self_val = self.guard_heap(block, self_val, state); let shape = self.load_shape(block, self_val); - self.guard_shape(block, shape, profiled_type.shape(), state, Some(Recompile::ProfileSelf)); + self.guard_shape(block, shape, profiled_type.shape(), state, Some(Recompile)); Ok(self.load_ivar(block, self_val, profiled_type, id)) } @@ -5154,8 +5116,7 @@ impl Function { if let Some(profiled_type) = profiled_type { // Guard receiver class - let argc = unsafe { vm_ci_argc(call_info) } as i32; - recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); + recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); fun.insn_types[recv.0] = fun.infer_type(recv); } @@ -5221,8 +5182,7 @@ impl Function { if let Some(profiled_type) = profiled_type { // Guard receiver class - let argc = unsafe { vm_ci_argc(call_info) } as i32; - recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); + recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile) }); fun.insn_types[recv.0] = fun.infer_type(recv); } @@ -5340,9 +5300,8 @@ impl Function { assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { cd, state, reason: SendFallbackReason::SendWithoutBlockNoProfiles | SendFallbackReason::SendNoProfiles, .. } => { - let argc = unsafe { vm_ci_argc((*cd).ci) } as i32; - self.push_insn(block, Insn::SideExit { state, reason: SideExitReason::NoProfileSend, recompile: Some(Recompile::ProfileSend { argc }) }); + Insn::Send { state, reason: SendFallbackReason::SendWithoutBlockNoProfiles | SendFallbackReason::SendNoProfiles, .. } => { + self.push_insn(block, Insn::SideExit { state, reason: SideExitReason::NoProfileSend, recompile: Some(Recompile) }); // SideExit is a terminator; don't add remaining instructions break; } @@ -7814,7 +7773,7 @@ fn add_iseq_to_hir( .and_then(can_optimize) { self_param = fun.guard_heap(block, self_param, exit_id); let shape = fun.load_shape(block, self_param); - fun.guard_shape(block, shape, profiled_shape, exit_id, Some(Recompile::ProfileSelf)); + fun.guard_shape(block, shape, profiled_shape, exit_id, Some(Recompile)); let mut ivar_index: attr_index_t = 0; let result = if unsafe { rb_shape_get_iv_index(profiled_shape.0, id, &mut ivar_index) } { fun.push_insn(block, Insn::Const { val: Const::Value(pushval) }) @@ -8232,7 +8191,7 @@ fn add_iseq_to_hir( // So to check for either of those cases we can use: val & 0x1 == 0x1 // Bail out if the block handler is neither ISEQ nor ifunc - fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyFallbackMiss, state: exit_id, recompile: Some(Recompile::ProfileBlockHandler) }); + fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyFallbackMiss, state: exit_id, recompile: Some(Recompile) }); // TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing let proxy_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) }); let mut args = vec![proxy_val]; @@ -8245,7 +8204,7 @@ fn add_iseq_to_hir( [profiled_handler] => match profiled_handler { ProfiledBlockHandlerFamily::Nil => { let block_handler = fun.load_ep_env_field(unmodified_block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); - fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id, recompile: Some(Recompile::ProfileBlockHandler) }); + fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id, recompile: Some(Recompile) }); let nil_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(Qnil) }); let mut args = vec![nil_val]; if let Some(local) = original_local { @@ -8262,7 +8221,7 @@ fn add_iseq_to_hir( // So to check for either of those cases we can use: val & 0x1 == 0x1 // Bail out if the block handler is neither ISEQ nor ifunc - fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id, recompile: Some(Recompile::ProfileBlockHandler) }); + fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id, recompile: Some(Recompile) }); // TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing let proxy_val = fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) }); let mut args = vec![proxy_val]; @@ -8282,7 +8241,7 @@ fn add_iseq_to_hir( return_type: types::BasicObject, elidable: true, }); - fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: is_proc, expected: Const::Value(Qtrue), reason: SideExitReason::BlockParamProxyNotProc, state: exit_id, recompile: Some(Recompile::ProfileBlockHandler) }); + fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: is_proc, expected: Const::Value(Qtrue), reason: SideExitReason::BlockParamProxyNotProc, state: exit_id, recompile: Some(Recompile) }); let mut args = vec![proc_val]; if let Some(local) = original_local { args.push(local); @@ -9033,7 +8992,7 @@ fn add_iseq_to_hir( let val = state.stack_pop()?; if let Some(profiled_type) = fun.monomorphic_summary(&profiles, self_param, exit_id) { // TODO(max): Assert ic is never null - let recompile = if ic.is_null() { None } else { Some(Recompile::ProfileSelf) }; + let recompile = (!ic.is_null()).then_some(Recompile); fun.try_emit_optimized_setivar(block, self_param, id, val, profiled_type, exit_id, recompile).unwrap_or_else(|counter| { fun.count(block, counter); fun.push_insn(block, Insn::SetIvar { self_val: self_param, id, ic, val, state: exit_id }); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 88355ed70143ef..9eb83b3ea9b6c7 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -16890,6 +16890,74 @@ mod hir_opt_tests { "); } + #[test] + fn test_recompile_no_profile_send_with_blockarg() { + // Test that no-profile send recompilation profiles explicit blockargs. + // The call remains a Send fallback because &block is still complex, but + // it should no longer be a NoProfileSend side exit after recompilation. + eval(" + def passthrough_recompile_blockarg(x, &block) + block.call(x) + end + + def test(flag, block) + if flag + passthrough_recompile_blockarg(42, &block) + else + 'hello' + end + end + "); + + // With call_threshold=2, num_profiles=1, the send is not profiled + // during initial profiling because flag=false skips that branch. + eval(" + block = proc { |x| x } + test(false, block) + test(false, block) + "); + + // This hits the NoProfileSend side exit, profiles the send including + // its explicit blockarg, and invalidates the ISEQ for recompilation. + eval(" + block = proc { |x| x } + test(true, block) + "); + + assert_snapshot!(hir_string("test"), @r" + fn test@:7: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :flag@0x1000 + v4:BasicObject = LoadField v2, :block@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :flag@1 + v9:BasicObject = LoadArg :block@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + CheckInterrupts + v19:CBool = Test v12 + v20:Falsy = RefineType v12, Falsy + CondBranch v19, bb5(), bb4(v11, v20, v13) + bb5(): + v22:Truthy = RefineType v12, Truthy + v26:Fixnum[42] = Const Value(42) + v29:BasicObject = Send v11, &block, :passthrough_recompile_blockarg, v26, v13 # SendFallbackReason: Complex argument passing + CheckInterrupts + Return v29 + bb4(v34:BasicObject, v35:Falsy, v36:BasicObject): + v40:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v41:StringExact = StringCopy v40 + CheckInterrupts + Return v41 + "); + } + #[test] fn test_no_profile_send_on_final_version() { // On the final ISEQ version (MAX_ISEQ_VERSIONS reached), no-profile sends should diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index fd830c35a308f7..22e6835a6ac071 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -59,9 +59,11 @@ pub extern "C" fn rb_zjit_profile_insn(bare_opcode: u32, ec: EcPtr) { } /// Profile a YARV instruction -fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { - let profiler = &mut Profiler::new(ec); - let profile = &mut get_or_create_iseq_payload(profiler.iseq).profile; +fn profile_insn_sample( + bare_opcode: ruby_vminsn_type, + profiler: &mut Profiler, + profile: &mut IseqProfile, +) -> bool { match bare_opcode { YARVINSN_opt_nil_p => profile_operands(profiler, profile, 1), YARVINSN_opt_plus => profile_operands(profiler, profile, 2), @@ -100,9 +102,18 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { profile_operands(profiler, profile, argc + 1); } YARVINSN_splatkw => profile_operands(profiler, profile, 2), - _ => {} + _ => return false, } + true +} + +/// Profile a YARV instruction +fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { + let profiler = &mut Profiler::new(ec); + let profile = &mut get_or_create_iseq_payload(profiler.iseq).profile; + let _ = profile_insn_sample(bare_opcode, profiler, profile); + // Once we profile the instruction enough times, we stop profiling it. let entry = profile.entry_mut(profiler.insn_idx); entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); @@ -111,6 +122,38 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { } } +/// Profile the instruction at the current CFP for a recompile side exit. +pub fn profile_recompile_insn(ec: EcPtr) -> bool { + let profiler = &mut Profiler::new(ec); + let pc = unsafe { get_cfp_pc(profiler.cfp) }; + let bare_opcode = unsafe { + rb_zjit_insn_to_bare_insn(rb_iseq_opcode_at_pc(profiler.iseq, pc)) + } as ruby_vminsn_type; + let profile = &mut get_or_create_iseq_payload(profiler.iseq).profile; + + let is_send = matches!(bare_opcode, YARVINSN_send | YARVINSN_opt_send_without_block); + // For now, send recompile exits only fill in missing profiles. Once the send site + // has finished profiling, don't recompile it on later exits. + if is_send && profile.done_profiling_at(profiler.insn_idx) { + return false; + } + // For now, non-send recompile exits reset the profiling counter before requesting recompilation + // so that we can collect enough samples. + if !is_send && profile.done_profiling_at(profiler.insn_idx) { + profile.entry_mut(profiler.insn_idx) + .set_profiles_remaining(get_option!(num_profiles)); + } + + // If this opcode can't be sampled here, this exit has no profile data to collect. + if !profile_insn_sample(bare_opcode, profiler, profile) { + return false; + } + + let entry = profile.entry_mut(profiler.insn_idx); + entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); + entry.profiles_remaining == 0 +} + /// Return the argc as stated in the calldata plus: /// * 1 if there is an explicit blockarg, since that will be passed on the stack pub fn num_arguments_on_stack(cd: *const rb_call_data) -> usize { @@ -426,70 +469,6 @@ impl IseqProfile { self.entry(insn_idx).map_or(false, |e| e.profiles_remaining == 0) } - /// Profile send operands from the stack at runtime. - /// `sp` is the current stack pointer (after the args and receiver). - /// `argc` is the number of arguments (not counting receiver). - /// Returns true if enough profiles have been gathered and the ISEQ should be recompiled. - pub fn profile_send_at(&mut self, iseq: IseqPtr, insn_idx: YarvInsnIdx, sp: *const VALUE, argc: usize) -> bool { - let n = argc + 1; // args + receiver - let entry = self.entry_mut(insn_idx); - if entry.opnd_types.is_empty() { - entry.opnd_types.resize(n, TypeDistribution::new()); - } - for i in 0..n { - let obj = unsafe { *sp.offset(i as isize - n as isize) }; - let ty = ProfiledType::new(obj); - VALUE::from(iseq).write_barrier(ty.class()); - entry.opnd_types[i].observe(ty); - } - entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); - entry.profiles_remaining == 0 - } - - /// Profile self for a shape guard exit at runtime. - /// This may be called on an instruction that was already profiled by YARV, - /// so we reset the counter to re-profile with the new shapes seen at runtime. - /// Returns true if enough profiles have been gathered and the ISEQ should be recompiled. - pub fn profile_self_at(&mut self, iseq: IseqPtr, insn_idx: YarvInsnIdx, self_val: VALUE) -> bool { - let entry = self.entry_mut(insn_idx); - // Reset profiling if the previous round already finished (stale YARV profiles). - // This ensures we collect num_profiles samples of the new shapes before recompiling. - if entry.profiles_remaining == 0 { - entry.profiles_remaining = get_option!(num_profiles); - } - if entry.opnd_types.is_empty() { - entry.opnd_types.resize(1, TypeDistribution::new()); - } - let ty = ProfiledType::new(self_val); - VALUE::from(iseq).write_barrier(ty.class()); - entry.opnd_types[0].observe(ty); - entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); - entry.profiles_remaining == 0 - } - - /// Profile the block handler for a getblockparamproxy guard exit at runtime. - pub fn profile_getblockparamproxy_at(&mut self, iseq: IseqPtr, insn_idx: YarvInsnIdx, cfp: CfpPtr) -> bool { - let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx as u32) }; - let level = unsafe { pc.add(2).read() }.as_u32(); - - let entry = self.entry_mut(insn_idx); - if entry.profiles_remaining == 0 { - entry.profiles_remaining = get_option!(num_profiles); - } - if entry.opnd_types.is_empty() { - entry.opnd_types.resize(1, TypeDistribution::new()); - } - let ep = unsafe { get_cfp_ep_level(cfp, level) }; - let block_handler = unsafe { *ep.offset(VM_ENV_DATA_INDEX_SPECVAL as isize) }; - let untagged = unsafe { rb_vm_untag_block_handler(block_handler) }; - - let ty = ProfiledType::object(untagged); - VALUE::from(iseq).write_barrier(ty.class()); - entry.opnd_types[0].observe(ty); - entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); - entry.profiles_remaining == 0 - } - /// Get profiled operand types for a given instruction index pub fn get_operand_types(&self, insn_idx: YarvInsnIdx) -> Option<&[TypeDistribution]> { self.entry(insn_idx).map(|e| e.opnd_types.as_slice()).filter(|s| !s.is_empty()) From ee74bca0e5ba36657024d676f0adc2466a9e1dbe Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 26 Jun 2026 22:03:46 +0900 Subject: [PATCH 12/15] Make ROBJECT_FIELDS private Commit 0ea210d renamed ROBJECT_IVPTR to ROBJECT_FIELDS, so it would have caused compatibility issues anyways. Since there has been no issues, it means that nobody is using it. We can make this dangerous API private since no C extension should be using it anyways. --- include/ruby/internal/core/robject.h | 28 ---------------------------- internal/object.h | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/include/ruby/internal/core/robject.h b/include/ruby/internal/core/robject.h index df1901eb1e98a5..b8743dcb739116 100644 --- a/include/ruby/internal/core/robject.h +++ b/include/ruby/internal/core/robject.h @@ -116,32 +116,4 @@ struct RObject { } as; }; -RBIMPL_ATTR_PURE_UNLESS_DEBUG() -RBIMPL_ATTR_ARTIFICIAL() -/** - * Queries the instance variables. - * - * @param[in] obj Object in question. - * @return Its instance variables, in C array. - * @pre `obj` must be an instance of ::RObject. - * - * @internal - * - * @shyouhei finds no reason for this to be visible from extension libraries. - */ -static inline VALUE * -ROBJECT_FIELDS(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - - struct RObject *const ptr = ROBJECT(obj); - - if (RB_UNLIKELY(RB_FL_ANY_RAW(obj, ROBJECT_HEAP))) { - return ptr->as.heap.fields; - } - else { - return ptr->as.ary; - } -} - #endif /* RBIMPL_ROBJECT_H */ diff --git a/internal/object.h b/internal/object.h index 99aa1f524bab5d..837dd7a457f6fd 100644 --- a/internal/object.h +++ b/internal/object.h @@ -61,6 +61,21 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) RB_OBJ_WRITTEN(obj, oldv, klass); } +static inline VALUE * +ROBJECT_FIELDS(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + + struct RObject *const ptr = ROBJECT(obj); + + if (RB_UNLIKELY(RB_FL_ANY_RAW(obj, ROBJECT_HEAP))) { + return ptr->as.heap.fields; + } + else { + return ptr->as.ary; + } +} + static inline size_t rb_obj_embedded_size(uint32_t fields_count) { From 010de50c5cb0324cd7b826f99bdb113b9c6e89a4 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 26 Jun 2026 16:34:32 -0700 Subject: [PATCH 13/15] ZJIT: Add perf symbol for block padding (#17517) * ZJIT: Add perf symbol for block padding * ZJIT: Explain HIR-only padding perf symbols --- zjit/src/backend/lir.rs | 48 ++++++++++++++++++++++++++++++++++++++--- zjit/src/codegen.rs | 2 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 67f43c1517da29..675c1c004b8ea0 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1,9 +1,10 @@ +use std::cell::RefCell; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt; use std::mem::take; use std::rc::Rc; use crate::bitset::BitSet; -use crate::codegen::{local_size_and_idx_to_ep_offset, perf_symbol_range_start, perf_symbol_range_end}; +use crate::codegen::{local_size_and_idx_to_ep_offset, perf_symbol_range_start, perf_symbol_range_end, register_with_perf}; use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, VALUE, ZJIT_STACK_MAP_SHIFT, ZJIT_STACK_MAP_VREG_TAG, vm_stack_canary, YarvInsnIdx, zjit_jit_frame}; use crate::hir::{Invariant, SideExitReason}; use crate::hir; @@ -1693,6 +1694,43 @@ impl Assembler } pub fn linearize_instructions(&self) -> Vec { + // Wrap instructions emitted by `push_insns` with PosMarkers and record + // the emitted byte range under `symbol_name` in the perf map. + fn push_insns_with_perf_symbol( + insns: &mut Vec, + symbol_name: &str, + push_insns: impl FnOnce(&mut Vec), + ) { + // ISEQ perf symbols cover the whole compiled ISEQ, including this + // padding. HIR perf needs a separate symbol because the padding + // doesn't belong to any HIR instruction. + if get_option!(perf) != Some(PerfMap::HIR) { + push_insns(insns); + return; + } + + let symbol_name = symbol_name.to_string(); + let start = Rc::new(RefCell::new(None)); + let current = start.clone(); + insns.push(Insn::PosMarker(Rc::new(move |code_ptr, _| { + let mut current = current.borrow_mut(); + assert!(current.is_none(), "perf symbol range already open"); + *current = Some(code_ptr); + }))); + + push_insns(insns); + + insns.push(Insn::PosMarker(Rc::new(move |end, cb| { + if let Some(start) = start.borrow_mut().take() { + let start_addr = start.raw_addr(cb); + let end_addr = end.raw_addr(cb); + if start_addr < end_addr { + register_with_perf(symbol_name.clone(), start_addr, end_addr - start_addr); + } + } + }))); + } + // Emit instructions with labels, expanding branch parameters let mut insns = Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY); @@ -1704,7 +1742,9 @@ impl Assembler // Entry blocks shouldn't ever be preceded by something that can // stomp on this block. if !block.is_entry { - insns.push(Insn::PadPatchPoint); + push_insns_with_perf_symbol(&mut insns, "PadPatchPoint", |insns| { + insns.push(Insn::PadPatchPoint); + }); } // Process each instruction, expanding branch params if needed @@ -1726,7 +1766,9 @@ impl Assembler // Make sure we don't stomp on the next function if block_id.0 == num_blocks - 1 { - insns.push(Insn::PadPatchPoint); + push_insns_with_perf_symbol(&mut insns, "PadPatchPoint", |insns| { + insns.push(Insn::PadPatchPoint); + }); } } insns diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 92a18e59f38cfe..94adf122b43e14 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -276,7 +276,7 @@ pub fn gen_iseq_call(cb: &mut CodeBlock, iseq_call: &IseqCallRef) -> Result<(), } /// Write an entry to the perf map in /tmp -fn register_with_perf(symbol_name: String, start_ptr: usize, code_size: usize) { +pub(crate) fn register_with_perf(symbol_name: String, start_ptr: usize, code_size: usize) { use std::io::Write; let perf_map = format!("/tmp/perf-{}.map", std::process::id()); let Ok(file) = std::fs::OpenOptions::new().create(true).append(true).open(&perf_map) else { From 7a4492a07ccaec98e5b5338fbbebb8b5da580789 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 26 Jun 2026 18:55:47 -0500 Subject: [PATCH 14/15] [DOC] Update Set#== documentation --- set.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index 09eeee112237e6..d0741dfbc23e94 100644 --- a/set.c +++ b/set.c @@ -1887,9 +1887,16 @@ set_recursive_eql(VALUE set, VALUE dt, int recur) /* * call-seq: - * set == other -> true or false + * self == object -> true or false * - * Returns true if two sets are equal. + * Returns whether +object+ is a set, and has the same elements as +self+: + * + * set = Set[0, 1, 2] + * set == Set[1, 2, 0] # => true + * set == [1, 2, 3] # => false + * set == Set[1, 2, '3'] # => false + * + * Related: see {Methods for Comparing}[rdoc-ref:Set@Methods+for+Comparing]. */ static VALUE set_i_eq(VALUE set, VALUE other) From 33ebed7094239f1ab524b5b80d20c88bc1d957c3 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 26 Jun 2026 19:29:17 -0500 Subject: [PATCH 15/15] [DOC] Update Set#| documentation --- set.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index d0741dfbc23e94..b6ca30331332ff 100644 --- a/set.c +++ b/set.c @@ -1343,8 +1343,10 @@ set_i_xor(VALUE set, VALUE other) * call-seq: * self | enumerable -> new_set * - * Returns a new \Set object containing the elements of both +self+ - * and the given +enumerable+. + * Returns a new \Set object containing + * the {union}[https://en.wikipedia.org/wiki/Union_(set_theory)] + * of +self+ and the given +enumerable+; + * that is, containing the elements of both +self+ and +enumerable+. * * set = Set[0, 1, 2] * set | Set[2, 1, 'a'] # => Set[0, 1, 2, "a"]