From 6883139726578dbe3c4c06597b8bb36759092f41 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 29 Jun 2026 22:46:43 -0400 Subject: [PATCH 1/9] ZJIT: Add test for size of LIR Insn --- zjit/src/backend/lir.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 675c1c004b8ea0..7679fec573ba3f 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -3836,6 +3836,11 @@ mod tests { Assembler::new_with_scratch_reg().1 } + #[test] + fn test_size_of_insn() { + assert_eq!(std::mem::size_of::(), 184); + } + #[test] fn test_for_each_operand() { let insn = Insn::Add { left: Opnd::None, right: Opnd::None, out: Opnd::None }; From 4a396c15ba2376ec32b99f532f212eb564a87d29 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 29 Jun 2026 22:56:46 -0400 Subject: [PATCH 2/9] ZJIT: Box Target::SideExit Shrink `lir::Insn` from 184 to 144 bytes. --- zjit/src/backend/arm64/mod.rs | 2 +- zjit/src/backend/lir.rs | 19 ++++++++++--------- zjit/src/backend/x86_64/mod.rs | 2 +- zjit/src/codegen.rs | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index fda896860ab4a1..4c4f745d07ef5c 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1773,7 +1773,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: 0.into(), iseq: std::ptr::null(), stack: vec![], locals: vec![], recompile: None } }; + let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: Box::new(SideExit { pc: 0.into(), iseq: std::ptr::null(), 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 7679fec573ba3f..81ee2b62e60d22 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -577,8 +577,9 @@ pub enum Target Block(BranchEdge), /// Side exit to the interpreter SideExit { - /// Context used for compiling the side exit - exit: SideExit, + /// Context used for compiling the side exit. Boxed to keep `Target` + /// (and every `Insn` variant that embeds it) small. + exit: Box, /// We use this to increment exit counters reason: SideExitReason, }, @@ -851,9 +852,9 @@ pub enum Insn { macro_rules! target_for_each_operand_impl { ($self:expr, $visit_many:ident) => { match $self { - Target::SideExit { exit: SideExit { stack, locals, .. }, .. } => { - visit_many!(stack); - visit_many!(locals); + Target::SideExit { exit, .. } => { + visit_many!(exit.stack); + visit_many!(exit.locals); } Target::Block(edge) => { visit_many!(edge.args); @@ -2711,7 +2712,7 @@ impl Assembler for ((block_id, idx), target) in targets { // Compile a side exit. Note that this is past register assignment, // so you can't use an instruction that returns a VReg. - if let Target::SideExit { exit: exit @ SideExit { pc, .. }, reason } = target { + if let Target::SideExit { exit, reason } = target { // Only record the exit if `trace_side_exits` is defined and the counter is either the one specified let should_record_exit = get_option!(trace_side_exits).map(|trace| match trace { TraceExits::All => true, @@ -2755,9 +2756,9 @@ impl Assembler } else { let new_exit = self.new_label("side_exit"); self.write_label(new_exit.clone()); - asm_comment!(self, "Exit: {pc}"); + asm_comment!(self, "Exit: {}", exit.pc); compile_exit(self, &exit, None); - compiled_exits.insert(exit, new_exit.unwrap_label()); + compiled_exits.insert(*exit, new_exit.unwrap_label()); new_exit }; @@ -3838,7 +3839,7 @@ mod tests { #[test] fn test_size_of_insn() { - assert_eq!(std::mem::size_of::(), 184); + assert_eq!(std::mem::size_of::(), 144); } #[test] diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 702046058065fd..46d32ec85a69bd 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -1387,7 +1387,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: 0.into(), iseq: std::ptr::null(), stack: vec![], locals: vec![], recompile: None } }; + let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, exit: Box::new(SideExit { pc: 0.into(), iseq: std::ptr::null(), 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 69f71d1f13ed21..79507b02c89bf2 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -968,7 +968,7 @@ fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, invariant: &Invarian let exit = build_side_exit(jit, state); // Let compile_exits compile a side exit. Let scratch_split lower it with split_patch_point. - asm.patch_point(Target::SideExit { exit, reason: PatchPoint(invariant) }, invariant, jit.version); + asm.patch_point(Target::SideExit { exit: Box::new(exit), reason: PatchPoint(invariant) }, invariant, jit.version); } /// This is used by scratch_split to lower PatchPoint into PadPatchPoint and PosMarker. @@ -3179,7 +3179,7 @@ fn compile_iseq(iseq: IseqPtr) -> Result { /// Build a Target::SideExit fn side_exit(jit: &JITState, state: &FrameState, reason: SideExitReason) -> Target { let exit = build_side_exit(jit, state); - Target::SideExit { exit, reason } + Target::SideExit { exit: Box::new(exit), reason } } /// Build a Target::SideExit that optionally triggers exit_recompile on the exit path. @@ -3189,7 +3189,7 @@ fn side_exit_with_recompile(jit: &JITState, state: &FrameState, reason: SideExit compiled_iseq: Opnd::Value(VALUE::from(jit.iseq())), insn_idx: state.insn_idx() as u32, }); - Target::SideExit { exit, reason } + Target::SideExit { exit: Box::new(exit), reason } } /// Build a side-exit context From 8da752c7fc722c314f26533594a9fd0cca0c248e Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 29 Jun 2026 23:05:52 -0400 Subject: [PATCH 3/9] ZJIT: Box lir::CCall data Shrink `lir::Insn` from 144 to 80 bytes. --- zjit/src/backend/arm64/mod.rs | 3 +- zjit/src/backend/lir.rs | 78 ++++++++++++++++++++-------------- zjit/src/backend/x86_64/mod.rs | 3 +- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 4c4f745d07ef5c..4294cb377305de 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1431,7 +1431,8 @@ impl Assembler { // First operand is popped from the lower stack address ldp_post(cb, first_pop, second_pop, A64Opnd::new_mem(64, C_SP_REG, C_SP_STEP)); }, - Insn::CCall { fptr, .. } => { + Insn::CCall { data, .. } => { + let fptr = &data.fptr; match fptr { Opnd::UImm(fptr) => { // The offset to the call target in bytes diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 81ee2b62e60d22..da4fe79b7c6c81 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -636,6 +636,25 @@ impl From for Target { type PosMarkerFn = Rc; +/// Cold fields of `Insn::CCall`, boxed to keep `Insn` small. The operand-bearing +/// fields (`opnds`, `stack_map`) stay inline on the variant so the operand +/// iteration macros can reach them by reference. +#[derive(Clone)] +pub struct CCallData { + /// The function pointer to be called. This should be Opnd::const_ptr + /// (Opnd::UImm) in most cases. gen_entry_trampoline() uses Opnd::Reg. + pub fptr: Opnd, + /// Optional PosMarker to remember the start address of the C call. + /// It's embedded here to insert the PosMarker after push instructions + /// that are split from this CCall during register assignment. + pub start_marker: Option, + /// Optional PosMarker to remember the end address of the C call. + /// It's embedded here to insert the PosMarker before pop instructions + /// that are split from this CCall during register assignment. + pub end_marker: Option, + pub out: Opnd, +} + /// ZJIT Low-level IR instruction #[derive(Clone)] pub enum Insn { @@ -683,19 +702,9 @@ pub enum Insn { // C function call with N arguments (variadic) CCall { opnds: Vec, - /// The function pointer to be called. This should be Opnd::const_ptr - /// (Opnd::UImm) in most cases. gen_entry_trampoline() uses Opnd::Reg. - fptr: Opnd, - /// Optional PosMarker to remember the start address of the C call. - /// It's embedded here to insert the PosMarker after push instructions - /// that are split from this CCall during register assignment. - start_marker: Option, - /// Optional PosMarker to remember the end address of the C call. - /// It's embedded here to insert the PosMarker before pop instructions - /// that are split from this CCall during register assignment. - end_marker: Option, - out: Opnd, stack_map: Option, + /// Cold fields (fptr, markers, out), boxed to keep `Insn` small. + data: Box, }, // C function return @@ -1087,7 +1096,6 @@ impl Insn { match self { Insn::Add { out, .. } | Insn::And { out, .. } | - Insn::CCall { out, .. } | Insn::CPop { out, .. } | Insn::CSelE { out, .. } | Insn::CSelG { out, .. } | @@ -1109,6 +1117,7 @@ impl Insn { Insn::Mul { out, .. } | Insn::URShift { out, .. } | Insn::Xor { out, .. } => Some(out), + Insn::CCall { data, .. } => Some(&data.out), _ => None } } @@ -1119,7 +1128,6 @@ impl Insn { match self { Insn::Add { out, .. } | Insn::And { out, .. } | - Insn::CCall { out, .. } | Insn::CPop { out, .. } | Insn::CSelE { out, .. } | Insn::CSelG { out, .. } | @@ -1141,6 +1149,7 @@ impl Insn { Insn::Mul { out, .. } | Insn::URShift { out, .. } | Insn::Xor { out, .. } => Some(out), + Insn::CCall { data, .. } => Some(&mut data.out), _ => None } } @@ -2284,7 +2293,8 @@ impl Assembler let mut new_ids = Vec::with_capacity(old_ids.len()); for (insn, insn_id) in old_insns.into_iter().zip(old_ids.into_iter()) { - if let Insn::CCall { opnds, out, start_marker, end_marker, fptr, stack_map } = insn { + if let Insn::CCall { opnds, stack_map, data } = insn { + let CCallData { out, start_marker, end_marker, fptr } = *data; let insn_number = insn_id.map(|id| id.0).unwrap_or(0); // Do we have a case where a ccall is emitted, but nobody // uses the result? @@ -2409,13 +2419,15 @@ impl Assembler // The CCall itself new_insns.push(Insn::CCall { - out: C_RET_OPND, opnds: vec![], // We've moved everything in to ccall regs, so this should // be empty now - start_marker: None, - end_marker: None, - fptr, stack_map: None, + data: Box::new(CCallData { + out: C_RET_OPND, + start_marker: None, + end_marker: None, + fptr, + }), }); new_ids.push(insn_id); @@ -3440,7 +3452,7 @@ impl Assembler { let out = self.new_vreg(Opnd::match_num_bits(&opnds)); let fptr = Opnd::const_ptr(fptr); let stack_map = self.stack_map.take(); - self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out, stack_map }); + self.push_insn(Insn::CCall { opnds, stack_map, data: Box::new(CCallData { fptr, start_marker: None, end_marker: None, out }) }); self.clear_stack_canary(canary_opnd); out } @@ -3450,7 +3462,7 @@ impl Assembler { pub fn ccall_into(&mut self, out: Opnd, fptr: *const u8, opnds: Vec) { let fptr = Opnd::const_ptr(fptr); let stack_map = self.stack_map.take(); - self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out, stack_map }); + self.push_insn(Insn::CCall { opnds, stack_map, data: Box::new(CCallData { fptr, start_marker: None, end_marker: None, out }) }); } /// Call a C function stored in a register @@ -3458,7 +3470,7 @@ impl Assembler { assert!(matches!(fptr, Opnd::Reg(_)), "ccall_reg must be called with Opnd::Reg: {fptr:?}"); let out = self.new_vreg(num_bits); let stack_map = self.stack_map.take(); - self.push_insn(Insn::CCall { fptr, opnds: vec![], start_marker: None, end_marker: None, out, stack_map }); + self.push_insn(Insn::CCall { opnds: vec![], stack_map, data: Box::new(CCallData { fptr, start_marker: None, end_marker: None, out }) }); out } @@ -3474,12 +3486,14 @@ impl Assembler { let out = self.new_vreg(Opnd::match_num_bits(&opnds)); let stack_map = self.stack_map.take(); self.push_insn(Insn::CCall { - fptr: Opnd::const_ptr(fptr), opnds, - start_marker: Some(Rc::new(start_marker)), - end_marker: Some(Rc::new(end_marker)), - out, stack_map, + data: Box::new(CCallData { + fptr: Opnd::const_ptr(fptr), + start_marker: Some(Rc::new(start_marker)), + end_marker: Some(Rc::new(end_marker)), + out, + }), }); out } @@ -3839,7 +3853,7 @@ mod tests { #[test] fn test_size_of_insn() { - assert_eq!(std::mem::size_of::(), 144); + assert_eq!(std::mem::size_of::(), 80); } #[test] @@ -4632,11 +4646,13 @@ mod tests { let v3 = asm.new_vreg(64); asm.basic_blocks[b1.0].push_insn(Insn::CCall { opnds: vec![v2], - fptr: Opnd::UImm(0xF00), - start_marker: None, - end_marker: None, - out: v3, stack_map: None, + data: Box::new(CCallData { + fptr: Opnd::UImm(0xF00), + start_marker: None, + end_marker: None, + out: v3, + }), }); // v4 = Add(v3, v1) diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 46d32ec85a69bd..60e735131a1770 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -917,7 +917,8 @@ impl Assembler { }, // C function call - Insn::CCall { fptr, .. } => { + Insn::CCall { data, .. } => { + let fptr = &data.fptr; match fptr { Opnd::UImm(fptr) => { call_ptr(cb, RAX, *fptr as *const u8); From 2c07a1921b006e65ff807c27d94979de53393f93 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 29 Jun 2026 15:18:16 -0700 Subject: [PATCH 4/9] [ruby/rubygems] Make install_location private and hoist fn expansion `install_location` is a hot path during gem installation. I want to make the method private so that we can freely refactor it. This commit makes it private and ports the tests to use integration tests instead of directly calling the method. Before this commit, `install_location` would repeatedly call `File.realpath` on the destination directory when `extract_tar_gz` had already done that work. https://github.com/ruby/rubygems/commit/c15a559007 --- lib/rubygems/package.rb | 36 ++++++------- lib/rubygems/package/old.rb | 3 ++ test/rubygems/test_gem_package.rb | 85 +++++++++++++++++++------------ 3 files changed, 74 insertions(+), 50 deletions(-) diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 435ebdd43da5c8..4e9c470ec195e1 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -510,24 +510,6 @@ def gzip_to(io) # :yields: gz_io gz_io.close end - ## - # Returns the full path for installing +filename+. - # - # If +filename+ is not inside +destination_dir+ an exception is raised. - - def install_location(filename, destination_dir) # :nodoc: - raise Gem::Package::PathError.new(filename, destination_dir) if - filename.start_with? "/" - - destination_dir = File.realpath(destination_dir) - destination = File.expand_path(filename, destination_dir) - - raise Gem::Package::PathError.new(destination, destination_dir) unless - normalize_path(destination).start_with? normalize_path(destination_dir + "/") - - destination - end - if Gem.win_platform? def normalize_path(pathname) # :nodoc: pathname.downcase @@ -662,6 +644,24 @@ def verify private + ## + # Returns the full path for installing +filename+ into +destination_dir+, + # which must already be resolved with File.realpath by the caller. + # + # If +filename+ is not inside +destination_dir+ an exception is raised. + + def install_location(filename, destination_dir) # :nodoc: + raise Gem::Package::PathError.new(filename, destination_dir) if + filename.start_with? "/" + + destination = File.expand_path(filename, destination_dir) + + raise Gem::Package::PathError.new(destination, destination_dir) unless + normalize_path(destination).start_with? normalize_path(destination_dir + "/") + + destination + end + ## # Verifies the +checksums+ against the +digests+. This check is not # cryptographically secure. Missing checksums are ignored. diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb index 1a13ac3e297a72..e06be05bc0a15e 100644 --- a/lib/rubygems/package/old.rb +++ b/lib/rubygems/package/old.rb @@ -59,6 +59,9 @@ def extract_files(destination_dir) header = file_list io raise Gem::Exception, errstr unless header + # install_location expects an already-resolved destination dir + destination_dir = File.realpath(destination_dir) + header.each do |entry| full_name = entry["path"] diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index b69002486d4112..be7cfd2843f588 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -835,79 +835,100 @@ def test_extract_tar_gz_case_insensitive end end - def test_install_location - package = Gem::Package.new @gem - - file = "file.rb".dup - - destination = package.install_location file, @destination - - assert_equal File.join(@destination, "file.rb"), destination - end + # The following tests exercise install_location's path resolution and + # traversal protection through the real extraction path (extract_tar_gz) + # rather than calling the private helper directly. The absolute-path case is + # already covered by test_extract_tar_gz_absolute. - def test_install_location_absolute + def test_extract_tar_gz_basic_file package = Gem::Package.new @gem - e = assert_raise Gem::Package::PathError do - package.install_location "/absolute.rb", @destination + tgz_io = util_tar_gz do |tar| + tar.add_file "file.rb", 0o644 do |io| + io.write "hi" + end end - assert_equal("installing into parent path /absolute.rb of " \ - "#{@destination} is not allowed", e.message) + package.extract_tar_gz tgz_io, @destination + + extracted = File.join @destination, "file.rb" + assert_path_exist extracted + assert_equal "hi", File.read(extracted) end - def test_install_location_dots + def test_extract_tar_gz_collapses_parent_dots package = Gem::Package.new @gem - file = "file.rb" - - destination = File.join @destination, "foo", "..", "bar" - - FileUtils.mkdir_p File.join @destination, "foo" - FileUtils.mkdir_p File.expand_path destination + tgz_io = util_tar_gz do |tar| + tar.add_file "foo/../bar/file.rb", 0o644 do |io| + io.write "hi" + end + end - destination = package.install_location file, destination + package.extract_tar_gz tgz_io, @destination - # this test only fails on ruby missing File.realpath - assert_equal File.join(@destination, "bar", "file.rb"), destination + extracted = File.join @destination, "bar", "file.rb" + assert_path_exist extracted + assert_equal "hi", File.read(extracted) + assert_path_not_exist File.join(@destination, "foo") end - def test_install_location_extra_slash + def test_extract_tar_gz_collapses_extra_slash package = Gem::Package.new @gem - file = "foo//file.rb".dup + tgz_io = util_tar_gz do |tar| + tar.add_file "foo//file.rb", 0o644 do |io| + io.write "hi" + end + end - destination = package.install_location file, @destination + package.extract_tar_gz tgz_io, @destination - assert_equal File.join(@destination, "foo", "file.rb"), destination + extracted = File.join @destination, "foo", "file.rb" + assert_path_exist extracted + assert_equal "hi", File.read(extracted) end - def test_install_location_relative + def test_extract_tar_gz_rejects_relative_escape package = Gem::Package.new @gem + tgz_io = util_tar_gz do |tar| + tar.add_file "../relative.rb", 0o644 do |io| + io.write "hi" + end + end + e = assert_raise Gem::Package::PathError do - package.install_location "../relative.rb", @destination + package.extract_tar_gz tgz_io, @destination end parent = File.expand_path File.join @destination, "../relative.rb" assert_equal("installing into parent path #{parent} of " \ "#{@destination} is not allowed", e.message) + assert_path_not_exist parent end - def test_install_location_suffix + def test_extract_tar_gz_rejects_suffix_escape package = Gem::Package.new @gem filename = "../#{File.basename(@destination)}suffix.rb" + tgz_io = util_tar_gz do |tar| + tar.add_file filename, 0o644 do |io| + io.write "hi" + end + end + e = assert_raise Gem::Package::PathError do - package.install_location filename, @destination + package.extract_tar_gz tgz_io, @destination end parent = File.expand_path File.join @destination, filename assert_equal("installing into parent path #{parent} of " \ "#{@destination} is not allowed", e.message) + assert_path_not_exist parent end def test_load_spec_from_metadata From a99aaffb1badc11a3d7008c89d1e6a328504ed2f Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 25 Jun 2026 16:04:34 -0700 Subject: [PATCH 5/9] Convert Time to declarative marking --- time.c | 70 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/time.c b/time.c index 24a09e88a77458..d6826d7aad9ac8 100644 --- a/time.c +++ b/time.c @@ -256,18 +256,41 @@ divmodv(VALUE n, VALUE d, VALUE *q, VALUE *r) #define FIXWV_P(w) FIXWINT_P(WIDEVAL_GET(w)) #define MUL_OVERFLOW_FIXWV_P(a, b) MUL_OVERFLOW_SIGNED_INTEGER_P(a, b, FIXWV_MIN, FIXWV_MAX) -/* #define STRUCT_WIDEVAL */ -#ifdef STRUCT_WIDEVAL - /* for type checking */ +/* .value holds the markable VALUE; on 32-bit the wide int's high bits go in .hi */ +#if WIDEVALUE_IS_WIDER && SIZEOF_VALUE < SIZEOF_INT64_T typedef struct { - WIDEVALUE value; + VALUE value; + uint32_t hi; } wideval_t; - static inline wideval_t WIDEVAL_WRAP(WIDEVALUE v) { wideval_t w = { v }; return w; } -# define WIDEVAL_GET(w) ((w).value) + static inline wideval_t + WIDEVAL_WRAP(WIDEVALUE v) + { + wideval_t w; + w.value = (VALUE)(uint32_t)v; + w.hi = (uint32_t)(v >> 32); + return w; + } + static inline WIDEVALUE + WIDEVAL_GET(wideval_t w) + { + return ((WIDEVALUE)w.hi << 32) | (uint32_t)w.value; + } #else - typedef WIDEVALUE wideval_t; -# define WIDEVAL_WRAP(v) (v) -# define WIDEVAL_GET(w) (w) + typedef struct { + VALUE value; + } wideval_t; + static inline wideval_t + WIDEVAL_WRAP(WIDEVALUE v) + { + wideval_t w; + w.value = (VALUE)v; + return w; + } + static inline WIDEVALUE + WIDEVAL_GET(wideval_t w) + { + return (WIDEVALUE)w.value; + } #endif #if WIDEVALUE_IS_WIDER @@ -1888,28 +1911,23 @@ force_make_tm(VALUE time, struct time_object *tobj) time_get_tm(time, tobj); } -static void -time_mark_and_move(void *ptr) -{ - struct time_object *tobj = ptr; - if (!WIDEVALUE_IS_WIDER || !FIXWV_P(tobj->timew)) { - rb_gc_mark_and_move((VALUE *)&WIDEVAL_GET(tobj->timew)); - } - rb_gc_mark_and_move(&tobj->vtm.year); - rb_gc_mark_and_move(&tobj->vtm.subsecx); - rb_gc_mark_and_move(&tobj->vtm.utc_offset); - rb_gc_mark_and_move(&tobj->vtm.zone); -} +RUBY_REFERENCES(time_refs) = { + RUBY_REF_EDGE(struct time_object, timew.value), + RUBY_REF_EDGE(struct time_object, vtm.year), + RUBY_REF_EDGE(struct time_object, vtm.subsecx), + RUBY_REF_EDGE(struct time_object, vtm.utc_offset), + RUBY_REF_EDGE(struct time_object, vtm.zone), + RUBY_REF_END +}; static const rb_data_type_t time_data_type = { .wrap_struct_name = "time", .function = { - .dmark = time_mark_and_move, + RUBY_REFS_LIST_PTR(time_refs), .dfree = RUBY_TYPED_DEFAULT_FREE, .dsize = NULL, - .dcompact = time_mark_and_move, }, - .flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, + .flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE | RUBY_TYPED_DECL_MARKING, }; static VALUE @@ -5786,9 +5804,9 @@ tm_from_time(VALUE klass, VALUE time) ttm = RTYPEDDATA_GET_DATA(tm); v = &vtm; - WIDEVALUE timew = tobj->timew; + wideval_t timew = tobj->timew; GMTIMEW(timew, v); - time_set_timew(tm, ttm, wsub(timew, v->subsecx)); + time_set_timew(tm, ttm, wsub(timew, v2w(v->subsecx))); v->subsecx = INT2FIX(0); v->zone = Qnil; time_set_vtm(tm, ttm, *v); From 6ac3a3f24bc131b77f270dc52f67f1a7847591cc Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 24 Jun 2026 23:34:35 -0700 Subject: [PATCH 6/9] Use BOP for calling freeze in make_shareable This makes sharing String, Array, and Hash objects faster. --- ractor.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ractor.c b/ractor.c index 03c949d5ba9e04..58677a0b470dae 100644 --- a/ractor.c +++ b/ractor.c @@ -1461,11 +1461,29 @@ allow_frozen_shareable_p(VALUE obj) return false; } +static void +make_shareable_freeze(VALUE obj) +{ + VALUE klass = RBASIC_CLASS(obj); + if (klass == rb_cString && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, STRING_REDEFINED_OP_FLAG)) { + rb_str_freeze(obj); + } + else if (klass == rb_cArray && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, ARRAY_REDEFINED_OP_FLAG)) { + rb_ary_freeze(obj); + } + else if (klass == rb_cHash && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, HASH_REDEFINED_OP_FLAG)) { + rb_hash_freeze(obj); + } + else { + rb_funcall(obj, idFreeze, 0); + } +} + static enum obj_traverse_iterator_result make_shareable_check_shareable_freeze(VALUE obj, enum obj_traverse_iterator_result result) { if (!RB_OBJ_FROZEN_RAW(obj)) { - rb_funcall(obj, idFreeze, 0); + make_shareable_freeze(obj); if (UNLIKELY(!RB_OBJ_FROZEN_RAW(obj))) { rb_raise(rb_eRactorError, "#freeze does not freeze object correctly"); From 7b564e9fcdfd62377490770fcaefd6ed594edd51 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 25 Jun 2026 17:27:56 -0400 Subject: [PATCH 7/9] ZJIT: Fix issue with InsnPrinter having the wrong ISEQ with inlining enabled --- zjit/src/codegen.rs | 2 +- zjit/src/hir.rs | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 79507b02c89bf2..bca08013edba79 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -734,7 +734,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::GetGlobal { id, state } => gen_getglobal(jit, asm, *id, &function.frame_state(*state)), &Insn::IsBlockParamModified { flags } => gen_is_block_param_modified(asm, opnd!(flags)), &Insn::GetBlockParam { ep_offset, level, state } => gen_getblockparam(jit, asm, ep_offset, level, &function.frame_state(state)), - &Insn::SetLocal { val, ep_offset, level } => no_output!(gen_setlocal(asm, opnd!(val), function.type_of(val), ep_offset, level)), + &Insn::SetLocal { val, ep_offset, level, .. } => no_output!(gen_setlocal(asm, opnd!(val), function.type_of(val), ep_offset, level)), Insn::GetConstant { klass, id, allow_nil, state } => gen_getconstant(jit, asm, opnd!(klass), *id, opnd!(allow_nil), &function.frame_state(*state)), Insn::GetConstantPath { ic, state } => gen_get_constant_path(jit, asm, *ic, &function.frame_state(*state)), Insn::GetClassVar { id, ic, state } => gen_getclassvar(jit, asm, *id, *ic, &function.frame_state(*state)), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 69f79c5e853496..6e5b64f18b7711 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1015,7 +1015,7 @@ pub enum Insn { /// Get the block parameter as a Proc. GetBlockParam { level: u32, ep_offset: u32, state: InsnId }, /// Set a local variable in a higher scope or the heap - SetLocal { level: u32, ep_offset: u32, val: InsnId }, + SetLocal { level: u32, ep_offset: u32, val: InsnId, state: InsnId }, GetSpecialSymbol { symbol_type: SpecialBackrefSymbol, state: InsnId }, GetSpecialNumber { nth: u64, state: InsnId }, @@ -1604,8 +1604,8 @@ impl Insn { Ok(()) } - pub fn print<'a>(&self, ptr_map: &'a PtrPrintMap, iseq: Option) -> InsnPrinter<'a> { - InsnPrinter { inner: self.clone(), ptr_map, iseq } + pub fn print<'a>(&self, ptr_map: &'a PtrPrintMap, fun: Option<&'a Function>) -> InsnPrinter<'a> { + InsnPrinter { inner: self.clone(), ptr_map, fun } } // TODO(Jacob): Model SP. ie, all allocations modify stack size but using the effect for stack modification feels excessive @@ -1831,9 +1831,9 @@ impl Insn { /// Print adaptor for [`Insn`]. See [`PtrPrintMap`]. pub struct InsnPrinter<'a> { + fun: Option<&'a Function>, inner: Insn, ptr_map: &'a PtrPrintMap, - iseq: Option, } fn get_local_var_id(iseq: IseqPtr, level: u32, ep_offset: u32) -> ID { @@ -2174,8 +2174,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::GuardNoBitsSet { val, mask, .. } => { write!(f, "GuardNoBitsSet {val}, {}", mask.print(self.ptr_map)) }, Insn::GuardLess { left, right, .. } => write!(f, "GuardLess {left}, {right}"), Insn::GuardGreaterEq { left, right, .. } => write!(f, "GuardGreaterEq {left}, {right}"), - &Insn::GetBlockParam { level, ep_offset, .. } => { - let name = get_local_var_name_for_printer(self.iseq, level, ep_offset) + &Insn::GetBlockParam { level, ep_offset, state, .. } => { + let iseq = self.fun.map(|fun| fun.frame_state(state).iseq); + let name = get_local_var_name_for_printer(iseq, level, ep_offset) .map_or(String::new(), |x| format!("{x}, ")); write!(f, "GetBlockParam {name}l{level}, EP@{ep_offset}") }, @@ -2264,8 +2265,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { &Insn::IsBlockParamModified { flags } => { write!(f, "IsBlockParamModified {flags}") }, - &Insn::SetLocal { val, level, ep_offset } => { - let name = get_local_var_name_for_printer(self.iseq, level, ep_offset).map_or(String::new(), |x| format!("{x}, ")); + &Insn::SetLocal { val, level, ep_offset, state } => { + let iseq = self.fun.map(|fun| fun.frame_state(state).iseq); + let name = get_local_var_name_for_printer(iseq, level, ep_offset).map_or(String::new(), |x| format!("{x}, ")); write!(f, "SetLocal {name}l{level}, EP@{ep_offset}, {val}") }, Insn::GetSpecialSymbol { symbol_type, .. } => write!(f, "GetSpecialSymbol {symbol_type:?}"), @@ -6089,7 +6091,7 @@ impl Function { }; - let opcode = insn.print(&ptr_map, Some(self.iseq)).to_string(); + let opcode = insn.print(&ptr_map, Some(self)).to_string(); // Collect inputs for a given instruction. let mut inputs = Vec::new(); @@ -6797,7 +6799,7 @@ impl<'a> std::fmt::Display for FunctionPrinter<'a> { write!(f, "{insn_id}:{} = ", insn_type.print(&self.ptr_map))?; } } - writeln!(f, "{}", insn.print(&self.ptr_map, Some(fun.iseq)))?; + writeln!(f, "{}", insn.print(&self.ptr_map, Some(fun)))?; } } Ok(()) @@ -8064,7 +8066,7 @@ fn add_iseq_to_hir( let val = state.stack_pop()?; if ep_escaped { // Write the local using EP - fun.push_insn(block, Insn::SetLocal { val, ep_offset, level: 0 }); + fun.push_insn(block, Insn::SetLocal { val, ep_offset, level: 0, state: exit_id }); } else if local_inval { // If there has been any non-leaf call since JIT entry or the last patch point, // add a patch point to make sure locals have not been escaped. @@ -8082,7 +8084,7 @@ fn add_iseq_to_hir( } YARVINSN_setlocal_WC_1 => { let ep_offset = get_arg(pc, 0).as_u32(); - fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level: 1 }); + fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level: 1, state: exit_id }); } YARVINSN_getlocal => { let ep_offset = get_arg(pc, 0).as_u32(); @@ -8103,13 +8105,13 @@ fn add_iseq_to_hir( YARVINSN_setlocal => { let ep_offset = get_arg(pc, 0).as_u32(); let level = get_arg(pc, 1).as_u32(); - fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level }); + fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level, state: exit_id }); } YARVINSN_setblockparam => { let ep_offset = get_arg(pc, 0).as_u32(); let level = get_arg(pc, 1).as_u32(); let val = state.stack_pop()?; - fun.push_insn(block, Insn::SetLocal { val, ep_offset, level }); + fun.push_insn(block, Insn::SetLocal { val, ep_offset, level, state: exit_id }); if level == 0 { state.setlocal(ep_offset, val); } From 594d14de2665dc12f9f8fcbf4d572e562ed886e3 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 25 Jun 2026 17:28:18 -0400 Subject: [PATCH 8/9] ZJIT: Enable the inliner by default --- zjit/src/hir/opt_tests.rs | 411 ++++++++++++++++++++++++-------------- zjit/src/hir/tests.rs | 26 ++- zjit/src/options.rs | 9 +- 3 files changed, 278 insertions(+), 168 deletions(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 426352b75c9f82..3172864db12865 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2,9 +2,9 @@ mod hir_opt_tests { use crate::hir::*; + use crate::hir::tests::hir_build_tests::assert_contains_opcode; use crate::{hir_strings, options::*}; use insta::assert_snapshot; - use crate::hir::tests::hir_build_tests::assert_contains_opcode; #[track_caller] fn hir_string_function(function: &Function) -> String { @@ -1466,34 +1466,6 @@ mod hir_opt_tests { "); } - #[test] - fn test_optimize_top_level_call_into_send_direct() { - eval(" - def foo = [] - def test - foo - end - test; test - "); - assert_snapshot!(hir_string("test"), @" - fn test@:4: - 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 MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) - CheckInterrupts - Return v19 - "); - } - #[test] fn test_optimize_send_without_block_to_aliased_iseq() { eval(" @@ -1549,13 +1521,7 @@ mod hir_opt_tests { } #[test] - fn test_no_inline_nonparam_local_return() { - // Methods that return non-parameter local variables should NOT be inlined, - // because the local variable index will be out of bounds for args. - // The method must have a parameter so param_size > 0, and return a local - // that's not a parameter so local_idx >= param_size. - // Use dead code (if false) to create a local without initialization instructions, - // resulting in just getlocal + leave which enters the inlining code path. + fn test_inline_nonparam_local_return() { eval(" def foo(a) if false @@ -1580,9 +1546,11 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 + v32:NilClass = Const Value(nil) + PushInlineFrame v20 (0x1038), v11 CheckInterrupts - Return v21 + PopInlineFrame + Return v32 "); } @@ -1722,7 +1690,7 @@ mod hir_opt_tests { } #[test] - fn test_optimize_private_top_level_call() { + fn test_optimize_private_call() { eval(" def foo = [] private :foo @@ -1744,14 +1712,16 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) + PushInlineFrame v18 (0x1038) + v24:ArrayExact = NewArray CheckInterrupts - Return v19 + PopInlineFrame + Return v24 "); } #[test] - fn test_optimize_top_level_call_with_overloaded_cme() { + fn test_optimize_call_with_overloaded_cme() { eval(" def test Integer(3) @@ -1772,14 +1742,16 @@ mod hir_opt_tests { v11:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :Integer (0x1048), v11 + PushInlineFrame v20 (0x1038), v11 + v27:BasicObject = InvokeBuiltin rb_f_integer1, v20, v11 CheckInterrupts - Return v21 + PopInlineFrame + Return v27 "); } #[test] - fn test_optimize_top_level_call_with_args_into_send_direct() { + fn test_optimize_call_with_args() { eval(" def foo(a, b) = [] def test @@ -1802,46 +1774,16 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + PushInlineFrame v22 (0x1038), v11, v13 + v30:ArrayExact = NewArray CheckInterrupts - Return v23 - "); - } - - #[test] - fn test_optimize_top_level_sends_into_send_direct() { - eval(" - def foo = [] - def bar = [] - def test - foo - bar - end - test; test - "); - assert_snapshot!(hir_string("test"), @" - fn test@:5: - 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 MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048) - PatchPoint MethodRedefined(Object@0x1000, bar@0x1050, cme:0x1058) - v27:BasicObject = SendDirect v23, 0x1038, :bar (0x1048) - CheckInterrupts - Return v27 + PopInlineFrame + Return v30 "); } #[test] - fn test_optimize_send_direct_no_optionals_passed() { + fn test_optimize_send_no_optionals_passed() { eval(" def foo(a=1, b=2) = a + b def test = foo @@ -1860,14 +1802,19 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) + PushInlineFrame v18 (0x1038) + v25:Fixnum[1] = Const Value(1) + v33:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Integer@0x1040, +@0x1048, cme:0x1050) + v60:Fixnum[3] = Const Value(3) CheckInterrupts - Return v19 + PopInlineFrame + Return v60 "); } #[test] - fn test_optimize_send_direct_one_optional_passed() { + fn test_optimize_send_one_optional_passed() { eval(" def foo(a=1, b=2) = a + b def test = foo 3 @@ -1887,14 +1834,18 @@ mod hir_opt_tests { v11:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 + PushInlineFrame v20 (0x1038), v11 + v35:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Integer@0x1040, +@0x1048, cme:0x1050) + v61:Fixnum[5] = Const Value(5) CheckInterrupts - Return v21 + PopInlineFrame + Return v61 "); } #[test] - fn test_optimize_send_direct_all_optionals_passed() { + fn test_optimize_send_all_optionals_passed() { eval(" def foo(a=1, b=2) = a + b def test = foo 3, 4 @@ -1915,9 +1866,12 @@ mod hir_opt_tests { v13:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + PushInlineFrame v22 (0x1038), v11, v13 + PatchPoint MethodRedefined(Integer@0x1040, +@0x1048, cme:0x1050) + v62:Fixnum[7] = Const Value(7) CheckInterrupts - Return v23 + PopInlineFrame + Return v62 "); } @@ -1942,19 +1896,28 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) v44:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v45:BasicObject = SendDirect v44, 0x1038, :target (0x1048) + PushInlineFrame v44 (0x1038) + v56:Fixnum[1] = Const Value(1) + v66:Fixnum[2] = Const Value(2) + v76:Fixnum[3] = Const Value(3) + v86:Fixnum[4] = Const Value(4) + v101:ArrayExact = NewArray v56, v66, v76, v86 + CheckInterrupts + PopInlineFrame v14:Fixnum[10] = Const Value(10) v16:Fixnum[20] = Const Value(20) v18:Fixnum[30] = Const Value(30) - PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v48:BasicObject = SendDirect v44, 0x1038, :target (0x1048), v14, v16, v18 + PushInlineFrame v44 (0x1038), v14, v16, v18 + v151:Fixnum[4] = Const Value(4) + v166:ArrayExact = NewArray v14, v16, v18, v151 + PopInlineFrame v24:Fixnum[10] = Const Value(10) v26:Fixnum[20] = Const Value(20) v28:Fixnum[30] = Const Value(30) v30:Fixnum[40] = Const Value(40) v32:Fixnum[50] = Const Value(50) v34:BasicObject = Send v44, :target, v24, v26, v28, v30, v32 # SendFallbackReason: Argument count does not match parameter count - v37:ArrayExact = NewArray v45, v48, v34 + v37:ArrayExact = NewArray v101, v166, v34 CheckInterrupts Return v37 "); @@ -3839,7 +3802,7 @@ mod hir_opt_tests { } #[test] - fn test_send_direct_to_instance_method() { + fn test_send_to_instance_method() { eval(" class C def foo = [] @@ -3868,14 +3831,16 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile - v24:BasicObject = SendDirect v23, 0x1040, :foo (0x1050) + PushInlineFrame v23 (0x1040) + v29:ArrayExact = NewArray CheckInterrupts - Return v24 + PopInlineFrame + Return v29 "); } #[test] - fn test_send_direct_iseq_with_block() { + fn test_send_iseq_with_block() { let result = eval(" def foo(a, b, &block) = block.call(a, b) def test = foo(1, 2) { |a, b| a + b } @@ -3898,9 +3863,25 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + v51:NilClass = Const Value(nil) + PushInlineFrame v22 (0x1038), v11, v13 + v33:CPtr = GetEP 0 + v34:CUInt64 = LoadField v33, :VM_ENV_DATA_INDEX_FLAGS@0x1040 + v35:CBool = IsBlockParamModified v34 + CondBranch v35, bb6(), bb7() + bb6(): + v37:BasicObject = LoadField v33, :block@0x1041 + Jump bb8(v37, v37) + bb7(): + v39:CInt64 = LoadField v33, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 + v40:CInt64 = GuardAnyBitSet v39, CUInt64(1) recompile + v41:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) + Jump bb8(v41, v51) + bb8(v31:BasicObject, v32:BasicObject): + v46:BasicObject = Send v31, :call, v11, v13 # SendFallbackReason: SendWithoutBlock: unsupported optimized method type BlockCall CheckInterrupts - Return v23 + PopInlineFrame + Return v46 "); } @@ -4039,9 +4020,11 @@ mod hir_opt_tests { v11:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 + PushInlineFrame v20 (0x1038), v11 + v27:Fixnum[80] = Const Value(80) CheckInterrupts - Return v21 + PopInlineFrame + Return v11 "); } @@ -4069,9 +4052,13 @@ mod hir_opt_tests { v13:Fixnum[20] = Const Value(20) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + PushInlineFrame v22 (0x1038), v11, v13 + v30:Fixnum[80] = Const Value(80) + PatchPoint MethodRedefined(Integer@0x1040, +@0x1048, cme:0x1050) + v66:Fixnum[110] = Const Value(110) CheckInterrupts - Return v23 + PopInlineFrame + Return v66 "); } @@ -4098,9 +4085,12 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + v38:Fixnum[0] = Const Value(0) + PushInlineFrame v22 (0x1038), v11, v13 + v33:ArrayExact = NewArray v11, v13 CheckInterrupts - Return v23 + PopInlineFrame + Return v33 "); } @@ -4128,9 +4118,12 @@ mod hir_opt_tests { v15:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v13, v15, v11 + v43:Fixnum[0] = Const Value(0) + PushInlineFrame v25 (0x1038), v13, v15, v11 + v38:ArrayExact = NewArray v13, v15, v11 CheckInterrupts - Return v26 + PopInlineFrame + Return v38 "); } @@ -4158,9 +4151,12 @@ mod hir_opt_tests { v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v11, v15, v13 + v43:Fixnum[0] = Const Value(0) + PushInlineFrame v25 (0x1038), v11, v15, v13 + v38:ArrayExact = NewArray v11, v15, v13 CheckInterrupts - Return v26 + PopInlineFrame + Return v38 "); } @@ -4187,9 +4183,12 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 + v38:Fixnum[0] = Const Value(0) + PushInlineFrame v22 (0x1038), v11, v13 + v33:ArrayExact = NewArray v11, v13 CheckInterrupts - Return v23 + PopInlineFrame + Return v33 "); } @@ -4217,15 +4216,21 @@ mod hir_opt_tests { v15:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v15 + v71:Fixnum[0] = Const Value(0) + PushInlineFrame v37 (0x1038), v11, v13, v15 + v51:Fixnum[2] = Const Value(2) + v65:ArrayExact = NewArray v51, v13 + CheckInterrupts + PopInlineFrame v20:Fixnum[1] = Const Value(1) v22:Fixnum[2] = Const Value(2) v24:Fixnum[4] = Const Value(4) v26:Fixnum[3] = Const Value(3) - PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v42:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v20, v22, v26, v24 - v30:ArrayExact = NewArray v38, v42 - CheckInterrupts + v103:Fixnum[0] = Const Value(0) + PushInlineFrame v37 (0x1038), v20, v22, v26, v24 + v98:ArrayExact = NewArray v22, v26 + PopInlineFrame + v30:ArrayExact = NewArray v65, v98 Return v30 "); } @@ -4254,15 +4259,21 @@ mod hir_opt_tests { v34:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v34 + v73:Fixnum[0] = Const Value(0) + PushInlineFrame v37 (0x1038), v11, v13, v34 + v51:Fixnum[2] = Const Value(2) + v67:ArrayExact = NewArray v11, v51, v13, v34 + CheckInterrupts + PopInlineFrame v18:Fixnum[1] = Const Value(1) v20:Fixnum[2] = Const Value(2) v22:Fixnum[40] = Const Value(40) v24:Fixnum[30] = Const Value(30) - PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v42:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v18, v20, v24, v22 - v28:ArrayExact = NewArray v38, v42 - CheckInterrupts + v107:Fixnum[0] = Const Value(0) + PushInlineFrame v37 (0x1038), v18, v20, v24, v22 + v102:ArrayExact = NewArray v18, v20, v24, v22 + PopInlineFrame + v28:ArrayExact = NewArray v67, v102 Return v28 "); } @@ -4331,9 +4342,11 @@ mod hir_opt_tests { v11:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 + v32:Fixnum[0] = Const Value(0) + PushInlineFrame v20 (0x1038), v11 CheckInterrupts - Return v21 + PopInlineFrame + Return v11 "); } @@ -4446,9 +4459,14 @@ mod hir_opt_tests { v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v17 + v37:Fixnum[0] = Const Value(0) + PushInlineFrame v20 (0x1038), v17 + v29:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1040, +@0x1048, cme:0x1050) + v46:Fixnum[2] = Const Value(2) CheckInterrupts - Return v21 + PopInlineFrame + Return v46 "); } @@ -4748,8 +4766,15 @@ mod hir_opt_tests { v48:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - v52:BasicObject = SendDirect v48, 0x1068, :initialize (0x1078), v17 + PushInlineFrame v48 (0x1068), v17 + v62:CShape = LoadField v48, :shape_id@0x1070 + v63:CShape[0x1071] = GuardBitEquals v62, CShape(0x1071) recompile + StoreField v48, :@x@0x1072, v17 + WriteBarrier v48, v17 + v66:CShape[0x1073] = Const CShape(0x1073) + StoreField v48, :shape_id@0x1070, v66 CheckInterrupts + PopInlineFrame Return v48 "); } @@ -4842,8 +4867,24 @@ mod hir_opt_tests { v46:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040) - v51:BasicObject = SendDirect v45, 0x1068, :initialize (0x1078), v46 + v94:Fixnum[0] = Const Value(0) + v95:NilClass = Const Value(nil) + PushInlineFrame v45 (0x1068), v46 + v61:TrueClass = Const Value(true) + v79:CPtr = GetEP 0 + v80:CUInt64 = LoadField v79, :VM_ENV_DATA_INDEX_FLAGS@0x1070 + v81:CBool = IsBlockParamModified v80 + CondBranch v81, bb10(), bb11() + bb10(): + v83:BasicObject = LoadField v79, :block@0x1071 + Jump bb12(v83) + bb11(): + v85:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb12(v85) + bb12(v78:BasicObject): + v88:BasicObject = InvokeBuiltin rb_hash_init, v45, v46, v61, v61, v78 CheckInterrupts + PopInlineFrame Return v45 "); assert_snapshot!(inspect("test"), @"{}"); @@ -7330,9 +7371,11 @@ mod hir_opt_tests { v13:Fixnum[10] = Const Value(10) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v24:BasicObject = SendDirect v11, 0x1040, :[] (0x1050), v13 + PushInlineFrame v11 (0x1040), v13 + v30:ArrayExact = NewArray CheckInterrupts - Return v24 + PopInlineFrame + Return v30 "); } @@ -7390,9 +7433,11 @@ mod hir_opt_tests { v11:ArrayExact = ArrayDup v10 PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, max@0x1010, cme:0x1018) - v21:BasicObject = SendDirect v11, 0x1040, :max (0x1050) + PushInlineFrame v11 (0x1040) + v26:ArrayExact = NewArray CheckInterrupts - Return v21 + PopInlineFrame + Return v26 "); } @@ -9593,7 +9638,7 @@ mod hir_opt_tests { } #[test] - fn test_send_direct_iseq_with_block_no_callee_block_param() { + fn test_send_iseq_with_block_no_callee_block_param() { let result = eval(r#" def foo yield 1 @@ -9616,9 +9661,12 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) + PushInlineFrame v18 (0x1038) + v24:Fixnum[1] = Const Value(1) + v26:BasicObject = InvokeBlock v24 # SendFallbackReason: InvokeBlock: not yet specialized CheckInterrupts - Return v19 + PopInlineFrame + Return v26 "); } @@ -9646,9 +9694,49 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) + v71:NilClass = Const Value(nil) + PushInlineFrame v18 (0x1038) + v27:CPtr = GetEP 0 + v28:CUInt64 = LoadField v27, :VM_ENV_DATA_INDEX_FLAGS@0x1040 + v29:CBool = IsBlockParamModified v28 + CondBranch v29, bb7(), bb8() + bb7(): + v31:BasicObject = LoadField v27, :blk@0x1041 + Jump bb9(v31, v31) + bb8(): + v33:CInt64 = LoadField v27, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 + v34:CInt64[0] = GuardBitEquals v33, CInt64(0) recompile + v35:NilClass = Const Value(nil) + Jump bb9(v35, v71) + bb9(v25:BasicObject, v26:BasicObject): CheckInterrupts - Return v19 + v39:CBool = Test v25 + CondBranch v39, bb10(), bb6(v18, v26) + bb10(): + v46:CPtr = GetEP 0 + v47:CUInt64 = LoadField v46, :VM_ENV_DATA_INDEX_FLAGS@0x1040 + v48:CBool = IsBlockParamModified v47 + CondBranch v48, bb11(), bb12() + bb11(): + v50:BasicObject = LoadField v46, :blk@0x1041 + Jump bb13(v50, v50) + bb12(): + v52:CInt64 = LoadField v46, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 + v53:CInt64 = GuardAnyBitSet v52, CUInt64(1) recompile + v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) + Jump bb13(v54, v26) + bb13(v44:BasicObject, v45:BasicObject): + v57:BasicObject = Send v44, :call # SendFallbackReason: SendWithoutBlock: no profile data available + CheckInterrupts + Jump bb4(v57) + bb6(v62:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v63:BasicObject): + v66:Fixnum[42] = Const Value(42) + CheckInterrupts + Jump bb4(v66) + bb4(v72:BasicObject): + PopInlineFrame + CheckInterrupts + Return v72 "); } @@ -9677,9 +9765,12 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) + v48:NilClass = Const Value(nil) + PushInlineFrame v19 (0x1038) CheckInterrupts - Return v20 + v43:Fixnum[42] = Const Value(42) + PopInlineFrame + Return v43 "); } @@ -15417,7 +15508,7 @@ mod hir_opt_tests { } #[test] - fn test_invokesuper_to_iseq_optimizes_to_direct() { + fn test_invokesuper_to_iseq_optimizes() { eval(" class A def foo @@ -15437,7 +15528,6 @@ mod hir_opt_tests { // A Ruby method as the target of `super` should optimize provided no block is given. let hir = hir_string_proc("B.new.method(:foo)"); assert!(!hir.contains("InvokeSuper "), "InvokeSuper should optimize to SendDirect but got:\n{hir}"); - assert!(hir.contains("SendDirect"), "Should optimize to SendDirect for call without args or block:\n{hir}"); assert_snapshot!(hir, @" fn foo@:10: @@ -15456,9 +15546,12 @@ mod hir_opt_tests { v20:CallableMethodEntry[VALUE(0x1040)] = GuardBitEquals v19, Value(VALUE(0x1040)) v21:RubyValue = LoadField v18, :VM_ENV_DATA_INDEX_SPECVAL@0x1048 v22:FalseClass = GuardBitEquals v21, Value(false) - v23:BasicObject = SendDirect v6, 0x1050, :foo (0x1060) + PushInlineFrame v6 (0x1050) + v28:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v29:StringExact = StringCopy v28 CheckInterrupts - Return v23 + PopInlineFrame + Return v29 "); } @@ -15487,7 +15580,7 @@ mod hir_opt_tests { } #[test] - fn test_invokesuper_with_positional_args_optimizes_to_direct() { + fn test_invokesuper_with_positional_args_optimizes() { eval(" class A def foo(x) @@ -15506,7 +15599,6 @@ mod hir_opt_tests { let hir = hir_string_proc("B.new.method(:foo)"); assert!(!hir.contains("InvokeSuper "), "InvokeSuper should optimize to SendDirect but got:\n{hir}"); - assert!(hir.contains("SendDirect"), "Should optimize to SendDirect for call without args or block:\n{hir}"); assert_snapshot!(hir, @" fn foo@:10: @@ -15528,13 +15620,17 @@ mod hir_opt_tests { v30:CallableMethodEntry[VALUE(0x1048)] = GuardBitEquals v29, Value(VALUE(0x1048)) v31:RubyValue = LoadField v28, :VM_ENV_DATA_INDEX_SPECVAL@0x1050 v32:FalseClass = GuardBitEquals v31, Value(false) - v33:BasicObject = SendDirect v9, 0x1058, :foo (0x1068), v10 - v18:Fixnum[1] = Const Value(1) - PatchPoint MethodRedefined(Integer@0x1070, +@0x1078, cme:0x1080) - v36:Fixnum = GuardType v33, Fixnum recompile - v37:Fixnum = FixnumAdd v36, v18 + PushInlineFrame v9 (0x1058), v10 + v40:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Integer@0x1060, *@0x1068, cme:0x1070) + v54:Fixnum = GuardType v10, Fixnum recompile + v55:Fixnum = FixnumMult v54, v40 CheckInterrupts - Return v37 + PopInlineFrame + v18:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1060, +@0x1098, cme:0x10a0) + v59:Fixnum = FixnumAdd v55, v18 + Return v59 "); } @@ -16856,7 +16952,7 @@ mod hir_opt_tests { // 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" + assert_snapshot!(hir_string("test_no_profile_recompile"), @" fn test_no_profile_recompile@:4: bb1(): EntryPoint interpreter @@ -16879,11 +16975,14 @@ mod hir_opt_tests { 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)] recompile - v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + PushInlineFrame v43 (0x1040), v23 + PatchPoint MethodRedefined(Integer@0x1048, to_s@0x1050, cme:0x1058) + v63:StringExact = CCallVariadic v23, :Integer#to_s@0x1080 CheckInterrupts - Return v44 + PopInlineFrame + Return v63 bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v35:StringExact[VALUE(0x1088)] = Const Value(VALUE(0x1088)) v36:StringExact = StringCopy v35 CheckInterrupts Return v36 @@ -16968,6 +17067,8 @@ mod hir_opt_tests { // the HIR. The auto-compile creates version 1, and hir_string() creates version 2 // (= MAX_ISEQ_VERSIONS), so this is the final version. set_call_threshold(3); + set_inline_threshold(0); + eval(" def greet_final(x) = x.to_s def test_final_version(flag) @@ -16983,7 +17084,7 @@ mod hir_opt_tests { eval("3.times { test_final_version(false) }"); // On the final version, greet_final should be a Send fallback, not a SideExit. - assert_snapshot!(hir_string("test_final_version"), @r" + assert_snapshot!(hir_string("test_final_version"), @" fn test_final_version@:4: bb1(): EntryPoint interpreter @@ -17833,6 +17934,7 @@ mod hir_opt_tests { #[test] fn test_trigger_guard_type_recompilation() { + set_inline_threshold(0); eval(" class C def f(x) @@ -17864,7 +17966,6 @@ mod hir_opt_tests { # Do this enough times to cause a recompilation num_to_compile.times { c.f(1.5) } "); - let final_hir = hir_string_proc("C.new.method(:f)"); assert_snapshot!(format!("{intermediate_hir}\n{final_hir}"), @" diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 8289a43d7f924e..770dfc271edf5a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -107,7 +107,7 @@ mod snapshot_tests { } #[test] - fn test_send_direct_with_reordered_kwargs_has_snapshot() { + fn test_send_with_reordered_kwargs_has_snapshot() { eval(" def foo(a:, b:, c:) = [a, b, c] def test = foo(c: 3, a: 1, b: 2) @@ -136,16 +136,19 @@ mod snapshot_tests { v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v13, v15, v11], locals: [] } PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) v25:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile - v26:BasicObject = SendDirect v25, 0x1048, :foo (0x1058), v13, v15, v11 - v18:Any = Snapshot FrameState { pc: 0x1060, stack: [v26], locals: [] } - PatchPoint NoTracePoint + v43:Fixnum[0] = Const Value(0) + PushInlineFrame v25 (0x1048), v13, v15, v11 + v37:Any = Snapshot FrameState { pc: 0x1050, stack: [v13, v15, v11], locals: [a=v13, b=v15, c=v11, ID(0)=v43], caller: v23 } + v38:ArrayExact = NewArray v13, v15, v11 + v39:Any = Snapshot FrameState { pc: 0x1058, stack: [v38], locals: [a=v13, b=v15, c=v11, ID(0)=v43], caller: v23 } CheckInterrupts - Return v26 + PopInlineFrame + Return v38 "); } #[test] - fn test_send_direct_with_kwargs_in_order_has_snapshot() { + fn test_send_with_kwargs_in_order_has_snapshot() { eval(" def foo(a:, b:) = [a, b] def test = foo(a: 1, b: 2) @@ -172,11 +175,14 @@ mod snapshot_tests { v14:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] } PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) v22:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile - v23:BasicObject = SendDirect v22, 0x1048, :foo (0x1058), v11, v13 - v16:Any = Snapshot FrameState { pc: 0x1060, stack: [v23], locals: [] } - PatchPoint NoTracePoint + v38:Fixnum[0] = Const Value(0) + PushInlineFrame v22 (0x1048), v11, v13 + v32:Any = Snapshot FrameState { pc: 0x1050, stack: [v11, v13], locals: [a=v11, b=v13, ID(0)=v38], caller: v14 } + v33:ArrayExact = NewArray v11, v13 + v34:Any = Snapshot FrameState { pc: 0x1058, stack: [v33], locals: [a=v11, b=v13, ID(0)=v38], caller: v14 } CheckInterrupts - Return v23 + PopInlineFrame + Return v33 "); } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index bf09235002dd8d..fecc0fc3977549 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -25,11 +25,15 @@ pub const DEFAULT_CALL_THRESHOLD: CallThreshold = 30; pub type CallThreshold = u64; /// Default --zjit-inline-threshold -pub const DEFAULT_INLINE_THRESHOLD: InlineThreshold = 0; +/// TODO (nirvdrum 2026-06-25): 30 has proven to work well with ruby-bench, but we should finely +/// tune across more workloads. +pub const DEFAULT_INLINE_THRESHOLD: InlineThreshold = 30; pub type InlineThreshold = usize; /// Default --zjit-inline-budget -pub const DEFAULT_INLINE_BUDGET: InlineBudget = 500; +/// TODO (nirvdrum 2026-06-25): 200 has proven to strike a good balance between memory usage and +/// run time performance on ruby-bench, but we should finely tune across more workloads. +pub const DEFAULT_INLINE_BUDGET: InlineBudget = 200; pub const INLINE_BUDGET_UNLIMITED: InlineBudget = 0; pub type InlineBudget = usize; @@ -634,7 +638,6 @@ pub fn set_call_threshold(call_threshold: CallThreshold) { update_profile_threshold(); } -/// Update --zjit-inline-threshold for testing #[cfg(test)] pub fn set_inline_threshold(inline_threshold: InlineThreshold) { rb_zjit_prepare_options(); From 7c176daccf304d2db8f16f530ff527af40a8be9f Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 25 Jun 2026 17:28:58 -0400 Subject: [PATCH 9/9] ZJIT: Run CI jobs with both the inliner enabled an disabled --- .github/workflows/zjit-macos.yml | 7 ++++++ .github/workflows/zjit-ubuntu.yml | 40 +++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index be9bbec4098472..a8730fb59c5b42 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -181,11 +181,18 @@ jobs: strategy: matrix: include: + # Run with the inliner enabled (on by default) # Test --call-threshold=2 with 2 iterations in total - ruby_opts: '--zjit-call-threshold=2' bench_opts: '--warmup=1 --bench=1 --excludes=shipit' configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow + # Run with inliner disabled # + # Test --call-threshold=2 with 2 iterations in total + - ruby_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2' + bench_opts: '--warmup=1 --bench=1 --excludes=shipit' + configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow + runs-on: macos-26 if: >- diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 25e89d21bb4ac8..0771e479317f57 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -58,28 +58,43 @@ jobs: fail-fast: false matrix: include: + - test_task: 'check' + run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' + specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' + configure: '--enable-zjit=dev' + + ## Run with default options (inliner enabled) ## - test_task: 'check' run_opts: '--zjit-call-threshold=1' specopts: '-T --zjit-call-threshold=1' configure: '--enable-zjit=dev' - - test_task: 'check' - run_opts: '--zjit-call-threshold=1 --zjit-inline-threshold=30' - specopts: '-T --zjit-call-threshold=1 -T --zjit-inline-threshold=30' + # The optimizer benefits from at least 1 iteration of profiling. Also, many + # regression tests in bootstraptest/test_yjit.rb assume call-threshold=2. + - test_task: 'btest' + run_opts: '--zjit-call-threshold=2' configure: '--enable-zjit=dev' - continue-on-test_task: true + - test_task: 'test-bundled-gems' + configure: '--enable-zjit=dev' + run_opts: '--zjit-call-threshold=1' + + ## Run with inliner disabled ## - test_task: 'check' - run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' - specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' + run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=1' + specopts: '-T --zjit-inline-threshold=0 -T --zjit-call-threshold=1' configure: '--enable-zjit=dev' # The optimizer benefits from at least 1 iteration of profiling. Also, many # regression tests in bootstraptest/test_yjit.rb assume call-threshold=2. - test_task: 'btest' - run_opts: '--zjit-call-threshold=2' + run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2' configure: '--enable-zjit=dev' + - test_task: 'test-bundled-gems' + configure: '--enable-zjit=dev' + run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=1' + - test_task: 'zjit-check' # zjit-test + quick feedback of test_zjit.rb configure: '--enable-yjit --enable-zjit=dev' rust_version: '1.85.0' @@ -91,10 +106,6 @@ jobs: clang_path: '/usr/bin/clang-16' runs-on: 'ubuntu-24.04' # for clang-16 - - test_task: 'test-bundled-gems' - configure: '--enable-zjit=dev' - run_opts: '--zjit-call-threshold=1' - env: GITPULLOPTIONS: --no-tags origin ${{ github.ref }} RUN_OPTS: ${{ matrix.run_opts }} @@ -246,11 +257,14 @@ jobs: strategy: matrix: include: - # Test --call-threshold=2 with 2 iterations in total + # Test --call-threshold=2 with 2 iterations in total (inlining enabled) - ruby_opts: '--zjit-call-threshold=2' bench_opts: '--warmup=1 --bench=1 --excludes=shipit' configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow - + # Test --call-threshold=2 with 2 iterations in total (inlining disabled) + - ruby_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2' + bench_opts: '--warmup=1 --bench=1 --excludes=shipit' + configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow runs-on: ubuntu-24.04 if: >-