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/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/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/internal/class.h b/internal/class.h index 164081b5696fbd..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) @@ -619,9 +620,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/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 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/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 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; 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/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__ 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) 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| 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 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 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 * 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..76b11f26b2613b 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -551,6 +551,18 @@ 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, + /// Number of arguments, not including the receiver. + pub argc: i32, } /// Branch target (something that we can jump to) @@ -2607,7 +2619,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 +2658,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..916e7d02a3d093 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}; @@ -193,6 +193,30 @@ 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. +/// +/// 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 + && 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 @@ -474,7 +498,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; @@ -644,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() => @@ -666,7 +690,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)), @@ -701,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!(), @@ -1186,8 +1211,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 @@ -1895,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, @@ -2824,6 +2881,7 @@ fn build_side_exit(jit: &JITState, state: &FrameState) -> SideExit { pc: Opnd::const_ptr(state.pc), stack, locals, + recompile: None, } } @@ -2844,22 +2902,57 @@ 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 { + if let Some(version) = payload.versions.last_mut() { + let cb = ZJITState::get_code_block(); + invalidate_iseq_version(cb, iseq, version); + 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/codegen_tests.rs b/zjit/src/codegen_tests.rs index 660119fb15b981..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 @@ -2278,31 +2278,31 @@ 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) "); 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] 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(" + assert_snapshot!(assert_compiles(" def test(x) - [:y, 1, Object.new].include?(x) + [:y, 1, Object.new].include?(x) end test(1) [test(1), test(\"n\")] @@ -2313,31 +2313,31 @@ 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) "); 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] 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(" + assert_snapshot!(assert_compiles(" def test(x) - [:y, 1].include?(x) + [:y, 1].include?(x) end test(1) [test(1), test(\"n\")] @@ -2348,12 +2348,12 @@ 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, "") "#); 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"]"#); @@ -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#" + assert_snapshot!(assert_compiles(r#" def test(num, buffer) - [num].pack('C', buffer:) + [num].pack('C', buffer:) end buf = "" test(65, buf) @@ -2389,12 +2389,12 @@ 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) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("test(20).class"), @"Integer"); + assert_snapshot!(assert_compiles("test(20).class"), @"Integer"); } #[test] @@ -2402,12 +2402,12 @@ 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) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(inspect("test(20)"), @"42"); + assert_snapshot!(assert_compiles("test(20)"), @"42"); } #[test] @@ -2417,22 +2417,22 @@ 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] 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 "); 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,23 +2446,23 @@ fn test_new_hash_empty() { test "); assert_contains_opcode("test", YARVINSN_newhash); - assert_snapshot!(inspect("test"), @"{}"); + assert_snapshot!(assert_compiles("test"), @"{}"); } #[test] 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 "#); 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,40 +2472,40 @@ 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] 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) "#); 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] 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"); } @@ -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 @@ -2586,32 +2586,32 @@ 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 "); assert_contains_opcode("test", YARVINSN_opt_hash_freeze); - assert_snapshot!(inspect("test"), @"5"); + assert_snapshot!(assert_compiles("test"), @"5"); } #[test] fn test_opt_aset_hash() { eval(" def test(h, k, v) - h[k] = v + h[k] = v end 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] 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({}) @@ -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 @@ -2691,13 +2691,13 @@ 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 "); 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 @@ -2720,13 +2720,13 @@ 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 "); 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 @@ -2749,13 +2749,13 @@ fn test_opt_str_uminus() { fn test_opt_str_uminus_rewritten() { eval(" class String - def -@ = 5 + def -@ = 5 end def test = -'' 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] @@ -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,31 +2868,31 @@ 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) "); 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] fn test_array_fixnum_aset_returns_value() { eval(" def test(arr, idx) - arr[idx] = 7 + arr[idx] = 7 end 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] 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,26 +2954,26 @@ 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) "); 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] 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"); } @@ -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/hir.rs b/zjit/src/hir.rs index 0891a59fa2c2b6..71d0a99afda49a 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)] @@ -794,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 }, @@ -915,8 +917,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 { @@ -1070,7 +1072,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), @@ -1127,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, .. } => { @@ -1470,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, @@ -1750,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 = " "; @@ -1964,15 +1979,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}")?; } @@ -1981,8 +1997,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}")?; } @@ -2061,7 +2077,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 ")?; @@ -2789,7 +2811,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, @@ -2819,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) }, @@ -2846,16 +2869,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)) } } @@ -2988,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, @@ -3565,10 +3589,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; } @@ -4092,13 +4114,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); @@ -4142,13 +4165,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); @@ -4271,6 +4295,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 }) } @@ -4653,7 +4678,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, @@ -4832,13 +4857,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) { @@ -4918,7 +4944,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, @@ -4993,6 +5019,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 +5181,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 +5751,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 +5784,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); @@ -6029,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)?; @@ -6385,6 +6439,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(); @@ -6980,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 }) @@ -6997,13 +7060,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 +7088,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 +7137,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 +7150,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 +7208,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 +7626,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 +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 } } @@ -7598,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 } } @@ -7610,7 +7673,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 +7685,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 +7731,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 +7748,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 +7767,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 +7854,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 +7893,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 +7927,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 +7966,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 +8001,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 +8033,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 +8088,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 +8205,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 +8228,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 +8246,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..8dd8f762c68231 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 "); } @@ -7816,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) @@ -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/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)) "); } 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; } } diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index a4981156a6c756..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,6 +427,31 @@ impl IseqProfile { .ok().map(|i| &self.entries[i]) } + /// 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.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); + if entry.opnd_types.is_empty() { + entry.opnd_types.resize(n, TypeDistribution::new()); + } + for i in 0..n { + let obj = unsafe { *sp.offset(i as isize - n as isize) }; + let ty = ProfiledType::new(obj); + VALUE::from(iseq).write_barrier(ty.class()); + entry.opnd_types[i].observe(ty); + } + entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); + entry.profiles_remaining == 0 + } + /// 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/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 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, } }