From 52312d53ca6da5eb61e3a1efa534eb221f5772d7 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 5 Aug 2025 14:58:00 -0500 Subject: [PATCH 1/7] [DOC] Tweaks for GC.start (#14093) --- gc.rb | 58 +++++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/gc.rb b/gc.rb index d3bb8db036b1cb..1298e3005660b6 100644 --- a/gc.rb +++ b/gc.rb @@ -7,31 +7,39 @@ # You may obtain information about the operation of the \GC through GC::Profiler. module GC - # Initiates garbage collection, even if manually disabled. - # - # The +full_mark+ keyword argument determines whether or not to perform a - # major garbage collection cycle. When set to +true+, a major garbage - # collection cycle is run, meaning all objects are marked. When set to - # +false+, a minor garbage collection cycle is run, meaning only young - # objects are marked. - # - # The +immediate_mark+ keyword argument determines whether or not to perform - # incremental marking. When set to +true+, marking is completed during the - # call to this method. When set to +false+, marking is performed in steps - # that are interleaved with future Ruby code execution, so marking might not - # be completed during this method call. Note that if +full_mark+ is +false+, - # then marking will always be immediate, regardless of the value of - # +immediate_mark+. - # - # The +immediate_sweep+ keyword argument determines whether or not to defer - # sweeping (using lazy sweep). When set to +false+, sweeping is performed in - # steps that are interleaved with future Ruby code execution, so sweeping might - # not be completed during this method call. When set to +true+, sweeping is - # completed during the call to this method. - # - # Note: These keyword arguments are implementation and version-dependent. They - # are not guaranteed to be future-compatible and may be ignored if the - # underlying implementation does not support them. + # Initiates garbage collection, even if explicitly disabled by GC.disable. + # + # Keyword arguments: + # + # - +full_mark+: + # its boolean value determines whether to perform a major garbage collection cycle: + # + # - +true+: initiates a major garbage collection cycle, + # meaning all objects (old and new) are marked. + # - +false+: initiates a minor garbage collection cycle, + # meaning only young objects are marked. + # + # - +immediate_mark+: + # its boolean value determines whether to perform incremental marking: + # + # - +true+: marking is completed before the method returns. + # - +false+: marking is performed by parts, + # interleaved with program execution both before the method returns and afterward; + # therefore marking may not be completed before the return. + # Note that if +full_mark+ is +false+, marking will always be immediate, + # regardless of the value of +immediate_mark+. + # + # - +immediate_sweep+: + # its boolean value determines whether to defer sweeping (using lazy sweep): + # + # - +true+: sweeping is completed before the method returns. + # - +false+: sweeping is performed by parts, + # interleaved with program execution both before the method returns and afterward; + # therefore sweeping may not be completed before the return. + # + # Note that these keword arguments are implementation- and version-dependent, + # are not guaranteed to be future-compatible, + # and may be ignored in some implementations. def self.start full_mark: true, immediate_mark: true, immediate_sweep: true Primitive.gc_start_internal full_mark, immediate_mark, immediate_sweep, false end From ef95e5ba3de65d42fe0e1d41519dcf05db11a4e8 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 5 Aug 2025 13:56:04 -0700 Subject: [PATCH 2/7] ZJIT: Profile type+shape distributions (#13901) ZJIT uses the interpreter to take type profiles of what objects pass through the code. It stores a compressed record of the history per opcode for the opcodes we select. Before this change, we re-used the HIR Type data-structure, a shallow type lattice, to store historical type information. This was quick for bringup but is quite lossy as profiles go: we get one bit per built-in type seen, and if we see a non-built-in type in addition, we end up with BasicObject. Not very helpful. Additionally, it does not give us any notion of cardinality: how many of each type did we see? This change brings with it a much more interesting slice of type history: a histogram. A Distribution holds a record of the top-N (where N is fixed at Ruby compile-time) `(Class, ShapeId)` pairs and their counts. It also holds an *other* count in case we see more than N pairs. Using this distribution, we can make more informed decisions about when we should use type information. We can determine if we are strictly monomorphic, very nearly monomorphic, or something else. Maybe the call-site is polymorphic, so we should have a polymorphic inline cache. Exciting stuff. I also plumb this new distribution into the HIR part of the compilation pipeline. --- zjit.c | 11 ++ zjit/bindgen/src/main.rs | 3 + zjit/src/cruby.rs | 10 +- zjit/src/cruby_bindings.inc.rs | 4 + zjit/src/distribution.rs | 266 +++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 114 ++++++++------ zjit/src/hir_type/mod.rs | 31 +++- zjit/src/lib.rs | 1 + zjit/src/profile.rs | 114 +++++++++++--- 9 files changed, 482 insertions(+), 72 deletions(-) create mode 100644 zjit/src/distribution.rs diff --git a/zjit.c b/zjit.c index abe74225404c98..09ab128ae3f6f8 100644 --- a/zjit.c +++ b/zjit.c @@ -346,6 +346,17 @@ rb_zjit_shape_obj_too_complex_p(VALUE obj) return rb_shape_obj_too_complex_p(obj); } +enum { + RB_SPECIAL_CONST_SHAPE_ID = SPECIAL_CONST_SHAPE_ID, + RB_INVALID_SHAPE_ID = INVALID_SHAPE_ID, +}; + +bool +rb_zjit_singleton_class_p(VALUE klass) +{ + return RCLASS_SINGLETON_P(klass); +} + // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_stats(rb_execution_context_t *ec, VALUE self); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index f67d8e91d31a62..77299c26574675 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -351,8 +351,11 @@ fn main() { .allowlist_function("rb_optimized_call") .allowlist_function("rb_zjit_icache_invalidate") .allowlist_function("rb_zjit_print_exception") + .allowlist_function("rb_zjit_singleton_class_p") .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") + .allowlist_var("RB_SPECIAL_CONST_SHAPE_ID") + .allowlist_var("RB_INVALID_SHAPE_ID") // From jit.c .allowlist_function("rb_assert_holding_vm_lock") diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index afa3ddfb4989c8..095a2988f81c83 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -265,6 +265,12 @@ pub struct ID(pub ::std::os::raw::c_ulong); /// Pointer to an ISEQ pub type IseqPtr = *const rb_iseq_t; +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ShapeId(pub u32); + +pub const SPECIAL_CONST_SHAPE_ID: ShapeId = ShapeId(RB_SPECIAL_CONST_SHAPE_ID); +pub const INVALID_SHAPE_ID: ShapeId = ShapeId(RB_INVALID_SHAPE_ID); + // Given an ISEQ pointer, convert PC to insn_idx pub fn iseq_pc_to_insn_idx(iseq: IseqPtr, pc: *mut VALUE) -> Option { let pc_zero = unsafe { rb_iseq_pc_at_idx(iseq, 0) }; @@ -487,8 +493,8 @@ impl VALUE { unsafe { rb_zjit_shape_obj_too_complex_p(self) } } - pub fn shape_id_of(self) -> u32 { - unsafe { rb_obj_shape_id(self) } + pub fn shape_id_of(self) -> ShapeId { + ShapeId(unsafe { rb_obj_shape_id(self) }) } pub fn embedded_p(self) -> bool { diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 7fe1a0406ad9d8..5c939fabe7f0d6 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -719,6 +719,9 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; +pub const RB_SPECIAL_CONST_SHAPE_ID: _bindgen_ty_38 = 33554432; +pub const RB_INVALID_SHAPE_ID: _bindgen_ty_38 = 4294967295; +pub type _bindgen_ty_38 = u32; pub type rb_iseq_param_keyword_struct = rb_iseq_constant_body__bindgen_ty_1_rb_iseq_param_keyword; unsafe extern "C" { pub fn ruby_xfree(ptr: *mut ::std::os::raw::c_void); @@ -938,6 +941,7 @@ unsafe extern "C" { pub fn rb_iseq_set_zjit_payload(iseq: *const rb_iseq_t, payload: *mut ::std::os::raw::c_void); pub fn rb_zjit_print_exception(); pub fn rb_zjit_shape_obj_too_complex_p(obj: VALUE) -> bool; + pub fn rb_zjit_singleton_class_p(klass: VALUE) -> bool; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; diff --git a/zjit/src/distribution.rs b/zjit/src/distribution.rs new file mode 100644 index 00000000000000..5927ffa5c944ba --- /dev/null +++ b/zjit/src/distribution.rs @@ -0,0 +1,266 @@ +/// This implementation was inspired by the type feedback module from Google's S6, which was +/// written in C++ for use with Python. This is a new implementation in Rust created for use with +/// Ruby instead of Python. +#[derive(Debug, Clone)] +pub struct Distribution { + /// buckets and counts have the same length + /// buckets[0] is always the most common item + buckets: [T; N], + counts: [usize; N], + /// if there is no more room, increment the fallback + other: usize, + // TODO(max): Add count disparity, which can help determine when to reset the distribution +} + +impl Distribution { + pub fn new() -> Self { + Self { buckets: [Default::default(); N], counts: [0; N], other: 0 } + } + + pub fn observe(&mut self, item: T) { + for (bucket, count) in self.buckets.iter_mut().zip(self.counts.iter_mut()) { + if *bucket == item || *count == 0 { + *bucket = item; + *count += 1; + // Keep the most frequent item at the front + self.bubble_up(); + return; + } + } + self.other += 1; + } + + /// Keep the highest counted bucket at index 0 + fn bubble_up(&mut self) { + if N == 0 { return; } + let max_index = self.counts.into_iter().enumerate().max_by_key(|(_, val)| *val).unwrap().0; + if max_index != 0 { + self.counts.swap(0, max_index); + self.buckets.swap(0, max_index); + } + } + + pub fn each_item(&self) -> impl Iterator + '_ { + self.buckets.iter().zip(self.counts.iter()) + .filter_map(|(&bucket, &count)| if count > 0 { Some(bucket) } else { None }) + } + + pub fn each_item_mut(&mut self) -> impl Iterator + '_ { + self.buckets.iter_mut().zip(self.counts.iter()) + .filter_map(|(bucket, &count)| if count > 0 { Some(bucket) } else { None }) + } +} + +#[derive(PartialEq, Debug, Clone, Copy)] +enum DistributionKind { + /// No types seen + Empty, + /// One type seen + Monomorphic, + /// Between 2 and (fixed) N types seen + Polymorphic, + /// Polymorphic, but with a significant skew towards one type + SkewedPolymorphic, + /// More than N types seen with no clear winner + Megamorphic, + /// Megamorphic, but with a significant skew towards one type + SkewedMegamorphic, +} + +#[derive(Debug)] +pub struct DistributionSummary { + kind: DistributionKind, + buckets: [T; N], + // TODO(max): Determine if we need some notion of stability +} + +const SKEW_THRESHOLD: f64 = 0.75; + +impl DistributionSummary { + pub fn new(dist: &Distribution) -> Self { + #[cfg(debug_assertions)] + { + let first_count = dist.counts[0]; + for &count in &dist.counts[1..] { + assert!(first_count >= count, "First count should be the largest"); + } + } + let num_seen = dist.counts.iter().sum::() + dist.other; + let kind = if dist.other == 0 { + // Seen <= N types total + if dist.counts[0] == 0 { + DistributionKind::Empty + } else if dist.counts[1] == 0 { + DistributionKind::Monomorphic + } else if (dist.counts[0] as f64)/(num_seen as f64) >= SKEW_THRESHOLD { + DistributionKind::SkewedPolymorphic + } else { + DistributionKind::Polymorphic + } + } else { + // Seen > N types total; considered megamorphic + if (dist.counts[0] as f64)/(num_seen as f64) >= SKEW_THRESHOLD { + DistributionKind::SkewedMegamorphic + } else { + DistributionKind::Megamorphic + } + }; + Self { kind, buckets: dist.buckets.clone() } + } + + pub fn is_monomorphic(&self) -> bool { + self.kind == DistributionKind::Monomorphic + } + + pub fn is_skewed_polymorphic(&self) -> bool { + self.kind == DistributionKind::SkewedPolymorphic + } + + pub fn is_skewed_megamorphic(&self) -> bool { + self.kind == DistributionKind::SkewedMegamorphic + } + + pub fn bucket(&self, idx: usize) -> T { + assert!(idx < N, "index {idx} out of bounds for buckets[{N}]"); + self.buckets[idx] + } +} + +#[cfg(test)] +mod distribution_tests { + use super::*; + + #[test] + fn start_empty() { + let dist = Distribution::::new(); + assert_eq!(dist.other, 0); + assert!(dist.counts.iter().all(|&b| b == 0)); + } + + #[test] + fn observe_adds_record() { + let mut dist = Distribution::::new(); + dist.observe(10); + assert_eq!(dist.buckets[0], 10); + assert_eq!(dist.counts[0], 1); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_increments_record() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + assert_eq!(dist.buckets[0], 10); + assert_eq!(dist.counts[0], 2); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_two() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(11); + assert_eq!(dist.buckets[0], 11); + assert_eq!(dist.counts[0], 3); + assert_eq!(dist.buckets[1], 10); + assert_eq!(dist.counts[1], 2); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_with_max_increments_other() { + let mut dist = Distribution::::new(); + dist.observe(10); + assert!(dist.buckets.is_empty()); + assert!(dist.counts.is_empty()); + assert_eq!(dist.other, 1); + } + + #[test] + fn empty_distribution_returns_empty_summary() { + let dist = Distribution::::new(); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Empty); + } + + #[test] + fn monomorphic_distribution_returns_monomorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Monomorphic); + assert_eq!(summary.buckets[0], 10); + } + + #[test] + fn polymorphic_distribution_returns_polymorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Polymorphic); + assert_eq!(summary.buckets[0], 11); + assert_eq!(summary.buckets[1], 10); + } + + #[test] + fn skewed_polymorphic_distribution_returns_skewed_polymorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::SkewedPolymorphic); + assert_eq!(summary.buckets[0], 11); + assert_eq!(summary.buckets[1], 10); + } + + #[test] + fn megamorphic_distribution_returns_megamorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(12); + dist.observe(13); + dist.observe(14); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Megamorphic); + assert_eq!(summary.buckets[0], 11); + } + + #[test] + fn skewed_megamorphic_distribution_returns_skewed_megamorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(13); + dist.observe(14); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::SkewedMegamorphic); + assert_eq!(summary.buckets[0], 12); + } +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 976580c85b25e1..203be0661e3f05 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -11,6 +11,7 @@ use std::{ }; use crate::hir_type::{Type, types}; use crate::bitset::BitSet; +use crate::profile::{TypeDistributionSummary, ProfiledType}; /// An index of an [`Insn`] in a [`Function`]. This is a popular /// type since this effectively acts as a pointer to an [`Insn`]. @@ -1357,19 +1358,23 @@ impl Function { /// Return the interpreter-profiled type of the HIR instruction at the given ISEQ instruction /// index, if it is known. This historical type record is not a guarantee and must be checked /// with a GuardType or similar. - fn profiled_type_of_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option { + fn profiled_type_of_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option { let Some(ref profiles) = self.profiles else { return None }; let Some(entries) = profiles.types.get(&iseq_insn_idx) else { return None }; - for &(entry_insn, entry_type) in entries { - if self.union_find.borrow().find_const(entry_insn) == self.union_find.borrow().find_const(insn) { - return Some(entry_type); + for (entry_insn, entry_type_summary) in entries { + if self.union_find.borrow().find_const(*entry_insn) == self.union_find.borrow().find_const(insn) { + if entry_type_summary.is_monomorphic() || entry_type_summary.is_skewed_polymorphic() { + return Some(entry_type_summary.bucket(0)); + } else { + return None; + } } } None } - fn likely_is_fixnum(&self, val: InsnId, profiled_type: Type) -> bool { - return self.is_a(val, types::Fixnum) || profiled_type.is_subtype(types::Fixnum); + fn likely_is_fixnum(&self, val: InsnId, profiled_type: ProfiledType) -> bool { + return self.is_a(val, types::Fixnum) || profiled_type.is_fixnum(); } fn coerce_to_fixnum(&mut self, block: BlockId, val: InsnId, state: InsnId) -> InsnId { @@ -1380,8 +1385,8 @@ impl Function { fn arguments_likely_fixnums(&mut self, left: InsnId, right: InsnId, state: InsnId) -> bool { let frame_state = self.frame_state(state); let iseq_insn_idx = frame_state.insn_idx as usize; - let left_profiled_type = self.profiled_type_of_at(left, iseq_insn_idx).unwrap_or(types::BasicObject); - let right_profiled_type = self.profiled_type_of_at(right, iseq_insn_idx).unwrap_or(types::BasicObject); + let left_profiled_type = self.profiled_type_of_at(left, iseq_insn_idx).unwrap_or(ProfiledType::empty()); + let right_profiled_type = self.profiled_type_of_at(right, iseq_insn_idx).unwrap_or(ProfiledType::empty()); self.likely_is_fixnum(left, left_profiled_type) && self.likely_is_fixnum(right, right_profiled_type) } @@ -1510,15 +1515,16 @@ impl Function { self.try_rewrite_aref(block, insn_id, self_val, args[0], state), Insn::SendWithoutBlock { mut self_val, 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() { + let (klass, profiled_type) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() { // If we know the class statically, use it to fold the lookup at compile-time. (klass, None) } else { - // If we know that self is top-self from profile information, guard and use it to fold the lookup at compile-time. - match self.profiled_type_of_at(self_val, frame_state.insn_idx) { - Some(self_type) if self_type.is_top_self() => (self_type.exact_ruby_class().unwrap(), self_type.ruby_object()), - _ => { self.push_insn_id(block, insn_id); continue; } - } + // If we know that self is reasonably monomorphic from profile information, guard and use it to fold the lookup at compile-time. + // TODO(max): Figure out how to handle top self? + let Some(recv_type) = self.profiled_type_of_at(self_val, frame_state.insn_idx) else { + self.push_insn_id(block, insn_id); continue; + }; + (recv_type.class(), Some(recv_type)) }; let ci = unsafe { get_call_data_ci(cd) }; // info about the call site let mid = unsafe { vm_ci_mid(ci) }; @@ -1542,8 +1548,8 @@ impl Function { self.push_insn_id(block, insn_id); continue; } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); - if let Some(expected) = guard_equal_to { - self_val = self.push_insn(block, Insn::GuardBitEquals { val: self_val, expected, state }); + if let Some(profiled_type) = profiled_type { + self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); } let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, cd, cme, iseq, args, state }); self.make_equal_to(insn_id, send_direct); @@ -1611,17 +1617,12 @@ impl Function { let method_id = unsafe { rb_vm_ci_mid(call_info) }; // If we have info about the class of the receiver - // - // TODO(alan): there was a seemingly a miscomp here if you swap with - // `inexact_ruby_class`. Theoretically it can call a method too general - // for the receiver. Confirm and add a test. - let (recv_class, guard_type) = if let Some(klass) = self_type.runtime_exact_ruby_class() { - (klass, None) + let (recv_class, profiled_type) = if let Some(class) = self_type.runtime_exact_ruby_class() { + (class, None) } else { let iseq_insn_idx = fun.frame_state(state).insn_idx; let Some(recv_type) = fun.profiled_type_of_at(self_val, iseq_insn_idx) else { return Err(()) }; - let Some(recv_class) = recv_type.runtime_exact_ruby_class() else { return Err(()) }; - (recv_class, Some(recv_type.unspecialized())) + (recv_type.class(), Some(recv_type)) }; // Do method lookup @@ -1661,9 +1662,9 @@ impl Function { if ci_flags & VM_CALL_ARGS_SIMPLE != 0 { // Commit to the replacement. Put PatchPoint. fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass: recv_class, method: method_id, cme: method }, state }); - if let Some(guard_type) = guard_type { + if let Some(profiled_type) = profiled_type { // Guard receiver class - self_val = fun.push_insn(block, Insn::GuardType { val: self_val, guard_type, state }); + self_val = fun.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); } let cfun = unsafe { get_mct_func(cfunc) }.cast(); let mut cfunc_args = vec![self_val]; @@ -2506,7 +2507,7 @@ struct ProfileOracle { /// instruction index. At a given ISEQ instruction, the interpreter has profiled the stack /// operands to a given ISEQ instruction, and this list of pairs of (InsnId, Type) map that /// profiling information into HIR instructions. - types: HashMap>, + types: HashMap>, } impl ProfileOracle { @@ -2521,9 +2522,9 @@ impl ProfileOracle { let entry = self.types.entry(iseq_insn_idx).or_insert_with(|| vec![]); // operand_types is always going to be <= stack size (otherwise it would have an underflow // at run-time) so use that to drive iteration. - for (idx, &insn_type) in operand_types.iter().rev().enumerate() { + for (idx, insn_type_distribution) in operand_types.iter().rev().enumerate() { let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling"); - entry.push((insn, insn_type)) + entry.push((insn, TypeDistributionSummary::new(insn_type_distribution))) } } } @@ -5548,8 +5549,8 @@ mod opt_tests { fn test@:5: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } @@ -5588,8 +5589,8 @@ mod opt_tests { fn test@:6: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } @@ -5607,8 +5608,8 @@ mod opt_tests { bb0(v0:BasicObject): v2:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) - v7:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1040), v2 + v7:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1038), v2 Return v8 "#]]); } @@ -5629,8 +5630,8 @@ mod opt_tests { v2:Fixnum[1] = Const Value(1) v3:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1040), v2, v3 + v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038), v2, v3 Return v9 "#]]); } @@ -5652,11 +5653,11 @@ mod opt_tests { fn test@:7: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1040) - PatchPoint MethodRedefined(Object@0x1000, bar@0x1048, cme:0x1050) - v11:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1040) + v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038) + PatchPoint MethodRedefined(Object@0x1000, bar@0x1040, cme:0x1048) + v11:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1038) Return v12 "#]]); } @@ -6438,6 +6439,31 @@ mod opt_tests { "#]]); } + #[test] + fn test_send_direct_to_instance_method() { + eval(" + class C + def foo + 3 + end + end + + def test(c) = c.foo + c = C.new + test c + test c + "); + + assert_optimized_method_hir("test", expect![[r#" + fn test@:8: + bb0(v0:BasicObject, v1:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) + v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v8:BasicObject = SendWithoutBlockDirect v7, :foo (0x1038) + Return v8 + "#]]); + } + #[test] fn dont_specialize_call_to_iseq_with_opt() { eval(" @@ -7385,8 +7411,8 @@ mod opt_tests { fn test@:3: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 9ad0bdc6492826..84679c419d9ba9 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -1,12 +1,13 @@ #![allow(non_upper_case_globals)] use crate::cruby::{Qfalse, Qnil, Qtrue, VALUE, RUBY_T_ARRAY, RUBY_T_STRING, RUBY_T_HASH, RUBY_T_CLASS, RUBY_T_MODULE}; -use crate::cruby::{rb_cInteger, rb_cFloat, rb_cArray, rb_cHash, rb_cString, rb_cSymbol, rb_cObject, rb_cTrueClass, rb_cFalseClass, rb_cNilClass, rb_cRange, rb_cSet, rb_cRegexp, rb_cClass, rb_cModule}; +use crate::cruby::{rb_cInteger, rb_cFloat, rb_cArray, rb_cHash, rb_cString, rb_cSymbol, rb_cObject, rb_cTrueClass, rb_cFalseClass, rb_cNilClass, rb_cRange, rb_cSet, rb_cRegexp, rb_cClass, rb_cModule, rb_zjit_singleton_class_p}; use crate::cruby::ClassRelationship; use crate::cruby::get_class_name; use crate::cruby::ruby_sym_to_rust_string; use crate::cruby::rb_mRubyVMFrozenCore; use crate::cruby::rb_obj_class; use crate::hir::PtrPrintMap; +use crate::profile::ProfiledType; #[derive(Copy, Clone, Debug, PartialEq)] /// Specialization of the type. If we know additional information about the object, we put it here. @@ -74,8 +75,14 @@ fn write_spec(f: &mut std::fmt::Formatter, printer: &TypePrinter) -> std::fmt::R Specialization::Object(val) if val == unsafe { rb_mRubyVMFrozenCore } => write!(f, "[VMFrozenCore]"), Specialization::Object(val) if ty.is_subtype(types::Symbol) => write!(f, "[:{}]", ruby_sym_to_rust_string(val)), Specialization::Object(val) => write!(f, "[{}]", val.print(printer.ptr_map)), + // TODO(max): Ensure singleton classes never have Type specialization + Specialization::Type(val) if unsafe { rb_zjit_singleton_class_p(val) } => + write!(f, "[class*:{}@{}]", get_class_name(val), val.print(printer.ptr_map)), Specialization::Type(val) => write!(f, "[class:{}]", get_class_name(val)), - Specialization::TypeExact(val) => write!(f, "[class_exact:{}]", get_class_name(val)), + Specialization::TypeExact(val) if unsafe { rb_zjit_singleton_class_p(val) } => + write!(f, "[class_exact*:{}@{}]", get_class_name(val), val.print(printer.ptr_map)), + Specialization::TypeExact(val) => + write!(f, "[class_exact:{}]", get_class_name(val)), Specialization::Int(val) if ty.is_subtype(types::CBool) => write!(f, "[{}]", val != 0), Specialization::Int(val) if ty.is_subtype(types::CInt8) => write!(f, "[{}]", (val as i64) >> 56), Specialization::Int(val) if ty.is_subtype(types::CInt16) => write!(f, "[{}]", (val as i64) >> 48), @@ -231,6 +238,20 @@ impl Type { } } + pub fn from_profiled_type(val: ProfiledType) -> Type { + if val.is_fixnum() { types::Fixnum } + else if val.is_flonum() { types::Flonum } + else if val.is_static_symbol() { types::StaticSymbol } + else if val.is_nil() { types::NilClass } + else if val.is_true() { types::TrueClass } + else if val.is_false() { types::FalseClass } + else if val.class() == unsafe { rb_cString } { types::StringExact } + else { + // TODO(max): Add more cases for inferring type bits from built-in types + Type { bits: bits::BasicObject, spec: Specialization::TypeExact(val.class()) } + } + } + /// Private. Only for creating type globals. const fn from_bits(bits: u64) -> Type { Type { @@ -274,12 +295,6 @@ impl Type { self.is_subtype(types::NilClass) || self.is_subtype(types::FalseClass) } - /// Top self is the Ruby global object, where top-level method definitions go. Return true if - /// this Type has a Ruby object specialization that is the top-level self. - pub fn is_top_self(&self) -> bool { - self.ruby_object() == Some(unsafe { crate::cruby::rb_vm_top_self() }) - } - /// Return the object specialization, if any. pub fn ruby_object(&self) -> Option { match self.spec { diff --git a/zjit/src/lib.rs b/zjit/src/lib.rs index d5ca2b74ba8773..b36bf6515ebadd 100644 --- a/zjit/src/lib.rs +++ b/zjit/src/lib.rs @@ -6,6 +6,7 @@ pub use std; mod state; +mod distribution; mod cruby; mod cruby_methods; mod hir; diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 7db8e44c7a8276..a99229604b0ce2 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -1,7 +1,8 @@ // We use the YARV bytecode constants which have a CRuby-style name #![allow(non_upper_case_globals)] -use crate::{cruby::*, gc::get_or_create_iseq_payload, hir_type::{types::{Empty}, Type}, options::get_option}; +use crate::{cruby::*, gc::get_or_create_iseq_payload, options::get_option}; +use crate::distribution::{Distribution, DistributionSummary}; /// Ephemeral state for profiling runtime information struct Profiler { @@ -79,25 +80,100 @@ fn profile_insn(profiler: &mut Profiler, bare_opcode: ruby_vminsn_type) { } } +const DISTRIBUTION_SIZE: usize = 4; + +pub type TypeDistribution = Distribution; + +pub type TypeDistributionSummary = DistributionSummary; + /// Profile the Type of top-`n` stack operands fn profile_operands(profiler: &mut Profiler, profile: &mut IseqProfile, n: usize) { let types = &mut profile.opnd_types[profiler.insn_idx]; - if types.len() <= n { - types.resize(n, Empty); + if types.is_empty() { + types.resize(n, TypeDistribution::new()); } for i in 0..n { - let opnd_type = Type::from_value(profiler.peek_at_stack((n - i - 1) as isize)); - types[i] = types[i].union(opnd_type); - if let Some(object) = types[i].gc_object() { - unsafe { rb_gc_writebarrier(profiler.iseq.into(), object) }; - } + let obj = profiler.peek_at_stack((n - i - 1) as isize); + // TODO(max): Handle GC-hidden classes like Array, Hash, etc and make them look normal or + // drop them or something + let ty = ProfiledType::new(obj.class_of(), obj.shape_id_of()); + unsafe { rb_gc_writebarrier(profiler.iseq.into(), ty.class()) }; + types[i].observe(ty); + } +} + +/// opt_send_without_block/opt_plus/... should store: +/// * the class of the receiver, so we can do method lookup +/// * the shape of the receiver, so we can optimize ivar lookup +/// with those two, pieces of information, we can also determine when an object is an immediate: +/// * Integer + SPECIAL_CONST_SHAPE_ID == Fixnum +/// * Float + SPECIAL_CONST_SHAPE_ID == Flonum +/// * Symbol + SPECIAL_CONST_SHAPE_ID == StaticSymbol +/// * NilClass == Nil +/// * TrueClass == True +/// * FalseClass == False +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProfiledType { + class: VALUE, + shape: ShapeId, +} + +impl Default for ProfiledType { + fn default() -> Self { + Self::empty() + } +} + +impl ProfiledType { + fn new(class: VALUE, shape: ShapeId) -> Self { + Self { class, shape } + } + + pub fn empty() -> Self { + Self { class: VALUE(0), shape: INVALID_SHAPE_ID } + } + + pub fn is_empty(&self) -> bool { + self.class == VALUE(0) + } + + pub fn class(&self) -> VALUE { + self.class + } + + pub fn shape(&self) -> ShapeId { + self.shape + } + + pub fn is_fixnum(&self) -> bool { + self.class == unsafe { rb_cInteger } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_flonum(&self) -> bool { + self.class == unsafe { rb_cFloat } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_static_symbol(&self) -> bool { + self.class == unsafe { rb_cSymbol } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_nil(&self) -> bool { + self.class == unsafe { rb_cNilClass } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_true(&self) -> bool { + self.class == unsafe { rb_cTrueClass } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_false(&self) -> bool { + self.class == unsafe { rb_cFalseClass } && self.shape == SPECIAL_CONST_SHAPE_ID } } #[derive(Debug)] pub struct IseqProfile { /// Type information of YARV instruction operands, indexed by the instruction index - opnd_types: Vec>, + opnd_types: Vec>, /// Number of profiled executions for each YARV instruction, indexed by the instruction index num_profiles: Vec, @@ -112,16 +188,17 @@ impl IseqProfile { } /// Get profiled operand types for a given instruction index - pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[Type]> { + pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[TypeDistribution]> { self.opnd_types.get(insn_idx).map(|v| &**v) } /// Run a given callback with every object in IseqProfile pub fn each_object(&self, callback: impl Fn(VALUE)) { - for types in &self.opnd_types { - for opnd_type in types { - if let Some(object) = opnd_type.gc_object() { - callback(object); + for operands in &self.opnd_types { + for distribution in operands { + for profiled_type in distribution.each_item() { + // If the type is a GC object, call the callback + callback(profiled_type.class); } } } @@ -129,10 +206,11 @@ impl IseqProfile { /// Run a given callback with a mutable reference to every object in IseqProfile pub fn each_object_mut(&mut self, callback: impl Fn(&mut VALUE)) { - for types in self.opnd_types.iter_mut() { - for opnd_type in types.iter_mut() { - if let Some(object) = opnd_type.gc_object_mut() { - callback(object); + for operands in &mut self.opnd_types { + for distribution in operands { + for ref mut profiled_type in distribution.each_item_mut() { + // If the type is a GC object, call the callback + callback(&mut profiled_type.class); } } } From 19336a639252ea75204eda040b9e916d1c7188bf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:26:39 +0900 Subject: [PATCH 3/7] [rubygems/rubygems] Removed compatibility.rb that file is for Ruby 1.9. https://github.com/rubygems/rubygems/commit/120c174e7f --- lib/rubygems.rb | 3 --- lib/rubygems/compatibility.rb | 41 ----------------------------------- 2 files changed, 44 deletions(-) delete mode 100644 lib/rubygems/compatibility.rb diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 0c40f8482f38d0..d4e88579e823db 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -12,9 +12,6 @@ module Gem VERSION = "3.8.0.dev" end -# Must be first since it unloads the prelude from 1.9.2 -require_relative "rubygems/compatibility" - require_relative "rubygems/defaults" require_relative "rubygems/deprecate" require_relative "rubygems/errors" diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb deleted file mode 100644 index 0d9df56f8af8f5..00000000000000 --- a/lib/rubygems/compatibility.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -#-- -# This file contains all sorts of little compatibility hacks that we've -# had to introduce over the years. Quarantining them into one file helps -# us know when we can get rid of them. -# -# Ruby 1.9.x has introduced some things that are awkward, and we need to -# support them, so we define some constants to use later. -# -# TODO remove at RubyGems 4 -#++ - -module Gem - # :stopdoc: - - RubyGemsVersion = VERSION - deprecate_constant(:RubyGemsVersion) - - RbConfigPriorities = %w[ - MAJOR - MINOR - TEENY - EXEEXT RUBY_SO_NAME arch bindir datadir libdir ruby_install_name - ruby_version rubylibprefix sitedir sitelibdir vendordir vendorlibdir - rubylibdir - ].freeze - - if defined?(ConfigMap) - RbConfigPriorities.each do |key| - ConfigMap[key.to_sym] = RbConfig::CONFIG[key] - end - else - ## - # Configuration settings from ::RbConfig - ConfigMap = Hash.new do |cm, key| - cm[key] = RbConfig::CONFIG[key.to_s] - end - deprecate_constant(:ConfigMap) - end -end From 51f88f9922ef0a8c670eb541de726ae0d11a1706 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:39:18 +0900 Subject: [PATCH 4/7] [rubygems/rubygems] Added ability for changing deprecated version from only next major https://github.com/rubygems/rubygems/commit/15177de84e --- lib/rubygems/deprecate.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 7d24f9cbfc02c3..e9acb5a02b39d3 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -126,7 +126,7 @@ def deprecate(name, repl, year, month) # telling the user of +repl+ (unless +repl+ is :none) and the # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none) + def rubygems_deprecate(name, replacement=:none, version=Gem::Deprecate.next_rubygems_major_version) class_eval do old = "_deprecated_#{name}" alias_method old, name @@ -136,7 +136,7 @@ def rubygems_deprecate(name, replacement=:none) msg = [ "NOTE: #{target}#{name} is deprecated", replacement == :none ? " with no replacement" : "; use #{replacement} instead", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", + ". It will be removed in Rubygems #{version}", "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", ] warn "#{msg.join}." unless Gem::Deprecate.skip From 052b38a5d9ca4d0ab2ad872bf896fe256a0186f5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:45:25 +0900 Subject: [PATCH 5/7] [rubygems/rubygems] Deprecate Gem::Specification#datadir and will remove it at RG 4.1 https://github.com/rubygems/rubygems/commit/e99cdab171 --- lib/rubygems/basic_specification.rb | 3 +++ test/rubygems/test_config.rb | 7 ------- test/rubygems/test_gem.rb | 29 ----------------------------- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index a0b552f63ceb2e..591b5557250b82 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -199,6 +199,9 @@ def datadir File.expand_path(File.join(gems_dir, full_name, "data", name)) end + extend Gem::Deprecate + rubygems_deprecate :datadir, :none, "4.1" + ## # Full path of the target library file. # If the file is not in this gem, return nil. diff --git a/test/rubygems/test_config.rb b/test/rubygems/test_config.rb index 657624d5268807..822b57b0dcb54b 100644 --- a/test/rubygems/test_config.rb +++ b/test/rubygems/test_config.rb @@ -5,13 +5,6 @@ require "shellwords" class TestGemConfig < Gem::TestCase - def test_datadir - util_make_gems - spec = Gem::Specification.find_by_name("a") - spec.activate - assert_equal "#{spec.full_gem_path}/data/a", spec.datadir - end - def test_good_rake_path_is_escaped path = Gem::TestCase.class_variable_get(:@@good_rake) ruby, rake = path.shellsplit diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index cdc3479e3739d3..49e81fcedb24bb 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -527,35 +527,6 @@ def test_self_configuration assert_equal expected, Gem.configuration end - def test_self_datadir - foo = nil - - Dir.chdir @tempdir do - FileUtils.mkdir_p "data" - File.open File.join("data", "foo.txt"), "w" do |fp| - fp.puts "blah" - end - - foo = util_spec "foo" do |s| - s.files = %w[data/foo.txt] - end - - install_gem foo - end - - gem "foo" - - expected = File.join @gemhome, "gems", foo.full_name, "data", "foo" - - assert_equal expected, Gem::Specification.find_by_name("foo").datadir - end - - def test_self_datadir_nonexistent_package - assert_raise(Gem::MissingSpecError) do - Gem::Specification.find_by_name("xyzzy").datadir - end - end - def test_self_default_exec_format ruby_install_name "ruby" do assert_equal "%s", Gem.default_exec_format From 4d26ccd2afaf33a813464d1abe4cf518950b2f2e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 20:20:49 +0900 Subject: [PATCH 6/7] [rubygems/rubygems] Allow to use Gem::Deprecate#rubygems_deprecate and rubygems_deprecate_command without rubygems.rb https://github.com/rubygems/rubygems/commit/4925403686 --- lib/rubygems/deprecate.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index e9acb5a02b39d3..a20649cbdab166 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -126,13 +126,14 @@ def deprecate(name, repl, year, month) # telling the user of +repl+ (unless +repl+ is :none) and the # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none, version=Gem::Deprecate.next_rubygems_major_version) + def rubygems_deprecate(name, replacement=:none, version=nil) class_eval do old = "_deprecated_#{name}" alias_method old, name define_method name do |*args, &block| klass = is_a? Module target = klass ? "#{self}." : "#{self.class}#" + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "NOTE: #{target}#{name} is deprecated", replacement == :none ? " with no replacement" : "; use #{replacement} instead", @@ -147,13 +148,14 @@ def rubygems_deprecate(name, replacement=:none, version=Gem::Deprecate.next_ruby end # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) + def rubygems_deprecate_command(version = nil) class_eval do define_method "deprecated?" do true end define_method "deprecation_warning" do + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "#{command} command is deprecated", ". It will be removed in Rubygems #{version}.\n", From 9c0ebff2cded1b60d5d7c922d7cf8dbaa54ecfe2 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 6 Aug 2025 01:00:04 +0100 Subject: [PATCH 7/7] ZJIT: Avoid matching built-in iseq's HIR line numbers in tests (#14124) ZJIT: Avoid matching built-in ISEQs' HIR line numbers in tests Co-authored-by: Author: Takashi Kokubun --- zjit/src/hir.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 203be0661e3f05..635120eb80a8a0 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2242,6 +2242,12 @@ impl<'a> std::fmt::Display for FunctionPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let fun = &self.fun; let iseq_name = iseq_get_location(fun.iseq, 0); + // In tests, strip the line number for builtin ISEQs to make tests stable across line changes + let iseq_name = if cfg!(test) && iseq_name.contains("@:197: + fn Float@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject): v6:Flonum = InvokeBuiltin rb_f_float, v0, v1, v2 Jump bb1(v0, v1, v2, v3, v6) @@ -5015,7 +5021,7 @@ mod tests { #[test] fn test_invokebuiltin_cexpr_annotated() { assert_method_hir_with_opcode("class", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#" - fn class@:20: + fn class@: bb0(v0:BasicObject): v3:Class = InvokeBuiltin _bi20, v0 Jump bb1(v0, v3) @@ -5031,7 +5037,7 @@ mod tests { assert!(iseq_contains_opcode(iseq, YARVINSN_opt_invokebuiltin_delegate), "iseq Dir.open does not contain invokebuiltin"); let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, expect![[r#" - fn open@:184: + fn open@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject): v5:NilClass = Const Value(nil) v8:BasicObject = InvokeBuiltin dir_s_open, v0, v1, v2 @@ -5045,7 +5051,7 @@ mod tests { assert!(iseq_contains_opcode(iseq, YARVINSN_opt_invokebuiltin_delegate_leave), "iseq GC.enable does not contain invokebuiltin"); let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, expect![[r#" - fn enable@:55: + fn enable@: bb0(v0:BasicObject): v3:BasicObject = InvokeBuiltin gc_enable, v0 Jump bb1(v0, v3) @@ -5060,7 +5066,7 @@ mod tests { 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@:36: + fn start@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject): v6:FalseClass = Const Value(false) v8:BasicObject = InvokeBuiltin gc_start_internal, v0, v1, v2, v3, v6