From e1f49ff79ded5737b0455cd6363e05a2551e7b8b Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 16 Feb 2026 10:18:19 -0800 Subject: [PATCH 1/4] Don't attempt to convert strings to hashes in gsub --- string.c | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/string.c b/string.c index 417e9893cef882..6b8cdf240d4192 100644 --- a/string.c +++ b/string.c @@ -6226,9 +6226,11 @@ rb_str_sub_bang(int argc, VALUE *argv, VALUE str) } else { repl = argv[1]; - hash = rb_check_hash_type(argv[1]); - if (NIL_P(hash)) { - StringValue(repl); + if (!RB_TYPE_P(repl, T_STRING)) { + hash = rb_check_hash_type(repl); + if (NIL_P(hash)) { + StringValue(repl); + } } } @@ -6356,15 +6358,17 @@ str_gsub(int argc, VALUE *argv, VALUE str, int bang) break; case 2: repl = argv[1]; - hash = rb_check_hash_type(argv[1]); - if (NIL_P(hash)) { - StringValue(repl); - } - else if (rb_hash_default_unredefined(hash) && !FL_TEST_RAW(hash, RHASH_PROC_DEFAULT)) { - mode = FAST_MAP; - } - else { - mode = MAP; + if (!RB_TYPE_P(repl, T_STRING)) { + hash = rb_check_hash_type(repl); + if (NIL_P(hash)) { + StringValue(repl); + } + else if (rb_hash_default_unredefined(hash) && !FL_TEST_RAW(hash, RHASH_PROC_DEFAULT)) { + mode = FAST_MAP; + } + else { + mode = MAP; + } } break; default: From 671ea6fb3ea64caad64bd2955bdd562673a651f9 Mon Sep 17 00:00:00 2001 From: Steven Webb Date: Thu, 19 Feb 2026 06:12:31 +0800 Subject: [PATCH 2/4] ZJIT: Constant fold modulus (%) operations (#16168) Similar to the way ZJIT already folds +, -, and * operations. One complication is that the % operator behaves differently in Ruby than in Rust for negative values. For example in Ruby: ``` ruby -e "puts 11 % -3" # => -1 ``` vs Rust: ``` println!("{}", 11 % -3); // => 2 ``` Rust has the `#rem_euclid()` method; however, it also differs from Ruby. My solution was to call into the cruby #rb_fix_mod_fix method to get the correct behaviour. Note that any `% 0` operations are not folded as they need to raise a ZeroDivisionError. One concern was an integer overflow. Fortunately that's not possible. Given any two Fixnums `a`, and `b`, by definition the result of `a % b` must be closer to 0 than `b`, making an overflow impossible. One adjacent edge case is when calculating `RUBY_FIXNUM_MIN % -1`. The result is 0 which is obviously fine; however, the related result of `RUBY_FIXNUM_MIN / -1` would overflow. The #rb_fix_mod_fix method avoids this problem by not calculating the `div` portion. See: https://github.com/ruby/ruby/blob/07f5126c104507ec70c5bfb2eb5204ceb22b7ebd/internal/fixnum.h#L164 --- zjit/src/hir.rs | 10 ++ zjit/src/hir/opt_tests.rs | 190 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 91938a95fb5e60..c43be0e1003f6b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4500,6 +4500,16 @@ impl Function { _ => None, }) } + Insn::FixnumMod { left, right, .. } => { + self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { + (Some(l), Some(r)) if r != 0 => { + let l_obj = VALUE::fixnum_from_isize(l as isize); + let r_obj = VALUE::fixnum_from_isize(r as isize); + Some(unsafe { rb_jit_fix_mod_fix(l_obj, r_obj) }.as_fixnum()) + }, + _ => None, + }) + } Insn::FixnumEq { left, right, .. } => { self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => Some(l == r), diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index db485e12554770..516bec1c510f5d 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -245,6 +245,196 @@ mod hir_opt_tests { "); } + + #[test] + fn test_fold_fixnum_mod_zero_by_zero() { + eval(" + def test + 0 % 0 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[0] = Const Value(0) + v12:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v22:Fixnum = FixnumMod v10, v12 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_fold_fixnum_mod_non_zero_by_zero() { + eval(" + def test + 11 % 0 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[11] = Const Value(11) + v12:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v22:Fixnum = FixnumMod v10, v12 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_fold_fixnum_mod_zero_by_non_zero() { + eval(" + def test + 0 % 11 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[0] = Const Value(0) + v12:Fixnum[11] = Const Value(11) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v24:Fixnum[0] = Const Value(0) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_fold_fixnum_mod() { + eval(" + def test + 11 % 3 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[11] = Const Value(11) + v12:Fixnum[3] = Const Value(3) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v24:Fixnum[2] = Const Value(2) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_fold_fixnum_mod_negative_numerator() { + eval(" + def test + -7 % 3 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[-7] = Const Value(-7) + v12:Fixnum[3] = Const Value(3) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v24:Fixnum[2] = Const Value(2) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_fold_fixnum_mod_negative_denominator() { + eval(" + def test + 7 % -3 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[7] = Const Value(7) + v12:Fixnum[-3] = Const Value(-3) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v24:Fixnum[-2] = Const Value(-2) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_fold_fixnum_mod_negative() { + eval(" + def test + -7 % -3 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[-7] = Const Value(-7) + v12:Fixnum[-3] = Const Value(-3) + PatchPoint MethodRedefined(Integer@0x1000, %@0x1008, cme:0x1010) + v24:Fixnum[-1] = Const Value(-1) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + #[test] fn test_fold_fixnum_less() { eval(" From e11c3cef1b0c44562f710e160a4ca7c12a7c348b Mon Sep 17 00:00:00 2001 From: Nathan Froyd Date: Wed, 18 Feb 2026 16:59:13 -0500 Subject: [PATCH 3/4] [ruby/prism] add `extern "C"` wrappers to `prism.h` when using C++ https://github.com/ruby/prism/commit/003ff5341d --- prism/prism.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/prism/prism.h b/prism/prism.h index c17d759fea7251..a71ccfef369b22 100644 --- a/prism/prism.h +++ b/prism/prism.h @@ -6,6 +6,10 @@ #ifndef PRISM_H #define PRISM_H +#ifdef __cplusplus +extern "C" { +#endif + #include "prism/defines.h" #include "prism/util/pm_buffer.h" #include "prism/util/pm_char.h" @@ -403,4 +407,8 @@ PRISM_EXPORTED_FUNCTION pm_string_query_t pm_string_query_method_name(const uint * ``` */ +#ifdef __cplusplus +} +#endif + #endif From e3183164a5549d21526cf9b8905a885814a580a0 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Wed, 18 Feb 2026 17:22:16 -0700 Subject: [PATCH 4/4] ZJIT: Save VM state before recording exit stack for trace-exits (#16195) When --zjit-trace-exits is enabled, rb_zjit_record_exit_stack was called before compile_exit restored the VM state (cfp->pc, cfp->sp, stack, locals). This ccall clobbers caller-saved registers that may hold stack/local operands, causing garbage values to be written to the VM stack when compile_exit runs afterward. This led to crashes like `klass: T_FALSE` in the interpreter when those corrupted values were used as receivers. Fix by splitting compile_exit into compile_exit_save_state and compile_exit_return, and emitting the full exit inline when recording: save VM state first, then call rb_zjit_record_exit_stack (so rb_profile_frames sees valid cfp->pc), then return. Before this I was able to cause a crash with ``` ruby --zjit-trace-exits -rffi -e1 ``` --- zjit/src/backend/lir.rs | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 0a504b8bcc8b4a..f8590b1049f150 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2220,9 +2220,8 @@ impl Assembler /// Compile Target::SideExit and convert it into Target::CodePtr for all instructions pub fn compile_exits(&mut self) { - /// Compile the main side-exit code. This function takes only SideExit so - /// that it can be safely deduplicated by using SideExit as a dedup key. - fn compile_exit(asm: &mut Assembler, exit: &SideExit) { + /// 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; // Side exit blocks are not part of the CFG at the moment, @@ -2249,12 +2248,22 @@ impl Assembler asm.store(Opnd::mem(64, SP, (-local_size_and_idx_to_ep_offset(locals.len(), idx) - 1) * SIZEOF_VALUE_I32), opnd); } } + } + /// Tear down the JIT frame and return to the interpreter. + fn compile_exit_return(asm: &mut Assembler) { asm_comment!(asm, "exit to the interpreter"); asm.frame_teardown(&[]); // matching the setup in gen_entry_point() asm.cret(Opnd::UImm(Qundef.as_u64())); } + /// Compile the main side-exit code. This function takes only SideExit so + /// 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); + compile_exit_return(asm); + } + fn join_opnds(opnds: &Vec, delimiter: &str) -> String { opnds.iter().map(|opnd| format!("{opnd}")).collect::>().join(delimiter) } @@ -2310,13 +2319,19 @@ impl Assembler } if should_record_exit { + // Save VM state before the ccall so that + // rb_profile_frames sees valid cfp->pc and the + // ccall doesn't clobber caller-saved registers + // holding stack/local operands. + compile_exit_save_state(self, &exit); asm_ccall!(self, rb_zjit_record_exit_stack, pc); - } - - // If the side exit has already been compiled, jump to it. - // Otherwise, let it fall through and compile the exit next. - if let Some(&exit_label) = compiled_exits.get(&exit) { - self.jmp(Target::Label(exit_label)); + compile_exit_return(self); + } else { + // If the side exit has already been compiled, jump to it. + // Otherwise, let it fall through and compile the exit next. + if let Some(&exit_label) = compiled_exits.get(&exit) { + self.jmp(Target::Label(exit_label)); + } } Some(counted_exit) } else {