From 68cb8d8282b4bda20d8cd04b2d387a9b60418dba Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Sat, 21 Mar 2026 01:54:14 +0100 Subject: [PATCH 01/18] [ruby/rubygems] Check happy path first when comparing gem version: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - During resolution, Gem::Version are compared against each other. Since comparing versions is a very hot path we can micro optimize it to check the happy path first. The speed gain on the overall resolution isn't significant but the ips gain is quite substantial. The diff chunk is small so I figure it's worth it anyway. ```ruby a = Gem::Version.new("5.3.1") b = Gem::Version.new("5.3.1") Benchmark.ips do |x| x.report("equal regular:") { a <=> c } x.report("equal optimized:") { a <=> c } x.hold!("equal_temp_results") x.compare!(order: :baseline) end ``` ``` Warming up -------------------------------------- equal optimized: 1.268M i/100ms Calculating ------------------------------------- equal optimized: 12.738M (± 1.5%) i/s (78.50 ns/i) - 64.680M in 5.078754s Comparison: equal regular:: 9866605.0 i/s equal optimized:: 12738310.3 i/s - 1.29x faster ``` ruby 3.4.8 (2025-12-17 revision https://github.com/ruby/rubygems/commit/995b59f666) +PRISM [arm64-darwin25] https://github.com/ruby/rubygems/commit/c940f7a547 --- lib/rubygems/version.rb | 84 ++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index b7966c3973f757..6e286bd628f0da 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -345,60 +345,58 @@ def approximate_recommendation # other types may raise an exception. def <=>(other) - if String === other - return unless self.class.correct?(other) - return self <=> self.class.new(other) - end - - return unless Gem::Version === other - - # Fast path for comparison when available. - if @sort_key && other.sort_key - return @sort_key <=> other.sort_key - end - - return 0 if @version == other.version || canonical_segments == other.canonical_segments + if Gem::Version === other + # Fast path for comparison when available. + if @sort_key && other.sort_key + return @sort_key <=> other.sort_key + end - lhsegments = canonical_segments - rhsegments = other.canonical_segments + return 0 if @version == other.version || canonical_segments == other.canonical_segments - lhsize = lhsegments.size - rhsize = rhsegments.size - limit = (lhsize > rhsize ? rhsize : lhsize) + lhsegments = canonical_segments + rhsegments = other.canonical_segments - i = 0 + lhsize = lhsegments.size + rhsize = rhsegments.size + limit = (lhsize > rhsize ? rhsize : lhsize) - while i < limit - lhs = lhsegments[i] - rhs = rhsegments[i] - i += 1 + i = 0 - next if lhs == rhs - return -1 if String === lhs && Numeric === rhs - return 1 if Numeric === lhs && String === rhs + while i < limit + lhs = lhsegments[i] + rhs = rhsegments[i] + i += 1 - return lhs <=> rhs - end + next if lhs == rhs + return -1 if String === lhs && Numeric === rhs + return 1 if Numeric === lhs && String === rhs - lhs = lhsegments[i] + return lhs <=> rhs + end - if lhs.nil? - rhs = rhsegments[i] + lhs = lhsegments[i] - while i < rhsize - return 1 if String === rhs - return -1 unless rhs.zero? - rhs = rhsegments[i += 1] - end - else - while i < lhsize - return -1 if String === lhs - return 1 unless lhs.zero? - lhs = lhsegments[i += 1] + if lhs.nil? + rhs = rhsegments[i] + + while i < rhsize + return 1 if String === rhs + return -1 unless rhs.zero? + rhs = rhsegments[i += 1] + end + else + while i < lhsize + return -1 if String === lhs + return 1 unless lhs.zero? + lhs = lhsegments[i += 1] + end end - end - 0 + 0 + elsif String === other + return unless self.class.correct?(other) + self <=> self.class.new(other) + end end # remove trailing zeros segments before first letter or at the end of the version From 109a20a72a6e92c7daf9dafd508b467764679d0b Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sat, 21 Mar 2026 09:08:51 -0700 Subject: [PATCH 02/18] Don't pass singleton to TypedData_Make_Struct We should never initialize a class with an existing singleton class (singleton classes definitionally should not be shared). The only cases this happened in Ruby itself is methods, which exposes a bug that dup did not behave correctly. --- gc.c | 1 + proc.c | 4 ++-- test/ruby/test_method.rb | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/gc.c b/gc.c index 5001bc5c01cb2e..a52ef98772a4b0 100644 --- a/gc.c +++ b/gc.c @@ -1086,6 +1086,7 @@ rb_gc_register_pinning_obj(VALUE obj) static inline void rb_data_object_check(VALUE klass) { + RUBY_ASSERT(!RCLASS_SINGLETON_P(klass)); if (klass != rb_cObject && (rb_get_alloc_func(klass) == rb_class_allocate_instance)) { rb_undef_alloc_func(klass); rb_warn("undefining the allocator of T_DATA class %"PRIsVALUE, klass); diff --git a/proc.c b/proc.c index 3e2afeab3ca1bb..f1fc9780607341 100644 --- a/proc.c +++ b/proc.c @@ -2626,7 +2626,7 @@ method_clone(VALUE self) struct METHOD *orig, *data; TypedData_Get_Struct(self, struct METHOD, &method_data_type, orig); - clone = TypedData_Make_Struct(CLASS_OF(self), struct METHOD, &method_data_type, data); + clone = TypedData_Make_Struct(rb_obj_class(self), struct METHOD, &method_data_type, data); rb_obj_clone_setup(self, clone, Qnil); RB_OBJ_WRITE(clone, &data->recv, orig->recv); RB_OBJ_WRITE(clone, &data->klass, orig->klass); @@ -2644,7 +2644,7 @@ method_dup(VALUE self) struct METHOD *orig, *data; TypedData_Get_Struct(self, struct METHOD, &method_data_type, orig); - clone = TypedData_Make_Struct(CLASS_OF(self), struct METHOD, &method_data_type, data); + clone = TypedData_Make_Struct(rb_obj_class(self), struct METHOD, &method_data_type, data); rb_obj_dup_setup(self, clone); RB_OBJ_WRITE(clone, &data->recv, orig->recv); RB_OBJ_WRITE(clone, &data->klass, orig->klass); diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb index 7c3e8e03a787e1..00512bf2c6207f 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -503,6 +503,20 @@ def m.bar; :bar; end end end + def test_clone_preserves_singleton_methods + m = method(:itself) + m.define_singleton_method(:foo) { :bar } + assert_equal(:bar, m.foo) + assert_equal(:bar, m.clone.foo) + end + + def test_dup_does_not_preserve_singleton_methods + m = method(:itself) + m.define_singleton_method(:foo) { :bar } + assert_equal(:bar, m.foo) + assert_raise(NoMethodError) { m.dup.foo } + end + def test_inspect o = Object.new def o.foo; end; line_no = __LINE__ From a0583318d3ab3495c5f1e7c03d65e24b9ddc60e3 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sat, 21 Mar 2026 09:09:32 -0700 Subject: [PATCH 03/18] Don't allow RCLASS_ALLOCATOR on a singleton --- internal/class.h | 4 +--- marshal.c | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/class.h b/internal/class.h index 164081b5696fbd..5ef60dd4dab306 100644 --- a/internal/class.h +++ b/internal/class.h @@ -619,9 +619,7 @@ static inline rb_alloc_func_t RCLASS_ALLOCATOR(VALUE klass) { RBIMPL_ASSERT_TYPE(klass, T_CLASS); - if (RCLASS_SINGLETON_P(klass)) { - return 0; - } + RUBY_ASSERT(!RCLASS_SINGLETON_P(klass)); return RCLASS_EXT_PRIME(klass)->as.class.allocator; } diff --git a/marshal.c b/marshal.c index a89a9eccba3d0e..967855529e6d76 100644 --- a/marshal.c +++ b/marshal.c @@ -947,8 +947,9 @@ w_object(VALUE obj, struct dump_arg *arg, int limit) hasiv = has_ivars(obj, (encname = encoding_name(obj, arg)), &ivobj); { st_data_t compat_data; - rb_alloc_func_t allocator = rb_get_alloc_func(RBASIC(obj)->klass); - if (st_lookup(compat_allocator_tbl, + VALUE klass = CLASS_OF(obj); + rb_alloc_func_t allocator = RCLASS_SINGLETON_P(klass) ? 0 : rb_get_alloc_func(klass); + if (allocator && st_lookup(compat_allocator_tbl, (st_data_t)allocator, &compat_data)) { marshal_compat_t *compat = (marshal_compat_t*)compat_data; From 111215de2758666894ba0a2095d86b731ffd5af3 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sat, 21 Mar 2026 09:09:57 -0700 Subject: [PATCH 04/18] Copy allocator to subclasses at boot and on change Copy the allocator from superclass to subclass at class creation time, so rb_get_alloc_func no longer needs to walk the ancestor chain. This expands on 68ffc8db088a7b29613a3746be5cc996be9f66fe, which did this copy, but only for classes defined in Ruby (ex. `class Foo`). And allows us to simple read the allocator field for any class to get the correct value. To keep this correct when rb_define_alloc_func is called after subclasses already exist, propagate the new allocator down to all subclasses. An RCLASS_ALLOCATOR_DEFINED flag distinguishes classes with an explicitly set allocator from those that inherited one, so propagation stops at subclasses that define their own. --- class.c | 3 +++ internal/class.h | 1 + object.c | 2 +- vm_insnhelper.c | 1 - vm_method.c | 34 +++++++++++++++++++--------------- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/class.c b/class.c index dbc8d760ef3615..35b66be920f7a0 100644 --- a/class.c +++ b/class.c @@ -786,6 +786,7 @@ class_boot_boxable(VALUE super, bool boxable) class_associate_super(klass, super, true); if (super && !UNDEF_P(super)) { + RCLASS_SET_ALLOCATOR(klass, RCLASS_ALLOCATOR(super)); rb_class_set_initialized(klass); } @@ -1428,6 +1429,8 @@ void Init_class_hierarchy(void) { rb_cBasicObject = boot_defclass("BasicObject", 0); + RCLASS_SET_ALLOCATOR(rb_cBasicObject, rb_class_allocate_instance); + FL_SET_RAW(rb_cBasicObject, RCLASS_ALLOCATOR_DEFINED); rb_cObject = boot_defclass("Object", rb_cBasicObject); rb_vm_register_global_object(rb_cObject); diff --git a/internal/class.h b/internal/class.h index 5ef60dd4dab306..5a6dda5d1070f7 100644 --- a/internal/class.h +++ b/internal/class.h @@ -255,6 +255,7 @@ static inline void RCLASS_WRITE_CLASSPATH(VALUE klass, VALUE classpath, bool per #define RCLASS_IS_INITIALIZED FL_USER3 // 3 is RMODULE_IS_REFINEMENT for RMODULE #define RCLASS_BOXABLE FL_USER4 +#define RCLASS_ALLOCATOR_DEFINED FL_USER5 static inline st_table * RCLASS_CLASSEXT_TBL(VALUE klass) diff --git a/object.c b/object.c index 4dcd5d615f85a9..c3241a198d369a 100644 --- a/object.c +++ b/object.c @@ -2253,6 +2253,7 @@ rb_class_initialize(int argc, VALUE *argv, VALUE klass) } } rb_class_set_super(klass, super); + RCLASS_SET_ALLOCATOR(klass, RCLASS_ALLOCATOR(super)); rb_make_metaclass(klass, RBASIC(super)->klass); rb_class_inherited(super, klass); rb_mod_initialize_exec(klass); @@ -4448,7 +4449,6 @@ InitVM_Object(void) #endif rb_define_private_method(rb_cBasicObject, "initialize", rb_obj_initialize, 0); - rb_define_alloc_func(rb_cBasicObject, rb_class_allocate_instance); rb_define_method(rb_cBasicObject, "==", rb_obj_equal, 1); rb_define_method(rb_cBasicObject, "equal?", rb_obj_equal, 1); rb_define_method(rb_cBasicObject, "!", rb_obj_not, 0); diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 7a4f0cf54a6e2c..ba7523eb285d07 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -5904,7 +5904,6 @@ vm_declare_class(ID id, rb_num_t flags, VALUE cbase, VALUE super) /* new class declaration */ VALUE s = VM_DEFINECLASS_HAS_SUPERCLASS_P(flags) ? super : rb_cObject; VALUE c = declare_under(id, cbase, rb_define_class_id(id, s)); - rb_define_alloc_func(c, rb_get_alloc_func(c)); rb_class_inherited(s, c); return c; } diff --git a/vm_method.c b/vm_method.c index 30ba186e9e19a4..521595d0b19666 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1703,6 +1703,17 @@ rb_method_entry_set(VALUE klass, ID mid, const rb_method_entry_t *me, rb_method_ #define UNDEF_ALLOC_FUNC ((rb_alloc_func_t)-1) +static void +propagate_alloc_func(VALUE subclass, VALUE arg) +{ + if (RB_TYPE_P(subclass, T_CLASS) && + !RCLASS_SINGLETON_P(subclass) && + !FL_TEST_RAW(subclass, RCLASS_ALLOCATOR_DEFINED)) { + RCLASS_SET_ALLOCATOR(subclass, (rb_alloc_func_t)arg); + rb_class_foreach_subclass(subclass, propagate_alloc_func, arg); + } +} + void rb_define_alloc_func(VALUE klass, VALUE (*func)(VALUE)) { @@ -1711,12 +1722,17 @@ rb_define_alloc_func(VALUE klass, VALUE (*func)(VALUE)) rb_raise(rb_eTypeError, "can't define an allocator for a singleton class"); } RCLASS_SET_ALLOCATOR(klass, func); + FL_SET_RAW(klass, RCLASS_ALLOCATOR_DEFINED); + rb_class_foreach_subclass(klass, propagate_alloc_func, (VALUE)func); } void rb_undef_alloc_func(VALUE klass) { - rb_define_alloc_func(klass, UNDEF_ALLOC_FUNC); + Check_Type(klass, T_CLASS); + RCLASS_SET_ALLOCATOR(klass, UNDEF_ALLOC_FUNC); + FL_SET_RAW(klass, RCLASS_ALLOCATOR_DEFINED); + rb_class_foreach_subclass(klass, propagate_alloc_func, (VALUE)UNDEF_ALLOC_FUNC); } rb_alloc_func_t @@ -1726,20 +1742,8 @@ rb_get_alloc_func(VALUE klass) rb_alloc_func_t allocator = RCLASS_ALLOCATOR(klass); if (allocator == UNDEF_ALLOC_FUNC) return 0; - if (allocator) return allocator; - - VALUE *superclasses = RCLASS_SUPERCLASSES(klass); - size_t depth = RCLASS_SUPERCLASS_DEPTH(klass); - - for (size_t i = depth; i > 0; i--) { - klass = superclasses[i - 1]; - RBIMPL_ASSERT_TYPE(klass, T_CLASS); - - allocator = RCLASS_ALLOCATOR(klass); - if (allocator == UNDEF_ALLOC_FUNC) break; - if (allocator) return allocator; - } - return 0; + RUBY_ASSERT(allocator); + return allocator; } const rb_method_entry_t * From cfaef1e5ff212ea190d786a182aaad2e455682a0 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 24 Mar 2026 08:45:00 -0700 Subject: [PATCH 05/18] ZJIT: Recompile ISEQs with no-profile sends via exit profiling When a send instruction has no profile data (e.g., the code path was not reached during interpreter profiling), the first JIT compilation converts it to a SideExit that profiles operands on exit. After --zjit-num-profiles exits (default 5), the exit triggers ISEQ invalidation and recompilation with the newly gathered profile data, allowing the send to be optimized (e.g., as SendDirect) in the second version. This approach avoids adding Profile instructions to the HIR (which could interfere with optimization passes) and keeps profile_time low by only profiling on side exits rather than during normal execution. --- zjit/src/backend/arm64/mod.rs | 2 +- zjit/src/backend/lir.rs | 27 ++++++- zjit/src/backend/x86_64/mod.rs | 2 +- zjit/src/codegen.rs | 73 +++++++++++++++--- zjit/src/hir.rs | 137 +++++++++++++++++++++------------ zjit/src/hir/opt_tests.rs | 100 ++++++++++++++++++++++-- zjit/src/profile.rs | 25 ++++++ zjit/src/stats.rs | 2 + 8 files changed, 299 insertions(+), 69 deletions(-) diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 90bf28e1c0002e..9083824c4b28af 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1754,7 +1754,7 @@ mod tests { let val64 = asm.add(CFP, Opnd::UImm(64)); asm.store(Opnd::mem(64, SP, 0x10), val64); - let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: SideExit { pc: Opnd::const_ptr(0 as *const u8), stack: vec![], locals: vec![] } }; + let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: SideExit { pc: Opnd::const_ptr(0 as *const u8), stack: vec![], locals: vec![], recompile: None } }; asm.push_insn(Insn::Joz(val64, side_exit)); asm.mov(C_ARG_OPNDS[0], C_RET_OPND.with_num_bits(32)); asm.mov(C_ARG_OPNDS[1], Opnd::mem(64, SP, -8)); diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 00a80b9cf4b32a..207538ec570f71 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -551,6 +551,17 @@ pub struct SideExit { pub pc: Opnd, pub stack: Vec, pub locals: Vec, + /// If set, the side exit will call the recompile function with these arguments + /// to profile the send and invalidate the ISEQ for recompilation. + pub recompile: Option, +} + +/// Arguments for the no-profile-send recompile callback. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct SideExitRecompile { + pub iseq: Opnd, + pub insn_idx: u32, + pub argc: i32, } /// Branch target (something that we can jump to) @@ -2607,7 +2618,7 @@ impl Assembler pub fn compile_exits(&mut self) -> Vec { /// Restore VM state (cfp->pc, cfp->sp, stack, locals) for the side exit. fn compile_exit_save_state(asm: &mut Assembler, exit: &SideExit) { - let SideExit { pc, stack, locals } = exit; + let SideExit { pc, stack, locals, .. } = exit; // Side exit blocks are not part of the CFG at the moment, // so we need to manually ensure that patchpoints get padded @@ -2646,6 +2657,20 @@ impl Assembler /// that it can be safely deduplicated by using SideExit as a dedup key. fn compile_exit(asm: &mut Assembler, exit: &SideExit) { compile_exit_save_state(asm, exit); + // If this side exit should trigger recompilation, call the recompile + // function after saving VM state. The ccall must happen after + // compile_exit_save_state because it clobbers caller-saved registers + // that may hold stack/local operands we need to save. + if let Some(recompile) = &exit.recompile { + use crate::codegen::no_profile_send_recompile; + asm_comment!(asm, "profile and maybe recompile for no-profile send"); + asm_ccall!(asm, no_profile_send_recompile, + EC, + recompile.iseq, + Opnd::UImm(recompile.insn_idx as u64), + Opnd::Imm(recompile.argc as i64) + ); + } compile_exit_return(asm); } diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 570546cbd28f37..3cf744990d8381 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -1388,7 +1388,7 @@ mod tests { let val64 = asm.add(CFP, Opnd::UImm(64)); asm.store(Opnd::mem(64, SP, 0x10), val64); - let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: SideExit { pc: Opnd::const_ptr(0 as *const u8), stack: vec![], locals: vec![] } }; + let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: SideExit { pc: Opnd::const_ptr(0 as *const u8), stack: vec![], locals: vec![], recompile: None } }; asm.push_insn(Insn::Joz(val64, side_exit)); asm.mov(C_ARG_OPNDS[0], C_RET_OPND.with_num_bits(32)); asm.mov(C_ARG_OPNDS[1], Opnd::mem(64, SP, -8)); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index af6e881bd02510..bbdb1c986f6931 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -19,7 +19,7 @@ use crate::state::ZJITState; use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type}; use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ns, exit_compile_error}}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; -use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_STACK_PTR, Opnd, SP, SideExit, Target, asm_ccall, asm_comment}; +use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, Target, asm_ccall, asm_comment}; use crate::hir::{iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; use crate::hir::{Const, FrameState, Function, Insn, InsnId, SendFallbackReason}; use crate::hir_type::{types, Type}; @@ -474,7 +474,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin, _ => SideExitReason::UnhandledHIRUnknown(insn_id), }; - gen_side_exit(&mut jit, &mut asm, &reason, &function.frame_state(last_snapshot)); + gen_side_exit(&mut jit, &mut asm, &reason, &None, &function.frame_state(last_snapshot)); // Don't bother generating code after a side-exit. We won't run it. // TODO(max): Generate ud2 or equivalent. break; @@ -666,7 +666,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::SetClassVar { id, val, ic, state } => no_output!(gen_setclassvar(jit, asm, *id, opnd!(val), *ic, &function.frame_state(*state))), Insn::SetIvar { self_val, id, ic, val, state } => no_output!(gen_setivar(jit, asm, opnd!(self_val), *id, *ic, opnd!(val), &function.frame_state(*state))), Insn::FixnumBitCheck { val, index } => gen_fixnum_bit_check(asm, opnd!(val), *index), - Insn::SideExit { state, reason } => no_output!(gen_side_exit(jit, asm, reason, &function.frame_state(*state))), + Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, reason, recompile, &function.frame_state(*state))), Insn::PutSpecialObject { value_type } => gen_putspecialobject(asm, *value_type), Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state)), Insn::Defined { op_type, obj, pushval, v, state } => gen_defined(jit, asm, *op_type, *obj, *pushval, opnd!(v), &function.frame_state(*state)), @@ -1186,8 +1186,14 @@ fn gen_setglobal(jit: &mut JITState, asm: &mut Assembler, id: ID, val: Opnd, sta } /// Side-exit into the interpreter -fn gen_side_exit(jit: &mut JITState, asm: &mut Assembler, reason: &SideExitReason, state: &FrameState) { - asm.jmp(side_exit(jit, state, *reason)); +fn gen_side_exit(jit: &mut JITState, asm: &mut Assembler, reason: &SideExitReason, recompile: &Option, state: &FrameState) { + let mut exit = build_side_exit(jit, state); + exit.recompile = recompile.map(|argc| SideExitRecompile { + iseq: Opnd::Value(VALUE::from(jit.iseq)), + insn_idx: state.insn_idx() as u32, + argc, + }); + asm.jmp(Target::SideExit { exit, reason: *reason }); } /// Emit a special object lookup @@ -2824,6 +2830,7 @@ fn build_side_exit(jit: &JITState, state: &FrameState) -> SideExit { pc: Opnd::const_ptr(state.pc), stack, locals, + recompile: None, } } @@ -2844,22 +2851,70 @@ fn max_num_params(function: &Function) -> usize { #[cfg(target_arch = "x86_64")] macro_rules! c_callable { ($(#[$outer:meta])* - fn $f:ident $args:tt $(-> $ret:ty)? $body:block) => { + $vis:vis fn $f:ident $args:tt $(-> $ret:ty)? $body:block) => { $(#[$outer])* - extern "sysv64" fn $f $args $(-> $ret)? $body + $vis extern "sysv64" fn $f $args $(-> $ret)? $body }; } #[cfg(target_arch = "aarch64")] macro_rules! c_callable { ($(#[$outer:meta])* - fn $f:ident $args:tt $(-> $ret:ty)? $body:block) => { + $vis:vis fn $f:ident $args:tt $(-> $ret:ty)? $body:block) => { $(#[$outer])* - extern "C" fn $f $args $(-> $ret)? $body + $vis extern "C" fn $f $args $(-> $ret)? $body }; } #[cfg(test)] pub(crate) use c_callable; +c_callable! { + /// Called from JIT side-exit code when a send instruction had no profile data. This function + /// profiles the receiver and arguments on the stack, then (once enough profiles are gathered) + /// invalidates the current ISEQ version so that the ISEQ will be recompiled with the new + /// profile data on the next call. + pub(crate) fn no_profile_send_recompile(ec: EcPtr, iseq_raw: VALUE, insn_idx: u32, argc: i32) { + with_vm_lock(src_loc!(), || { + let iseq: IseqPtr = iseq_raw.as_iseq(); + let payload = get_or_create_iseq_payload(iseq); + + // Already gathered enough profiles; nothing to do + if payload.profile.done_profiling_at(insn_idx as usize) { + return; + } + + with_time_stat(Counter::profile_time_ns, || { + let cfp = unsafe { get_ec_cfp(ec) }; + let sp = unsafe { get_cfp_sp(cfp) }; + + // Profile the receiver and arguments for this send instruction + let should_recompile = payload.profile.profile_send_at(iseq, insn_idx as usize, sp, argc as usize); + + // Once we have enough profiles, invalidate and recompile the ISEQ + if should_recompile { + let num_versions = payload.versions.len(); + if let Some(version) = payload.versions.last_mut() { + if unsafe { version.as_ref() }.status != IseqStatus::Invalidated + && num_versions < MAX_ISEQ_VERSIONS + { + unsafe { version.as_mut() }.status = IseqStatus::Invalidated; + unsafe { rb_iseq_reset_jit_func(iseq) }; + + // Recompile JIT-to-JIT calls into the invalidated ISEQ + let cb = ZJITState::get_code_block(); + for incoming in unsafe { version.as_ref() }.incoming.iter() { + if let Err(err) = gen_iseq_call(cb, incoming) { + debug!("{err:?}: gen_iseq_call failed on no-profile recompile: {}", iseq_get_location(incoming.iseq.get(), 0)); + } + } + cb.mark_all_executable(); + } + } + } + }); + }); + } +} + c_callable! { /// Generated code calls this function with the SysV calling convention. See [gen_function_stub]. /// This function is expected to be called repeatedly when ZJIT fails to compile the stub. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 0891a59fa2c2b6..e7c6b06980cd34 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -537,6 +537,7 @@ pub enum SideExitReason { SplatKwNotProfiled, DirectiveInduced, SendWhileTracing, + NoProfileSend, } #[derive(Debug, Clone, Copy)] @@ -1070,7 +1071,9 @@ pub enum Insn { PatchPoint { invariant: Invariant, state: InsnId }, /// Side-exit into the interpreter. - SideExit { state: InsnId, reason: SideExitReason }, + /// If `recompile` is set, the side exit will profile the send and invalidate the ISEQ + /// so that it gets recompiled with the new profile data. + SideExit { state: InsnId, reason: SideExitReason, recompile: Option }, /// Increment a counter in ZJIT stats IncrCounter(Counter), @@ -2061,7 +2064,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { 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, .. } => write!(f, "SideExit {reason}"), + Insn::SideExit { reason, recompile, .. } => { + if recompile.is_some() { + write!(f, "SideExit {reason} recompile") + } else { + write!(f, "SideExit {reason}") + } + } Insn::PutSpecialObject { value_type } => write!(f, "PutSpecialObject {value_type}"), Insn::Throw { throw_state, val, .. } => { write!(f, "Throw ")?; @@ -2846,16 +2855,16 @@ impl Function { /// Update DynamicSendReason for the instruction at insn_id fn set_dynamic_send_reason(&mut self, insn_id: InsnId, dynamic_send_reason: SendFallbackReason) { use Insn::*; - if get_option!(stats) || get_option!(dump_hir_opt).is_some() || cfg!(test) { - match self.insns.get_mut(insn_id.0).unwrap() { - Send { reason, .. } - | SendForward { reason, .. } - | InvokeSuper { reason, .. } - | InvokeSuperForward { reason, .. } - | InvokeBlock { reason, .. } - => *reason = dynamic_send_reason, - _ => unreachable!("unexpected instruction {} at {insn_id}", self.find(insn_id)) - } + // Always set the reason: convert_no_profile_sends depends on it to identify + // sends that should be converted to side exits for exit-based recompilation. + match self.insns.get_mut(insn_id.0).unwrap() { + Send { reason, .. } + | SendForward { reason, .. } + | InvokeSuper { reason, .. } + | InvokeSuperForward { reason, .. } + | InvokeBlock { reason, .. } + => *reason = dynamic_send_reason, + _ => unreachable!("unexpected instruction {} at {insn_id}", self.find(insn_id)) } } @@ -3565,10 +3574,8 @@ impl Function { continue; } ReceiverTypeResolution::NoProfile => { - if get_option!(stats) { - let reason = if has_block { SendNoProfiles } else { SendWithoutBlockNoProfiles }; - self.set_dynamic_send_reason(insn_id, reason); - } + let reason = if has_block { SendNoProfiles } else { SendWithoutBlockNoProfiles }; + self.set_dynamic_send_reason(insn_id, reason); self.push_insn_id(block, insn_id); continue; } @@ -4993,6 +5000,31 @@ impl Function { self.infer_types(); } + /// Convert `Send` instructions with no profile data into `SideExit` with recompile info. + /// This runs after strength reduction passes (type_specialize, inline, optimize_c_calls) so + /// that sends that can be optimized without profiling (e.g. known CFUNCs) are already handled. + /// The remaining no-profile sends are turned into side exits that trigger recompilation with + /// fresh profile data. + fn convert_no_profile_sends(&mut self) { + for block in self.rpo() { + let old_insns = std::mem::take(&mut self.blocks[block.0].insns); + 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, .. } => { + let argc = unsafe { vm_ci_argc((*cd).ci) } as i32; + self.push_insn(block, Insn::SideExit { state, reason: SideExitReason::NoProfileSend, recompile: Some(argc) }); + // SideExit is a terminator; don't add remaining instructions + break; + } + _ => { + self.push_insn_id(block, insn_id); + } + } + } + } + } + fn optimize_load_store(&mut self) { for block in self.rpo() { let mut compile_time_heap: HashMap<(InsnId, i32), InsnId> = HashMap::new(); @@ -5130,7 +5162,7 @@ impl Function { self.make_equal_to(insn_id, left); continue }, - (Some(_), Some(_)) => self.new_insn(Insn::SideExit { state, reason }), + (Some(_), Some(_)) => self.new_insn(Insn::SideExit { state, reason, recompile: None }), _ => insn_id, } }, @@ -5700,6 +5732,7 @@ impl Function { (inline) => { Counter::compile_hir_strength_reduce_time_ns }; (optimize_getivar) => { Counter::compile_hir_strength_reduce_time_ns }; (optimize_c_calls) => { Counter::compile_hir_strength_reduce_time_ns }; + (convert_no_profile_sends) => { Counter::compile_hir_strength_reduce_time_ns }; // End strength reduction bucket (optimize_load_store) => { Counter::compile_hir_optimize_load_store_time_ns }; (fold_constants) => { Counter::compile_hir_fold_constants_time_ns }; @@ -5732,6 +5765,7 @@ impl Function { run_pass!(inline); run_pass!(optimize_getivar); run_pass!(optimize_c_calls); + run_pass!(convert_no_profile_sends); run_pass!(optimize_load_store); run_pass!(fold_constants); run_pass!(clean_cfg); @@ -6385,6 +6419,11 @@ pub struct FrameState { } impl FrameState { + /// Get the YARV instruction index for the current instruction + pub fn insn_idx(&self) -> usize { + self.insn_idx + } + /// Return itself without locals. Useful for side-exiting without spilling locals. fn without_locals(&self) -> Self { let mut state = self.clone(); @@ -6997,13 +7036,13 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } _ => { // Unknown opcode; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledNewarraySend(method) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledNewarraySend(method), recompile: None }); break; // End the block } }; if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, ARRAY_REDEFINED_OP_FLAG) } { // If the basic operation is already redefined, we cannot optimize it. - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }), recompile: None }); break; // End the block } fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }, state: exit_id }); @@ -7025,12 +7064,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let bop = match method_id { x if x == ID!(include_p).0 => BOP_INCLUDE_P, _ => { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledDuparraySend(method_id) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledDuparraySend(method_id), recompile: None }); break; }, }; if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, ARRAY_REDEFINED_OP_FLAG) } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }), recompile: None }); break; } fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }, state: exit_id }); @@ -7074,11 +7113,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { .and_then(|types| types.first()) .map(|dist| TypeDistributionSummary::new(dist)); let Some(summary) = summary else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwNotProfiled }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwNotProfiled, recompile: None }); break; // End the block }; if !summary.is_monomorphic() { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwPolymorphic }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwPolymorphic, recompile: None }); break; // End the block } let ty = Type::from_profiled_type(summary.bucket(0)); @@ -7087,7 +7126,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } else if ty.is_subtype(types::HashExact) { fun.push_insn(block, Insn::GuardType { val: hash, guard_type: types::HashExact, state: exit_id }) } else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwNotNilOrHash }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwNotNilOrHash, recompile: None }); break; // End the block }; state.stack_push(obj); @@ -7145,7 +7184,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // This can only happen in iseqs taking more than 32 keywords. // In this case, we side exit to the interpreter. if unsafe {(*rb_get_iseq_body_param_keyword(iseq)).num >= VM_KW_SPECIFIED_BITS_MAX.try_into().unwrap()} { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::TooManyKeywordParameters }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::TooManyKeywordParameters, recompile: None }); break; } let ep_offset = get_arg(pc, 0).as_u32(); @@ -7563,14 +7602,14 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let flags = unsafe { rb_vm_ci_flag(call_info) }; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle the call type; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let args = state.stack_pop_n(argc as usize)?; @@ -7586,7 +7625,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); state.stack_push(recv); } else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }), recompile: None }); break; // End the block } } @@ -7598,7 +7637,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); state.stack_push(recv); } else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }), recompile: None }); break; // End the block } } @@ -7610,7 +7649,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); state.stack_push(recv); } else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }), recompile: None }); break; // End the block } } @@ -7622,7 +7661,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); state.stack_push(recv); } else { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::PatchPoint(Invariant::BOPRedefined { klass, bop }), recompile: None }); break; // End the block } } @@ -7668,7 +7707,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let flags = unsafe { rb_vm_ci_flag(call_info) }; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle tailcall; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7685,7 +7724,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { if mid == ID!(induce_side_exit_bang) && state::zjit_module_method_match_serial(ID!(induce_side_exit_bang), &state::INDUCE_SIDE_EXIT_SERIAL) { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::DirectiveInduced }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::DirectiveInduced, recompile: None }); break; // End the block } if mid == ID!(induce_compile_failure_bang) @@ -7704,7 +7743,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } @@ -7791,12 +7830,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let flags = unsafe { rb_vm_ci_flag(call_info) }; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle tailcall; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7830,12 +7869,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let forwarding = (flags & VM_CALL_FORWARDING) != 0; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle the call type; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7864,12 +7903,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let flags = unsafe { rb_vm_ci_flag(call_info) }; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle tailcall; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7903,12 +7942,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let forwarding = (flags & VM_CALL_FORWARDING) != 0; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle tailcall; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7938,12 +7977,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let flags = unsafe { rb_vm_ci_flag(call_info) }; if let Err(call_type) = unhandled_call_type(flags) { // Can't handle tailcall; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -7970,7 +8009,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // TODO: We only really need this if self_val is a class/module if !fun.assume_single_ractor_mode(block, exit_id) { // gen_getivar assumes single Ractor; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode), recompile: None }); break; // End the block } if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) { @@ -8025,7 +8064,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // TODO: We only really need this if self_val is a class/module if !fun.assume_single_ractor_mode(block, exit_id) { // gen_setivar assumes single Ractor; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode), recompile: None }); break; // End the block } let val = state.stack_pop()?; @@ -8142,7 +8181,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { if svar == 0 { // TODO: Handle non-backref - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnknownSpecialVariable(key) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnknownSpecialVariable(key), recompile: None }); // End the block break; } else if svar & 0x01 != 0 { @@ -8165,7 +8204,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // (reverse?) // // Unhandled opcode; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode), recompile: None }); break; // End the block } let val = state.stack_pop()?; @@ -8183,7 +8222,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } _ => { // Unhandled opcode; side-exit into the interpreter - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode), recompile: None }); break; // End the block } } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index af438c361b8af9..610748c818608b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4740,7 +4740,7 @@ mod hir_opt_tests { v18:CInt64 = LoadField v15, :_env_data_index_specval@0x1002 v19:CInt64 = GuardAnyBitSet v18, CUInt64(1) v20:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v22:BasicObject = Send v9, 0x1001, :tap, v20 # SendFallbackReason: Uncategorized(send) + v22:BasicObject = Send v9, 0x1001, :tap, v20 # SendFallbackReason: Send: no profile data available CheckInterrupts Return v22 "); @@ -6254,9 +6254,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v17:Fixnum[1] = Const Value(1) v19:Fixnum[10] = Const Value(10) - v23:BasicObject = Send v10, :[]=, v17, v19 # SendFallbackReason: Uncategorized(opt_aset) - CheckInterrupts - Return v19 + SideExit NoProfileSend recompile "); } @@ -10506,9 +10504,7 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :y@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v18:BasicObject = Send v12, :^ # SendFallbackReason: Uncategorized(opt_send_without_block) - CheckInterrupts - Return v18 + SideExit NoProfileSend recompile "); } @@ -11365,7 +11361,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:StaticSymbol[:the_block] = Const Value(VALUE(0x1000)) - v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Uncategorized(send) + v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Send: no profile data available CheckInterrupts Return v13 "); @@ -14860,4 +14856,92 @@ mod hir_opt_tests { Return v13 "); } + + #[test] + fn test_recompile_no_profile_send() { + // Define a callee method and a test method that calls it + eval(" + def greet_recompile(x) = x.to_s + def test_no_profile_recompile(flag) + if flag + greet_recompile(42) + else + 'hello' + end + end + "); + + // With call_threshold=2, num_profiles=1: + // 1st call profiles (flag=false, so greet is never reached) + // 2nd call compiles (greet has no profile data -> SideExit recompile) + eval("test_no_profile_recompile(false); test_no_profile_recompile(false)"); + + // The first compilation should have SideExit NoProfileSend recompile + // for the greet_recompile(42) callsite since it was never profiled. + assert_snapshot!(hir_string("test_no_profile_recompile"), @r" + fn test_no_profile_recompile@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :flag@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :flag@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + CheckInterrupts + v16:CBool = Test v10 + v17:Falsy = RefineType v10, Falsy + IfFalse v16, bb4(v9, v17) + v19:Truthy = RefineType v10, Truthy + v23:Fixnum[42] = Const Value(42) + SideExit NoProfileSend recompile + bb4(v30:BasicObject, v31:Falsy): + v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v36:StringExact = StringCopy v35 + CheckInterrupts + Return v36 + "); + + // Now call with flag=true. This hits the SideExit, which profiles + // the send and invalidates the ISEQ for recompilation. + eval("test_no_profile_recompile(true)"); + + // After profiling via the side exit, rebuilding HIR should now + // have a SendDirect for greet_recompile instead of SideExit. + assert_snapshot!(hir_string("test_no_profile_recompile"), @r" + fn test_no_profile_recompile@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :flag@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :flag@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + CheckInterrupts + v16:CBool = Test v10 + v17:Falsy = RefineType v10, Falsy + IfFalse v16, bb4(v9, v17) + v19:Truthy = RefineType v10, Truthy + v23:Fixnum[42] = Const Value(42) + PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) + v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + CheckInterrupts + Return v44 + bb4(v30:BasicObject, v31:Falsy): + v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v36:StringExact = StringCopy v35 + CheckInterrupts + Return v36 + "); + } } diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index a4981156a6c756..fd2ccdeb5f11e8 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -427,6 +427,31 @@ impl IseqProfile { .ok().map(|i| &self.entries[i]) } + /// 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. + /// Check if enough profiles have been gathered for this instruction. + pub fn done_profiling_at(&self, insn_idx: usize) -> bool { + self.entry(insn_idx).map_or(false, |e| e.num_profiles >= get_option!(num_profiles)) + } + + pub fn profile_send_at(&mut self, iseq: IseqPtr, insn_idx: usize, 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(-1 - (n - i - 1) as isize) }; + let ty = ProfiledType::new(obj); + VALUE::from(iseq).write_barrier(ty.class()); + entry.opnd_types[i].observe(ty); + } + entry.num_profiles = entry.num_profiles.saturating_add(1); + entry.num_profiles == get_option!(num_profiles) + } + /// Get profiled operand types for a given instruction index pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[TypeDistribution]> { self.entry(insn_idx).map(|e| e.opnd_types.as_slice()).filter(|s| !s.is_empty()) diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 28bd6238939183..bd36464bb73a32 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -229,6 +229,7 @@ make_counters! { exit_block_param_proxy_not_nil, exit_block_param_wb_required, exit_too_many_keyword_parameters, + exit_no_profile_send, exit_splatkw_not_nil_or_hash, exit_splatkw_polymorphic, exit_splatkw_not_profiled, @@ -626,6 +627,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { PatchPoint(Invariant::RootBoxOnly) => exit_patchpoint_root_box_only, SendWhileTracing => exit_send_while_tracing, + NoProfileSend => exit_no_profile_send, } } From 7dff7b351e7081936673f4188768111699f5af91 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 24 Mar 2026 22:39:11 -0700 Subject: [PATCH 06/18] ZJIT: Unify invalidation logic into invalidate_iseq_version() Extract the duplicated invalidation + JIT-to-JIT call recompilation logic from both PatchPoint handling and no-profile-send recompilation into a single invalidate_iseq_version() function. This is a starting point toward a cohesive "lifecycle of a JITed method" state machine where all compile/recompile decisions flow through one place. --- zjit/src/codegen.rs | 39 +++++++++++++++++++++++---------------- zjit/src/invariants.rs | 21 +++++---------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index bbdb1c986f6931..cf6155d8d74de9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -193,6 +193,26 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) Ok(start_ptr) } +/// Invalidate an ISEQ version and allow it to be recompiled on the next call. +/// Both PatchPoint invalidation and exit-profiling recompilation go through this +/// function, serving as the central point for all invalidation/recompile decisions. +pub fn invalidate_iseq_version(cb: &mut CodeBlock, iseq: IseqPtr, version: &mut IseqVersionRef) { + let payload = get_or_create_iseq_payload(iseq); + if unsafe { version.as_ref() }.status != IseqStatus::Invalidated + && payload.versions.len() < MAX_ISEQ_VERSIONS + { + unsafe { version.as_mut() }.status = IseqStatus::Invalidated; + unsafe { rb_iseq_reset_jit_func(iseq) }; + + // Recompile JIT-to-JIT calls into the invalidated ISEQ + for incoming in unsafe { version.as_ref() }.incoming.iter() { + if let Err(err) = gen_iseq_call(cb, incoming) { + debug!("{err:?}: gen_iseq_call failed during invalidation: {}", iseq_get_location(incoming.iseq.get(), 0)); + } + } + } +} + /// Stub a branch for a JIT-to-JIT call pub fn gen_iseq_call(cb: &mut CodeBlock, iseq_call: &IseqCallRef) -> Result<(), CompileError> { // Compile a function stub @@ -2891,23 +2911,10 @@ c_callable! { // Once we have enough profiles, invalidate and recompile the ISEQ if should_recompile { - let num_versions = payload.versions.len(); if let Some(version) = payload.versions.last_mut() { - if unsafe { version.as_ref() }.status != IseqStatus::Invalidated - && num_versions < MAX_ISEQ_VERSIONS - { - unsafe { version.as_mut() }.status = IseqStatus::Invalidated; - unsafe { rb_iseq_reset_jit_func(iseq) }; - - // Recompile JIT-to-JIT calls into the invalidated ISEQ - let cb = ZJITState::get_code_block(); - for incoming in unsafe { version.as_ref() }.incoming.iter() { - if let Err(err) = gen_iseq_call(cb, incoming) { - debug!("{err:?}: gen_iseq_call failed on no-profile recompile: {}", iseq_get_location(incoming.iseq.get(), 0)); - } - } - cb.mark_all_executable(); - } + let cb = ZJITState::get_code_block(); + invalidate_iseq_version(cb, iseq, version); + cb.mark_all_executable(); } } }); diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 2b11c4116d18a8..c7dad37b5e8e5f 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -3,9 +3,9 @@ use std::{collections::{HashMap, HashSet}, mem}; use crate::{backend::lir::{Assembler, asm_comment}, cruby::{ID, IseqPtr, RedefinitionFlag, VALUE, iseq_name, rb_callable_method_entry_t, rb_gc_location, ruby_basic_operators, src_loc, with_vm_lock}, hir::Invariant, options::debug, state::{ZJITState, zjit_enabled_p}, virtualmem::CodePtr}; -use crate::payload::{IseqVersionRef, IseqStatus, get_or_create_iseq_payload}; -use crate::codegen::{MAX_ISEQ_VERSIONS, gen_iseq_call}; -use crate::cruby::{rb_iseq_reset_jit_func, iseq_get_location}; +use crate::payload::{IseqVersionRef, get_or_create_iseq_payload}; +use crate::codegen::invalidate_iseq_version; +use crate::cruby::rb_iseq_reset_jit_func; use crate::stats::with_time_stat; use crate::stats::Counter::invalidation_time_ns; use crate::gc::remove_gc_offsets; @@ -27,21 +27,10 @@ macro_rules! compile_patch_points { let mut version = patch_point.version; let iseq = unsafe { version.as_ref() }.iseq; if !iseq.is_null() { - let payload = get_or_create_iseq_payload(iseq); - // If the ISEQ doesn't have max versions, invalidate this version. - if unsafe { version.as_ref() }.status != IseqStatus::Invalidated && payload.versions.len() < MAX_ISEQ_VERSIONS { - unsafe { version.as_mut() }.status = IseqStatus::Invalidated; - unsafe { rb_iseq_reset_jit_func(version.as_ref().iseq) }; - - // Recompile JIT-to-JIT calls into the invalidated ISEQ - for incoming in unsafe { version.as_ref() }.incoming.iter() { - if let Err(err) = gen_iseq_call($cb, incoming) { - debug!("{err:?}: gen_iseq_call failed on PatchPoint: {}", iseq_get_location(incoming.iseq.get(), 0)); - } - } - } + invalidate_iseq_version($cb, iseq, &mut version); // Remember NoSingletonClass busts on the payload if is_no_singleton_class!($cause) { + let payload = get_or_create_iseq_payload(iseq); payload.was_invalidated_for_singleton_class_creation = true; } } From 75a396c6d3d298bcb6079eb40a7d6ae67d7ad5bd Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 07:49:47 -0700 Subject: [PATCH 07/18] ZJIT: Count profiles remaining down instead of up Count down from --zjit-num-profiles to 0 instead of counting up. This avoids reading options state in done_profiling_at and profile_send_at at check time. Also simplify the stack offset arithmetic in profile_send_at: -1 - (n - i - 1) simplifies to (i - n). --- zjit/src/profile.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index fd2ccdeb5f11e8..1ea27dcb45cee3 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -103,10 +103,10 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { _ => {} } - // Once we profile the instruction num_profiles times, we stop profiling it. + // Once we profile the instruction enough times, we stop profiling it. let entry = profile.entry_mut(profiler.insn_idx); - entry.num_profiles = entry.num_profiles.saturating_add(1); - if entry.num_profiles == get_option!(num_profiles) { + entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); + if entry.profiles_remaining == 0 { unsafe { rb_zjit_iseq_insn_set(profiler.iseq, profiler.insn_idx as u32, bare_opcode); } } } @@ -382,8 +382,8 @@ struct ProfileEntry { insn_idx: u32, /// Type information of YARV instruction operands opnd_types: Vec, - /// Number of profiled executions for this YARV instruction - num_profiles: NumProfiles, + /// Number of profiles remaining before recompilation. Counts down from --zjit-num-profiles. + profiles_remaining: NumProfiles, } #[derive(Debug)] @@ -413,7 +413,7 @@ impl IseqProfile { self.entries.insert(i, ProfileEntry { insn_idx: idx, opnd_types: Vec::new(), - num_profiles: 0, + profiles_remaining: get_option!(num_profiles), }); &mut self.entries[i] } @@ -427,15 +427,15 @@ impl IseqProfile { .ok().map(|i| &self.entries[i]) } - /// 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. /// Check if enough profiles have been gathered for this instruction. pub fn done_profiling_at(&self, insn_idx: usize) -> bool { - self.entry(insn_idx).map_or(false, |e| e.num_profiles >= get_option!(num_profiles)) + 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: usize, sp: *const VALUE, argc: usize) -> bool { let n = argc + 1; // args + receiver let entry = self.entry_mut(insn_idx); @@ -443,13 +443,13 @@ impl IseqProfile { entry.opnd_types.resize(n, TypeDistribution::new()); } for i in 0..n { - let obj = unsafe { *sp.offset(-1 - (n - i - 1) as isize) }; + 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.num_profiles = entry.num_profiles.saturating_add(1); - entry.num_profiles == get_option!(num_profiles) + entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); + entry.profiles_remaining == 0 } /// Get profiled operand types for a given instruction index From 532a426cd19bae66e587cabe3bcda25713465cbe Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 07:50:03 -0700 Subject: [PATCH 08/18] ZJIT: Add TODO for handle_event state machine on invalidate_iseq_version --- zjit/src/codegen.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index cf6155d8d74de9..5655997fb4e1fc 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -196,6 +196,10 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) /// Invalidate an ISEQ version and allow it to be recompiled on the next call. /// Both PatchPoint invalidation and exit-profiling recompilation go through this /// function, serving as the central point for all invalidation/recompile decisions. +/// +/// TODO: evolve this into a general `handle_event(iseq, event)` state machine that +/// handles all compile lifecycle events (interpreter profiles, JIT profiles, invalidation, +/// GC) so that all compile/recompile tuning decisions live in one place. pub fn invalidate_iseq_version(cb: &mut CodeBlock, iseq: IseqPtr, version: &mut IseqVersionRef) { let payload = get_or_create_iseq_payload(iseq); if unsafe { version.as_ref() }.status != IseqStatus::Invalidated From e756863df2e14b93fdfbf52102bc53527194036d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 07:50:13 -0700 Subject: [PATCH 09/18] ZJIT: Document that SideExitRecompile::argc does not include receiver --- zjit/src/backend/lir.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 207538ec570f71..76b11f26b2613b 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -561,6 +561,7 @@ pub struct SideExit { pub struct SideExitRecompile { pub iseq: Opnd, pub insn_idx: u32, + /// Number of arguments, not including the receiver. pub argc: i32, } From 15dcd9c067331b83c333f8b19e479012b4def612 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 13:36:07 -0700 Subject: [PATCH 10/18] Rescue exceptions from Tempfile#closed? in LeakChecker (#16549) ObjectSpace.each_object(Tempfile) can find partially-constructed Tempfile objects where @delegate_dc_obj is not yet set (between ivar initialization and the super() call in Tempfile#initialize). Calling closed? on such objects raises ArgumentError ("not delegated") from DelegateClass's __getobj__. Under rare conditions (observed in ZJIT CI with --zjit-call-threshold=1), this can also trigger SystemStackError. Use `rescue true` to treat such tempfiles as closed, avoiding test worker crashes in the leak checker. --- tool/lib/leakchecker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb index 69df9a64b87192..513ffd23eb5f7c 100644 --- a/tool/lib/leakchecker.rb +++ b/tool/lib/leakchecker.rb @@ -156,7 +156,7 @@ def find_tempfiles(prev_count=-1) [prev_count, []] else tempfiles = ObjectSpace.each_object(Tempfile).reject {|t| - t.instance_variables.empty? || t.closed? + t.instance_variables.empty? || (t.closed? rescue true) } [count, tempfiles] end From 600fea42b875947aa92184a63da1de8b3b0e53a4 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Wed, 18 Mar 2026 21:45:47 +0100 Subject: [PATCH 11/18] [ruby/rubygems] Cache package version selection: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ### Problem Selecting a version for a given package is a extremelly hot path. In a very large gemfile and a deep dependency tree (like the one we have in our monolith at Shopify), this codepath is hit around 3.2 million times in total during the resolution phase. ### Context When the resolution starts, Bundler fetch and turn every possible versions of a gem that was ever released on Rubygems.org into a possible candidate. We end up with a massive matrix of possibilites that PubGrub has to go through. In the case of a large Gemfile like we have, we end up with ~55,000 candidates. Many of this candidate have conflicting dependencies requirements and as pubgrub progress, it will continously ask over and over the same things: "Return the possible candidates given this version constraint" (`range.select_versions(@sorted_versions[package])`). Since this path is called so frequently (sometimes more than 8000 times for a single candidate and the same constraint), the returned value can be cached for faster access. ### Solution Cache the selected versions for a given constraint in a hash. The key being an array where the first element is the package we want to resolve and the second element is the constraint. The associated value is all possible candidates matching. If the resolver end up not finding a candidate (in example you run `bundle install --prefer-local`) then Bundler will allow finding candidates on the remote. In this case we need to invalidate the cache as otherwise the candidates from the remotes will not be considered. ### Benchmark This change has a huge impact on resolution time. Those measures were taken on Shopify monolith by removing the lockfile and measuring only resolution time (no network or external factors affect these results.) ┌─────┬──────────┬───────────┬─────────┐ │ Run │ Original │ Optimized │ Speedup │ ├─────┼──────────┼───────────┼─────────┤ │ 1 │ 19.20s │ 10.32s │ 46.3% │ ├─────┼──────────┼───────────┼─────────┤ │ 2 │ 19.17s │ 10.46s │ 45.4% │ ├─────┼──────────┼───────────┼─────────┤ │ 3 │ 18.95s │ 10.29s │ 45.7% │ ├─────┼──────────┼───────────┼─────────┤ │ 4 │ 19.17s │ 10.37s │ 45.9% │ ├─────┼──────────┼───────────┼─────────┤ │ 5 │ 19.26s │ 10.30s │ 46.5% │ ├─────┼──────────┼───────────┼─────────┤ │ Avg │ 19.15s │ 10.35s │ 46.0% │ └─────┴──────────┴───────────┴─────────┘ https://github.com/ruby/rubygems/commit/a4f5973f95 --- lib/bundler/resolver/strategy.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/bundler/resolver/strategy.rb b/lib/bundler/resolver/strategy.rb index 4f343bf0ce7224..7519d38968c043 100644 --- a/lib/bundler/resolver/strategy.rb +++ b/lib/bundler/resolver/strategy.rb @@ -5,6 +5,7 @@ class Resolver class Strategy def initialize(source) @source = source + @package_priority_cache = {} end def next_package_and_version(unsatisfied) @@ -17,10 +18,12 @@ def next_package_and_version(unsatisfied) def next_term_to_try_from(unsatisfied) unsatisfied.min_by do |package, range| - matching_versions = @source.versions_for(package, range) - higher_versions = @source.versions_for(package, range.upper_invert) + @package_priority_cache[[package, range]] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) - [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end end end From 84d71b88ad57995dd206a3c2211973985e162774 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 24 Mar 2026 23:05:54 -0700 Subject: [PATCH 12/18] ZJIT: Use 2-space Ruby indentation consistently in codegen_tests.rs Some tests migrated from test_zjit.rb retained 4-space indentation for Ruby code inside string literals. Normalize to 2-space indentation to match the convention used by most tests (e.g. test_getglobal_with_warning, test_duphash). --- zjit/src/codegen_tests.rs | 190 +++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 660119fb15b981..53d539cff43510 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2278,7 +2278,7 @@ fn test_opt_new_invalidate_new() { fn test_opt_newarray_send_include_p() { eval(" def test(x) - [:y, 1, Object.new].include?(x) + [:y, 1, Object.new].include?(x) end test(1) "); @@ -2290,19 +2290,19 @@ fn test_opt_newarray_send_include_p() { fn test_opt_newarray_send_include_p_redefined() { eval(" class Array - alias_method :old_include?, :include? - def include?(x) - old_include?(x) ? :true : :false - end + alias_method :old_include?, :include? + def include?(x) + old_include?(x) ? :true : :false + end end def test(x) - [:y, 1, Object.new].include?(x) + [:y, 1, Object.new].include?(x) end "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); assert_snapshot!(inspect(" def test(x) - [:y, 1, Object.new].include?(x) + [:y, 1, Object.new].include?(x) end test(1) [test(1), test(\"n\")] @@ -2313,7 +2313,7 @@ fn test_opt_newarray_send_include_p_redefined() { fn test_opt_duparray_send_include_p() { eval(" def test(x) - [:y, 1].include?(x) + [:y, 1].include?(x) end test(1) "); @@ -2325,19 +2325,19 @@ fn test_opt_duparray_send_include_p() { fn test_opt_duparray_send_include_p_redefined() { eval(" class Array - alias_method :old_include?, :include? - def include?(x) - old_include?(x) ? :true : :false - end + alias_method :old_include?, :include? + def include?(x) + old_include?(x) ? :true : :false + end end def test(x) - [:y, 1].include?(x) + [:y, 1].include?(x) end "); assert_contains_opcode("test", YARVINSN_opt_duparray_send); assert_snapshot!(inspect(" def test(x) - [:y, 1].include?(x) + [:y, 1].include?(x) end test(1) [test(1), test(\"n\")] @@ -2348,7 +2348,7 @@ fn test_opt_duparray_send_include_p_redefined() { fn test_opt_newarray_send_pack_buffer() { eval(r#" def test(num, buffer) - [num].pack('C', buffer:) + [num].pack('C', buffer:) end test(65, "") "#); @@ -2363,20 +2363,20 @@ fn test_opt_newarray_send_pack_buffer() { fn test_opt_newarray_send_pack_buffer_redefined() { eval(r#" class Array - alias_method :old_pack, :pack - def pack(fmt, buffer: nil) - old_pack(fmt, buffer: buffer) - "b" - end + alias_method :old_pack, :pack + def pack(fmt, buffer: nil) + old_pack(fmt, buffer: buffer) + "b" + end end def test(num, buffer) - [num].pack('C', buffer:) + [num].pack('C', buffer:) end "#); assert_contains_opcode("test", YARVINSN_opt_newarray_send); assert_snapshot!(inspect(r#" def test(num, buffer) - [num].pack('C', buffer:) + [num].pack('C', buffer:) end buf = "" test(65, buf) @@ -2389,7 +2389,7 @@ fn test_opt_newarray_send_pack_buffer_redefined() { fn test_opt_newarray_send_hash() { eval(" def test(x) - [1, 2, x].hash + [1, 2, x].hash end test(20) "); @@ -2402,7 +2402,7 @@ fn test_opt_newarray_send_hash_redefined() { eval(" Array.class_eval { def hash = 42 } def test(x) - [1, 2, x].hash + [1, 2, x].hash end test(20) "); @@ -2424,10 +2424,10 @@ fn test_opt_newarray_send_max() { fn test_opt_newarray_send_max_redefined() { eval(" class Array - alias_method :old_max, :max - def max - old_max * 2 - end + alias_method :old_max, :max + def max + old_max * 2 + end end def test(a,b) = [a,b].max "); @@ -2453,11 +2453,11 @@ fn test_new_hash_empty() { fn test_new_hash_nonempty() { eval(r#" def test - key = "key" - value = "value" - num = 42 - result = 100 - {key => value, num => result} + key = "key" + value = "value" + num = 42 + result = 100 + {key => value, num => result} end test "#); @@ -2479,7 +2479,7 @@ fn test_new_hash_single_key_value() { fn test_new_hash_with_computation() { eval(r#" def test(a, b) - {"sum" => a + b, "product" => a * b} + {"sum" => a + b, "product" => a * b} end test(2, 3) "#); @@ -2491,21 +2491,21 @@ fn test_new_hash_with_computation() { fn test_new_hash_with_user_defined_hash_method() { assert_snapshot!(inspect(r#" class CustomKey - attr_reader :val - def initialize(val) - @val = val - end - def hash - @val.hash - end - def eql?(other) - other.is_a?(CustomKey) && @val == other.val - end + attr_reader :val + def initialize(val) + @val = val + end + def hash + @val.hash + end + def eql?(other) + other.is_a?(CustomKey) && @val == other.val + end end def test - key = CustomKey.new("key") - hash = {key => "value"} - hash[key] == "value" + key = CustomKey.new("key") + hash = {key => "value"} + hash[key] == "value" end test test @@ -2516,23 +2516,23 @@ fn test_new_hash_with_user_defined_hash_method() { fn test_new_hash_with_user_hash_method_exception() { assert_snapshot!(inspect(r#" class BadKey - def hash - raise "Hash method failed!" - end + def hash + raise "Hash method failed!" + end end def test - key = BadKey.new - {key => "value"} + key = BadKey.new + {key => "value"} end begin - test + test rescue => e - e.class + e.class end begin - test + test rescue => e - e.class + e.class end "#), @"RuntimeError"); } @@ -2541,27 +2541,27 @@ fn test_new_hash_with_user_hash_method_exception() { fn test_new_hash_with_user_eql_method_exception() { assert_snapshot!(inspect(r#" class BadKey - def hash - 42 - end - def eql?(other) - raise "Eql method failed!" - end + def hash + 42 + end + def eql?(other) + raise "Eql method failed!" + end end def test - key1 = BadKey.new - key2 = BadKey.new - {key1 => "value1", key2 => "value2"} + key1 = BadKey.new + key2 = BadKey.new + {key1 => "value1", key2 => "value2"} end begin - test + test rescue => e - e.class + e.class end begin - test + test rescue => e - e.class + e.class end "#), @"RuntimeError"); } @@ -2586,7 +2586,7 @@ fn test_opt_hash_freeze() { fn test_opt_hash_freeze_rewritten() { eval(" class Hash - def freeze = 5 + def freeze = 5 end def test = {}.freeze test @@ -2599,7 +2599,7 @@ fn test_opt_hash_freeze_rewritten() { fn test_opt_aset_hash() { eval(" def test(h, k, v) - h[k] = v + h[k] = v end test({}, :key, 42) "); @@ -2611,7 +2611,7 @@ fn test_opt_aset_hash() { fn test_opt_aset_hash_returns_value() { assert_snapshot!(inspect(" def test(h, k, v) - h[k] = v + h[k] = v end test({}, :key, 100) test({}, :key, 100) @@ -2622,7 +2622,7 @@ fn test_opt_aset_hash_returns_value() { fn test_opt_aset_hash_string_key() { assert_snapshot!(inspect(r#" def test(h, k, v) - h[k] = v + h[k] = v end h = {} test(h, "foo", "bar") @@ -2636,7 +2636,7 @@ fn test_opt_aset_hash_subclass() { assert_snapshot!(inspect(" class MyHash < Hash; end def test(h, k, v) - h[k] = v + h[k] = v end h = MyHash.new test(h, :key, 42) @@ -2649,9 +2649,9 @@ fn test_opt_aset_hash_subclass() { fn test_opt_aset_hash_too_few_args() { assert_snapshot!(inspect(r#" def test(h) - h.[]= 123 + h.[]= 123 rescue ArgumentError - "ArgumentError" + "ArgumentError" end test({}) test({}) @@ -2662,9 +2662,9 @@ fn test_opt_aset_hash_too_few_args() { fn test_opt_aset_hash_too_many_args() { assert_snapshot!(inspect(r#" def test(h) - h[:a, :b] = :c + h[:a, :b] = :c rescue ArgumentError - "ArgumentError" + "ArgumentError" end test({}) test({}) @@ -2691,7 +2691,7 @@ fn test_opt_ary_freeze() { fn test_opt_ary_freeze_rewritten() { eval(" class Array - def freeze = 5 + def freeze = 5 end def test = [].freeze test @@ -2720,7 +2720,7 @@ fn test_opt_str_freeze() { fn test_opt_str_freeze_rewritten() { eval(" class String - def freeze = 5 + def freeze = 5 end def test = ''.freeze test @@ -2749,7 +2749,7 @@ fn test_opt_str_uminus() { fn test_opt_str_uminus_rewritten() { eval(" class String - def -@ = 5 + def -@ = 5 end def test = -'' test @@ -2857,9 +2857,9 @@ fn test_array_aref_non_fixnum_index() { test([1,2,3], 1) test([1,2,3], 1) begin - test([1,2,3], "1") + test([1,2,3], "1") rescue => e - e.class + e.class end "#), @"TypeError"); } @@ -2868,7 +2868,7 @@ fn test_array_aref_non_fixnum_index() { fn test_array_fixnum_aset() { eval(" def test(arr, idx) - arr[idx] = 7 + arr[idx] = 7 end test([1,2,3], 2) "); @@ -2880,7 +2880,7 @@ fn test_array_fixnum_aset() { fn test_array_fixnum_aset_returns_value() { eval(" def test(arr, idx) - arr[idx] = 7 + arr[idx] = 7 end test([1,2,3], 2) "); @@ -2892,7 +2892,7 @@ fn test_array_fixnum_aset_returns_value() { fn test_array_fixnum_aset_out_of_bounds() { assert_snapshot!(inspect(" def test(arr) - arr[5] = 7 + arr[5] = 7 end arr = [1,2,3] test(arr) @@ -2906,7 +2906,7 @@ fn test_array_fixnum_aset_out_of_bounds() { fn test_array_fixnum_aset_negative_index() { assert_snapshot!(inspect(" def test(arr) - arr[-1] = 7 + arr[-1] = 7 end arr = [1,2,3] test(arr) @@ -2920,7 +2920,7 @@ fn test_array_fixnum_aset_negative_index() { fn test_array_fixnum_aset_shared() { assert_snapshot!(inspect(" def test(arr, idx, val) - arr[idx] = val + arr[idx] = val end arr = (0..50).to_a test(arr, 0, -1) @@ -2935,16 +2935,16 @@ fn test_array_fixnum_aset_shared() { fn test_array_fixnum_aset_frozen() { assert_snapshot!(inspect(" def test(arr, idx, val) - arr[idx] = val + arr[idx] = val end arr = [1,2,3] test(arr, 1, 9) test(arr, 1, 9) arr.freeze begin - test(arr, 1, 9) + test(arr, 1, 9) rescue => e - e.class + e.class end "), @"FrozenError"); } @@ -2954,7 +2954,7 @@ fn test_array_fixnum_aset_array_subclass() { eval(" class MyArray < Array; end def test(arr, idx) - arr[idx] = 7 + arr[idx] = 7 end test(MyArray.new, 0) "); @@ -2966,14 +2966,14 @@ fn test_array_fixnum_aset_array_subclass() { fn test_array_aset_non_fixnum_index() { assert_snapshot!(inspect(r#" def test(arr, idx) - arr[idx] = 7 + arr[idx] = 7 end test([1,2,3], 0) test([1,2,3], 0) begin - test([1,2,3], "0") + test([1,2,3], "0") rescue => e - e.class + e.class end "#), @"TypeError"); } From d8353e1bc5c36832a47e710cfda1e3350640f921 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 24 Mar 2026 23:09:09 -0700 Subject: [PATCH 13/18] ZJIT: Add assert_compiles for codegen tests to assert successful compilation In the old Ruby-based tests, assert_compiles asserted that every compilation triggered by the test script succeeds, while assert_runs (now inspect) allowed compilation failures. This distinction was lost during the migration to Rust tests. Introduce assert_compiles() in Rust that temporarily enables ZJITState::assert_compiles during program evaluation, causing a panic if any compilation fails. Replace inspect() with assert_compiles() in 146 tests that compile successfully, keeping inspect() for 224 tests that intentionally test graceful compilation failures or side exits. --- zjit/src/codegen_tests.rs | 292 +++++++++++++++++++------------------- zjit/src/cruby.rs | 9 ++ zjit/src/state.rs | 6 + 3 files changed, 161 insertions(+), 146 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 53d539cff43510..703901b6bc30f4 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -76,7 +76,7 @@ fn test_putstring() { test "##); assert_contains_opcode("test", YARVINSN_putstring); - assert_snapshot!(inspect(r##"test"##), @r#""""#); + assert_snapshot!(assert_compiles(r##"test"##), @r#""""#); } #[test] @@ -86,7 +86,7 @@ fn test_putchilledstring() { test "#); assert_contains_opcode("test", YARVINSN_putchilledstring); - assert_snapshot!(inspect(r#"test"#), @r#""""#); + assert_snapshot!(assert_compiles(r#"test"#), @r#""""#); } #[test] @@ -118,7 +118,7 @@ fn test_getglobal_with_warning() { test "#); assert_contains_opcode("test", YARVINSN_getglobal); - assert_snapshot!(inspect(r#"test"#), @r#""rescued""#); + assert_snapshot!(assert_compiles(r#"test"#), @r#""rescued""#); } #[test] @@ -131,7 +131,7 @@ fn test_setglobal() { test "); assert_contains_opcode("test", YARVINSN_setglobal); - assert_snapshot!(inspect("test"), @"1"); + assert_snapshot!(assert_compiles("test"), @"1"); } #[test] @@ -143,7 +143,7 @@ fn test_string_intern() { test "#); assert_contains_opcode("test", YARVINSN_intern); - assert_snapshot!(inspect(r#"test"#), @":foo123"); + assert_snapshot!(assert_compiles(r#"test"#), @":foo123"); } #[test] @@ -155,7 +155,7 @@ fn test_duphash() { test "); assert_contains_opcode("test", YARVINSN_duphash); - assert_snapshot!(inspect("test"), @"{a: 1}"); + assert_snapshot!(assert_compiles("test"), @"{a: 1}"); } #[test] @@ -167,7 +167,7 @@ fn test_pushtoarray() { test "); assert_contains_opcode("test", YARVINSN_pushtoarray); - assert_snapshot!(inspect("test"), @"[1, 2, 3]"); + assert_snapshot!(assert_compiles("test"), @"[1, 2, 3]"); } #[test] @@ -179,7 +179,7 @@ fn test_splatarray_new_array() { test [1, 2] "); assert_contains_opcode("test", YARVINSN_splatarray); - assert_snapshot!(inspect("test [1, 2]"), @"[1, 2, 3]"); + assert_snapshot!(assert_compiles("test [1, 2]"), @"[1, 2, 3]"); } #[test] @@ -194,7 +194,7 @@ fn test_splatarray_existing_array() { test [3] "); assert_contains_opcode("test", YARVINSN_splatarray); - assert_snapshot!(inspect("test [3]"), @"[1, 2, 3]"); + assert_snapshot!(assert_compiles("test [3]"), @"[1, 2, 3]"); } #[test] @@ -206,7 +206,7 @@ fn test_concattoarray() { test 3 "); assert_contains_opcode("test", YARVINSN_concattoarray); - assert_snapshot!(inspect("test 3"), @"[1, 2, 3]"); + assert_snapshot!(assert_compiles("test 3"), @"[1, 2, 3]"); } #[test] @@ -223,7 +223,7 @@ fn test_definedivar() { test "); assert_contains_opcode("test", YARVINSN_definedivar); - assert_snapshot!(inspect("test"), @r#"[nil, "instance-variable", nil]"#); + assert_snapshot!(assert_compiles("test"), @r#"[nil, "instance-variable", nil]"#); } #[test] @@ -238,7 +238,7 @@ fn test_setglobal_with_trace_var_exception() { test "#); assert_contains_opcode("test", YARVINSN_setglobal); - assert_snapshot!(inspect(r#"test"#), @r#""rescued""#); + assert_snapshot!(assert_compiles(r#"test"#), @r#""rescued""#); } #[test] @@ -417,7 +417,7 @@ fn test_getblockparamproxy() { test { 1 } "); assert_contains_opcode("test", YARVINSN_getblockparamproxy); - assert_snapshot!(inspect("test { 1 }"), @"1"); + assert_snapshot!(assert_compiles("test { 1 }"), @"1"); } #[test] @@ -429,7 +429,7 @@ fn test_getblockparam() { test { 2 }.call "); assert_contains_opcode("test", YARVINSN_getblockparam); - assert_snapshot!(inspect("test { 2 }.call"), @"2"); + assert_snapshot!(assert_compiles("test { 2 }.call"), @"2"); } #[test] @@ -443,7 +443,7 @@ fn test_getblockparam_proxy_side_exit_restores_block_local() { test {} "); assert_contains_opcode("test", YARVINSN_getblockparam); - assert_snapshot!(inspect("test {}"), @"2"); + assert_snapshot!(assert_compiles("test {}"), @"2"); } #[test] @@ -458,7 +458,7 @@ fn test_getblockparam_used_twice_in_args() { test {1}.call "); assert_contains_opcode("test", YARVINSN_getblockparam); - assert_snapshot!(inspect("test {1}.call"), @"1"); + assert_snapshot!(assert_compiles("test {1}.call"), @"1"); } #[test] @@ -470,7 +470,7 @@ fn test_optimized_method_call_proc_call() { test(proc { |x| x * 2 }) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test(proc { |x| x * 2 })"), @"2"); + assert_snapshot!(assert_compiles("test(proc { |x| x * 2 })"), @"2"); } #[test] @@ -482,7 +482,7 @@ fn test_optimized_method_call_proc_aref() { test(proc { |x| x * 2 }) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(proc { |x| x * 2 })"), @"4"); + assert_snapshot!(assert_compiles("test(proc { |x| x * 2 })"), @"4"); } #[test] @@ -494,7 +494,7 @@ fn test_optimized_method_call_proc_yield() { test(proc { |x| x * 2 }) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test(proc { |x| x * 2 })"), @"6"); + assert_snapshot!(assert_compiles("test(proc { |x| x * 2 })"), @"6"); } #[test] @@ -506,7 +506,7 @@ fn test_optimized_method_call_proc_kw_splat() { test(proc { |**kw| kw[:a] + kw[:b] }, { a: 1, b: 2 }) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test(proc { |**kw| kw[:a] + kw[:b] }, { a: 1, b: 2 })"), @"3"); + assert_snapshot!(assert_compiles("test(proc { |**kw| kw[:a] + kw[:b] }, { a: 1, b: 2 })"), @"3"); } #[test] @@ -924,7 +924,7 @@ fn test_sendforward() { test(1, 2) "); assert_contains_opcode("test", YARVINSN_sendforward); - assert_snapshot!(inspect("test(1, 2)"), @"[1, 2]"); + assert_snapshot!(assert_compiles("test(1, 2)"), @"[1, 2]"); } #[test] @@ -1873,7 +1873,7 @@ fn test_opt_eq() { test(0, 2) # profile opt_eq "); assert_contains_opcode("test", YARVINSN_opt_eq); - assert_snapshot!(inspect("[test(1, 1), test(0, 1)]"), @"[true, false]"); + assert_snapshot!(assert_compiles("[test(1, 1), test(0, 1)]"), @"[true, false]"); } #[test] @@ -1883,7 +1883,7 @@ fn test_opt_eq_with_minus_one() { test(1) # profile opt_eq "); assert_contains_opcode("test", YARVINSN_opt_eq); - assert_snapshot!(inspect("[test(0), test(-1)]"), @"[false, true]"); + assert_snapshot!(assert_compiles("[test(0), test(-1)]"), @"[false, true]"); } #[test] @@ -1893,7 +1893,7 @@ fn test_opt_neq_dynamic() { test(0, 2) # profile opt_neq "); assert_contains_opcode("test", YARVINSN_opt_neq); - assert_snapshot!(inspect("[test(1, 1), test(0, 1)]"), @"[false, true]"); + assert_snapshot!(assert_compiles("[test(1, 1), test(0, 1)]"), @"[false, true]"); } #[test] @@ -2011,7 +2011,7 @@ fn test_opt_lt() { test(2, 3) # profile opt_lt "); assert_contains_opcode("test", YARVINSN_opt_lt); - assert_snapshot!(inspect("[test(0, 1), test(0, 0), test(1, 0)]"), @"[true, false, false]"); + assert_snapshot!(assert_compiles("[test(0, 1), test(0, 0), test(1, 0)]"), @"[true, false, false]"); } #[test] @@ -2021,7 +2021,7 @@ fn test_opt_lt_with_literal_lhs() { test(2) # profile opt_lt "); assert_contains_opcode("test", YARVINSN_opt_lt); - assert_snapshot!(inspect("[test(1), test(2), test(3)]"), @"[false, false, true]"); + assert_snapshot!(assert_compiles("[test(1), test(2), test(3)]"), @"[false, false, true]"); } #[test] @@ -2031,7 +2031,7 @@ fn test_opt_le() { test(2, 3) # profile opt_le "); assert_contains_opcode("test", YARVINSN_opt_le); - assert_snapshot!(inspect("[test(0, 1), test(0, 0), test(1, 0)]"), @"[true, true, false]"); + assert_snapshot!(assert_compiles("[test(0, 1), test(0, 0), test(1, 0)]"), @"[true, true, false]"); } #[test] @@ -2041,7 +2041,7 @@ fn test_opt_gt() { test(2, 3) # profile opt_gt "); assert_contains_opcode("test", YARVINSN_opt_gt); - assert_snapshot!(inspect("[test(0, 1), test(0, 0), test(1, 0)]"), @"[false, false, true]"); + assert_snapshot!(assert_compiles("[test(0, 1), test(0, 0), test(1, 0)]"), @"[false, false, true]"); } #[test] @@ -2050,7 +2050,7 @@ fn test_opt_empty_p() { def test(x) = x.empty? "); assert_contains_opcode("test", YARVINSN_opt_empty_p); - assert_snapshot!(inspect("[test([1]), test(\"1\"), test({})]"), @"[false, false, true]"); + assert_snapshot!(assert_compiles("[test([1]), test(\"1\"), test({})]"), @"[false, false, true]"); } #[test] @@ -2059,7 +2059,7 @@ fn test_opt_succ() { def test(obj) = obj.succ "); assert_contains_opcode("test", YARVINSN_opt_succ); - assert_snapshot!(inspect(r#"[test(-1), test("A")]"#), @r#"[0, "B"]"#); + assert_snapshot!(assert_compiles(r#"[test(-1), test("A")]"#), @r#"[0, "B"]"#); } #[test] @@ -2068,7 +2068,7 @@ fn test_opt_and() { def test(x, y) = x & y "); assert_contains_opcode("test", YARVINSN_opt_and); - assert_snapshot!(inspect("[test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3])]"), @"[1, [3, 2, 1]]"); + assert_snapshot!(assert_compiles("[test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3])]"), @"[1, [3, 2, 1]]"); } #[test] @@ -2077,7 +2077,7 @@ fn test_opt_or() { def test(x, y) = x | y "); assert_contains_opcode("test", YARVINSN_opt_or); - assert_snapshot!(inspect("[test(0b1000, 3), test([3, 2, 1], [1, 2, 3])]"), @"[11, [3, 2, 1]]"); + assert_snapshot!(assert_compiles("[test(0b1000, 3), test([3, 2, 1], [1, 2, 3])]"), @"[11, [3, 2, 1]]"); } #[test] @@ -2086,7 +2086,7 @@ fn test_fixnum_and() { def test(a, b) = a & b "); assert_contains_opcode("test", YARVINSN_opt_and); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" [ test(5, 3), test(0b011, 0b110), @@ -2101,7 +2101,7 @@ fn test_fixnum_and_side_exit() { def test(a, b) = a & b "); assert_contains_opcode("test", YARVINSN_opt_and); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" [ test(2, 2), test(0b011, 0b110), @@ -2116,7 +2116,7 @@ fn test_fixnum_or() { def test(a, b) = a | b "); assert_contains_opcode("test", YARVINSN_opt_or); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" [ test(5, 3), test(1, 2), @@ -2131,7 +2131,7 @@ fn test_fixnum_or_side_exit() { def test(a, b) = a | b "); assert_contains_opcode("test", YARVINSN_opt_or); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" [ test(1, 2), test(2, 2), @@ -2173,7 +2173,7 @@ fn test_fixnum_mul() { test(4) "); assert_contains_opcode("test", YARVINSN_opt_mult); - assert_snapshot!(inspect("test(4)"), @"12"); + assert_snapshot!(assert_compiles("test(4)"), @"12"); } #[test] @@ -2184,7 +2184,7 @@ fn test_fixnum_div() { test(4) "); assert_contains_opcode("test", YARVINSN_opt_div); - assert_snapshot!(inspect("test(4)"), @"12"); + assert_snapshot!(assert_compiles("test(4)"), @"12"); } #[test] @@ -2195,7 +2195,7 @@ fn test_fixnum_floor() { test(4) "); assert_contains_opcode("test", YARVINSN_opt_div); - assert_snapshot!(inspect("test(4)"), @"0"); + assert_snapshot!(assert_compiles("test(4)"), @"0"); } #[test] @@ -2204,7 +2204,7 @@ fn test_opt_not() { def test(obj) = !obj "); assert_contains_opcode("test", YARVINSN_opt_not); - assert_snapshot!(inspect("[test(nil), test(false), test(0)]"), @"[true, true, false]"); + assert_snapshot!(assert_compiles("[test(nil), test(false), test(0)]"), @"[true, true, false]"); } #[test] @@ -2213,7 +2213,7 @@ fn test_opt_regexpmatch2() { def test(haystack) = /needle/ =~ haystack "); assert_contains_opcode("test", YARVINSN_opt_regexpmatch2); - assert_snapshot!(inspect(r#"[test("kneedle"), test("")]"#), @"[1, nil]"); + assert_snapshot!(assert_compiles(r#"[test("kneedle"), test("")]"#), @"[1, nil]"); } #[test] @@ -2223,7 +2223,7 @@ fn test_opt_ge() { test(2, 3) # profile opt_ge "); assert_contains_opcode("test", YARVINSN_opt_ge); - assert_snapshot!(inspect("[test(0, 1), test(0, 0), test(1, 0)]"), @"[false, true, true]"); + assert_snapshot!(assert_compiles("[test(0, 1), test(0, 0), test(1, 0)]"), @"[false, true, true]"); } #[test] @@ -2239,7 +2239,7 @@ fn test_opt_new_does_not_push_frame() { test "); assert_contains_opcode("test", YARVINSN_opt_new); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" foo = test foo.backtrace.find { |frame| frame.include?('Class#new') } "), @"nil"); @@ -2256,7 +2256,7 @@ fn test_opt_new_with_redefined() { test "#); assert_contains_opcode("test", YARVINSN_opt_new); - assert_snapshot!(inspect(r#"test"#), @r#""foo""#); + assert_snapshot!(assert_compiles(r#"test"#), @r#""foo""#); } #[test] @@ -2267,7 +2267,7 @@ fn test_opt_new_invalidate_new() { test "#); assert_contains_opcode("test", YARVINSN_opt_new); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" result = [test.class.name] def Foo.new = "foo" result << test @@ -2283,7 +2283,7 @@ fn test_opt_newarray_send_include_p() { test(1) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("[test(1), test(\"n\")]"), @"[true, false]"); + assert_snapshot!(assert_compiles("[test(1), test(\"n\")]"), @"[true, false]"); } #[test] @@ -2300,7 +2300,7 @@ fn test_opt_newarray_send_include_p_redefined() { end "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" def test(x) [:y, 1, Object.new].include?(x) end @@ -2318,7 +2318,7 @@ fn test_opt_duparray_send_include_p() { test(1) "); assert_contains_opcode("test", YARVINSN_opt_duparray_send); - assert_snapshot!(inspect("[test(1), test(\"n\")]"), @"[true, false]"); + assert_snapshot!(assert_compiles("[test(1), test(\"n\")]"), @"[true, false]"); } #[test] @@ -2335,7 +2335,7 @@ fn test_opt_duparray_send_include_p_redefined() { end "); assert_contains_opcode("test", YARVINSN_opt_duparray_send); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" def test(x) [:y, 1].include?(x) end @@ -2353,7 +2353,7 @@ fn test_opt_newarray_send_pack_buffer() { test(65, "") "#); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" buf = "" [test(65, buf), test(66, buf), test(67, buf), buf] "#), @r#"["ABC", "ABC", "ABC", "ABC"]"#); @@ -2374,7 +2374,7 @@ fn test_opt_newarray_send_pack_buffer_redefined() { end "#); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" def test(num, buffer) [num].pack('C', buffer:) end @@ -2394,7 +2394,7 @@ fn test_opt_newarray_send_hash() { test(20) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("test(20).class"), @"Integer"); + assert_snapshot!(assert_compiles("test(20).class"), @"Integer"); } #[test] @@ -2407,7 +2407,7 @@ fn test_opt_newarray_send_hash_redefined() { test(20) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("test(20)"), @"42"); + assert_snapshot!(assert_compiles("test(20)"), @"42"); } #[test] @@ -2417,7 +2417,7 @@ fn test_opt_newarray_send_max() { test(10, 20) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("[test(10, 20), test(40, 30)]"), @"[20, 40]"); + assert_snapshot!(assert_compiles("[test(10, 20), test(40, 30)]"), @"[20, 40]"); } #[test] @@ -2432,7 +2432,7 @@ fn test_opt_newarray_send_max_redefined() { def test(a,b) = [a,b].max "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" def test(a,b) = [a,b].max test(15, 30) [test(15, 30), test(45, 35)] @@ -2446,7 +2446,7 @@ fn test_new_hash_empty() { test "); assert_contains_opcode("test", YARVINSN_newhash); - assert_snapshot!(inspect("test"), @"{}"); + assert_snapshot!(assert_compiles("test"), @"{}"); } #[test] @@ -2462,7 +2462,7 @@ fn test_new_hash_nonempty() { test "#); assert_contains_opcode("test", YARVINSN_newhash); - assert_snapshot!(inspect(r#"test"#), @r#"{"key" => "value", 42 => 100}"#); + assert_snapshot!(assert_compiles(r#"test"#), @r#"{"key" => "value", 42 => 100}"#); } #[test] @@ -2472,7 +2472,7 @@ fn test_new_hash_single_key_value() { test "#); assert_contains_opcode("test", YARVINSN_newhash); - assert_snapshot!(inspect(r#"test"#), @r#"{"key" => "value"}"#); + assert_snapshot!(assert_compiles(r#"test"#), @r#"{"key" => "value"}"#); } #[test] @@ -2484,7 +2484,7 @@ fn test_new_hash_with_computation() { test(2, 3) "#); assert_contains_opcode("test", YARVINSN_newhash); - assert_snapshot!(inspect(r#"test(2, 3)"#), @r#"{"sum" => 5, "product" => 6}"#); + assert_snapshot!(assert_compiles(r#"test(2, 3)"#), @r#"{"sum" => 5, "product" => 6}"#); } #[test] @@ -2573,7 +2573,7 @@ fn test_opt_hash_freeze() { test "); assert_contains_opcode("test", YARVINSN_opt_hash_freeze); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" result = [test] class Hash def freeze = 5 @@ -2592,7 +2592,7 @@ fn test_opt_hash_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_hash_freeze); - assert_snapshot!(inspect("test"), @"5"); + assert_snapshot!(assert_compiles("test"), @"5"); } #[test] @@ -2604,7 +2604,7 @@ fn test_opt_aset_hash() { test({}, :key, 42) "); assert_contains_opcode("test", YARVINSN_opt_aset); - assert_snapshot!(inspect("h = {}; test(h, :key, 42); h[:key]"), @"42"); + assert_snapshot!(assert_compiles("h = {}; test(h, :key, 42); h[:key]"), @"42"); } #[test] @@ -2678,7 +2678,7 @@ fn test_opt_ary_freeze() { test "); assert_contains_opcode("test", YARVINSN_opt_ary_freeze); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" result = [test] class Array def freeze = 5 @@ -2697,7 +2697,7 @@ fn test_opt_ary_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_ary_freeze); - assert_snapshot!(inspect("test"), @"5"); + assert_snapshot!(assert_compiles("test"), @"5"); } #[test] @@ -2707,7 +2707,7 @@ fn test_opt_str_freeze() { test "); assert_contains_opcode("test", YARVINSN_opt_str_freeze); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" result = [test] class String def freeze = 5 @@ -2726,7 +2726,7 @@ fn test_opt_str_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_str_freeze); - assert_snapshot!(inspect("test"), @"5"); + assert_snapshot!(assert_compiles("test"), @"5"); } #[test] @@ -2736,7 +2736,7 @@ fn test_opt_str_uminus() { test "); assert_contains_opcode("test", YARVINSN_opt_str_uminus); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" result = [test] class String def -@ = 5 @@ -2755,7 +2755,7 @@ fn test_opt_str_uminus_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_str_uminus); - assert_snapshot!(inspect("test"), @"5"); + assert_snapshot!(assert_compiles("test"), @"5"); } #[test] @@ -2765,7 +2765,7 @@ fn test_new_array_empty() { test "); assert_contains_opcode("test", YARVINSN_newarray); - assert_snapshot!(inspect("test"), @"[]"); + assert_snapshot!(assert_compiles("test"), @"[]"); } #[test] @@ -2806,7 +2806,7 @@ fn test_array_fixnum_aref() { test(2) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(2)"), @"3"); + assert_snapshot!(assert_compiles("test(2)"), @"3"); } #[test] @@ -2816,7 +2816,7 @@ fn test_array_fixnum_aref_negative_index() { test(-1) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(-1)"), @"3"); + assert_snapshot!(assert_compiles("test(-1)"), @"3"); } #[test] @@ -2826,7 +2826,7 @@ fn test_array_fixnum_aref_out_of_bounds_positive() { test(10) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(10)"), @"nil"); + assert_snapshot!(assert_compiles("test(10)"), @"nil"); } #[test] @@ -2836,7 +2836,7 @@ fn test_array_fixnum_aref_out_of_bounds_negative() { test(-10) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(-10)"), @"nil"); + assert_snapshot!(assert_compiles("test(-10)"), @"nil"); } #[test] @@ -2847,7 +2847,7 @@ fn test_array_fixnum_aref_array_subclass() { test(MyArray[1,2,3], 2) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(inspect("test(MyArray[1,2,3], 2)"), @"3"); + assert_snapshot!(assert_compiles("test(MyArray[1,2,3], 2)"), @"3"); } #[test] @@ -2873,7 +2873,7 @@ fn test_array_fixnum_aset() { test([1,2,3], 2) "); assert_contains_opcode("test", YARVINSN_opt_aset); - assert_snapshot!(inspect("arr = [1,2,3]; test(arr, 2); arr"), @"[1, 2, 7]"); + assert_snapshot!(assert_compiles("arr = [1,2,3]; test(arr, 2); arr"), @"[1, 2, 7]"); } #[test] @@ -2885,7 +2885,7 @@ fn test_array_fixnum_aset_returns_value() { test([1,2,3], 2) "); assert_contains_opcode("test", YARVINSN_opt_aset); - assert_snapshot!(inspect("test([1,2,3], 2)"), @"7"); + assert_snapshot!(assert_compiles("test([1,2,3], 2)"), @"7"); } #[test] @@ -2959,7 +2959,7 @@ fn test_array_fixnum_aset_array_subclass() { test(MyArray.new, 0) "); assert_contains_opcode("test", YARVINSN_opt_aset); - assert_snapshot!(inspect("arr = MyArray.new; test(arr, 0); arr[0]"), @"7"); + assert_snapshot!(assert_compiles("arr = MyArray.new; test(arr, 0); arr[0]"), @"7"); } #[test] @@ -3041,7 +3041,7 @@ fn test_new_range_fixnum_both_literals_inclusive() { end "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test; test"), @"1..2"); + assert_snapshot!(assert_compiles("test; test"), @"1..2"); } #[test] @@ -3053,7 +3053,7 @@ fn test_new_range_fixnum_both_literals_exclusive() { end "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test; test"), @"1...2"); + assert_snapshot!(assert_compiles("test; test"), @"1...2"); } #[test] @@ -3062,7 +3062,7 @@ fn test_new_range_fixnum_low_literal_inclusive() { def test(a) = (1..a) "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test(2); test(3)"), @"1..3"); + assert_snapshot!(assert_compiles("test(2); test(3)"), @"1..3"); } #[test] @@ -3071,7 +3071,7 @@ fn test_new_range_fixnum_low_literal_exclusive() { def test(a) = (1...a) "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test(2); test(3)"), @"1...3"); + assert_snapshot!(assert_compiles("test(2); test(3)"), @"1...3"); } #[test] @@ -3080,7 +3080,7 @@ fn test_new_range_fixnum_high_literal_inclusive() { def test(a) = (a..10) "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test(2); test(3)"), @"3..10"); + assert_snapshot!(assert_compiles("test(2); test(3)"), @"3..10"); } #[test] @@ -3089,7 +3089,7 @@ fn test_new_range_fixnum_high_literal_exclusive() { def test(a) = (a...10) "); assert_contains_opcode("test", YARVINSN_newrange); - assert_snapshot!(inspect("test(2); test(3)"), @"3...10"); + assert_snapshot!(assert_compiles("test(2); test(3)"), @"3...10"); } #[test] @@ -3516,7 +3516,7 @@ fn test_attr_reader() { test(C.new) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("c = C.new; [test(c), test(c)]"), @"[4, 4]"); + assert_snapshot!(assert_compiles("c = C.new; [test(c), test(c)]"), @"[4, 4]"); } #[test] @@ -3532,7 +3532,7 @@ fn test_attr_accessor_getivar() { test(C.new) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("c = C.new; [test(c), test(c)]"), @"[4, 4]"); + assert_snapshot!(assert_compiles("c = C.new; [test(c), test(c)]"), @"[4, 4]"); } #[test] @@ -3551,7 +3551,7 @@ fn test_attr_accessor_setivar() { test(C.new) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("c = C.new; [test(c), test(c)]"), @"[5, 5]"); + assert_snapshot!(assert_compiles("c = C.new; [test(c), test(c)]"), @"[5, 5]"); } #[test] @@ -3571,7 +3571,7 @@ fn test_attr_writer() { test(C.new) "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("c = C.new; [test(c), test(c)]"), @"[5, 5]"); + assert_snapshot!(assert_compiles("c = C.new; [test(c), test(c)]"), @"[5, 5]"); } #[test] @@ -3586,7 +3586,7 @@ fn test_getconstant() { test(Foo) "); assert_contains_opcode("test", YARVINSN_getconstant); - assert_snapshot!(inspect("test(Foo)"), @"1"); + assert_snapshot!(assert_compiles("test(Foo)"), @"1"); } #[test] @@ -3599,7 +3599,7 @@ fn test_expandarray_no_splat() { test [3, 4] "); assert_contains_opcode("test", YARVINSN_expandarray); - assert_snapshot!(inspect("test [3, 4]"), @"[3, 4]"); + assert_snapshot!(assert_compiles("test [3, 4]"), @"[3, 4]"); } #[test] @@ -3612,7 +3612,7 @@ fn test_expandarray_splat() { test [3, 4] "); assert_contains_opcode("test", YARVINSN_expandarray); - assert_snapshot!(inspect("test [3, 4]"), @"[3, [4]]"); + assert_snapshot!(assert_compiles("test [3, 4]"), @"[3, [4]]"); } #[test] @@ -3625,7 +3625,7 @@ fn test_expandarray_splat_post() { test [3, 4, 5] "); assert_contains_opcode("test", YARVINSN_expandarray); - assert_snapshot!(inspect("test [3, 4, 5]"), @"[3, [4], 5]"); + assert_snapshot!(assert_compiles("test [3, 4, 5]"), @"[3, [4], 5]"); } #[test] @@ -3638,7 +3638,7 @@ fn test_constant_invalidation() { C = 123 "); assert_contains_opcode("test", YARVINSN_opt_getconstant_path); - assert_snapshot!(inspect("test"), @"123"); + assert_snapshot!(assert_compiles("test"), @"123"); } #[test] @@ -3654,7 +3654,7 @@ fn test_constant_path_invalidation() { def test = A::B::C "); assert_contains_opcode("test", YARVINSN_opt_getconstant_path); - assert_snapshot!(inspect(r#" + assert_snapshot!(assert_compiles(r#" module A module B; end end @@ -3682,7 +3682,7 @@ fn test_dupn() { test([1, 1]) "); assert_contains_opcode("test", YARVINSN_dupn); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" one = [1, 1] start_empty = [] [test(one), one, test(start_empty), start_empty] @@ -3711,7 +3711,7 @@ fn test_defined_with_defined_values() { test "); assert_contains_opcode("test", YARVINSN_defined); - assert_snapshot!(inspect("test"), @r#"["constant", "method", "global-variable"]"#); + assert_snapshot!(assert_compiles("test"), @r#"["constant", "method", "global-variable"]"#); } #[test] @@ -3721,7 +3721,7 @@ fn test_defined_with_undefined_values() { test "); assert_contains_opcode("test", YARVINSN_defined); - assert_snapshot!(inspect("test"), @"[nil, nil, nil]"); + assert_snapshot!(assert_compiles("test"), @"[nil, nil, nil]"); } #[test] @@ -3731,7 +3731,7 @@ fn test_defined_with_method_call() { test "#); assert_contains_opcode("test", YARVINSN_defined); - assert_snapshot!(inspect(r#"test"#), @r#"["method", nil]"#); + assert_snapshot!(assert_compiles(r#"test"#), @r#"["method", nil]"#); } #[test] @@ -3762,7 +3762,7 @@ fn test_defined_yield() { def test = defined?(yield) "); assert_contains_opcode("test", YARVINSN_defined); - assert_snapshot!(inspect("[test, test, test{}]"), @r#"[nil, nil, "yield"]"#); + assert_snapshot!(assert_compiles("[test, test, test{}]"), @r#"[nil, nil, "yield"]"#); } #[test] @@ -3819,7 +3819,7 @@ fn test_putspecialobject_vm_core_and_cbase() { test "); assert_contains_opcode("test", YARVINSN_putspecialobject); - assert_snapshot!(inspect("bar"), @"10"); + assert_snapshot!(assert_compiles("bar"), @"10"); } #[test] @@ -3841,7 +3841,7 @@ fn test_branchnil() { test(0) "); assert_contains_opcode("test", YARVINSN_branchnil); - assert_snapshot!(inspect("[test(1), test(nil)]"), @"[2, nil]"); + assert_snapshot!(assert_compiles("[test(1), test(nil)]"), @"[2, nil]"); } #[test] @@ -3851,7 +3851,7 @@ fn test_nil_nil() { test "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test"), @"true"); + assert_snapshot!(assert_compiles("test"), @"true"); } #[test] @@ -3861,7 +3861,7 @@ fn test_non_nil_nil() { test "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test"), @"false"); + assert_snapshot!(assert_compiles("test"), @"false"); } #[test] @@ -3874,7 +3874,7 @@ fn test_getspecial_last_match() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#""hello""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#""hello""#); } #[test] @@ -3887,7 +3887,7 @@ fn test_getspecial_match_pre() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#""hello ""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#""hello ""#); } #[test] @@ -3900,7 +3900,7 @@ fn test_getspecial_match_post() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#"" world""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#"" world""#); } #[test] @@ -3913,7 +3913,7 @@ fn test_getspecial_match_last_group() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#""world""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#""world""#); } #[test] @@ -3926,7 +3926,7 @@ fn test_getspecial_numbered_match_1() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#""hello""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#""hello""#); } #[test] @@ -3939,7 +3939,7 @@ fn test_getspecial_numbered_match_2() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @r#""world""#); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @r#""world""#); } #[test] @@ -3952,7 +3952,7 @@ fn test_getspecial_numbered_match_nonexistent() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @"nil"); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @"nil"); } #[test] @@ -3965,7 +3965,7 @@ fn test_getspecial_no_match() { test("hello world") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("hello world")"#), @"nil"); + assert_snapshot!(assert_compiles(r#"test("hello world")"#), @"nil"); } #[test] @@ -3978,7 +3978,7 @@ fn test_getspecial_complex_pattern() { test("abc123def") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("abc123def")"#), @r#""123""#); + assert_snapshot!(assert_compiles(r#"test("abc123def")"#), @r#""123""#); } #[test] @@ -3991,7 +3991,7 @@ fn test_getspecial_multiple_groups() { test("123-456") "#); assert_contains_opcode("test", YARVINSN_getspecial); - assert_snapshot!(inspect(r#"test("123-456")"#), @r#""456""#); + assert_snapshot!(assert_compiles(r#"test("123-456")"#), @r#""456""#); } #[test] @@ -4233,7 +4233,7 @@ fn test_nil_value_nil_opt_with_guard() { test(nil) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4244,7 +4244,7 @@ fn test_nil_value_nil_opt_with_guard_side_exit() { test(nil) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(1)"), @"false"); + assert_snapshot!(assert_compiles("test(1)"), @"false"); } #[test] @@ -4254,7 +4254,7 @@ fn test_true_nil_opt_with_guard() { test(true) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(true)"), @"false"); + assert_snapshot!(assert_compiles("test(true)"), @"false"); } #[test] @@ -4265,7 +4265,7 @@ fn test_true_nil_opt_with_guard_side_exit() { test(true) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4275,7 +4275,7 @@ fn test_false_nil_opt_with_guard() { test(false) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(false)"), @"false"); + assert_snapshot!(assert_compiles("test(false)"), @"false"); } #[test] @@ -4286,7 +4286,7 @@ fn test_false_nil_opt_with_guard_side_exit() { test(false) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4296,7 +4296,7 @@ fn test_integer_nil_opt_with_guard() { test(1) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(2)"), @"false"); + assert_snapshot!(assert_compiles("test(2)"), @"false"); } #[test] @@ -4307,7 +4307,7 @@ fn test_integer_nil_opt_with_guard_side_exit() { test(2) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4317,7 +4317,7 @@ fn test_float_nil_opt_with_guard() { test(1.0) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(2.0)"), @"false"); + assert_snapshot!(assert_compiles("test(2.0)"), @"false"); } #[test] @@ -4328,7 +4328,7 @@ fn test_float_nil_opt_with_guard_side_exit() { test(2.0) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4338,7 +4338,7 @@ fn test_symbol_nil_opt_with_guard() { test(:foo) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(:bar)"), @"false"); + assert_snapshot!(assert_compiles("test(:bar)"), @"false"); } #[test] @@ -4349,7 +4349,7 @@ fn test_symbol_nil_opt_with_guard_side_exit() { test(:bar) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4359,7 +4359,7 @@ fn test_class_nil_opt_with_guard() { test(String) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(Integer)"), @"false"); + assert_snapshot!(assert_compiles("test(Integer)"), @"false"); } #[test] @@ -4370,7 +4370,7 @@ fn test_class_nil_opt_with_guard_side_exit() { test(Integer) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4380,7 +4380,7 @@ fn test_module_nil_opt_with_guard() { test(Enumerable) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(Kernel)"), @"false"); + assert_snapshot!(assert_compiles("test(Kernel)"), @"false"); } #[test] @@ -4391,7 +4391,7 @@ fn test_module_nil_opt_with_guard_side_exit() { test(Kernel) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(inspect("test(nil)"), @"true"); + assert_snapshot!(assert_compiles("test(nil)"), @"true"); } #[test] @@ -4423,7 +4423,7 @@ fn test_string_concat() { test "##); assert_contains_opcode("test", YARVINSN_concatstrings); - assert_snapshot!(inspect(r##"test"##), @r#""123""#); + assert_snapshot!(assert_compiles(r##"test"##), @r#""123""#); } #[test] @@ -4433,7 +4433,7 @@ fn test_string_concat_empty() { test "##); assert_contains_opcode("test", YARVINSN_concatstrings); - assert_snapshot!(inspect(r##"test"##), @r#""""#); + assert_snapshot!(assert_compiles(r##"test"##), @r#""""#); } #[test] @@ -4443,7 +4443,7 @@ fn test_regexp_interpolation() { test "##); assert_contains_opcode("test", YARVINSN_toregexp); - assert_snapshot!(inspect(r##"test"##), @"/123/"); + assert_snapshot!(assert_compiles(r##"test"##), @"/123/"); } #[test] @@ -4504,7 +4504,7 @@ fn test_opt_case_dispatch() { test(:warmup) "); assert_contains_opcode("test", YARVINSN_opt_case_dispatch); - assert_snapshot!(inspect("[test(:foo), test(1)]"), @"[true, false]"); + assert_snapshot!(assert_compiles("[test(:foo), test(1)]"), @"[true, false]"); } #[test] @@ -4520,7 +4520,7 @@ fn test_checkmatch_case() { end "#); assert_contains_opcode("test", YARVINSN_checkmatch); - assert_snapshot!(inspect(r#"[test(1), test(2), test("3")]"#), @"[1, 1, 2]"); + assert_snapshot!(assert_compiles(r#"[test(1), test(2), test("3")]"#), @"[1, 1, 2]"); } #[test] @@ -4536,7 +4536,7 @@ fn test_checkmatch_case_splat_array() { end "#); assert_contains_opcode("test", YARVINSN_checkmatch); - assert_snapshot!(inspect("[test(1), test(2), test(3)]"), @"[1, 1, 2]"); + assert_snapshot!(assert_compiles("[test(1), test(2), test(3)]"), @"[1, 1, 2]"); } #[test] @@ -4552,7 +4552,7 @@ fn test_checkmatch_when_splat_array() { end "#); assert_contains_opcode("test", YARVINSN_checkmatch); - assert_snapshot!(inspect("[test, test]"), @"[1, 1]"); + assert_snapshot!(assert_compiles("[test, test]"), @"[1, 1]"); } #[test] @@ -4568,7 +4568,7 @@ fn test_checkmatch_rescue() { end end "#); - assert_snapshot!(inspect("[test, test]"), @"[1, 1]"); + assert_snapshot!(assert_compiles("[test, test]"), @"[1, 1]"); } #[test] @@ -4582,7 +4582,7 @@ fn test_checkmatch_rescue_splat_array() { end end "#); - assert_snapshot!(inspect("[test, test]"), @"[1, 1]"); + assert_snapshot!(assert_compiles("[test, test]"), @"[1, 1]"); } #[test] @@ -4611,7 +4611,7 @@ fn test_invokeblock() { test { 41 } "); assert_contains_opcode("test", YARVINSN_invokeblock); - assert_snapshot!(inspect("test { 42 }"), @"42"); + assert_snapshot!(assert_compiles("test { 42 }"), @"42"); } #[test] @@ -4623,7 +4623,7 @@ fn test_invokeblock_with_args() { test(1, 2) { |a, b| a + b } "); assert_contains_opcode("test", YARVINSN_invokeblock); - assert_snapshot!(inspect("test(1, 2) { |a, b| a + b }"), @"3"); + assert_snapshot!(assert_compiles("test(1, 2) { |a, b| a + b }"), @"3"); } #[test] @@ -4635,7 +4635,7 @@ fn test_invokeblock_no_block_given() { test { } "); assert_contains_opcode("test", YARVINSN_invokeblock); - assert_snapshot!(inspect("test"), @":error"); + assert_snapshot!(assert_compiles("test"), @":error"); } #[test] @@ -4649,7 +4649,7 @@ fn test_invokeblock_multiple_yields() { test { |x| x } "); assert_contains_opcode("test", YARVINSN_invokeblock); - assert_snapshot!(inspect(" + assert_snapshot!(assert_compiles(" results = [] test { |x| results << x } results @@ -4667,7 +4667,7 @@ fn test_ccall_variadic_with_multiple_args() { test "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test"), @"[1, 2, 3]"); + assert_snapshot!(assert_compiles("test"), @"[1, 2, 3]"); } #[test] @@ -4680,7 +4680,7 @@ fn test_ccall_variadic_with_no_args() { test "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test"), @"[1]"); + assert_snapshot!(assert_compiles("test"), @"[1]"); } #[test] @@ -4694,7 +4694,7 @@ fn test_ccall_variadic_with_no_args_causing_argument_error() { test "); assert_contains_opcode("test", YARVINSN_opt_send_without_block); - assert_snapshot!(inspect("test"), @":error"); + assert_snapshot!(assert_compiles("test"), @":error"); } #[test] @@ -4712,7 +4712,7 @@ fn test_allocating_in_hir_c_method_is() { second "); assert_contains_opcode("test", YARVINSN_opt_new); - assert_snapshot!(inspect("a(Foo)"), @":k"); + assert_snapshot!(assert_compiles("a(Foo)"), @":k"); } #[test] @@ -4839,7 +4839,7 @@ fn test_fixnum_div_zero() { test(0) "); assert_contains_opcode("test", YARVINSN_opt_div); - assert_snapshot!(inspect(r#"test(0)"#), @r#""divided by 0""#); + assert_snapshot!(assert_compiles(r#"test(0)"#), @r#""divided by 0""#); } #[test] diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index b92df55d48dbfe..6ca9a97b83865e 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1267,6 +1267,15 @@ pub mod test_utils { ruby_str_to_rust_string(eval(&inspect)) } + /// Like inspect, but also asserts that all compilations triggered by this program succeed. + pub fn assert_compiles(program: &str) -> String { + use crate::state::ZJITState; + ZJITState::enable_assert_compiles(); + let result = inspect(program); + ZJITState::disable_assert_compiles(); + result + } + /// Get IseqPtr for a specified method pub fn get_method_iseq(recv: &str, name: &str) -> *const rb_iseq_t { get_proc_iseq(&format!("{}.method(:{})", recv, name)) diff --git a/zjit/src/state.rs b/zjit/src/state.rs index b9f8033e7fa4a6..6c61bc77efd314 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -201,6 +201,12 @@ impl ZJITState { instance.assert_compiles = true; } + /// Stop asserting successful compilation + pub fn disable_assert_compiles() { + let instance = ZJITState::get_instance(); + instance.assert_compiles = false; + } + /// Get a mutable reference to counters for ZJIT stats pub fn get_counters() -> &'static mut Counters { &mut ZJITState::get_instance().counters From c351ae720acb76351858633313f6143d1ddf95f9 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Wed, 25 Mar 2026 22:53:50 +0100 Subject: [PATCH 14/18] [ruby/rubygems] Fallback to copy symlinks on Windows (https://github.com/ruby/rubygems/pull/9296) Symlinks are not permitted by default for a Windows user. To use them, a switch called "Development Mode" in the system settings has to be enabled. ## What was the end-user or developer problem that led to this PR? Ordinary users as well as administrators are unable per default to install gems using symlinks. One such problematical gem is `haml-rails-3.0.0`. It uses symlinks for [files and directories](https://github.com/haml/haml-rails/tree/9f4703ddff0644ba52529c5cf41c1624829b16a7/lib/generators/haml/scaffold/templates). The resulting error message is not very helpful: ``` $ gem inst haml-rails Fetching haml-rails-3.0.0.gem ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the directory. (Gem::FilePermissionError) C:/ruby/lib/ruby/4.0.0/rubygems/installer.rb:308:in 'Gem::Installer#install' C:/ruby/lib/ruby/4.0.0/rubygems/resolver/specification.rb:105:in 'Gem::Resolver::Specification#install' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:192:in 'block in Gem::RequestSet#install' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Array#each' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Gem::RequestSet#install' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:207:in 'Gem::Commands::InstallCommand#install_gem' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:223:in 'block in Gem::Commands::InstallCommand#install_gems' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Array#each' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Gem::Commands::InstallCommand#install_gems' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:162:in 'Gem::Commands::InstallCommand#execute' C:/ruby/lib/ruby/4.0.0/rubygems/command.rb:326:in 'Gem::Command#invoke_with_build_args' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:252:in 'Gem::CommandManager#invoke_command' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:193:in 'Gem::CommandManager#process_args' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:151:in 'Gem::CommandManager#run' C:/ruby/lib/ruby/4.0.0/rubygems/gem_runner.rb:56:in 'Gem::GemRunner#run' C:/ruby/bin/gem.cmd:20:in '
' ``` ## What is your fix for the problem, implemented in this PR? Instead of working around the situation in the affected gem or to skip symlinks completely, I think the better solution would be to make copies of the files in question. This would allow Windows users to install and use the gem smoothly. The switch for the "Developer Mode" is available in the Windows registry under `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock` entry `AllowDevelopmentWithoutDevLicense` https://github.com/ruby/rubygems/commit/ca6c5791fe --- lib/rubygems/package.rb | 17 ++++++- test/rubygems/helper.rb | 18 ++++++++ test/rubygems/installer_test_case.rb | 17 ------- test/rubygems/test_gem_installer.rb | 8 +++- test/rubygems/test_gem_package.rb | 69 +++++++++++++++------------- 5 files changed, 78 insertions(+), 51 deletions(-) diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index c433cf1a77983a..235ccdb458f1d4 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -470,7 +470,7 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: symlinks.each do |name, target, destination, real_destination| if File.exist?(real_destination) - File.symlink(target, destination) + create_symlink(target, destination) else alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring" end @@ -725,6 +725,21 @@ def limit_read(io, name, limit) raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit bytes end + + if Gem.win_platform? + # Create a symlink and fallback to copy the file or directory on Windows, + # where symlink creation needs special privileges in form of the Developer Mode. + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) + rescue Errno::EACCES + from = File.expand_path(old_name, File.dirname(new_name)) + FileUtils.cp_r(from, new_name) + end + else + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) + end + end end require_relative "package/digest_io" diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 759dedc037d8b1..b274273069560a 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1260,6 +1260,24 @@ def nmake_found? system("nmake /? 1>NUL 2>&1") end + @@symlink_supported = nil + + # This is needed for Windows environment without symlink support enabled (the default + # for non admin) to be able to skip test for features using symlinks. + def symlink_supported? + if @@symlink_supported.nil? + begin + File.symlink(File.join(@tempdir, "a"), File.join(@tempdir, "b")) + rescue NotImplementedError, SystemCallError + @@symlink_supported = false + else + File.unlink(File.join(@tempdir, "b")) + @@symlink_supported = true + end + end + @@symlink_supported + end + # In case we're building docs in a background process, this method waits for # that process to exit (or if it's already been reaped, or never happened, # swallows the Errno::ECHILD error). diff --git a/test/rubygems/installer_test_case.rb b/test/rubygems/installer_test_case.rb index ded205c5f56273..9e0cbf9c692bd5 100644 --- a/test/rubygems/installer_test_case.rb +++ b/test/rubygems/installer_test_case.rb @@ -237,21 +237,4 @@ def test_ensure_writable_dir_creates_missing_parent_directories assert_directory_exists non_existent_parent, "Parent directory should exist now" assert_directory_exists target_dir, "Target directory should exist now" end - - @@symlink_supported = nil - - # This is needed for Windows environment without symlink support enabled (the default - # for non admin) to be able to skip test for features using symlinks. - def symlink_supported? - if @@symlink_supported.nil? - begin - File.symlink("", "") - rescue Errno::ENOENT, Errno::EEXIST - @@symlink_supported = true - rescue NotImplementedError, SystemCallError - @@symlink_supported = false - end - end - @@symlink_supported - end end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 0220a41f88a4f2..f20771c5f02e0e 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -759,8 +759,12 @@ def test_generate_bin_with_dangling_symlink errors = @ui.error.split("\n") assert_equal "WARNING: ascii_binder-0.1.10.1 ships with a dangling symlink named bin/ascii_binder pointing to missing bin/asciibinder file. Ignoring", errors.shift - assert_empty errors - + if symlink_supported? + assert_empty errors + else + assert_match(/Unable to use symlinks, installing wrapper/i, + errors.to_s) + end assert_empty @ui.output end diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 2a653dab97d2bb..43d4a07bd496e0 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -175,6 +175,9 @@ def test_add_files end def test_add_files_symlink + unless symlink_supported? + omit("symlink - developer mode must be enabled on Windows") + end spec = Gem::Specification.new spec.files = %w[lib/code.rb lib/code_sym.rb lib/code_sym2.rb] @@ -185,16 +188,8 @@ def test_add_files_symlink end # NOTE: 'code.rb' is correct, because it's relative to lib/code_sym.rb - begin - File.symlink("code.rb", "lib/code_sym.rb") - File.symlink("../lib/code.rb", "lib/code_sym2.rb") - rescue Errno::EACCES => e - if Gem.win_platform? - pend "symlink - must be admin with no UAC on Windows" - else - raise e - end - end + File.symlink("code.rb", "lib/code_sym.rb") + File.symlink("../lib/code.rb", "lib/code_sym2.rb") package = Gem::Package.new "bogus.gem" package.spec = spec @@ -583,25 +578,45 @@ def test_extract_tar_gz_symlink_relative_path tar.add_symlink "lib/foo.rb", "../relative.rb", 0o644 end - begin - package.extract_tar_gz tgz_io, @destination - rescue Errno::EACCES => e - if Gem.win_platform? - pend "symlink - must be admin with no UAC on Windows" - else - raise e - end - end + package.extract_tar_gz tgz_io, @destination extracted = File.join @destination, "lib/foo.rb" assert_path_exist extracted - assert_equal "../relative.rb", - File.readlink(extracted) + if symlink_supported? + assert_equal "../relative.rb", + File.readlink(extracted) + end assert_equal "hi", + File.read(extracted), + "should read file content either by following symlink or on Windows by reading copy" + end + + def test_extract_tar_gz_symlink_directory + package = Gem::Package.new @gem + package.verify + + tgz_io = util_tar_gz do |tar| + tar.add_symlink "link", "lib/orig", 0o644 + tar.mkdir "lib", 0o755 + tar.mkdir "lib/orig", 0o755 + tar.add_file "lib/orig/file.rb", 0o644 do |io| + io.write "ok" + end + end + + package.extract_tar_gz tgz_io, @destination + extracted = File.join @destination, "link/file.rb" + assert_path_exist extracted + if symlink_supported? + assert_equal "lib/orig", + File.readlink(File.dirname(extracted)) + end + assert_equal "ok", File.read(extracted) end def test_extract_symlink_into_symlink_dir + omit "Symlinks not supported or not enabled" unless symlink_supported? package = Gem::Package.new @gem tgz_io = util_tar_gz do |tar| tar.mkdir "lib", 0o755 @@ -665,14 +680,10 @@ def test_extract_symlink_parent destination_subdir = File.join @destination, "subdir" FileUtils.mkdir_p destination_subdir - expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError] - - e = assert_raise(*expected_exceptions) do + e = assert_raise(Gem::Package::SymlinkError) do package.extract_tar_gz tgz_io, destination_subdir end - pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e - assert_equal("installing symlink 'lib/link' pointing to parent path #{@destination} of " \ "#{destination_subdir} is not allowed", e.message) @@ -700,14 +711,10 @@ def test_extract_symlink_parent_doesnt_delete_user_dir tar.add_symlink "link/dir", ".", 16_877 end - expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError] - - e = assert_raise(*expected_exceptions) do + e = assert_raise(Gem::Package::SymlinkError) do package.extract_tar_gz tgz_io, destination_subdir end - pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e - assert_equal("installing symlink 'link' pointing to parent path #{destination_user_dir} of " \ "#{destination_subdir} is not allowed", e.message) From 62a21cca65a26640aa6823b806e18441f3287b0e Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 15:15:31 -0700 Subject: [PATCH 15/18] ZJIT: Avoid allocating Ruby strings during compilation (#16550) When optimizing sends to C function calls, ZJIT was calling rust_str_to_id(qualified_method_name(...)) to create display names like "ObjectSpace.allocation_generation". This calls rb_intern() which creates a dynamic symbol and allocates a Ruby String on the heap. These allocations interfered with ObjectSpace.trace_object_allocations, causing test_string_memory.rb to fail intermittently when run with ZJIT enabled (--zjit-call-threshold=1). Fix by using the CME's called_id directly (already interned, no new allocation) and reconstructing qualified names lazily in Display impls. --- zjit/src/codegen.rs | 2 +- zjit/src/hir.rs | 39 ++++++++++++++++++++++----------------- zjit/src/hir/opt_tests.rs | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 5655997fb4e1fc..39e0da546f7865 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -668,7 +668,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state, .. } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), - Insn::CCall { cfunc, recv, args, name, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)), + Insn::CCall { cfunc, recv, args, name, owner: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)), // Give up CCallWithFrame for 7+ args since asm.ccall() supports at most 6 args (recv + args). // There's no test case for this because no core cfuncs have this many parameters. But C extensions could have such methods. Insn::CCallWithFrame { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e7c6b06980cd34..c44dcdc52762ff 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -916,8 +916,8 @@ pub enum Insn { IfFalse { val: InsnId, target: BranchEdge }, /// Call a C function without pushing a frame - /// `name` is for printing purposes only - CCall { cfunc: *const u8, recv: InsnId, args: Vec, name: ID, return_type: Type, elidable: bool }, + /// `name` and `owner` are for printing purposes only + CCall { cfunc: *const u8, recv: InsnId, args: Vec, name: ID, owner: VALUE, return_type: Type, elidable: bool }, /// Call a C function that pushes a frame CCallWithFrame { @@ -1967,15 +1967,16 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) }, Insn::IsBlockGiven { lep } => { write!(f, "IsBlockGiven {lep}") }, Insn::FixnumBitCheck {val, index} => { write!(f, "FixnumBitCheck {val}, {index}") }, - Insn::CCall { cfunc, recv, args, name, return_type: _, elidable: _ } => { - write!(f, "CCall {recv}, :{}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfunc))?; + Insn::CCall { cfunc, recv, args, name, owner, return_type: _, elidable: _ } => { + let display_name = if *owner == Qnil { name.contents_lossy().to_string() } else { qualified_method_name(*owner, *name) }; + write!(f, "CCall {recv}, :{}@{:p}", display_name, self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } Ok(()) }, - Insn::CCallWithFrame { cfunc, recv, args, name, blockiseq, .. } => { - write!(f, "CCallWithFrame {recv}, :{}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfunc))?; + Insn::CCallWithFrame { cfunc, recv, args, name, cme, blockiseq, .. } => { + write!(f, "CCallWithFrame {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } @@ -1984,8 +1985,8 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) }, - Insn::CCallVariadic { cfunc, recv, args, name, .. } => { - write!(f, "CCallVariadic {recv}, :{}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfunc))?; + Insn::CCallVariadic { cfunc, recv, args, name, cme, .. } => { + write!(f, "CCallVariadic {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } @@ -2798,7 +2799,7 @@ impl Function { &HashAset { hash, key, val, state } => HashAset { hash: find!(hash), key: find!(key), val: find!(val), state }, &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, - &CCall { cfunc, recv, ref args, name, return_type, elidable } => CCall { cfunc, recv: find!(recv), args: find_vec!(args), name, return_type, elidable }, + &CCall { cfunc, recv, ref args, name, owner, return_type, elidable } => CCall { cfunc, recv: find!(recv), args: find_vec!(args), name, owner, return_type, elidable }, &CCallWithFrame { cd, cfunc, recv, ref args, cme, name, state, return_type, elidable, blockiseq } => CCallWithFrame { cd, cfunc, @@ -4099,13 +4100,14 @@ impl Function { } // Use CCallWithFrame for the C function. - let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id })); + let name = unsafe { (*super_cme).called_id }; + let owner = unsafe { (*super_cme).owner }; let return_type = props.return_type; let elidable = props.elidable; // Filter for a leaf and GC free function let ccall = if props.leaf && props.no_gc { self.count(block, Counter::inline_cfunc_optimized_send_count); - self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, return_type, elidable }) + self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, owner, return_type, elidable }) } else { if get_option!(stats) { self.count_not_inlined_cfunc(block, super_cme); @@ -4149,13 +4151,14 @@ impl Function { } // Use CCallVariadic for the variadic C function. - let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id })); + let name = unsafe { (*super_cme).called_id }; + let owner = unsafe { (*super_cme).owner }; let return_type = props.return_type; let elidable = props.elidable; // Filter for a leaf and GC free function let ccall = if props.leaf && props.no_gc { self.count(block, Counter::inline_cfunc_optimized_send_count); - self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, return_type, elidable }) + self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, owner, return_type, elidable }) } else { if get_option!(stats) { self.count_not_inlined_cfunc(block, super_cme); @@ -4278,6 +4281,7 @@ impl Function { recv, args: vec![ivar_index_insn], name: ID!(rb_ivar_get_at_no_ractor_check), + owner: Qnil, return_type: types::BasicObject, elidable: true }) } @@ -4660,7 +4664,7 @@ impl Function { // Emit a call let cfunc = unsafe { get_mct_func(cfunc) }.cast(); - let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); + let name = unsafe { (*cme).called_id }; let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, @@ -4839,13 +4843,14 @@ impl Function { // No inlining; emit a call let cfunc = unsafe { get_mct_func(cfunc) }.cast(); - let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); + let name = unsafe { (*cme).called_id }; + let owner = unsafe { (*cme).owner }; let return_type = props.return_type; let elidable = props.elidable; // Filter for a leaf and GC free function if props.leaf && props.no_gc { fun.count(block, Counter::inline_cfunc_optimized_send_count); - let ccall = fun.push_insn(block, Insn::CCall { cfunc, recv, args, name, return_type, elidable }); + let ccall = fun.push_insn(block, Insn::CCall { cfunc, recv, args, name, owner, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } else { if get_option!(stats) { @@ -4925,7 +4930,7 @@ impl Function { } let return_type = props.return_type; let elidable = props.elidable; - let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); + let name = unsafe { (*cme).called_id }; let ccall = fun.push_insn(block, Insn::CCallVariadic { cfunc, recv, diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 610748c818608b..8dd8f762c68231 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7814,7 +7814,7 @@ mod hir_opt_tests { v39:ArrayExact[VALUE(0x1018)] = Const Value(VALUE(0x1018)) PatchPoint NoSingletonClass(Array@0x1020) PatchPoint MethodRedefined(Array@0x1020, zip@0x1028, cme:0x1030) - v43:BasicObject = CCallVariadic v36, :zip@0x1058, v39 + v43:BasicObject = CCallVariadic v36, :Array#zip@0x1058, v39 v22:CPtr = GetEP 0 v23:BasicObject = LoadField v22, :result@0x1060 PatchPoint NoEPEscape(test) From b78a84e7c0327d8d6a682551ef9a64cb6215d5c5 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 25 Mar 2026 17:52:32 -0400 Subject: [PATCH 16/18] Revert "Skip initializing optional arguments to `nil`" and add test This reverts commit 1596853428393136ee9964ad4c11b0120ed648d1. Expressions that run when the local is not given can observe the initial state of the optional parameter locals. --- bootstraptest/test_method.rb | 6 ++++++ vm_args.c | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb index 78aab734854fee..e894f6f601415a 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -1434,3 +1434,9 @@ def x = [1] def forwarder(...) = target(*x, 2, ...) forwarder(3).inspect }, '[Bug #21832] post-splat args before forwarding' + +assert_equal '[nil, nil]', %q{ + def self_reading(a = a, kw:) = a + def through_binding(a = binding.local_variable_get(:a), kw:) = a + [self_reading(kw: 1), through_binding(kw: 1)] +}, 'nil initialization of optional parameters' diff --git a/vm_args.c b/vm_args.c index 62c63caa9833eb..3d67c6540a3b48 100644 --- a/vm_args.c +++ b/vm_args.c @@ -247,6 +247,7 @@ args_setup_opt_parameters(struct args_info *args, int opt_max, VALUE *locals) i = opt_max; } else { + int j; i = args->argc; args->argc = 0; @@ -258,6 +259,11 @@ args_setup_opt_parameters(struct args_info *args, int opt_max, VALUE *locals) locals[i] = argv[args->rest_index]; } } + + /* initialize by nil */ + for (j=i; j Date: Wed, 25 Mar 2026 19:38:49 -0400 Subject: [PATCH 17/18] ZJIT: Support opt_newarray_send with min (#16547) Make an ArrayMin opcode and call the fallback for now. --- zjit/src/codegen.rs | 27 ++++++++++++++ zjit/src/hir.rs | 19 ++++++++++ zjit/src/hir/tests.rs | 87 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 39e0da546f7865..916e7d02a3d093 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -725,6 +725,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::ArrayHash { elements, state } => gen_opt_newarray_hash(jit, asm, opnds!(elements), &function.frame_state(*state)), &Insn::IsA { val, class } => gen_is_a(jit, asm, opnd!(val), opnd!(class)), &Insn::ArrayMax { ref elements, state } => gen_array_max(jit, asm, opnds!(elements), &function.frame_state(state)), + &Insn::ArrayMin { ref elements, state } => gen_array_min(jit, asm, opnds!(elements), &function.frame_state(state)), &Insn::Throw { state, .. } => return Err(state), &Insn::IfFalse { .. } | Insn::IfTrue { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), @@ -1925,6 +1926,32 @@ fn gen_array_max( ) } +/// Find the minimum element among array elements +fn gen_array_min( + jit: &JITState, + asm: &mut Assembler, + elements: Vec, + state: &FrameState, +) -> lir::Opnd { + gen_prepare_non_leaf_call(jit, asm, state); + + let array_len: u32 = elements.len().try_into().expect("Unable to fit length of elements into u32"); + + // After gen_prepare_non_leaf_call, the elements are spilled to the Ruby stack. + // Get a pointer to the first element on the Ruby stack. + let stack_bottom = state.stack().len() - elements.len(); + let elements_ptr = asm.lea(Opnd::mem(VALUE_BITS, SP, stack_bottom as i32 * SIZEOF_VALUE_I32)); + + unsafe extern "C" { + fn rb_vm_opt_newarray_min(ec: EcPtr, num: u32, elts: *const VALUE) -> VALUE; + } + + asm.ccall( + rb_vm_opt_newarray_min as *const u8, + vec![EC, array_len.into(), elements_ptr], + ) +} + fn gen_array_include( jit: &JITState, asm: &mut Assembler, diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c44dcdc52762ff..71d0a99afda49a 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -795,6 +795,7 @@ pub enum Insn { ArrayDup { val: InsnId, state: InsnId }, ArrayHash { elements: Vec, state: InsnId }, ArrayMax { elements: Vec, state: InsnId }, + ArrayMin { elements: Vec, state: InsnId }, ArrayInclude { elements: Vec, target: InsnId, state: InsnId }, ArrayPackBuffer { elements: Vec, fmt: InsnId, buffer: InsnId, state: InsnId }, DupArrayInclude { ary: VALUE, target: InsnId, state: InsnId }, @@ -1130,6 +1131,7 @@ macro_rules! for_each_operand_impl { $visit_one!(val); } Insn::ArrayMax { elements, state, .. } + | Insn::ArrayMin { elements, state, .. } | Insn::ArrayHash { elements, state, .. } | Insn::NewHash { elements, state, .. } | Insn::NewArray { elements, state, .. } => { @@ -1473,6 +1475,7 @@ impl Insn { Insn::ArrayDup { .. } => allocates, Insn::ArrayHash { .. } => effects::Any, Insn::ArrayMax { .. } => effects::Any, + Insn::ArrayMin { .. } => effects::Any, Insn::ArrayInclude { .. } => effects::Any, Insn::ArrayPackBuffer { .. } => effects::Any, Insn::DupArrayInclude { .. } => effects::Any, @@ -1753,6 +1756,15 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) } + Insn::ArrayMin { elements, .. } => { + write!(f, "ArrayMin")?; + let mut prefix = " "; + for element in elements { + write!(f, "{prefix}{element}")?; + prefix = ", "; + } + Ok(()) + } Insn::ArrayHash { elements, .. } => { write!(f, "ArrayHash")?; let mut prefix = " "; @@ -2829,6 +2841,7 @@ impl Function { &ArrayLength { array } => ArrayLength { array: find!(array) }, &AdjustBounds { index, length } => AdjustBounds { index: find!(index), length: find!(length) }, &ArrayMax { ref elements, state } => ArrayMax { elements: find_vec!(elements), state: find!(state) }, + &ArrayMin { ref elements, state } => ArrayMin { elements: find_vec!(elements), state: find!(state) }, &ArrayInclude { ref elements, target, state } => ArrayInclude { elements: find_vec!(elements), target: find!(target), state: find!(state) }, &ArrayPackBuffer { ref elements, fmt, buffer, state } => ArrayPackBuffer { elements: find_vec!(elements), fmt: find!(fmt), buffer: find!(buffer), state: find!(state) }, &DupArrayInclude { ary, target, state } => DupArrayInclude { ary, target: find!(target), state: find!(state) }, @@ -2998,6 +3011,7 @@ impl Function { Insn::IsBlockGiven { .. } => types::BoolExact, Insn::FixnumBitCheck { .. } => types::BoolExact, Insn::ArrayMax { .. } => types::BasicObject, + Insn::ArrayMin { .. } => types::BasicObject, Insn::ArrayInclude { .. } => types::BoolExact, Insn::ArrayPackBuffer { .. } => types::String, Insn::DupArrayInclude { .. } => types::BoolExact, @@ -6068,6 +6082,7 @@ impl Function { Insn::InvokeBlock { ref args, .. } | Insn::NewArray { elements: ref args, .. } | Insn::ArrayHash { elements: ref args, .. } + | Insn::ArrayMin { elements: ref args, .. } | Insn::ArrayMax { elements: ref args, .. } => { for &arg in args { self.assert_subtype(insn_id, arg, types::BasicObject)?; @@ -7024,6 +7039,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let elements = state.stack_pop_n(count)?; (BOP_MAX, Insn::ArrayMax { elements, state: exit_id }) } + VM_OPT_NEWARRAY_SEND_MIN => { + let elements = state.stack_pop_n(count)?; + (BOP_MIN, Insn::ArrayMin { elements, state: exit_id }) + } VM_OPT_NEWARRAY_SEND_HASH => { let elements = state.stack_pop_n(count)?; (BOP_HASH, Insn::ArrayHash { elements, state: exit_id }) diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index b56320254f8c9e..7a36d99cdf491c 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2598,39 +2598,90 @@ pub mod hir_build_tests { "); } + #[test] + fn test_opt_newarray_send_min_no_elements() { + eval(" + def test = [].min + "); + // TODO(max): Rewrite to nil + assert_contains_opcode("test", YARVINSN_opt_newarray_send); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MIN) + v11:BasicObject = ArrayMin + CheckInterrupts + Return v11 + "); + } + #[test] fn test_opt_newarray_send_min() { eval(" - def test(a,b) - sum = a+b - result = [a,b].min - puts [1,2,3] - result + def test(a,b) = [a,b].min + "); + assert_contains_opcode("test", YARVINSN_opt_newarray_send); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MIN) + v20:BasicObject = ArrayMin v12, v13 + CheckInterrupts + Return v20 + "); + } + + #[test] + fn test_opt_newarray_send_min_redefined() { + eval(" + class Array + alias_method :old_min, :min + def min + old_min * 2 + end end + + def test(a,b) = [a,b].min "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); assert_snapshot!(hir_string("test"), @" - fn test@:3: + fn test@:9: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :a@0x1000 v4:BasicObject = LoadField v2, :b@0x1001 - v5:NilClass = Const Value(nil) - v6:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4, v5, v6) + Jump bb3(v1, v3, v4) bb2(): EntryPoint JIT(0) - v9:BasicObject = LoadArg :self@0 - v10:BasicObject = LoadArg :a@1 - v11:BasicObject = LoadArg :b@2 - v12:NilClass = Const Value(nil) - v13:NilClass = Const Value(nil) - Jump bb3(v9, v10, v11, v12, v13) - bb3(v15:BasicObject, v16:BasicObject, v17:BasicObject, v18:NilClass, v19:NilClass): - v26:BasicObject = Send v16, :+, v17 # SendFallbackReason: Uncategorized(opt_plus) - SideExit UnhandledNewarraySend(MIN) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MIN)) "); } From eb8051185122d4b7bc9c6a6df694a85f34ced681 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 25 Mar 2026 17:44:13 -0700 Subject: [PATCH 18/18] test/socket: rescue Errno::ENOBUFS in test_udp_server (#16553) The server thread's msg_src.reply can raise Errno::ENOBUFS ("No buffer space available") transiently on macOS CI. This exception was not caught, propagating through th.join in the ensure block and failing the test. Rescue it in the server thread so the client side times out on IO.select, raises RuntimeError, and hits the existing omit path. --- test/socket/test_socket.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index c42527f3703173..29812c423ff2e7 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -418,12 +418,16 @@ def test_udp_server ping_p = false th = Thread.new { - Socket.udp_server_loop_on(sockets) {|msg, msg_src| - break if msg == "exit" - rmsg = Marshal.dump([msg, msg_src.remote_address, msg_src.local_address]) - ping_p = true - msg_src.reply rmsg - } + begin + Socket.udp_server_loop_on(sockets) {|msg, msg_src| + break if msg == "exit" + rmsg = Marshal.dump([msg, msg_src.remote_address, msg_src.local_address]) + ping_p = true + msg_src.reply rmsg + } + rescue Errno::ENOBUFS + # transient OS error on macOS CI, let client timeout and omit + end } ifaddrs.each {|ifa|