From 3bb97e7707a0be8c371cb9c704cb1e21062e1fc6 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 10 Dec 2025 04:32:34 +0900 Subject: [PATCH 01/32] `_RUBY_DEBUG_LOG` usable anywhere even if `USE_RUBY_DEBUG_LOG=0`. It becomes `fprintf(stderr, ...)`. --- debug.c | 18 ++++++++++++++++++ vm_debug.h | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/debug.c b/debug.c index b92faa8f369398..4daee2bd1cbd0d 100644 --- a/debug.c +++ b/debug.c @@ -708,4 +708,22 @@ ruby_debug_log_dump(const char *fname, unsigned int n) fclose(fp); } } + +#else + +#undef ruby_debug_log +void +ruby_debug_log(const char *file, int line, const char *func_name, const char *fmt, ...) +{ + va_list args; + + fprintf(stderr, "[%s:%d] %s: ", file, line, func_name); + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + + fprintf(stderr, "\n"); +} + #endif // #if USE_RUBY_DEBUG_LOG diff --git a/vm_debug.h b/vm_debug.h index d0bc81574a31b5..cf80232f3a5eb0 100644 --- a/vm_debug.h +++ b/vm_debug.h @@ -88,6 +88,10 @@ void ruby_debug_log(const char *file, int line, const char *func_name, const cha void ruby_debug_log_print(unsigned int n); bool ruby_debug_log_filter(const char *func_name, const char *file_name); +// convenient macro to log even if the USE_RUBY_DEBUG_LOG macro is not specified. +// You can use this macro for temporary usage (you should not commit it). +#define _RUBY_DEBUG_LOG(...) ruby_debug_log(__FILE__, __LINE__, RUBY_FUNCTION_NAME_STRING, "" __VA_ARGS__) + #if RBIMPL_COMPILER_IS(GCC) && defined(__OPTIMIZE__) # define ruby_debug_log(...) \ RB_GNUC_EXTENSION_BLOCK( \ @@ -97,10 +101,6 @@ bool ruby_debug_log_filter(const char *func_name, const char *file_name); RBIMPL_WARNING_POP()) #endif -// convenient macro to log even if the USE_RUBY_DEBUG_LOG macro is not specified. -// You can use this macro for temporary usage (you should not commit it). -#define _RUBY_DEBUG_LOG(...) ruby_debug_log(__FILE__, __LINE__, RUBY_FUNCTION_NAME_STRING, "" __VA_ARGS__) - #if USE_RUBY_DEBUG_LOG # define RUBY_DEBUG_LOG_ENABLED(func_name, file_name) \ (ruby_debug_log_mode && ruby_debug_log_filter(func_name, file_name)) From 3636277dc5837bcedcd5ef43d49423194064a676 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 10 Dec 2025 12:09:50 +0900 Subject: [PATCH 02/32] Add `NUM2PTR` and `PTR2NUM` macros These macros have been defined here and there, so collect them. --- box.c | 2 +- compile.c | 2 -- ext/-test-/fatal/invalid.c | 6 ------ gc.c | 7 +------ include/ruby/internal/arithmetic/intptr_t.h | 12 ++++++++++++ internal/box.h | 10 ---------- load.c | 4 ++-- spec/ruby/optional/capi/ext/digest_spec.c | 2 ++ yjit.c | 2 -- zjit.c | 2 -- 10 files changed, 18 insertions(+), 31 deletions(-) diff --git a/box.c b/box.c index 0ce8d0aee02f0f..7907e0ff632a1e 100644 --- a/box.c +++ b/box.c @@ -760,7 +760,7 @@ static int cleanup_local_extension_i(VALUE key, VALUE value, VALUE arg) { #if defined(_WIN32) - HMODULE h = (HMODULE)NUM2SVALUE(value); + HMODULE h = (HMODULE)NUM2PTR(value); WCHAR module_path[MAXPATHLEN]; DWORD len = GetModuleFileNameW(h, module_path, numberof(module_path)); diff --git a/compile.c b/compile.c index a5d821eb810cf1..bcf22243cfc7af 100644 --- a/compile.c +++ b/compile.c @@ -610,8 +610,6 @@ branch_coverage_valid_p(rb_iseq_t *iseq, int first_line) return 1; } -#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x))) - static VALUE setup_branch(const rb_code_location_t *loc, const char *type, VALUE structure, VALUE key) { diff --git a/ext/-test-/fatal/invalid.c b/ext/-test-/fatal/invalid.c index 393465416a0f6c..6fd970b181191c 100644 --- a/ext/-test-/fatal/invalid.c +++ b/ext/-test-/fatal/invalid.c @@ -1,11 +1,5 @@ #include -#if SIZEOF_LONG == SIZEOF_VOIDP -# define NUM2PTR(x) NUM2ULONG(x) -#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP -# define NUM2PTR(x) NUM2ULL(x) -#endif - static VALUE invalid_call(VALUE obj, VALUE address) { diff --git a/gc.c b/gc.c index 79eec5d96bf7af..5f0f2307c8c9fd 100644 --- a/gc.c +++ b/gc.c @@ -2122,14 +2122,9 @@ rb_gc_obj_free_vm_weak_references(VALUE obj) static VALUE id2ref(VALUE objid) { -#if SIZEOF_LONG == SIZEOF_VOIDP -#define NUM2PTR(x) NUM2ULONG(x) -#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP -#define NUM2PTR(x) NUM2ULL(x) -#endif objid = rb_to_int(objid); if (FIXNUM_P(objid) || rb_big_size(objid) <= SIZEOF_VOIDP) { - VALUE ptr = NUM2PTR(objid); + VALUE ptr = (VALUE)NUM2PTR(objid); if (SPECIAL_CONST_P(ptr)) { if (ptr == Qtrue) return Qtrue; if (ptr == Qfalse) return Qfalse; diff --git a/include/ruby/internal/arithmetic/intptr_t.h b/include/ruby/internal/arithmetic/intptr_t.h index a354f4469cdf95..70090f88e6b37c 100644 --- a/include/ruby/internal/arithmetic/intptr_t.h +++ b/include/ruby/internal/arithmetic/intptr_t.h @@ -32,6 +32,18 @@ #define rb_int_new rb_int2inum /**< @alias{rb_int2inum} */ #define rb_uint_new rb_uint2inum /**< @alias{rb_uint2inum} */ +// These definitions are same as fiddle/conversions.h +#if SIZEOF_VOIDP <= SIZEOF_LONG +# define PTR2NUM(x) (LONG2NUM((long)(x))) +# define NUM2PTR(x) ((void*)(NUM2ULONG(x))) +#elif SIZEOF_VOIDP <= SIZEOF_LONG_LONG +# define PTR2NUM(x) (LL2NUM((LONG_LONG)(x))) +# define NUM2PTR(x) ((void*)(NUM2ULL(x))) +#else +// should have been an error in ruby/internal/value.h +# error Need integer for VALUE +#endif + RBIMPL_SYMBOL_EXPORT_BEGIN() /** diff --git a/internal/box.h b/internal/box.h index 72263cc9dc5e5d..b62b6a9bc946f2 100644 --- a/internal/box.h +++ b/internal/box.h @@ -3,16 +3,6 @@ #include "ruby/ruby.h" /* for VALUE */ -#if SIZEOF_VALUE <= SIZEOF_LONG -# define SVALUE2NUM(x) LONG2NUM((long)(x)) -# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x) -#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG -# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x)) -# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x) -#else -# error Need integer for VALUE -#endif - /** * @author Ruby developers * @copyright This file is a part of the programming language Ruby. diff --git a/load.c b/load.c index 466517f465b098..144f095b04d2d3 100644 --- a/load.c +++ b/load.c @@ -1345,7 +1345,7 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa reset_ext_config = true; ext_config_push(th, &prev_ext_config); handle = rb_vm_call_cfunc_in_box(box->top_self, load_ext, path, fname, path, box); - rb_hash_aset(box->ruby_dln_libmap, path, SVALUE2NUM((SIGNED_VALUE)handle)); + rb_hash_aset(box->ruby_dln_libmap, path, PTR2NUM(handle)); break; } result = TAG_RETURN; @@ -1666,7 +1666,7 @@ rb_ext_resolve_symbol(const char* fname, const char* symbol) if (NIL_P(handle)) { return NULL; } - return dln_symbol((void *)NUM2SVALUE(handle), symbol); + return dln_symbol(NUM2PTR(handle), symbol); } void diff --git a/spec/ruby/optional/capi/ext/digest_spec.c b/spec/ruby/optional/capi/ext/digest_spec.c index 9993238cf227d3..65c8defa20adce 100644 --- a/spec/ruby/optional/capi/ext/digest_spec.c +++ b/spec/ruby/optional/capi/ext/digest_spec.c @@ -135,7 +135,9 @@ VALUE digest_spec_context_size(VALUE self, VALUE meta) { return SIZET2NUM(algo->ctx_size); } +#ifndef PTR2NUM #define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x))) +#endif VALUE digest_spec_context(VALUE self, VALUE digest) { return PTR2NUM(context); diff --git a/yjit.c b/yjit.c index 6d909a0da61ee3..f3c256093eda0b 100644 --- a/yjit.c +++ b/yjit.c @@ -64,8 +64,6 @@ STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM); // The "_yjit_" part is for trying to be informative. We might want different // suffixes for symbols meant for Rust and symbols meant for broader CRuby. -# define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x))) - // For a given raw_sample (frame), set the hash with the caller's // name, file, and line number. Return the hash with collected frame_info. static void diff --git a/zjit.c b/zjit.c index 75cca281a45a91..df44821fe54643 100644 --- a/zjit.c +++ b/zjit.c @@ -35,8 +35,6 @@ enum zjit_struct_offsets { ISEQ_BODY_OFFSET_PARAM = offsetof(struct rb_iseq_constant_body, param) }; -#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x))) - // For a given raw_sample (frame), set the hash with the caller's // name, file, and line number. Return the hash with collected frame_info. static void From df4fc0f7fcda6c552084ea0638c7185b4a98c939 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 14:07:11 +0900 Subject: [PATCH 03/32] [ruby/psych] v5.3.0 https://github.com/ruby/psych/commit/d8053b0d16 --- ext/psych/lib/psych/versions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/psych/lib/psych/versions.rb b/ext/psych/lib/psych/versions.rb index 3202b10296f549..2c2319e7a38e67 100644 --- a/ext/psych/lib/psych/versions.rb +++ b/ext/psych/lib/psych/versions.rb @@ -2,7 +2,7 @@ module Psych # The version of Psych you are using - VERSION = '5.2.6' + VERSION = '5.3.0' if RUBY_ENGINE == 'jruby' DEFAULT_SNAKEYAML_VERSION = '2.9'.freeze From e4786376d68fc62a0bfcdd4d0e644de0e41e0d4d Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 05:09:13 +0000 Subject: [PATCH 04/32] Update default gems list at df4fc0f7fcda6c552084ea0638c718 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 784b92b717d1c5..a92df627636bd0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -256,7 +256,7 @@ The following default gems are updated. * optparse 0.8.0 * pp 0.6.3 * prism 1.6.0 -* psych 5.2.6 +* psych 5.3.0 * resolv 0.6.3 * stringio 3.1.9.dev * strscan 3.1.6.dev From 814f23747b5fd7b0d5fb6cd8e45833ec39482858 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 14:11:45 +0900 Subject: [PATCH 05/32] [ruby/resolv] v0.7.0 https://github.com/ruby/resolv/commit/a0e89bbe48 --- lib/resolv.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index b98c7ecdd2aa32..0e62aaf8510496 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -35,7 +35,7 @@ class Resolv # The version string - VERSION = "0.6.3" + VERSION = "0.7.0" ## # Looks up the first IP address for +name+. From 238e69d1258663e3385853dca36719c08e089f06 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 05:13:39 +0000 Subject: [PATCH 06/32] Update default gems list at 814f23747b5fd7b0d5fb6cd8e45833 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index a92df627636bd0..8ea6e93b1b5acc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -257,7 +257,7 @@ The following default gems are updated. * pp 0.6.3 * prism 1.6.0 * psych 5.3.0 -* resolv 0.6.3 +* resolv 0.7.0 * stringio 3.1.9.dev * strscan 3.1.6.dev * timeout 0.5.0 From ec862b41dc9aaa2a22d80961b62417a347bc84ec Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 9 Dec 2025 21:18:03 -0800 Subject: [PATCH 07/32] ZJIT: Prohibit ZJIT support with USE_FLONUM=0 (#15471) --- .github/workflows/compilers.yml | 2 +- zjit.c | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 5e54d39e9fa6b9..8c0ca54e0b100b 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -244,7 +244,7 @@ jobs: - { uses: './.github/actions/compilers', name: 'RGENGC_CHECK_MODE', with: { cppflags: '-DRGENGC_CHECK_MODE' }, timeout-minutes: 5 } - { uses: './.github/actions/compilers', name: 'VM_CHECK_MODE', with: { cppflags: '-DVM_CHECK_MODE' }, timeout-minutes: 5 } - { uses: './.github/actions/compilers', name: 'USE_EMBED_CI=0', with: { cppflags: '-DUSE_EMBED_CI=0' }, timeout-minutes: 5 } - - { uses: './.github/actions/compilers', name: 'USE_FLONUM=0', with: { cppflags: '-DUSE_FLONUM=0', append_configure: '--disable-yjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_FLONUM=0', with: { cppflags: '-DUSE_FLONUM=0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } compileX: name: 'omnibus compilations, #10' diff --git a/zjit.c b/zjit.c index df44821fe54643..05fb3e1f028ff6 100644 --- a/zjit.c +++ b/zjit.c @@ -31,6 +31,10 @@ #include +// This build config impacts the pointer tagging scheme and we only want to +// support one scheme for simplicity. +STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM); + enum zjit_struct_offsets { ISEQ_BODY_OFFSET_PARAM = offsetof(struct rb_iseq_constant_body, param) }; From 5f444cba4741b2ff0e1e95f4a179329b1ebc74a2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 14:21:59 +0900 Subject: [PATCH 08/32] [ruby/ipaddr] v1.2.8 https://github.com/ruby/ipaddr/commit/93ef50bc04 --- lib/ipaddr.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ipaddr.rb b/lib/ipaddr.rb index 513b7778a92f0b..725ff309d27f48 100644 --- a/lib/ipaddr.rb +++ b/lib/ipaddr.rb @@ -41,7 +41,7 @@ class IPAddr # The version string - VERSION = "1.2.7" + VERSION = "1.2.8" # 32 bit mask for IPv4 IN4MASK = 0xffffffff From ab80d05fef77c20abb77b08b0c14882a8772ecfb Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 05:23:39 +0000 Subject: [PATCH 09/32] Update default gems list at 5f444cba4741b2ff0e1e95f4a17932 [ci skip] --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 8ea6e93b1b5acc..0c2682d8b96aa3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -250,6 +250,7 @@ The following default gems are updated. * io-console 0.8.1 * io-nonblock 0.3.2 * io-wait 0.4.0.dev +* ipaddr 1.2.8 * json 2.17.1 * net-http 0.8.0 * openssl 4.0.0.pre From 4523a905327d8438f845f5a75822225ccd041beb Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 14:28:10 +0900 Subject: [PATCH 10/32] [ruby/date] v3.5.1 https://github.com/ruby/date/commit/1d0aadc295 --- ext/date/lib/date.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/date/lib/date.rb b/ext/date/lib/date.rb index b33f6e65f466b0..0cb763017f22c0 100644 --- a/ext/date/lib/date.rb +++ b/ext/date/lib/date.rb @@ -4,7 +4,7 @@ require 'date_core' class Date - VERSION = "3.5.0" # :nodoc: + VERSION = "3.5.1" # :nodoc: # call-seq: # infinite? -> false From 74376fefbb2d79d9c2df355445689058fab828e6 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 05:30:06 +0000 Subject: [PATCH 11/32] Update default gems list at 4523a905327d8438f845f5a7582222 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 0c2682d8b96aa3..8c1d15142f71b4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -240,7 +240,7 @@ The following default gems are updated. * RubyGems 4.0.1 * bundler 4.0.1 -* date 3.5.0 +* date 3.5.1 * digest 3.2.1 * english 0.8.1 * erb 6.0.0 From bbee62abbd26e3bf526dbbfddd17d72b81402a72 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 14:48:32 +0900 Subject: [PATCH 12/32] We don't need to check the latest release of pathname Pathname is now embedded class of Ruby --- tool/sync_default_gems.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index 780f923b55598e..6945c6cdce52a6 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -427,7 +427,7 @@ def sync_default_gems(gem) end def check_prerelease_version(gem) - return if ["rubygems", "mmtk", "cgi"].include?(gem) + return if ["rubygems", "mmtk", "cgi", "pathname"].include?(gem) require "net/https" require "json" From 842f91aec09e419481af6358657e06973f2410c2 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 6 Dec 2025 23:15:47 -0600 Subject: [PATCH 13/32] [ruby/stringio] [DOC] Tweaks for StringIO#getc (https://github.com/ruby/stringio/pull/189) https://github.com/ruby/stringio/commit/e3d16d30ed --- doc/stringio/getc.rdoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/stringio/getc.rdoc b/doc/stringio/getc.rdoc index c021789c911b8d..b2ab46843c8466 100644 --- a/doc/stringio/getc.rdoc +++ b/doc/stringio/getc.rdoc @@ -12,9 +12,9 @@ Returns +nil+ if at end-of-stream: Returns characters, not bytes: - strio = StringIO.new('тест') - strio.getc # => "т" - strio.getc # => "е" + strio = StringIO.new('Привет') + strio.getc # => "П" + strio.getc # => "р" strio = StringIO.new('こんにちは') strio.getc # => "こ" @@ -31,4 +31,4 @@ in other cases that need not be true: strio.pos = 5 # => 5 # At third byte of second character; returns byte. strio.getc # => "\x93" -Related: StringIO.getbyte. +Related: #getbyte, #putc, #ungetc. From 668fe01182df185c3592061e22087b6454132fec Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 5 Dec 2025 16:07:45 +0000 Subject: [PATCH 14/32] [ruby/stringio] [DOC] Fix link https://github.com/ruby/stringio/commit/e2d24ae8d7 --- doc/stringio/stringio.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/stringio/stringio.md b/doc/stringio/stringio.md index aebfa6d6f1f4c2..8931d1c30c7d79 100644 --- a/doc/stringio/stringio.md +++ b/doc/stringio/stringio.md @@ -324,7 +324,7 @@ a binary stream may not be changed to text. ### Encodings -A stream has an encoding; see the [encodings document][encodings document]. +A stream has an encoding; see [Encodings][encodings document]. The initial encoding for a new or re-opened stream depends on its [data mode][data mode]: @@ -683,7 +683,7 @@ Reading: - #each_codepoint: reads each remaining codepoint, passing it to the block. [bom]: https://en.wikipedia.org/wiki/Byte_order_mark -[encodings document]: https://docs.ruby-lang.org/en/master/encodings_rdoc.html +[encodings document]: https://docs.ruby-lang.org/en/master/language/encodings_rdoc.html [io class]: https://docs.ruby-lang.org/en/master/IO.html [kernel#puts]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-puts [kernel#readline]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-readline From f623fcc7d069cdfcaf25285c986ed995530a686f Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 6 Dec 2025 17:18:18 -0600 Subject: [PATCH 15/32] [ruby/stringio] [DOC] Tweaks for StringIO.getbyte (https://github.com/ruby/stringio/pull/188) https://github.com/ruby/stringio/commit/66360ee5f1 --- doc/stringio/getbyte.rdoc | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/stringio/getbyte.rdoc b/doc/stringio/getbyte.rdoc index 48c334b5252a58..5e524941bca4ae 100644 --- a/doc/stringio/getbyte.rdoc +++ b/doc/stringio/getbyte.rdoc @@ -14,16 +14,18 @@ Returns +nil+ if at end-of-stream: Returns a byte, not a character: - s = 'тест' - s.bytes # => [209, 130, 208, 181, 209, 129, 209, 130] + s = 'Привет' + s.bytes + # => [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130] strio = StringIO.new(s) - strio.getbyte # => 209 - strio.getbyte # => 130 + strio.getbyte # => 208 + strio.getbyte # => 159 s = 'こんにちは' - s.bytes # => [227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175] + s.bytes + # => [227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175] strio = StringIO.new(s) strio.getbyte # => 227 strio.getbyte # => 129 -Related: StringIO.getc. +Related: #each_byte, #ungetbyte, #getc. From 5bc65db5550719a808858af361d5152931469c88 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 6 Dec 2025 17:20:13 -0600 Subject: [PATCH 16/32] [ruby/stringio] [DOC] Tweaks for StringIO#gets (https://github.com/ruby/stringio/pull/190) https://github.com/ruby/stringio/commit/77209fac20 --- doc/stringio/gets.rdoc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/stringio/gets.rdoc b/doc/stringio/gets.rdoc index 892c3feb53a9bf..bbefeb008ae245 100644 --- a/doc/stringio/gets.rdoc +++ b/doc/stringio/gets.rdoc @@ -19,10 +19,10 @@ With no arguments given, reads a line using the default record separator strio.eof? # => true strio.gets # => nil - strio = StringIO.new('тест') # Four 2-byte characters. + strio = StringIO.new('Привет') # Six 2-byte characters strio.pos # => 0 - strio.gets # => "тест" - strio.pos # => 8 + strio.gets # => "Привет" + strio.pos # => 12 Argument +sep+ @@ -67,11 +67,11 @@ but in other cases the position may be anywhere: The position need not be at a character boundary: - strio = StringIO.new('тест') # Four 2-byte characters. - strio.pos = 2 # At beginning of second character. - strio.gets # => "ест" - strio.pos = 3 # In middle of second character. - strio.gets # => "\xB5ст" + strio = StringIO.new('Привет') # Six 2-byte characters. + strio.pos = 2 # At beginning of second character. + strio.gets # => "ривет" + strio.pos = 3 # In middle of second character. + strio.gets # => "\x80ивет" Special Record Separators @@ -95,4 +95,5 @@ removes the trailing newline (if any) from the returned line: strio.gets # => "First line\n" strio.gets(chomp: true) # => "Second line" -Related: StringIO.each_line. +Related: #each_line, #readlines, +{Kernel#puts}[rdoc-ref:Kernel#puts]. From b4a1f170583eb5553814261c311b183cbb390ba2 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 15 Nov 2025 07:47:46 -0600 Subject: [PATCH 17/32] [ruby/stringio] [DOC] Tweaks for StringIO#each_line (https://github.com/ruby/stringio/pull/165) Adds to "Position": pos inside a character. Makes a couple of minor corrections. --------- https://github.com/ruby/stringio/commit/ff332abafa Co-authored-by: Sutou Kouhei --- doc/stringio/each_line.md | 189 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 doc/stringio/each_line.md diff --git a/doc/stringio/each_line.md b/doc/stringio/each_line.md new file mode 100644 index 00000000000000..e29640a12a9203 --- /dev/null +++ b/doc/stringio/each_line.md @@ -0,0 +1,189 @@ +With a block given calls the block with each remaining line (see "Position" below) in the stream; +returns `self`. + +Leaves stream position at end-of-stream. + +**No Arguments** + +With no arguments given, +reads lines using the default record separator +(global variable `$/`, whose initial value is `"\n"`). + +```ruby +strio = StringIO.new(TEXT) +strio.each_line {|line| p line } +strio.eof? # => true +``` + +Output: + +``` +"First line\n" +"Second line\n" +"\n" +"Fourth line\n" +"Fifth line\n" +``` + +**Argument `sep`** + +With only string argument `sep` given, +reads lines using that string as the record separator: + +```ruby +strio = StringIO.new(TEXT) +strio.each_line(' ') {|line| p line } +``` + +Output: + +``` +"First " +"line\nSecond " +"line\n\nFourth " +"line\nFifth " +"line\n" +``` + +**Argument `limit`** + +With only integer argument `limit` given, +reads lines using the default record separator; +also limits the size (in characters) of each line to the given limit: + +```ruby +strio = StringIO.new(TEXT) +strio.each_line(10) {|line| p line } +``` + +Output: + +``` +"First line" +"\n" +"Second lin" +"e\n" +"\n" +"Fourth lin" +"e\n" +"Fifth line" +"\n" +``` + +**Arguments `sep` and `limit`** + +With arguments `sep` and `limit` both given, +honors both: + +```ruby +strio = StringIO.new(TEXT) +strio.each_line(' ', 10) {|line| p line } +``` + +Output: + +``` +"First " +"line\nSecon" +"d " +"line\n\nFour" +"th " +"line\nFifth" +" " +"line\n" +``` + +**Position** + +As stated above, method `each` _remaining_ line in the stream. + +In the examples above each `strio` object starts with its position at beginning-of-stream; +but in other cases the position may be anywhere (see StringIO#pos): + +```ruby +strio = StringIO.new(TEXT) +strio.pos = 30 # Set stream position to character 30. +strio.each_line {|line| p line } +``` + +Output: + +``` +" line\n" +"Fifth line\n" +``` + +In all the examples above, the stream position is at the beginning of a character; +in other cases, that need not be so: + +```ruby +s = 'こんにちは' # Five 3-byte characters. +strio = StringIO.new(s) +strio.pos = 3 # At beginning of second character. +strio.each_line {|line| p line } +strio.pos = 4 # At second byte of second character. +strio.each_line {|line| p line } +strio.pos = 5 # At third byte of second character. +strio.each_line {|line| p line } +``` + +Output: + +``` +"んにちは" +"\x82\x93にちは" +"\x93にちは" +``` + +**Special Record Separators** + +Like some methods in class `IO`, StringIO.each honors two special record separators; +see {Special Line Separators}[https://docs.ruby-lang.org/en/master/IO.html#class-IO-label-Special+Line+Separator+Values]. + +```ruby +strio = StringIO.new(TEXT) +strio.each_line('') {|line| p line } # Read as paragraphs (separated by blank lines). +``` + +Output: + +``` +"First line\nSecond line\n\n" +"Fourth line\nFifth line\n" +``` + +```ruby +strio = StringIO.new(TEXT) +strio.each_line(nil) {|line| p line } # "Slurp"; read it all. +``` + +Output: + +``` +"First line\nSecond line\n\nFourth line\nFifth line\n" +``` + +**Keyword Argument `chomp`** + +With keyword argument `chomp` given as `true` (the default is `false`), +removes trailing newline (if any) from each line: + +```ruby +strio = StringIO.new(TEXT) +strio.each_line(chomp: true) {|line| p line } +``` + +Output: + +``` +"First line" +"Second line" +"" +"Fourth line" +"Fifth line" +``` + +With no block given, returns a new {Enumerator}[https://docs.ruby-lang.org/en/master/Enumerator.html]. + + +Related: StringIO.each_byte, StringIO.each_char, StringIO.each_codepoint. From 6ec5c5f1c8ac438752b53061d0cdd68a7e4ca8f9 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 15 Nov 2025 07:48:35 -0600 Subject: [PATCH 18/32] [ruby/stringio] [DOC] Doc for StringIO.size (https://github.com/ruby/stringio/pull/171) https://github.com/ruby/stringio/commit/95a111017a --- doc/stringio/size.rdoc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/stringio/size.rdoc diff --git a/doc/stringio/size.rdoc b/doc/stringio/size.rdoc new file mode 100644 index 00000000000000..9323adf8c3783a --- /dev/null +++ b/doc/stringio/size.rdoc @@ -0,0 +1,5 @@ +Returns the number of bytes in the string in +self+: + + StringIO.new('hello').size # => 5 # Five 1-byte characters. + StringIO.new('тест').size # => 8 # Four 2-byte characters. + StringIO.new('こんにちは').size # => 15 # Five 3-byte characters. From 254653db8521618e08aaccaa63efdb42bd6ee84b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 15:38:52 +0900 Subject: [PATCH 19/32] [ruby/win32-registry] v0.1.2 https://github.com/ruby/win32-registry/commit/2a6ab00f67 --- ext/win32/win32-registry.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/win32/win32-registry.gemspec b/ext/win32/win32-registry.gemspec index 9b65af4a5b9976..9bd57bd7d1368f 100644 --- a/ext/win32/win32-registry.gemspec +++ b/ext/win32/win32-registry.gemspec @@ -1,7 +1,7 @@ # frozen_string_literal: true Gem::Specification.new do |spec| spec.name = "win32-registry" - spec.version = "0.1.1" + spec.version = "0.1.2" spec.authors = ["U.Nakamura"] spec.email = ["usa@garbagecollect.jp"] From a8b7fb7ed6990df00514240a247ed9dd97bbbf8b Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 06:40:40 +0000 Subject: [PATCH 20/32] Update default gems list at 254653db8521618e08aaccaa63efdb [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 8c1d15142f71b4..769a9c53d66062 100644 --- a/NEWS.md +++ b/NEWS.md @@ -234,7 +234,7 @@ releases. The following default gem is added. -* win32-registry 0.1.1 +* win32-registry 0.1.2 The following default gems are updated. From 8e87f201cf54b112642ed0421ddabd57336d672e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 15:42:31 +0900 Subject: [PATCH 21/32] [ruby/optparse] v0.8.1 https://github.com/ruby/optparse/commit/f2e31e81a5 --- lib/optparse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optparse.rb b/lib/optparse.rb index aae73c86b32787..97178e284bd53c 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -426,7 +426,7 @@ # class OptionParser # The version string - VERSION = "0.8.0" + VERSION = "0.8.1" # An alias for compatibility Version = VERSION From 492b1c73b35ab97d17d48ddd868e61cb76703dac Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 06:44:02 +0000 Subject: [PATCH 22/32] Update default gems list at 8e87f201cf54b112642ed0421ddabd [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 769a9c53d66062..6cf5c8a450a0e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -254,7 +254,7 @@ The following default gems are updated. * json 2.17.1 * net-http 0.8.0 * openssl 4.0.0.pre -* optparse 0.8.0 +* optparse 0.8.1 * pp 0.6.3 * prism 1.6.0 * psych 5.3.0 From 81fbdff8fdf2ae7afb2fa19319ff7d40379521fe Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 24 Nov 2025 16:50:29 -0800 Subject: [PATCH 23/32] Use continuation bit in concurrent set This refactors the concurrent set to examine and reserve a slot via CAS with the hash, before then doing the same with the key. This allows us to use an extra bit from the hash as a "continuation bit" which marks whether we have ever probed past this key while inserting. When that bit isn't set on deletion we can clear the field instead of placing a tombstone. --- bootstraptest/test_ractor.rb | 3 + concurrent_set.c | 173 +++++++++++++++++++++++++---------- 2 files changed, 129 insertions(+), 47 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 81dc2d6b8d04be..13c4652d3760a8 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1494,6 +1494,9 @@ class C unless a[i].equal?(b[i]) raise [a[i], b[i]].inspect end + unless a[i] == i.to_s + raise [i, a[i], b[i]].inspect + end end :ok } diff --git a/concurrent_set.c b/concurrent_set.c index 3aa61507aaa7d2..eebf7df9cb6407 100644 --- a/concurrent_set.c +++ b/concurrent_set.c @@ -4,6 +4,9 @@ #include "ruby/atomic.h" #include "vm_sync.h" +#define CONCURRENT_SET_CONTINUATION_BIT ((VALUE)1 << (sizeof(VALUE) * CHAR_BIT - 1)) +#define CONCURRENT_SET_HASH_MASK (~CONCURRENT_SET_CONTINUATION_BIT) + enum concurrent_set_special_values { CONCURRENT_SET_EMPTY, CONCURRENT_SET_DELETED, @@ -24,6 +27,36 @@ struct concurrent_set { struct concurrent_set_entry *entries; }; +static void +concurrent_set_mark_continuation(struct concurrent_set_entry *entry, VALUE curr_hash_and_flags) +{ + if (curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT) return; + + RUBY_ASSERT((curr_hash_and_flags & CONCURRENT_SET_HASH_MASK) != 0); + + VALUE new_hash = curr_hash_and_flags | CONCURRENT_SET_CONTINUATION_BIT; + VALUE prev_hash = rbimpl_atomic_value_cas(&entry->hash, curr_hash_and_flags, new_hash, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + + // At the moment we only expect to be racing concurrently against another + // thread also setting the continuation bit. + // In the future if deletion is concurrent this will need adjusting + RUBY_ASSERT(prev_hash == curr_hash_and_flags || prev_hash == new_hash); + (void)prev_hash; +} + +static VALUE +concurrent_set_hash(const struct concurrent_set *set, VALUE key) +{ + VALUE hash = set->funcs->hash(key); + hash &= CONCURRENT_SET_HASH_MASK; + if (hash == 0) { + hash ^= CONCURRENT_SET_HASH_MASK; + } + RUBY_ASSERT(hash != 0); + RUBY_ASSERT(!(hash & CONCURRENT_SET_CONTINUATION_BIT)); + return hash; +} + static void concurrent_set_free(void *ptr) { @@ -141,13 +174,9 @@ concurrent_set_try_resize_without_locking(VALUE old_set_obj, VALUE *set_obj_ptr) if (key < CONCURRENT_SET_SPECIAL_VALUE_COUNT) continue; if (!RB_SPECIAL_CONST_P(key) && rb_objspace_garbage_object_p(key)) continue; - VALUE hash = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_RELAXED); - if (hash == 0) { - // Either in-progress insert or extremely unlikely 0 hash. - // Re-calculate the hash. - hash = old_set->funcs->hash(key); - } - RUBY_ASSERT(hash == old_set->funcs->hash(key)); + VALUE hash = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_RELAXED) & CONCURRENT_SET_HASH_MASK; + RUBY_ASSERT(hash != 0); + RUBY_ASSERT(hash == concurrent_set_hash(old_set, key)); // Insert key into new_set. struct concurrent_set_probe probe; @@ -156,20 +185,19 @@ concurrent_set_try_resize_without_locking(VALUE old_set_obj, VALUE *set_obj_ptr) while (true) { struct concurrent_set_entry *entry = &new_set->entries[idx]; - if (entry->key == CONCURRENT_SET_EMPTY) { - new_set->size++; + if (entry->hash == CONCURRENT_SET_EMPTY) { + RUBY_ASSERT(entry->key == CONCURRENT_SET_EMPTY); + new_set->size++; RUBY_ASSERT(new_set->size <= new_set->capacity / 2); - RUBY_ASSERT(entry->hash == 0); entry->key = key; entry->hash = hash; break; } - else { - RUBY_ASSERT(entry->key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT); - } + RUBY_ASSERT(entry->key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT); + entry->hash |= CONCURRENT_SET_CONTINUATION_BIT; idx = concurrent_set_probe_next(&probe); } } @@ -203,20 +231,37 @@ rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key) if (hash == 0) { // We don't need to recompute the hash on every retry because it should // never change. - hash = set->funcs->hash(key); + hash = concurrent_set_hash(set, key); } - RUBY_ASSERT(hash == set->funcs->hash(key)); + RUBY_ASSERT(hash == concurrent_set_hash(set, key)); struct concurrent_set_probe probe; int idx = concurrent_set_probe_start(&probe, set, hash); while (true) { struct concurrent_set_entry *entry = &set->entries[idx]; + VALUE curr_hash_and_flags = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_ACQUIRE); + VALUE curr_hash = curr_hash_and_flags & CONCURRENT_SET_HASH_MASK; + bool continuation = curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT; + + if (curr_hash_and_flags == CONCURRENT_SET_EMPTY) { + return 0; + } + + if (curr_hash != hash) { + if (!continuation) { + return 0; + } + idx = concurrent_set_probe_next(&probe); + continue; + } + VALUE curr_key = rbimpl_atomic_value_load(&entry->key, RBIMPL_ATOMIC_ACQUIRE); switch (curr_key) { case CONCURRENT_SET_EMPTY: - return 0; + // In-progress insert: hash written but key not yet + break; case CONCURRENT_SET_DELETED: break; case CONCURRENT_SET_MOVED: @@ -225,13 +270,9 @@ rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key) goto retry; default: { - VALUE curr_hash = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_RELAXED); - if (curr_hash != 0 && curr_hash != hash) break; - if (UNLIKELY(!RB_SPECIAL_CONST_P(curr_key) && rb_objspace_garbage_object_p(curr_key))) { // This is a weakref set, so after marking but before sweeping is complete we may find a matching garbage object. - // Skip it and mark it as deleted. - rbimpl_atomic_value_cas(&entry->key, curr_key, CONCURRENT_SET_DELETED, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + // Skip it and let the GC pass clean it up break; } @@ -241,6 +282,10 @@ rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key) return curr_key; } + if (!continuation) { + return 0; + } + break; } } @@ -266,23 +311,49 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) if (hash == 0) { // We don't need to recompute the hash on every retry because it should // never change. - hash = set->funcs->hash(key); + hash = concurrent_set_hash(set, key); } - RUBY_ASSERT(hash == set->funcs->hash(key)); + RUBY_ASSERT(hash == concurrent_set_hash(set, key)); struct concurrent_set_probe probe; int idx = concurrent_set_probe_start(&probe, set, hash); while (true) { struct concurrent_set_entry *entry = &set->entries[idx]; + VALUE curr_hash_and_flags = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_ACQUIRE); + VALUE curr_hash = curr_hash_and_flags & CONCURRENT_SET_HASH_MASK; + + if (curr_hash_and_flags == CONCURRENT_SET_EMPTY) { + if (!inserting) { + key = set->funcs->create(key, data); + RUBY_ASSERT(hash == concurrent_set_hash(set, key)); + inserting = true; + } + + // Reserve this slot for our hash value + curr_hash_and_flags = rbimpl_atomic_value_cas(&entry->hash, CONCURRENT_SET_EMPTY, hash, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + if (curr_hash_and_flags != CONCURRENT_SET_EMPTY) { + // Lost race, retry same slot to check winner's hash + continue; + } + + // CAS succeeded, so these are the values stored + curr_hash_and_flags = hash; + curr_hash = hash; + // Fall through to try to claim key + } + + if (curr_hash != hash) { + goto probe_next; + } + VALUE curr_key = rbimpl_atomic_value_load(&entry->key, RBIMPL_ATOMIC_ACQUIRE); switch (curr_key) { - case CONCURRENT_SET_EMPTY: { - // Not in set + case CONCURRENT_SET_EMPTY: if (!inserting) { key = set->funcs->create(key, data); - RUBY_ASSERT(hash == set->funcs->hash(key)); + RUBY_ASSERT(hash == concurrent_set_hash(set, key)); inserting = true; } @@ -293,14 +364,11 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) if (UNLIKELY(load_factor_reached)) { concurrent_set_try_resize(set_obj, set_obj_ptr); - goto retry; } - curr_key = rbimpl_atomic_value_cas(&entry->key, CONCURRENT_SET_EMPTY, key, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); - if (curr_key == CONCURRENT_SET_EMPTY) { - rbimpl_atomic_value_store(&entry->hash, hash, RBIMPL_ATOMIC_RELAXED); - + VALUE prev_key = rbimpl_atomic_value_cas(&entry->key, CONCURRENT_SET_EMPTY, key, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + if (prev_key == CONCURRENT_SET_EMPTY) { RB_GC_GUARD(set_obj); return key; } @@ -311,22 +379,16 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) // Another thread won the race, try again at the same location. continue; } - } case CONCURRENT_SET_DELETED: break; case CONCURRENT_SET_MOVED: // Wait RB_VM_LOCKING(); - goto retry; - default: { - VALUE curr_hash = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_RELAXED); - if (curr_hash != 0 && curr_hash != hash) break; - + default: if (UNLIKELY(!RB_SPECIAL_CONST_P(curr_key) && rb_objspace_garbage_object_p(curr_key))) { // This is a weakref set, so after marking but before sweeping is complete we may find a matching garbage object. - // Skip it and mark it as deleted. - rbimpl_atomic_value_cas(&entry->key, curr_key, CONCURRENT_SET_DELETED, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + // Skip it and let the GC pass clean it up break; } @@ -343,15 +405,33 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) return curr_key; } - break; - } } + probe_next: + RUBY_ASSERT(curr_hash_and_flags != CONCURRENT_SET_EMPTY); + concurrent_set_mark_continuation(entry, curr_hash_and_flags); idx = concurrent_set_probe_next(&probe); } } +static void +concurrent_set_delete_entry_locked(struct concurrent_set *set, struct concurrent_set_entry *entry) +{ + ASSERT_vm_locking_with_barrier(); + + if (entry->hash & CONCURRENT_SET_CONTINUATION_BIT) { + entry->hash = CONCURRENT_SET_CONTINUATION_BIT; + entry->key = CONCURRENT_SET_DELETED; + set->deleted_entries++; + } + else { + entry->hash = CONCURRENT_SET_EMPTY; + entry->key = CONCURRENT_SET_EMPTY; + set->size--; + } +} + VALUE rb_concurrent_set_delete_by_identity(VALUE set_obj, VALUE key) { @@ -359,7 +439,7 @@ rb_concurrent_set_delete_by_identity(VALUE set_obj, VALUE key) struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj); - VALUE hash = set->funcs->hash(key); + VALUE hash = concurrent_set_hash(set, key); struct concurrent_set_probe probe; int idx = concurrent_set_probe_start(&probe, set, hash); @@ -379,8 +459,8 @@ rb_concurrent_set_delete_by_identity(VALUE set_obj, VALUE key) break; default: if (key == curr_key) { - entry->key = CONCURRENT_SET_DELETED; - set->deleted_entries++; + RUBY_ASSERT((entry->hash & CONCURRENT_SET_HASH_MASK) == hash); + concurrent_set_delete_entry_locked(set, entry); return curr_key; } break; @@ -399,7 +479,7 @@ rb_concurrent_set_foreach_with_replace(VALUE set_obj, int (*callback)(VALUE *key for (unsigned int i = 0; i < set->capacity; i++) { struct concurrent_set_entry *entry = &set->entries[i]; - VALUE key = set->entries[i].key; + VALUE key = entry->key; switch (key) { case CONCURRENT_SET_EMPTY: @@ -414,8 +494,7 @@ rb_concurrent_set_foreach_with_replace(VALUE set_obj, int (*callback)(VALUE *key case ST_STOP: return; case ST_DELETE: - set->entries[i].key = CONCURRENT_SET_DELETED; - set->deleted_entries++; + concurrent_set_delete_entry_locked(set, entry); break; } break; From 462df17f8689e9ee87a45b88bb3283fd339e1247 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 9 Dec 2025 02:17:38 -0800 Subject: [PATCH 24/32] Attempt to reuse garbage slots in concurrent hash This removes all allocations from the find_or_insert loop, which requires us to start the search over after calling the provided create function. In exchange that allows us to assume that all concurrent threads insert will get the same view of the GC state, and so should all be attempting to clear and reuse a slot containing a garbage object. --- concurrent_set.c | 85 ++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/concurrent_set.c b/concurrent_set.c index eebf7df9cb6407..376b20d7d4e45c 100644 --- a/concurrent_set.c +++ b/concurrent_set.c @@ -222,11 +222,14 @@ rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key) VALUE set_obj; VALUE hash = 0; + struct concurrent_set *set; + struct concurrent_set_probe probe; + int idx; retry: set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE); RUBY_ASSERT(set_obj); - struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj); + set = RTYPEDDATA_GET_DATA(set_obj); if (hash == 0) { // We don't need to recompute the hash on every retry because it should @@ -235,8 +238,7 @@ rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key) } RUBY_ASSERT(hash == concurrent_set_hash(set, key)); - struct concurrent_set_probe probe; - int idx = concurrent_set_probe_start(&probe, set, hash); + idx = concurrent_set_probe_start(&probe, set, hash); while (true) { struct concurrent_set_entry *entry = &set->entries[idx]; @@ -299,37 +301,43 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) { RUBY_ASSERT(key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT); - bool inserting = false; - VALUE set_obj; - VALUE hash = 0; + // First attempt to find + { + VALUE result = rb_concurrent_set_find(set_obj_ptr, key); + if (result) return result; + } - retry: - set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE); + // First time we need to call create, and store the hash + VALUE set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE); RUBY_ASSERT(set_obj); + struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj); + key = set->funcs->create(key, data); + VALUE hash = concurrent_set_hash(set, key); + + struct concurrent_set_probe probe; + int idx; + + goto start_search; + +retry: + // On retries we only need to load the hash object + set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE); + RUBY_ASSERT(set_obj); + set = RTYPEDDATA_GET_DATA(set_obj); - if (hash == 0) { - // We don't need to recompute the hash on every retry because it should - // never change. - hash = concurrent_set_hash(set, key); - } RUBY_ASSERT(hash == concurrent_set_hash(set, key)); - struct concurrent_set_probe probe; - int idx = concurrent_set_probe_start(&probe, set, hash); +start_search: + idx = concurrent_set_probe_start(&probe, set, hash); while (true) { struct concurrent_set_entry *entry = &set->entries[idx]; VALUE curr_hash_and_flags = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_ACQUIRE); VALUE curr_hash = curr_hash_and_flags & CONCURRENT_SET_HASH_MASK; + bool continuation = curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT; if (curr_hash_and_flags == CONCURRENT_SET_EMPTY) { - if (!inserting) { - key = set->funcs->create(key, data); - RUBY_ASSERT(hash == concurrent_set_hash(set, key)); - inserting = true; - } - // Reserve this slot for our hash value curr_hash_and_flags = rbimpl_atomic_value_cas(&entry->hash, CONCURRENT_SET_EMPTY, hash, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); if (curr_hash_and_flags != CONCURRENT_SET_EMPTY) { @@ -340,6 +348,7 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) // CAS succeeded, so these are the values stored curr_hash_and_flags = hash; curr_hash = hash; + // Fall through to try to claim key } @@ -350,13 +359,7 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) VALUE curr_key = rbimpl_atomic_value_load(&entry->key, RBIMPL_ATOMIC_ACQUIRE); switch (curr_key) { - case CONCURRENT_SET_EMPTY: - if (!inserting) { - key = set->funcs->create(key, data); - RUBY_ASSERT(hash == concurrent_set_hash(set, key)); - inserting = true; - } - + case CONCURRENT_SET_EMPTY: { rb_atomic_t prev_size = rbimpl_atomic_fetch_add(&set->size, 1, RBIMPL_ATOMIC_RELAXED); // Load_factor reached at 75% full. ex: prev_size: 32, capacity: 64, load_factor: 50%. @@ -369,6 +372,7 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) VALUE prev_key = rbimpl_atomic_value_cas(&entry->key, CONCURRENT_SET_EMPTY, key, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); if (prev_key == CONCURRENT_SET_EMPTY) { + RUBY_ASSERT(rb_concurrent_set_find(set_obj_ptr, key) == key); RB_GC_GUARD(set_obj); return key; } @@ -379,6 +383,7 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) // Another thread won the race, try again at the same location. continue; } + } case CONCURRENT_SET_DELETED: break; case CONCURRENT_SET_MOVED: @@ -386,22 +391,26 @@ rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data) RB_VM_LOCKING(); goto retry; default: + // We're never GC during our search + // If the continuation bit wasn't set at the start of our search, + // any concurrent find with the same hash value would also look at + // this location and try to swap curr_key if (UNLIKELY(!RB_SPECIAL_CONST_P(curr_key) && rb_objspace_garbage_object_p(curr_key))) { - // This is a weakref set, so after marking but before sweeping is complete we may find a matching garbage object. - // Skip it and let the GC pass clean it up - break; + if (continuation) { + goto probe_next; + } + rbimpl_atomic_value_cas(&entry->key, curr_key, CONCURRENT_SET_EMPTY, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED); + continue; } if (set->funcs->cmp(key, curr_key)) { - // We've found a match. + // We've found a live match. RB_GC_GUARD(set_obj); - if (inserting) { - // We created key using set->funcs->create, but we didn't end - // up inserting it into the set. Free it here to prevent memory - // leaks. - if (set->funcs->free) set->funcs->free(key); - } + // We created key using set->funcs->create, but we didn't end + // up inserting it into the set. Free it here to prevent memory + // leaks. + if (set->funcs->free) set->funcs->free(key); return curr_key; } From 375025a3864fc944dc9f42909a6c5386c749d5fd Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 9 Dec 2025 15:42:04 -0800 Subject: [PATCH 25/32] Fix typo and shadowing --- concurrent_set.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/concurrent_set.c b/concurrent_set.c index 376b20d7d4e45c..234b6408b6b938 100644 --- a/concurrent_set.c +++ b/concurrent_set.c @@ -167,14 +167,14 @@ concurrent_set_try_resize_without_locking(VALUE old_set_obj, VALUE *set_obj_ptr) struct concurrent_set *new_set = RTYPEDDATA_GET_DATA(new_set_obj); for (int i = 0; i < old_capacity; i++) { - struct concurrent_set_entry *entry = &old_entries[i]; - VALUE key = rbimpl_atomic_value_exchange(&entry->key, CONCURRENT_SET_MOVED, RBIMPL_ATOMIC_ACQUIRE); + struct concurrent_set_entry *old_entry = &old_entries[i]; + VALUE key = rbimpl_atomic_value_exchange(&old_entry->key, CONCURRENT_SET_MOVED, RBIMPL_ATOMIC_ACQUIRE); RUBY_ASSERT(key != CONCURRENT_SET_MOVED); if (key < CONCURRENT_SET_SPECIAL_VALUE_COUNT) continue; if (!RB_SPECIAL_CONST_P(key) && rb_objspace_garbage_object_p(key)) continue; - VALUE hash = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_RELAXED) & CONCURRENT_SET_HASH_MASK; + VALUE hash = rbimpl_atomic_value_load(&old_entry->hash, RBIMPL_ATOMIC_RELAXED) & CONCURRENT_SET_HASH_MASK; RUBY_ASSERT(hash != 0); RUBY_ASSERT(hash == concurrent_set_hash(old_set, key)); From 14ff851185bb8ff399e98b74cc107302a4e08e18 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 24 Nov 2025 08:48:19 +0100 Subject: [PATCH 26/32] [ruby/forwardable] Simpler and faster check for the delegation fastpath Fix: https://github.com/ruby/forwardable/issues/35 [Bug #21708] Trying to compile code to check if a method can use the delegation fastpath is a bit wasteful and cause `RUPYOPT=-d` to be full of misleading errors. It's simpler and faster to use a simple regexp to do the same check. https://github.com/ruby/forwardable/commit/de1fbd182e --- lib/forwardable.rb | 34 ++++++++++++----------------- lib/forwardable/forwardable.gemspec | 2 +- lib/forwardable/impl.rb | 17 --------------- 3 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 lib/forwardable/impl.rb diff --git a/lib/forwardable.rb b/lib/forwardable.rb index 76267c2cd18c57..040f467b231a0d 100644 --- a/lib/forwardable.rb +++ b/lib/forwardable.rb @@ -109,8 +109,6 @@ # +delegate.rb+. # module Forwardable - require 'forwardable/impl' - # Version of +forwardable.rb+ VERSION = "1.3.3" VERSION.freeze @@ -206,37 +204,33 @@ def self._delegator_method(obj, accessor, method, ali) if Module === obj ? obj.method_defined?(accessor) || obj.private_method_defined?(accessor) : obj.respond_to?(accessor, true) - accessor = "#{accessor}()" + accessor = "(#{accessor}())" end args = RUBY_VERSION >= '2.7' ? '...' : '*args, &block' method_call = ".__send__(:#{method}, #{args})" - if _valid_method?(method) + if method.match?(/\A[_a-zA-Z]\w*[?!]?\z/) loc, = caller_locations(2,1) pre = "_ =" mesg = "#{Module === obj ? obj : obj.class}\##{ali} at #{loc.path}:#{loc.lineno} forwarding to private method " - method_call = "#{<<-"begin;"}\n#{<<-"end;".chomp}" - begin; - unless defined? _.#{method} - ::Kernel.warn #{mesg.dump}"\#{_.class}"'##{method}', uplevel: 1 - _#{method_call} - else - _.#{method}(#{args}) - end - end; + method_call = <<~RUBY.chomp + if defined?(_.#{method}) + _.#{method}(#{args}) + else + ::Kernel.warn #{mesg.dump}"\#{_.class}"'##{method}', uplevel: 1 + _#{method_call} + end + RUBY end - _compile_method("#{<<-"begin;"}\n#{<<-"end;"}", __FILE__, __LINE__+1) - begin; + eval(<<~RUBY, nil, __FILE__, __LINE__ + 1) proc do def #{ali}(#{args}) - #{pre} - begin - #{accessor} - end#{method_call} + #{pre}#{accessor} + #{method_call} end end - end; + RUBY end end diff --git a/lib/forwardable/forwardable.gemspec b/lib/forwardable/forwardable.gemspec index 9ad59c5f8a8830..1b539bcfcb3daf 100644 --- a/lib/forwardable/forwardable.gemspec +++ b/lib/forwardable/forwardable.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.licenses = ["Ruby", "BSD-2-Clause"] spec.required_ruby_version = '>= 2.4.0' - spec.files = ["forwardable.gemspec", "lib/forwardable.rb", "lib/forwardable/impl.rb"] + spec.files = ["forwardable.gemspec", "lib/forwardable.rb"] spec.bindir = "exe" spec.executables = [] spec.require_paths = ["lib"] diff --git a/lib/forwardable/impl.rb b/lib/forwardable/impl.rb deleted file mode 100644 index 0322c136db4a64..00000000000000 --- a/lib/forwardable/impl.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Forwardable - # :stopdoc: - - def self._valid_method?(method) - catch {|tag| - eval("BEGIN{throw tag}; ().#{method}", binding, __FILE__, __LINE__) - } - rescue SyntaxError - false - else - true - end - - def self._compile_method(src, file, line) - eval(src, nil, file, line) - end -end From e8a55274f202df1cfddc25aa14da34ba5a0e538d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 10 Dec 2025 16:07:17 +0900 Subject: [PATCH 27/32] [ruby/forwardable] v1.4.0 https://github.com/ruby/forwardable/commit/0257b590c2 --- lib/forwardable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forwardable.rb b/lib/forwardable.rb index 040f467b231a0d..175d6d9c6b6858 100644 --- a/lib/forwardable.rb +++ b/lib/forwardable.rb @@ -110,7 +110,7 @@ # module Forwardable # Version of +forwardable.rb+ - VERSION = "1.3.3" + VERSION = "1.4.0" VERSION.freeze # Version for backward compatibility From ef4490d6b166fc1fd802d58faceeb038737afc7e Mon Sep 17 00:00:00 2001 From: git Date: Wed, 10 Dec 2025 07:09:05 +0000 Subject: [PATCH 28/32] Update default gems list at e8a55274f202df1cfddc25aa14da34 [ci skip] --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 6cf5c8a450a0e4..e7d1da919e631d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -247,6 +247,7 @@ The following default gems are updated. * etc 1.4.6 * fcntl 1.3.0 * fileutils 1.8.0 +* forwardable 1.4.0 * io-console 0.8.1 * io-nonblock 0.3.2 * io-wait 0.4.0.dev From c5608ab4d79f409aaa469b4f4a20962a0ba4a688 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 3 Dec 2025 15:35:03 +0100 Subject: [PATCH 29/32] Monitor: avoid repeated calls to `rb_fiber_current()` That call is surprisingly expensive, so trying doing it once in `#synchronize` and then passing the fiber to enter and exit saves quite a few cycles. --- ext/monitor/monitor.c | 103 +++++++++++++++++++++++++++++------------- thread_sync.c | 51 ++++++++++----------- 2 files changed, 95 insertions(+), 59 deletions(-) diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c index 819c6e76996f61..3106c0302378f1 100644 --- a/ext/monitor/monitor.c +++ b/ext/monitor/monitor.c @@ -50,10 +50,10 @@ monitor_ptr(VALUE monitor) return mc; } -static int -mc_owner_p(struct rb_monitor *mc) +static bool +mc_owner_p(struct rb_monitor *mc, VALUE current_fiber) { - return mc->owner == rb_fiber_current(); + return mc->owner == current_fiber; } /* @@ -67,17 +67,44 @@ monitor_try_enter(VALUE monitor) { struct rb_monitor *mc = monitor_ptr(monitor); - if (!mc_owner_p(mc)) { + VALUE current_fiber = rb_fiber_current(); + if (!mc_owner_p(mc, current_fiber)) { if (!rb_mutex_trylock(mc->mutex)) { return Qfalse; } - RB_OBJ_WRITE(monitor, &mc->owner, rb_fiber_current()); + RB_OBJ_WRITE(monitor, &mc->owner, current_fiber); mc->count = 0; } mc->count += 1; return Qtrue; } + +struct monitor_args { + VALUE monitor; + struct rb_monitor *mc; + VALUE current_fiber; +}; + +static inline void +monitor_args_init(struct monitor_args *args, VALUE monitor) +{ + args->monitor = monitor; + args->mc = monitor_ptr(monitor); + args->current_fiber = rb_fiber_current(); +} + +static void +monitor_enter0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber)) { + rb_mutex_lock(args->mc->mutex); + RB_OBJ_WRITE(args->monitor, &args->mc->owner, args->current_fiber); + args->mc->count = 0; + } + args->mc->count++; +} + /* * call-seq: * enter -> nil @@ -87,27 +114,44 @@ monitor_try_enter(VALUE monitor) static VALUE monitor_enter(VALUE monitor) { - struct rb_monitor *mc = monitor_ptr(monitor); - if (!mc_owner_p(mc)) { - rb_mutex_lock(mc->mutex); - RB_OBJ_WRITE(monitor, &mc->owner, rb_fiber_current()); - mc->count = 0; - } - mc->count++; + struct monitor_args args; + monitor_args_init(&args, monitor); + monitor_enter0(&args); return Qnil; } +static inline void +monitor_check_owner0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber)) { + rb_raise(rb_eThreadError, "current fiber not owner"); + } +} + /* :nodoc: */ static VALUE monitor_check_owner(VALUE monitor) { - struct rb_monitor *mc = monitor_ptr(monitor); - if (!mc_owner_p(mc)) { - rb_raise(rb_eThreadError, "current fiber not owner"); - } + struct monitor_args args; + monitor_args_init(&args, monitor); + monitor_check_owner0(&args); return Qnil; } +static void +monitor_exit0(struct monitor_args *args) +{ + monitor_check_owner(args->monitor); + + if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); + args->mc->count--; + + if (args->mc->count == 0) { + RB_OBJ_WRITE(args->monitor, &args->mc->owner, Qnil); + rb_mutex_unlock(args->mc->mutex); + } +} + /* * call-seq: * exit -> nil @@ -117,17 +161,9 @@ monitor_check_owner(VALUE monitor) static VALUE monitor_exit(VALUE monitor) { - monitor_check_owner(monitor); - - struct rb_monitor *mc = monitor_ptr(monitor); - - if (mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)mc->count); - mc->count--; - - if (mc->count == 0) { - RB_OBJ_WRITE(monitor, &mc->owner, Qnil); - rb_mutex_unlock(mc->mutex); - } + struct monitor_args args; + monitor_args_init(&args, monitor); + monitor_exit0(&args); return Qnil; } @@ -144,7 +180,7 @@ static VALUE monitor_owned_p(VALUE monitor) { struct rb_monitor *mc = monitor_ptr(monitor); - return (rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc)) ? Qtrue : Qfalse; + return rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc, rb_fiber_current()) ? Qtrue : Qfalse; } static VALUE @@ -210,9 +246,10 @@ monitor_sync_body(VALUE monitor) } static VALUE -monitor_sync_ensure(VALUE monitor) +monitor_sync_ensure(VALUE v_args) { - return monitor_exit(monitor); + monitor_exit0((struct monitor_args *)v_args); + return Qnil; } /* @@ -226,8 +263,10 @@ monitor_sync_ensure(VALUE monitor) static VALUE monitor_synchronize(VALUE monitor) { - monitor_enter(monitor); - return rb_ensure(monitor_sync_body, monitor, monitor_sync_ensure, monitor); + struct monitor_args args; + monitor_args_init(&args, monitor); + monitor_enter0(&args); + return rb_ensure(monitor_sync_body, (VALUE)&args, monitor_sync_ensure, (VALUE)&args); } void diff --git a/thread_sync.c b/thread_sync.c index 8967e24e341bad..9fb1639e9b7dd2 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -239,23 +239,34 @@ thread_mutex_remove(rb_thread_t *thread, rb_mutex_t *mutex) } static void -mutex_set_owner(VALUE self, rb_thread_t *th, rb_fiber_t *fiber) +mutex_set_owner(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) { - rb_mutex_t *mutex = mutex_ptr(self); - mutex->thread = th->self; mutex->fiber_serial = rb_fiber_serial(fiber); } static void -mutex_locked(rb_thread_t *th, rb_fiber_t *fiber, VALUE self) +mutex_locked(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) { - rb_mutex_t *mutex = mutex_ptr(self); - - mutex_set_owner(self, th, fiber); + mutex_set_owner(mutex, th, fiber); thread_mutex_insert(th, mutex); } +static inline bool +mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) +{ + if (mutex->fiber_serial == 0) { + RUBY_DEBUG_LOG("%p ok", mutex); + + mutex_locked(mutex, th, fiber); + return true; + } + else { + RUBY_DEBUG_LOG("%p ng", mutex); + return false; + } +} + /* * call-seq: * mutex.try_lock -> true or false @@ -266,21 +277,7 @@ mutex_locked(rb_thread_t *th, rb_fiber_t *fiber, VALUE self) VALUE rb_mutex_trylock(VALUE self) { - rb_mutex_t *mutex = mutex_ptr(self); - - if (mutex->fiber_serial == 0) { - RUBY_DEBUG_LOG("%p ok", mutex); - - rb_fiber_t *fiber = GET_EC()->fiber_ptr; - rb_thread_t *th = GET_THREAD(); - - mutex_locked(th, fiber, self); - return Qtrue; - } - else { - RUBY_DEBUG_LOG("%p ng", mutex); - return Qfalse; - } + return RBOOL(mutex_trylock(mutex_ptr(self), GET_THREAD(), GET_EC()->fiber_ptr)); } static VALUE @@ -321,7 +318,7 @@ do_mutex_lock(VALUE self, int interruptible_p) rb_raise(rb_eThreadError, "can't be called from trap context"); } - if (rb_mutex_trylock(self) == Qfalse) { + if (!mutex_trylock(mutex, th, fiber)) { if (mutex->fiber_serial == rb_fiber_serial(fiber)) { rb_raise(rb_eThreadError, "deadlock; recursive locking"); } @@ -342,7 +339,7 @@ do_mutex_lock(VALUE self, int interruptible_p) rb_ensure(call_rb_fiber_scheduler_block, self, delete_from_waitq, (VALUE)&sync_waiter); if (!mutex->fiber_serial) { - mutex_set_owner(self, th, fiber); + mutex_set_owner(mutex, th, fiber); } } else { @@ -383,7 +380,7 @@ do_mutex_lock(VALUE self, int interruptible_p) // unlocked by another thread while sleeping if (!mutex->fiber_serial) { - mutex_set_owner(self, th, fiber); + mutex_set_owner(mutex, th, fiber); } rb_ractor_sleeper_threads_dec(th->ractor); @@ -402,7 +399,7 @@ do_mutex_lock(VALUE self, int interruptible_p) } RUBY_VM_CHECK_INTS_BLOCKING(th->ec); /* may release mutex */ if (!mutex->fiber_serial) { - mutex_set_owner(self, th, fiber); + mutex_set_owner(mutex, th, fiber); } } else { @@ -421,7 +418,7 @@ do_mutex_lock(VALUE self, int interruptible_p) } if (saved_ints) th->ec->interrupt_flag = saved_ints; - if (mutex->fiber_serial == rb_fiber_serial(fiber)) mutex_locked(th, fiber, self); + if (mutex->fiber_serial == rb_fiber_serial(fiber)) mutex_locked(mutex, th, fiber); } RUBY_DEBUG_LOG("%p locked", mutex); From 6777d1012d24b4dd3585809030333ccfb1c46799 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 10 Dec 2025 10:35:37 +0100 Subject: [PATCH 30/32] Modernize Monitor TypedData Make it embedded and compaction aware. --- ext/monitor/monitor.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c index 3106c0302378f1..aeb96d7701242d 100644 --- a/ext/monitor/monitor.c +++ b/ext/monitor/monitor.c @@ -4,28 +4,35 @@ struct rb_monitor { long count; - const VALUE owner; - const VALUE mutex; + VALUE owner; + VALUE mutex; }; static void monitor_mark(void *ptr) { struct rb_monitor *mc = ptr; - rb_gc_mark(mc->owner); - rb_gc_mark(mc->mutex); + rb_gc_mark_movable(mc->owner); + rb_gc_mark_movable(mc->mutex); } -static size_t -monitor_memsize(const void *ptr) +static void +monitor_compact(void *ptr) { - return sizeof(struct rb_monitor); + struct rb_monitor *mc = ptr; + mc->owner = rb_gc_location(mc->owner); + mc->mutex = rb_gc_location(mc->mutex); } static const rb_data_type_t monitor_data_type = { - "monitor", - {monitor_mark, RUBY_TYPED_DEFAULT_FREE, monitor_memsize,}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED + .wrap_struct_name = "monitor", + .function = { + .dmark = monitor_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = NULL, // Fully embeded + .dcompact = monitor_compact, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, }; static VALUE From 023c6d808a85bd8fb711ac3986972618037f12ad Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 10 Dec 2025 10:54:18 +0100 Subject: [PATCH 31/32] [ruby/json] Add a specific error for unescaped newlines It's the most likely control character so it's worth giving a better error message for it. https://github.com/ruby/json/commit/1da3fd9233 --- ext/json/parser/parser.c | 3 +++ test/json/json_parser_test.rb | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index c84c7ed660a06d..45de8d1ff62f1d 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -752,6 +752,9 @@ NOINLINE(static) VALUE json_string_unescape(JSON_ParserState *state, JSON_Parser break; default: if ((unsigned char)*pe < 0x20) { + if (*pe == '\n') { + raise_parse_error_at("Invalid unescaped newline character (\\n) in string: %s", state, pe - 1); + } raise_parse_error_at("invalid ASCII control character in string: %s", state, pe - 1); } raise_parse_error_at("invalid escape character in string: %s", state, pe - 1); diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 257e4f1736a2d0..3f0fb7522dc671 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -164,6 +164,14 @@ def test_parse_complex_objects end end + def test_parse_control_chars_in_string + 0.upto(31) do |ord| + assert_raise JSON::ParserError do + parse(%("#{ord.chr}")) + end + end + end + def test_parse_arrays assert_equal([1,2,3], parse('[1,2,3]')) assert_equal([1.2,2,3], parse('[1.2,2,3]')) From 2b66fc763a6657c9a25719c5f70ae7b66abc2232 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 10 Dec 2025 12:42:33 +0100 Subject: [PATCH 32/32] Fix typos in comment of rb_current_execution_context() --- vm_core.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vm_core.h b/vm_core.h index 0a1914f57a955b..4195ea5e59c9a4 100644 --- a/vm_core.h +++ b/vm_core.h @@ -2084,9 +2084,9 @@ rb_current_execution_context(bool expect_ec) * and the address of the `ruby_current_ec` can be stored on a function * frame. However, this address can be mis-used after native thread * migration of a coroutine. - * 1) Get `ptr =&ruby_current_ec` op NT1 and store it on the frame. + * 1) Get `ptr = &ruby_current_ec` on NT1 and store it on the frame. * 2) Context switch and resume it on the NT2. - * 3) `ptr` is used on NT2 but it accesses to the TLS on NT1. + * 3) `ptr` is used on NT2 but it accesses the TLS of NT1. * This assertion checks such misusage. * * To avoid accidents, `GET_EC()` should be called once on the frame.