From d02c97157476bbd9774f2bf6425a69166b99da1b Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 10:57:06 -0800 Subject: [PATCH 01/20] Stop bumping RUBY_PATCHLEVEL in release versions (#15502) [[Misc #21770]](https://bugs.ruby-lang.org/issues/21770) --- test/ruby/test_rubyoptions.rb | 6 ++++++ tool/merger.rb | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index a057e64a4a3596..2ec6478a7fd058 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -1319,4 +1319,10 @@ def test_free_at_exit_env_var def test_toplevel_ruby assert_instance_of Module, ::Ruby end + + def test_ruby_patchlevel + # We stopped bumping RUBY_PATCHLEVEL at Ruby 4.0.0. + # Released versions have RUBY_PATCHLEVEL 0, and un-released versions have -1. + assert_include [-1, 0], RUBY_PATCHLEVEL + end end diff --git a/tool/merger.rb b/tool/merger.rb index 795e97a86ee529..2eedded66c1a1b 100755 --- a/tool/merger.rb +++ b/tool/merger.rb @@ -65,7 +65,8 @@ def version_up(teeny: false) if teeny v[2].succ! end - if pl != '-1' # trunk does not have patchlevel + # We stopped bumping RUBY_PATCHLEVEL at Ruby 4.0.0. + if Integer(v[0]) <= 3 pl.succ! end From 1b7c8b7993c596cba7251dad16ff8e7234fd976f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 11 Dec 2025 21:19:25 +0100 Subject: [PATCH 02/20] [ruby/timeout] Skip signal test on windows Windows has no SIGUSR1. There might be another usable signal, but this is breaking ruby master so I just want a quick fix for now. https://github.com/ruby/timeout/commit/b19043e8d0 --- test/test_timeout.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_timeout.rb b/test/test_timeout.rb index 7421b5ba4174c8..199b18e7bc9fb9 100644 --- a/test/test_timeout.rb +++ b/test/test_timeout.rb @@ -444,5 +444,5 @@ def test_timeout_in_trap_handler assert_equal "OK", rd.read rd.close - end + end if Signal.list["USR1"] # Windows has no SIGUSR1 end From 832aac6c283c59a1bcd3dc81badd380d0fc4dc2f Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 11 Dec 2025 02:15:11 -0500 Subject: [PATCH 03/20] Tune AS_CASE indentation style and remove `[*]` for default case There are many indentation styles for AS_CASE in this file but no one uses `[*]` for the default case. --- configure.ac | 76 ++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/configure.ac b/configure.ac index f992e4c015332a..766c75089c87d6 100644 --- a/configure.ac +++ b/configure.ac @@ -4051,44 +4051,44 @@ AS_CASE(["${ZJIT_SUPPORT}"], JIT_RUST_FLAGS='--crate-type=staticlib --cfg feature=\"stats_allocator\"' RLIB_DIR= AS_CASE(["$JIT_CARGO_SUPPORT:$YJIT_SUPPORT:$ZJIT_SUPPORT"], - [no:yes:yes], [ # release build of YJIT+ZJIT - YJIT_LIBS= - ZJIT_LIBS= - JIT_RUST_FLAGS="--crate-type=rlib" - RLIB_DIR="target/release" - RUST_LIB="target/release/libruby.a" - ], - [no:*], [], - [*], [ # JIT_CARGO_SUPPORT not "no" -- cargo required. - AC_CHECK_TOOL(CARGO, [cargo], [no]) - AS_IF([test x"$CARGO" = "xno"], - AC_MSG_ERROR([this build configuration requires cargo. Installation instructions available at https://www.rust-lang.org/tools/install])) - - YJIT_LIBS= - ZJIT_LIBS= - - # There's more processing below to get the feature set for the - # top-level crate, so capture at this point for feature set of - # just the zjit crate. - ZJIT_TEST_FEATURES="${rb_cargo_features}" - - AS_IF([test x"${YJIT_SUPPORT}" != x"no"], [ - rb_cargo_features="$rb_cargo_features,yjit" - ]) - AS_IF([test x"${ZJIT_SUPPORT}" != x"no"], [ - AC_SUBST(ZJIT_TEST_FEATURES) - rb_cargo_features="$rb_cargo_features,zjit" - ]) - # if YJIT and ZJIT release mode - AS_IF([test "${YJIT_SUPPORT}:${ZJIT_SUPPORT}" = "yes:yes"], [ - JIT_CARGO_SUPPORT=release - ]) - CARGO_BUILD_ARGS="--profile ${JIT_CARGO_SUPPORT} --features ${rb_cargo_features}" - AS_IF([test "${JIT_CARGO_SUPPORT}" = "dev"], [ - RUST_LIB="target/debug/libruby.a" - ], [ - RUST_LIB="target/${JIT_CARGO_SUPPORT}/libruby.a" - ]) +[no:yes:yes], [ # release build of YJIT+ZJIT + YJIT_LIBS= + ZJIT_LIBS= + JIT_RUST_FLAGS="--crate-type=rlib" + RLIB_DIR="target/release" + RUST_LIB="target/release/libruby.a" +], +[no:*], [], +[ # JIT_CARGO_SUPPORT not "no" -- cargo required. + AC_CHECK_TOOL(CARGO, [cargo], [no]) + AS_IF([test x"$CARGO" = "xno"], + AC_MSG_ERROR([this build configuration requires cargo. Installation instructions available at https://www.rust-lang.org/tools/install])) + + YJIT_LIBS= + ZJIT_LIBS= + + # There's more processing below to get the feature set for the + # top-level crate, so capture at this point for feature set of + # just the zjit crate. + ZJIT_TEST_FEATURES="${rb_cargo_features}" + + AS_IF([test x"${YJIT_SUPPORT}" != x"no"], [ + rb_cargo_features="$rb_cargo_features,yjit" + ]) + AS_IF([test x"${ZJIT_SUPPORT}" != x"no"], [ + AC_SUBST(ZJIT_TEST_FEATURES) + rb_cargo_features="$rb_cargo_features,zjit" + ]) + # if YJIT and ZJIT release mode + AS_IF([test "${YJIT_SUPPORT}:${ZJIT_SUPPORT}" = "yes:yes"], [ + JIT_CARGO_SUPPORT=release + ]) + CARGO_BUILD_ARGS="--profile ${JIT_CARGO_SUPPORT} --features ${rb_cargo_features}" + AS_IF([test "${JIT_CARGO_SUPPORT}" = "dev"], [ + RUST_LIB="target/debug/libruby.a" + ], [ + RUST_LIB="target/${JIT_CARGO_SUPPORT}/libruby.a" + ]) ]) # In case either we're linking rust code From c092c294d4ef08135c5d63db00824b3ec27274d2 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 11 Dec 2025 02:41:55 -0500 Subject: [PATCH 04/20] ZJIT: [DOC] Mention build prerequisites --- doc/jit/zjit.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/jit/zjit.md b/doc/jit/zjit.md index 1e5a36fd5e6f04..c66235269bd265 100644 --- a/doc/jit/zjit.md +++ b/doc/jit/zjit.md @@ -13,6 +13,10 @@ You can change how much executable memory is allocated using [ZJIT's command-lin ## Build Instructions +Refer to [Building Ruby](rdoc-ref:contributing/building_ruby.md) for general build prerequists. +Additionally, ZJIT requires Rust 1.85.0 or later. Release builds need only `rustc`. Development +builds require `cargo` and may download dependencies. + ### For normal use To build ZJIT on macOS: From eb889e474d02f79565f5d8cf001d86bd8aeb737d Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 11 Dec 2025 02:55:05 -0500 Subject: [PATCH 05/20] ZJIT: s/checking possible to build ZJIT/checking prerequisites for ZJIT/ Reads better to me: > checking prerequisites for ZJIT... yes --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 766c75089c87d6..5dadf13168a867 100644 --- a/configure.ac +++ b/configure.ac @@ -3957,7 +3957,7 @@ AC_ARG_ENABLE(zjit, [AS_CASE(["$JIT_TARGET_OK"], [yes], [ rb_zjit_build_possible=no - AC_MSG_CHECKING([possible to build ZJIT])dnl only checked when --enable-zjit is not specified + AC_MSG_CHECKING([prerequisites for ZJIT])dnl only checked when --enable-zjit is not specified # Fails in case rustc target doesn't match ruby target. Can happen on Rosetta, for example. # 1.85.0 is the first stable version that supports the 2024 edition. AS_IF([test "$RUSTC" != "no" && echo "#[cfg(target_arch = \"$JIT_TARGET_ARCH\")] fn main() {}" | From f8f8ff61062080887ad26a36bd58b51e631eee80 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 11 Dec 2025 03:06:54 -0500 Subject: [PATCH 06/20] auto_request_review.yml: Update path for jit related docs --- .github/auto_request_review.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/auto_request_review.yml b/.github/auto_request_review.yml index 814150c90e1da9..38496d5cebbebe 100644 --- a/.github/auto_request_review.yml +++ b/.github/auto_request_review.yml @@ -2,16 +2,15 @@ files: 'yjit*': [team:jit] 'yjit/**/*': [team:jit] 'yjit/src/cruby_bindings.inc.rs': [] - 'doc/yjit/*': [team:jit] 'bootstraptest/test_yjit*': [team:jit] 'test/ruby/test_yjit*': [team:jit] 'zjit*': [team:jit] 'zjit/**/*': [team:jit] 'zjit/src/cruby_bindings.inc.rs': [] - 'doc/zjit*': [team:jit] 'test/ruby/test_zjit*': [team:jit] 'defs/jit.mk': [team:jit] 'tool/zjit_bisect.rb': [team:jit] + 'doc/jit/*': [team:jit] # Skip github workflow files because the team don't necessarily need to review dependabot updates for GitHub Actions. It's noisy in notifications, and they're auto-merged anyway. options: ignore_draft: true From fb80587f88e2cb1d52098fe967aed6c43d74a82e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 11 Dec 2025 21:24:19 +0100 Subject: [PATCH 07/20] [ruby/timeout] Add windows to CI matrix https://github.com/ruby/timeout/commit/c8d63ce3fe --- test/test_timeout.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_timeout.rb b/test/test_timeout.rb index 199b18e7bc9fb9..42db4ebbbe6de2 100644 --- a/test/test_timeout.rb +++ b/test/test_timeout.rb @@ -425,7 +425,9 @@ def test_timeout_in_trap_handler rd, wr = IO.pipe - trap("SIGUSR1") do + signal = Signal.list["USR1"] ? :USR1 : :TERM + + trap(signal) do begin Timeout.timeout(0.1) do sleep 1 @@ -440,9 +442,9 @@ def test_timeout_in_trap_handler end end - Process.kill :USR1, Process.pid + Process.kill signal, Process.pid assert_equal "OK", rd.read rd.close - end if Signal.list["USR1"] # Windows has no SIGUSR1 + end end From 64062792c61d13d1eca9f637db3ed7da12a61154 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 11 Dec 2025 16:12:00 -0500 Subject: [PATCH 08/20] ZJIT: Check method visibility when optimizing sends (#15501) Fix https://github.com/Shopify/ruby/issues/874 --- zjit/src/hir.rs | 39 +++++++- zjit/src/hir/opt_tests.rs | 197 ++++++++++++++++++++++++++++++++++++++ zjit/src/stats.rs | 5 + 3 files changed, 239 insertions(+), 2 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bc6f1fb5f564fa..4992f177991d47 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -614,6 +614,7 @@ pub enum SendFallbackReason { SendWithoutBlockCfuncArrayVariadic, SendWithoutBlockNotOptimizedMethodType(MethodType), SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType), + SendWithoutBlockNotOptimizedNeedPermission, SendWithoutBlockBopRedefined, SendWithoutBlockOperandsNotFixnum, SendWithoutBlockDirectKeywordMismatch, @@ -626,6 +627,7 @@ pub enum SendFallbackReason { SendCfuncVariadic, SendCfuncArrayVariadic, SendNotOptimizedMethodType(MethodType), + SendNotOptimizedNeedPermission, CCallWithFrameTooManyArgs, ObjToStringNotString, TooManyArgsForLir, @@ -653,6 +655,8 @@ impl Display for SendFallbackReason { SendWithoutBlockCfuncArrayVariadic => write!(f, "SendWithoutBlock: C function expects array variadic"), SendWithoutBlockNotOptimizedMethodType(method_type) => write!(f, "SendWithoutBlock: unsupported method type {:?}", method_type), SendWithoutBlockNotOptimizedMethodTypeOptimized(opt_type) => write!(f, "SendWithoutBlock: unsupported optimized method type {:?}", opt_type), + SendWithoutBlockNotOptimizedNeedPermission => write!(f, "SendWithoutBlock: method private or protected and no FCALL"), + SendNotOptimizedNeedPermission => write!(f, "Send: method private or protected and no FCALL"), SendWithoutBlockBopRedefined => write!(f, "SendWithoutBlock: basic operation was redefined"), SendWithoutBlockOperandsNotFixnum => write!(f, "SendWithoutBlock: operands are not fixnums"), SendWithoutBlockDirectKeywordMismatch => write!(f, "SendWithoutBlockDirect: keyword mismatch"), @@ -2634,6 +2638,16 @@ impl Function { // Load an overloaded cme if applicable. See vm_search_cc(). // It allows you to use a faster ISEQ if possible. cme = unsafe { rb_check_overloaded_cme(cme, ci) }; + let visibility = unsafe { METHOD_ENTRY_VISI(cme) }; + match (visibility, flags & VM_CALL_FCALL != 0) { + (METHOD_VISI_PUBLIC, _) => {} + (METHOD_VISI_PRIVATE, true) => {} + (METHOD_VISI_PROTECTED, true) => {} + _ => { + self.set_dynamic_send_reason(insn_id, SendWithoutBlockNotOptimizedNeedPermission); + self.push_insn_id(block, insn_id); continue; + } + } let mut def_type = unsafe { get_cme_def_type(cme) }; while def_type == VM_METHOD_TYPE_ALIAS { cme = unsafe { rb_aliased_callable_method_entry(cme) }; @@ -3256,7 +3270,18 @@ impl Function { return Err(()); } + let ci_flags = unsafe { vm_ci_flag(call_info) }; + let visibility = unsafe { METHOD_ENTRY_VISI(cme) }; + match (visibility, ci_flags & VM_CALL_FCALL != 0) { + (METHOD_VISI_PUBLIC, _) => {} + (METHOD_VISI_PRIVATE, true) => {} + (METHOD_VISI_PROTECTED, true) => {} + _ => { + fun.set_dynamic_send_reason(send_insn_id, SendNotOptimizedNeedPermission); + return Err(()); + } + } // When seeing &block argument, fall back to dynamic dispatch for now // TODO: Support block forwarding @@ -3398,6 +3423,18 @@ impl Function { return Err(()); } + let ci_flags = unsafe { vm_ci_flag(call_info) }; + let visibility = unsafe { METHOD_ENTRY_VISI(cme) }; + match (visibility, ci_flags & VM_CALL_FCALL != 0) { + (METHOD_VISI_PUBLIC, _) => {} + (METHOD_VISI_PRIVATE, true) => {} + (METHOD_VISI_PROTECTED, true) => {} + _ => { + fun.set_dynamic_send_reason(send_insn_id, SendWithoutBlockNotOptimizedNeedPermission); + return Err(()); + } + } + // Find the `argc` (arity) of the C method, which describes the parameters it expects let cfunc = unsafe { get_cme_def_body_cfunc(cme) }; let cfunc_argc = unsafe { get_mct_argc(cfunc) }; @@ -3410,8 +3447,6 @@ impl Function { return Err(()); } - let ci_flags = unsafe { vm_ci_flag(call_info) }; - // Filter for simple call sites (i.e. no splats etc.) if ci_flags & VM_CALL_ARGS_SIMPLE == 0 { fun.count_complex_call_features(block, ci_flags); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 62ea7c11b0c672..a602121a0cc51d 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -9932,4 +9932,201 @@ mod hir_opt_tests { Return v25 "); } + + #[test] + fn optimize_call_to_private_method_iseq_with_fcall() { + eval(r#" + class C + def callprivate = secret + private def secret = 42 + end + C.new.callprivate + "#); + assert_snapshot!(hir_string_proc("C.instance_method(:callprivate)"), @r" + fn callprivate@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint MethodRedefined(C@0x1000, secret@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(C@0x1000) + v18:HeapObject[class_exact:C] = GuardType v6, HeapObject[class_exact:C] + IncrCounter inline_iseq_optimized_send_count + v21:Fixnum[42] = Const Value(42) + CheckInterrupts + Return v21 + "); + } + + #[test] + fn dont_optimize_call_to_private_method_iseq() { + eval(r#" + class C + private def secret = 42 + end + Obj = C.new + def test = Obj.secret rescue $! + test + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Obj) + v21:HeapObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:BasicObject = SendWithoutBlock v21, :secret # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + CheckInterrupts + Return v13 + "); + } + + #[test] + fn optimize_call_to_private_method_cfunc_with_fcall() { + eval(r#" + class BasicObject + def callprivate = initialize rescue $! + end + Obj = BasicObject.new.callprivate + "#); + assert_snapshot!(hir_string_proc("BasicObject.instance_method(:callprivate)"), @r" + fn callprivate@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint MethodRedefined(BasicObject@0x1000, initialize@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(BasicObject@0x1000) + v20:BasicObjectExact = GuardType v6, BasicObjectExact + v21:NilClass = Const Value(nil) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v21 + "); + } + + #[test] + fn dont_optimize_call_to_private_method_cfunc() { + eval(r#" + Obj = BasicObject.new + def test = Obj.initialize rescue $! + test + "#); + 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): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Obj) + v21:BasicObjectExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:BasicObject = SendWithoutBlock v21, :initialize # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + CheckInterrupts + Return v13 + "); + } + + #[test] + fn dont_optimize_call_to_private_top_level_method() { + eval(r#" + def toplevel_method = :OK + Obj = Object.new + def test = Obj.toplevel_method rescue $! + test + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Obj) + v21:ObjectExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:BasicObject = SendWithoutBlock v21, :toplevel_method # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + CheckInterrupts + Return v13 + "); + } + + #[test] + fn optimize_call_to_protected_method_iseq_with_fcall() { + eval(r#" + class C + def callprotected = secret + protected def secret = 42 + end + C.new.callprotected + "#); + assert_snapshot!(hir_string_proc("C.instance_method(:callprotected)"), @r" + fn callprotected@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint MethodRedefined(C@0x1000, secret@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(C@0x1000) + v18:HeapObject[class_exact:C] = GuardType v6, HeapObject[class_exact:C] + IncrCounter inline_iseq_optimized_send_count + v21:Fixnum[42] = Const Value(42) + CheckInterrupts + Return v21 + "); + } + + #[test] + fn dont_optimize_call_to_protected_method_iseq() { + eval(r#" + class C + protected def secret = 42 + end + Obj = C.new + def test = Obj.secret rescue $! + test + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Obj) + v21:HeapObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:BasicObject = SendWithoutBlock v21, :secret # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + CheckInterrupts + Return v13 + "); + } } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 25f0c638be2998..38e8df170d33fd 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -219,6 +219,7 @@ make_counters! { send_fallback_send_without_block_cfunc_array_variadic, send_fallback_send_without_block_not_optimized_method_type, send_fallback_send_without_block_not_optimized_method_type_optimized, + send_fallback_send_without_block_not_optimized_need_permission, send_fallback_too_many_args_for_lir, send_fallback_send_without_block_bop_redefined, send_fallback_send_without_block_operands_not_fixnum, @@ -230,6 +231,7 @@ make_counters! { send_fallback_send_megamorphic, send_fallback_send_no_profiles, send_fallback_send_not_optimized_method_type, + send_fallback_send_not_optimized_need_permission, send_fallback_ccall_with_frame_too_many_args, send_fallback_argc_param_mismatch, // The call has at least one feature on the caller or callee side @@ -552,6 +554,8 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter SendWithoutBlockNotOptimizedMethodType(_) => send_fallback_send_without_block_not_optimized_method_type, SendWithoutBlockNotOptimizedMethodTypeOptimized(_) => send_fallback_send_without_block_not_optimized_method_type_optimized, + SendWithoutBlockNotOptimizedNeedPermission + => send_fallback_send_without_block_not_optimized_need_permission, TooManyArgsForLir => send_fallback_too_many_args_for_lir, SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined, SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum, @@ -569,6 +573,7 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter ArgcParamMismatch => send_fallback_argc_param_mismatch, BmethodNonIseqProc => send_fallback_bmethod_non_iseq_proc, SendNotOptimizedMethodType(_) => send_fallback_send_not_optimized_method_type, + SendNotOptimizedNeedPermission => send_fallback_send_not_optimized_need_permission, CCallWithFrameTooManyArgs => send_fallback_ccall_with_frame_too_many_args, ObjToStringNotString => send_fallback_obj_to_string_not_string, Uncategorized(_) => send_fallback_uncategorized, From dc58d58a723cf56d2a59db52252b82755248b539 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 11 Dec 2025 17:12:03 -0500 Subject: [PATCH 09/20] [ruby/timeout] Fix failing timeout test ``` Run options: "--ruby=./miniruby -I../ruby/lib -I. -I.ext/common ../ruby/tool/runruby.rb --extout=.ext -- --disable-gems" --excludes-dir=../ruby/test/.excludes --name=!/memory_leak/ --seed=9843 [ 1/31] TestTimeout#test_timeout_in_trap_handler = 0.00 s 1) Error: TestTimeout#test_timeout_in_trap_handler: NoMethodError: undefined method 'kill' for nil /Users/luke/workspace/ruby-dev/ruby/test/test_timeout.rb:9:in 'TestTimeout#kill_timeout_thread' /Users/luke/workspace/ruby-dev/ruby/test/test_timeout.rb:424:in 'TestTimeout#test_timeout_in_trap_handler' Finished tests in 2.715032s, 11.4179 tests/s, 52.3014 assertions/s. 31 tests, 142 assertions, 0 failures, 1 errors, 0 skips ruby -v: ruby 4.0.0dev (2025-12-11T21:56:23Z fix_timeout_test https://github.com/ruby/timeout/commit/1c5eacbf9a) +PRISM [arm64-darwin24] make: *** [yes-test-all] Error 1 ``` https://github.com/ruby/timeout/commit/e5bc1de901 --- test/test_timeout.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_timeout.rb b/test/test_timeout.rb index 42db4ebbbe6de2..91940085c46ac3 100644 --- a/test/test_timeout.rb +++ b/test/test_timeout.rb @@ -6,8 +6,10 @@ class TestTimeout < Test::Unit::TestCase private def kill_timeout_thread thread = Timeout.const_get(:State).instance.instance_variable_get(:@timeout_thread) - thread.kill - thread.join + if thread + thread.kill + thread.join + end end def test_public_methods From 07b2356a6ad314b9a7b2bb9fc0527b440f004faa Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 10 Dec 2025 11:44:27 +0100 Subject: [PATCH 10/20] Mutex: avoid repeated calls to `GET_EC` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That call is surprisingly expensive, so trying doing it once in `#synchronize` and then passing the EC to lock and unlock saves quite a few cycles. Before: ``` ruby 4.0.0dev (2025-12-10T09:30:18Z master c5608ab4d7) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 1.888M i/100ms Monitor 1.633M i/100ms Calculating ------------------------------------- Mutex 22.610M (± 0.2%) i/s (44.23 ns/i) - 113.258M in 5.009097s Monitor 19.148M (± 0.3%) i/s (52.22 ns/i) - 96.366M in 5.032755s ``` After: ``` ruby 4.0.0dev (2025-12-10T10:40:07Z speedup-mutex 1c901cd4f8) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.095M i/100ms Monitor 1.578M i/100ms Calculating ------------------------------------- Mutex 24.456M (± 0.4%) i/s (40.89 ns/i) - 123.584M in 5.053418s Monitor 19.176M (± 0.1%) i/s (52.15 ns/i) - 96.243M in 5.018977s ``` Bench: ``` require 'bundler/inline' gemfile do gem "benchmark-ips" end mutex = Mutex.new require "monitor" monitor = Monitor.new Benchmark.ips do |x| x.report("Mutex") { mutex.synchronize { } } x.report("Monitor") { monitor.synchronize { } } end ``` --- eval.c | 9 +- internal/eval.h | 2 + thread_sync.c | 222 +++++++++++++++++++++++------------------------- thread_sync.rb | 83 ++++++++++++++++++ 4 files changed, 197 insertions(+), 119 deletions(-) diff --git a/eval.c b/eval.c index ee5bc43f9cc4c1..0c80872bee76e5 100644 --- a/eval.c +++ b/eval.c @@ -1133,12 +1133,11 @@ rb_protect(VALUE (* proc) (VALUE), VALUE data, int *pstate) } VALUE -rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) +rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) { enum ruby_tag_type state; volatile VALUE result = Qnil; VALUE errinfo; - rb_execution_context_t * volatile ec = GET_EC(); EC_PUSH_TAG(ec); if ((state = EC_EXEC_TAG()) == TAG_NONE) { result = (*b_proc) (data1); @@ -1155,6 +1154,12 @@ rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE dat return result; } +VALUE +rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) +{ + return rb_ec_ensure(GET_EC(), b_proc, data1, e_proc, data2); +} + static ID frame_func_id(const rb_control_frame_t *cfp) { diff --git a/internal/eval.h b/internal/eval.h index 4c1c045b4e1042..17ade0a7f1e667 100644 --- a/internal/eval.h +++ b/internal/eval.h @@ -11,6 +11,7 @@ * header (related to this file, but not the same role). */ #include "ruby/ruby.h" /* for ID */ +#include "vm_core.h" /* for ID */ #define id_signo ruby_static_id_signo #define id_status ruby_static_id_status @@ -30,6 +31,7 @@ VALUE rb_exception_setup(int argc, VALUE *argv); void rb_refinement_setup(struct rb_refinements_data *data, VALUE module, VALUE klass); void rb_vm_using_module(VALUE module); VALUE rb_top_main_class(const char *method); +VALUE rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2); /* eval_error.c */ VALUE rb_get_backtrace(VALUE info); diff --git a/thread_sync.c b/thread_sync.c index 9fb1639e9b7dd2..9942d08e0a5c8f 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -123,7 +123,7 @@ rb_mutex_num_waiting(rb_mutex_t *mutex) rb_thread_t* rb_fiber_threadptr(const rb_fiber_t *fiber); static bool -locked_p(rb_mutex_t *mutex) +mutex_locked_p(rb_mutex_t *mutex) { return mutex->fiber_serial != 0; } @@ -132,7 +132,7 @@ static void mutex_free(void *ptr) { rb_mutex_t *mutex = ptr; - if (locked_p(mutex)) { + if (mutex_locked_p(mutex)) { const char *err = rb_mutex_unlock_th(mutex, rb_thread_ptr(mutex->thread), NULL); if (err) rb_bug("%s", err); } @@ -179,36 +179,18 @@ mutex_alloc(VALUE klass) return obj; } -/* - * call-seq: - * Thread::Mutex.new -> mutex - * - * Creates a new Mutex - */ -static VALUE -mutex_initialize(VALUE self) -{ - return self; -} - VALUE rb_mutex_new(void) { return mutex_alloc(rb_cMutex); } -/* - * call-seq: - * mutex.locked? -> true or false - * - * Returns +true+ if this lock is currently held by some thread. - */ VALUE rb_mutex_locked_p(VALUE self) { rb_mutex_t *mutex = mutex_ptr(self); - return RBOOL(locked_p(mutex)); + return RBOOL(mutex_locked_p(mutex)); } static void @@ -267,17 +249,16 @@ mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) } } -/* - * call-seq: - * mutex.try_lock -> true or false - * - * Attempts to obtain the lock and returns immediately. Returns +true+ if the - * lock was granted. - */ +static VALUE +rb_mut_trylock(rb_execution_context_t *ec, VALUE self) +{ + return RBOOL(mutex_trylock(mutex_ptr(self), ec->thread_ptr, ec->fiber_ptr)); +} + VALUE rb_mutex_trylock(VALUE self) { - return RBOOL(mutex_trylock(mutex_ptr(self), GET_THREAD(), GET_EC()->fiber_ptr)); + return rb_mut_trylock(GET_EC(), self); } static VALUE @@ -303,13 +284,28 @@ delete_from_waitq(VALUE value) static inline rb_atomic_t threadptr_get_interrupts(rb_thread_t *th); +struct mutex_args { + VALUE self; + rb_mutex_t *mutex; + rb_execution_context_t *ec; +}; + +static inline void +mutex_args_init(struct mutex_args *args, VALUE mutex) +{ + args->self = mutex; + args->mutex = mutex_ptr(mutex); + args->ec = GET_EC(); +} + static VALUE -do_mutex_lock(VALUE self, int interruptible_p) +do_mutex_lock(struct mutex_args *args, int interruptible_p) { - rb_execution_context_t *ec = GET_EC(); + VALUE self = args->self; + rb_execution_context_t *ec = args->ec; rb_thread_t *th = ec->thread_ptr; rb_fiber_t *fiber = ec->fiber_ptr; - rb_mutex_t *mutex = mutex_ptr(self); + rb_mutex_t *mutex = args->mutex; rb_atomic_t saved_ints = 0; /* When running trap handler */ @@ -432,35 +428,40 @@ do_mutex_lock(VALUE self, int interruptible_p) static VALUE mutex_lock_uninterruptible(VALUE self) { - return do_mutex_lock(self, 0); + struct mutex_args args; + mutex_args_init(&args, self); + return do_mutex_lock(&args, 0); +} + +static VALUE +rb_mut_lock(rb_execution_context_t *ec, VALUE self) +{ + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + return do_mutex_lock(&args, 1); } -/* - * call-seq: - * mutex.lock -> self - * - * Attempts to grab the lock and waits if it isn't available. - * Raises +ThreadError+ if +mutex+ was locked by the current thread. - */ VALUE rb_mutex_lock(VALUE self) { - return do_mutex_lock(self, 1); + struct mutex_args args; + mutex_args_init(&args, self); + return do_mutex_lock(&args, 1); +} + +static VALUE +rb_mut_owned_p(rb_execution_context_t *ec, VALUE self) +{ + return mutex_owned_p(ec->fiber_ptr, mutex_ptr(self)); } -/* - * call-seq: - * mutex.owned? -> true or false - * - * Returns +true+ if this lock is currently held by current thread. - */ VALUE rb_mutex_owned_p(VALUE self) { - rb_fiber_t *fiber = GET_EC()->fiber_ptr; - rb_mutex_t *mutex = mutex_ptr(self); - - return mutex_owned_p(fiber, mutex); + return rb_mut_owned_p(GET_EC(), self); } static const char * @@ -508,6 +509,24 @@ rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) return NULL; } +static void +do_mutex_unlock(struct mutex_args *args) +{ + const char *err; + rb_mutex_t *mutex = args->mutex; + rb_thread_t *th = rb_ec_thread_ptr(args->ec); + + err = rb_mutex_unlock_th(mutex, th, args->ec->fiber_ptr); + if (err) rb_raise(rb_eThreadError, "%s", err); +} + +static VALUE +do_mutex_unlock_safe(VALUE args) +{ + do_mutex_unlock((struct mutex_args *)args); + return Qnil; +} + /* * call-seq: * mutex.unlock -> self @@ -518,13 +537,21 @@ rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) VALUE rb_mutex_unlock(VALUE self) { - const char *err; - rb_mutex_t *mutex = mutex_ptr(self); - rb_thread_t *th = GET_THREAD(); - - err = rb_mutex_unlock_th(mutex, th, GET_EC()->fiber_ptr); - if (err) rb_raise(rb_eThreadError, "%s", err); + struct mutex_args args; + mutex_args_init(&args, self); + do_mutex_unlock(&args); + return self; +} +static VALUE +rb_mut_unlock(rb_execution_context_t *ec, VALUE self) +{ + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + do_mutex_unlock(&args); return self; } @@ -593,17 +620,15 @@ mutex_sleep_begin(VALUE _arguments) return woken; } -VALUE -rb_mutex_sleep(VALUE self, VALUE timeout) +static VALUE +rb_mut_sleep(rb_execution_context_t *ec, VALUE self, VALUE timeout) { - rb_execution_context_t *ec = GET_EC(); - if (!NIL_P(timeout)) { // Validate the argument: rb_time_interval(timeout); } - rb_mutex_unlock(self); + rb_mut_unlock(ec, self); time_t beg = time(0); struct rb_mutex_sleep_arguments arguments = { @@ -611,7 +636,7 @@ rb_mutex_sleep(VALUE self, VALUE timeout) .timeout = timeout, }; - VALUE woken = rb_ensure(mutex_sleep_begin, (VALUE)&arguments, mutex_lock_uninterruptible, self); + VALUE woken = rb_ec_ensure(ec, mutex_sleep_begin, (VALUE)&arguments, mutex_lock_uninterruptible, self); RUBY_VM_CHECK_INTS_BLOCKING(ec); if (!woken) return Qnil; @@ -619,61 +644,32 @@ rb_mutex_sleep(VALUE self, VALUE timeout) return TIMET2NUM(end); } -/* - * call-seq: - * mutex.sleep(timeout = nil) -> number or nil - * - * Releases the lock and sleeps +timeout+ seconds if it is given and - * non-nil or forever. Raises +ThreadError+ if +mutex+ wasn't locked by - * the current thread. - * - * When the thread is next woken up, it will attempt to reacquire - * the lock. - * - * Note that this method can wakeup without explicit Thread#wakeup call. - * For example, receiving signal and so on. - * - * Returns the slept time in seconds if woken up, or +nil+ if timed out. - */ -static VALUE -mutex_sleep(int argc, VALUE *argv, VALUE self) +VALUE +rb_mutex_sleep(VALUE self, VALUE timeout) { - VALUE timeout; - - timeout = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; - return rb_mutex_sleep(self, timeout); + return rb_mut_sleep(GET_EC(), self, timeout); } -/* - * call-seq: - * mutex.synchronize { ... } -> result of the block - * - * Obtains a lock, runs the block, and releases the lock when the block - * completes. See the example under Thread::Mutex. - */ VALUE -rb_mutex_synchronize(VALUE mutex, VALUE (*func)(VALUE arg), VALUE arg) +rb_mutex_synchronize(VALUE self, VALUE (*func)(VALUE arg), VALUE arg) { - rb_mutex_lock(mutex); - return rb_ensure(func, arg, rb_mutex_unlock, mutex); + struct mutex_args args; + mutex_args_init(&args, self); + do_mutex_lock(&args, 1); + return rb_ec_ensure(args.ec, func, arg, do_mutex_unlock_safe, (VALUE)&args); } -/* - * call-seq: - * mutex.synchronize { ... } -> result of the block - * - * Obtains a lock, runs the block, and releases the lock when the block - * completes. See the example under Thread::Mutex. - */ -static VALUE -rb_mutex_synchronize_m(VALUE self) +VALUE +rb_mut_synchronize(rb_execution_context_t *ec, VALUE self) { - if (!rb_block_given_p()) { - rb_raise(rb_eThreadError, "must be called with a block"); - } - - return rb_mutex_synchronize(self, rb_yield, Qundef); + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + do_mutex_lock(&args, 1); + return rb_ec_ensure(args.ec, rb_yield, Qundef, do_mutex_unlock_safe, (VALUE)&args); } void @@ -1688,14 +1684,6 @@ Init_thread_sync(void) /* Mutex */ DEFINE_CLASS(Mutex, Object); rb_define_alloc_func(rb_cMutex, mutex_alloc); - rb_define_method(rb_cMutex, "initialize", mutex_initialize, 0); - rb_define_method(rb_cMutex, "locked?", rb_mutex_locked_p, 0); - rb_define_method(rb_cMutex, "try_lock", rb_mutex_trylock, 0); - rb_define_method(rb_cMutex, "lock", rb_mutex_lock, 0); - rb_define_method(rb_cMutex, "unlock", rb_mutex_unlock, 0); - rb_define_method(rb_cMutex, "sleep", mutex_sleep, -1); - rb_define_method(rb_cMutex, "synchronize", rb_mutex_synchronize_m, 0); - rb_define_method(rb_cMutex, "owned?", rb_mutex_owned_p, 0); /* Queue */ DEFINE_CLASS(Queue, Object); diff --git a/thread_sync.rb b/thread_sync.rb index f8fa69900b39d0..28c70b1e9ce8fb 100644 --- a/thread_sync.rb +++ b/thread_sync.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Thread class Queue # call-seq: @@ -65,4 +67,85 @@ def push(object, non_block = false, timeout: nil) alias_method :enq, :push alias_method :<<, :push end + + class Mutex + # call-seq: + # Thread::Mutex.new -> mutex + # + # Creates a new Mutex + def initialize + end + + # call-seq: + # mutex.locked? -> true or false + # + # Returns +true+ if this lock is currently held by some thread. + def locked? + Primitive.cexpr! %q{ RBOOL(mutex_locked_p(mutex_ptr(self))) } + end + + # call-seq: + # mutex.owned? -> true or false + # + # Returns +true+ if this lock is currently held by current thread. + def owned? + Primitive.rb_mut_owned_p + end + + # call-seq: + # mutex.lock -> self + # + # Attempts to grab the lock and waits if it isn't available. + # Raises +ThreadError+ if +mutex+ was locked by the current thread. + def lock + Primitive.rb_mut_lock + end + + # call-seq: + # mutex.try_lock -> true or false + # + # Attempts to obtain the lock and returns immediately. Returns +true+ if the + # lock was granted. + def try_lock + Primitive.rb_mut_trylock + end + + # call-seq: + # mutex.lock -> self + # + # Attempts to grab the lock and waits if it isn't available. + # Raises +ThreadError+ if +mutex+ was locked by the current thread. + def unlock + Primitive.rb_mut_unlock + end + + # call-seq: + # mutex.synchronize { ... } -> result of the block + # + # Obtains a lock, runs the block, and releases the lock when the block + # completes. See the example under Thread::Mutex. + def synchronize + raise ThreadError, "must be called with a block" unless defined?(yield) + + Primitive.rb_mut_synchronize + end + + # call-seq: + # mutex.sleep(timeout = nil) -> number or nil + # + # Releases the lock and sleeps +timeout+ seconds if it is given and + # non-nil or forever. Raises +ThreadError+ if +mutex+ wasn't locked by + # the current thread. + # + # When the thread is next woken up, it will attempt to reacquire + # the lock. + # + # Note that this method can wakeup without explicit Thread#wakeup call. + # For example, receiving signal and so on. + # + # Returns the slept time in seconds if woken up, or +nil+ if timed out. + def sleep(timeout = nil) + Primitive.rb_mut_sleep(timeout) + end + end end From 8210a117814662e5ad405d5824c5f1d100f450a5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 11 Dec 2025 22:05:16 +0100 Subject: [PATCH 11/20] test_ractor.rb: old object while calling _id2ref --- bootstraptest/test_ractor.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 0b7a43272c3ca9..6b35bbb46be896 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1169,7 +1169,8 @@ class C # Inserting into the id2ref table should be Ractor-safe assert_equal 'ok', <<~'RUBY' # Force all calls to Kernel#object_id to insert into the id2ref table - ObjectSpace._id2ref(Object.new.object_id) + obj = Object.new + ObjectSpace._id2ref(obj.object_id) rescue nil 10.times.map do Ractor.new do From 0564214a00450371527c7bd69fc13618e5f25f30 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 14:27:44 -0800 Subject: [PATCH 12/20] tool/merger.rb: Update the tag format for Ruby 4.0+ --- tool/merger.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tool/merger.rb b/tool/merger.rb index 2eedded66c1a1b..a205fd96831c6b 100755 --- a/tool/merger.rb +++ b/tool/merger.rb @@ -114,7 +114,13 @@ def tag(relname) abort 'no relname is given and not in a release branch even if this is patch release' end end - tagname = "v#{v.join('_')}#{("_#{pl}" if v[0] < "2" || (v[0] == "2" && v[1] < "1") || /^(?:preview|rc)/ =~ pl)}" + if /^(?:preview|rc)/ =~ pl + tagname = "v#{v.join('.')}-#{pl}" + elsif Integer(v[0]) >= 4 + tagname = "v#{v.join('.')}" + else + tagname = "v#{v.join('_')}" + end unless execute('git', 'diff', '--exit-code') abort 'uncommitted changes' From a973526c050fec044ffd7ceeba0ac8e8a1fed299 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 14:41:36 -0800 Subject: [PATCH 13/20] tool/format-release: Fix the tag format for Ruby 4.0+ --- tool/format-release | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tool/format-release b/tool/format-release index b7ad74a095c6fd..b38263f9f4fb05 100755 --- a/tool/format-release +++ b/tool/format-release @@ -62,19 +62,18 @@ eom if z != 0 prev_tag = nil elsif y != 0 - prev_tag = "v#{x}_#{y-1}_0" prev_ver = "#{x}.#{y-1}.0" + prev_tag = version_tag(prev_ver) else # y == 0 && z == 0 case x when 3 - prev_tag = "v2_7_0" prev_ver = "2.7.0" when 4 - prev_tag = "v3_4_0" prev_ver = "3.4.0" else raise "it doesn't know what is the previous version of '#{version}'" end + prev_tag = version_tag(prev_ver) end uri = "https://cache.ruby-lang.org/pub/tmp/ruby-info-#{version}-draft.yml" @@ -95,7 +94,7 @@ eom if prev_tag # show diff shortstat - tag = "v#{version.gsub(/[.\-]/, '_')}" + tag = version_tag(version) stat = `git -C #{rubydir} diff -l0 --shortstat #{prev_tag}..#{tag}` files_changed, insertions, deletions = stat.scan(/\d+/) end @@ -189,7 +188,7 @@ eom if /\.0(?:-\w+)?\z/ =~ ver # preview, rc, or first release entry <<= <= 4 + "v#{version}" + else + "v#{version.tr('.-', '_')}" + end + end end def main From 04299ca184daa836b143eabb7835540928076595 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 11 Dec 2025 23:54:22 +0100 Subject: [PATCH 14/20] monitor.c: skip GET_EC() on exit --- ext/monitor/monitor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c index aeb96d7701242d..c43751c4e21f72 100644 --- a/ext/monitor/monitor.c +++ b/ext/monitor/monitor.c @@ -148,7 +148,7 @@ monitor_check_owner(VALUE monitor) static void monitor_exit0(struct monitor_args *args) { - monitor_check_owner(args->monitor); + monitor_check_owner0(args); if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); args->mc->count--; From aff0c6dad2486e939e7f6678c519314925dad866 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 15:52:46 -0800 Subject: [PATCH 15/20] tool/merger.rb: Support the new format in remove_tag --- tool/merger.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tool/merger.rb b/tool/merger.rb index a205fd96831c6b..4c096087fc9a25 100755 --- a/tool/merger.rb +++ b/tool/merger.rb @@ -142,10 +142,12 @@ def remove_tag(relname) unless relname raise ArgumentError, 'relname is not specified' end - if /^v/ !~ relname - tagname = "v#{relname.gsub(/[.-]/, '_')}" - else + if relname.start_with?('v') tagname = relname + elsif Integer(relname.split('.', 2).first) >= 4 + tagname = "v#{relname}" + else + tagname = "v#{relname.gsub(/[.-]/, '_')}" end execute('git', 'tag', '-d', tagname) From 12bf3a99d72f5f6f0a7633863e285029aa407c57 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 16:18:37 -0800 Subject: [PATCH 16/20] update-www-meta.rb: Update the tag format for Ruby 4.0+ Just copied format-release fixes in a973526c050fec044ffd7ceeba0ac8e8a1fed299 to this file. --- tool/releng/update-www-meta.rb | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tool/releng/update-www-meta.rb b/tool/releng/update-www-meta.rb index 8a5651dcd05cf1..100f0bee181806 100755 --- a/tool/releng/update-www-meta.rb +++ b/tool/releng/update-www-meta.rb @@ -49,13 +49,18 @@ def self.parse(wwwdir, version) if z != 0 prev_tag = nil elsif y != 0 - prev_tag = "v#{x}_#{y-1}_0" prev_ver = "#{x}.#{y-1}.0" - elsif x == 3 && y == 0 && z == 0 - prev_tag = "v2_7_0" - prev_ver = "2.7.0" - else - raise "unexpected version for prev_ver '#{version}'" + prev_tag = version_tag(prev_ver) + else # y == 0 && z == 0 + case x + when 3 + prev_ver = "2.7.0" + when 4 + prev_ver = "3.4.0" + else + raise "it doesn't know what is the previous version of '#{version}'" + end + prev_tag = version_tag(prev_ver) end uri = "https://cache.ruby-lang.org/pub/tmp/ruby-info-#{version}-draft.yml" @@ -76,7 +81,7 @@ def self.parse(wwwdir, version) if prev_tag # show diff shortstat - tag = "v#{version.gsub(/[.\-]/, '_')}" + tag = version_tag(version) rubydir = File.expand_path(File.join(__FILE__, '../../../')) puts %`git -C #{rubydir} diff --shortstat #{prev_tag}..#{tag}` stat = `git -C #{rubydir} diff --shortstat #{prev_tag}..#{tag}` @@ -155,7 +160,7 @@ def self.update_releases_yml(ver, xy, ary, wwwdir, files_changed, insertions, de date = Time.now.utc # use utc to use previous day in midnight entry = <= 4 + "v#{version}" + else + "v#{version.tr('.-', '_')}" + end + end end # Confirm current directory is www.ruby-lang.org's working directory From 58f9aca0ccc51a1f9c02280438518c9be8047dbd Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 16:33:49 -0800 Subject: [PATCH 17/20] make-snapshot: Drop X.Y.Z-pN support We no longer make patchlevel releases. --- tool/make-snapshot | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tool/make-snapshot b/tool/make-snapshot index 8251fa6324f970..cdda0c59940f39 100755 --- a/tool/make-snapshot +++ b/tool/make-snapshot @@ -274,10 +274,6 @@ def package(vcs, rev, destdir, tmp = nil) prerelease = true tag = "#{$4}#{$5}" url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}#{$5}") - when /\A(.*)\.(.*)\.(.*)-p(\d+)/ - patchlevel = true - tag = "p#{$4}" - url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}") when /\A(\d+)\.(\d+)(?:\.(\d+))?\z/ if $3 && ($1 > "2" || $1 == "2" && $2 >= "1") patchlevel = true From 0ecf68963571b70103f7fa35b7403585585d032c Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 16:35:12 -0800 Subject: [PATCH 18/20] make-snapshot: Make preview/rc match stricter to make it a bit more consistent with the other branch --- tool/make-snapshot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/make-snapshot b/tool/make-snapshot index cdda0c59940f39..2ea35f8feaa2da 100755 --- a/tool/make-snapshot +++ b/tool/make-snapshot @@ -270,7 +270,7 @@ def package(vcs, rev, destdir, tmp = nil) when /\Astable\z/ vcs.branch_list("ruby_[0-9]*") {|n| url = n[/\Aruby_\d+_\d+\z/]} url &&= vcs.branch(url) - when /\A(.*)\.(.*)\.(.*)-(preview|rc)(\d+)/ + when /\A(\d+)\.(\d+)\.(\d+)-(preview|rc)(\d+)/ prerelease = true tag = "#{$4}#{$5}" url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}#{$5}") From 6601640c68b02f0bf1ad58d9122e76c84b735f91 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 16:37:49 -0800 Subject: [PATCH 19/20] make-snapshot: Branch if it's X.Y.Z or X.Y using when It just seems like a completely different input, so it makes more sense to me to have it as a separate case. Also, we don't need to support Ruby 2.0 or older. --- tool/make-snapshot | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tool/make-snapshot b/tool/make-snapshot index 2ea35f8feaa2da..0a03fb2a227ae7 100755 --- a/tool/make-snapshot +++ b/tool/make-snapshot @@ -274,14 +274,12 @@ def package(vcs, rev, destdir, tmp = nil) prerelease = true tag = "#{$4}#{$5}" url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}#{$5}") - when /\A(\d+)\.(\d+)(?:\.(\d+))?\z/ - if $3 && ($1 > "2" || $1 == "2" && $2 >= "1") - patchlevel = true - tag = "" - url = vcs.tag("v#{$1}_#{$2}_#{$3}") - else - url = vcs.branch("ruby_#{rev.tr('.', '_')}") - end + when /\A(\d+)\.(\d+)\.(\d+)\z/ + patchlevel = true + tag = "" + url = vcs.tag("v#{$1}_#{$2}_#{$3}") + when /\A(\d+)\.(\d+)\z/ + url = vcs.branch("ruby_#{rev.tr('.', '_')}") else warn "#{$0}: unknown version - #{rev}" return From 06a6ad44f6faf35542e0bd1e15658340d449c2cf Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Dec 2025 16:42:23 -0800 Subject: [PATCH 20/20] make-snapshot: Remove an unnecessary variable This is a refactoring change, which should have no impact on behaviors. Now, if patchlevel is true, tag is empty. So `if patchlevel` always does nothing. Given that prerelease is false and tag is not nil, removing `if patchlevel` should have no impact. --- tool/make-snapshot | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tool/make-snapshot b/tool/make-snapshot index 0a03fb2a227ae7..041b1a0f2aba6d 100755 --- a/tool/make-snapshot +++ b/tool/make-snapshot @@ -253,7 +253,6 @@ end def package(vcs, rev, destdir, tmp = nil) pwd = Dir.pwd - patchlevel = false prerelease = false if rev and revision = rev[/@(\h+)\z/, 1] rev = $` @@ -275,7 +274,6 @@ def package(vcs, rev, destdir, tmp = nil) tag = "#{$4}#{$5}" url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}#{$5}") when /\A(\d+)\.(\d+)\.(\d+)\z/ - patchlevel = true tag = "" url = vcs.tag("v#{$1}_#{$2}_#{$3}") when /\A(\d+)\.(\d+)\z/ @@ -351,13 +349,7 @@ def package(vcs, rev, destdir, tmp = nil) [api_major_version, api_minor_version, version_teeny].join('.') end version or return - if patchlevel - unless tag.empty? - versionhdr ||= File.read("#{v}/version.h") - patchlevel = versionhdr[/^\#define\s+RUBY_PATCHLEVEL\s+(\d+)/, 1] - tag = (patchlevel ? "p#{patchlevel}" : vcs.revision_name(revision)) - end - elsif prerelease + if prerelease versionhdr ||= File.read("#{v}/version.h") versionhdr.sub!(/^\#\s*define\s+RUBY_PATCHLEVEL_STR\s+"\K.+?(?=")/, tag) or raise "no match of RUBY_PATCHLEVEL_STR to replace" File.write("#{v}/version.h", versionhdr)