From 946165bd48b7f47dbe38f7a130983e429e1dd25d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 10:57:19 +0900 Subject: [PATCH 001/157] Added clang-22 build --- .github/workflows/compilers.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index d0be762cee..f52cab9708 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -107,6 +107,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'clang 22', with: { tag: 'clang-22' } } - { uses: './.github/actions/compilers', name: 'clang 21', with: { tag: 'clang-21' } } - { uses: './.github/actions/compilers', name: 'clang 20', with: { tag: 'clang-20' } } - { uses: './.github/actions/compilers', name: 'clang 19', with: { tag: 'clang-19' } } From 015d8741339a8f134e00638dad388549be58c988 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 11:38:32 +0900 Subject: [PATCH 002/157] Sync https://github.com/ruby/test-unit-ruby-core/pull/8 --- tool/lib/core_assertions.rb | 8 +++----- tool/lib/envutil.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb index ece6ca1dc8..958306c0c5 100644 --- a/tool/lib/core_assertions.rb +++ b/tool/lib/core_assertions.rb @@ -690,17 +690,15 @@ eom assert_warning(*args) {$VERBOSE = false; yield} end - def assert_deprecated_warning(mesg = /deprecated/) + def assert_deprecated_warning(mesg = /deprecated/, &block) assert_warning(mesg) do - Warning[:deprecated] = true if Warning.respond_to?(:[]=) - yield + EnvUtil.deprecation_warning(&block) end end def assert_deprecated_warn(mesg = /deprecated/) assert_warn(mesg) do - Warning[:deprecated] = true if Warning.respond_to?(:[]=) - yield + EnvUtil.deprecation_warning(&block) end end diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb index fe166d8514..ab5e8d84e9 100644 --- a/tool/lib/envutil.rb +++ b/tool/lib/envutil.rb @@ -297,6 +297,21 @@ module EnvUtil end module_function :verbose_warning + if defined?(Warning.[]=) + def deprecation_warning + previous_deprecated = Warning[:deprecated] + Warning[:deprecated] = true + yield + ensure + Warning[:deprecated] = previous_deprecated + end + else + def deprecation_warning + yield + end + end + module_function :deprecation_warning + def default_warning $VERBOSE = false yield From 06b14f29a34fe621696d6beae0c00486a6879539 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 12:13:27 +0900 Subject: [PATCH 003/157] Added missing block argument --- tool/lib/core_assertions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb index 958306c0c5..00d180fa8c 100644 --- a/tool/lib/core_assertions.rb +++ b/tool/lib/core_assertions.rb @@ -696,7 +696,7 @@ eom end end - def assert_deprecated_warn(mesg = /deprecated/) + def assert_deprecated_warn(mesg = /deprecated/, &block) assert_warn(mesg) do EnvUtil.deprecation_warning(&block) end From d5e7e88f3248c59070b6e0d1dcb1166534d02a02 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 5 Aug 2025 15:48:11 +0900 Subject: [PATCH 004/157] Show mkmf.log contents even when `pkg-config` command failed --- test/mkmf/test_pkg_config.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/mkmf/test_pkg_config.rb b/test/mkmf/test_pkg_config.rb index f6a960c7d9..adf5fa6e92 100644 --- a/test/mkmf/test_pkg_config.rb +++ b/test/mkmf/test_pkg_config.rb @@ -46,21 +46,26 @@ class TestMkmfPkgConfig < TestMkmf def test_pkgconfig_with_libs_option_returns_output pend("skipping because pkg-config is not installed") unless PKG_CONFIG expected = ["-L#{@fixtures_lib_dir}", "-ltest1-public"].sort - actual = pkg_config("test1", "libs").shellsplit.sort - assert_equal(expected, actual, MKMFLOG) + actual = pkg_config("test1", "libs") + assert_equal_sorted(expected, actual, MKMFLOG) end def test_pkgconfig_with_cflags_option_returns_output pend("skipping because pkg-config is not installed") unless PKG_CONFIG expected = ["--cflags-other", "-I#{@fixtures_inc_dir}/cflags-I"].sort - actual = pkg_config("test1", "cflags").shellsplit.sort - assert_equal(expected, actual, MKMFLOG) + actual = pkg_config("test1", "cflags") + assert_equal_sorted(expected, actual, MKMFLOG) end def test_pkgconfig_with_multiple_options pend("skipping because pkg-config is not installed") unless PKG_CONFIG expected = ["-L#{@fixtures_lib_dir}", "-ltest1-public", "-ltest1-private"].sort - actual = pkg_config("test1", "libs", "static").shellsplit.sort - assert_equal(expected, actual, MKMFLOG) + actual = pkg_config("test1", "libs", "static") + assert_equal_sorted(expected, actual, MKMFLOG) + end + + private def assert_equal_sorted(expected, actual, msg = nil) + actual = actual.shellsplit.sort if actual + assert_equal(expected, actual, msg) end end From 3d8af5df11e8dba8ccd37b03f9f5b92b2bc66dcb Mon Sep 17 00:00:00 2001 From: ydah Date: Tue, 5 Aug 2025 21:46:41 +0900 Subject: [PATCH 005/157] Fix typo in documentation comment for exc_inspect method in error.c --- error.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/error.c b/error.c index e14ecd2393..9a758a7dd7 100644 --- a/error.c +++ b/error.c @@ -1883,7 +1883,7 @@ exc_inspect(VALUE exc) * # String * end * - * The value returned by this method migth be adjusted when raising (see Kernel#raise), + * The value returned by this method might be adjusted when raising (see Kernel#raise), * or during intermediate handling by #set_backtrace. * * See also #backtrace_locations that provide the same value, as structured objects. From a6aaeb9acfa47ebfafed051069f7ea870ded4b99 Mon Sep 17 00:00:00 2001 From: ArtSin Date: Tue, 5 Aug 2025 16:39:26 +0400 Subject: [PATCH 006/157] load.c: fix `prev_ext_config` clobbering in `require_internal` The variable `prev_ext_config` is modified by `ext_config_push` between `setjmp` and `longjmp` calls. Since `ext_config_push` and `ext_config_pop` are small and likely to be inlined, `prev_ext_config` can be allocated on a register and get clobbered. Fix by making it `volatile`. This bug can be observed by adding a check for values greater than 1 in `th2->ext_config.ractor_safe` after `ext_config_pop` and building with Clang. --- load.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/load.c b/load.c index 329b0f4b3b..017c236483 100644 --- a/load.c +++ b/load.c @@ -1346,14 +1346,14 @@ rb_resolve_feature_path(VALUE klass, VALUE fname) } static void -ext_config_push(rb_thread_t *th, struct rb_ext_config *prev) +ext_config_push(rb_thread_t *th, volatile struct rb_ext_config *prev) { *prev = th->ext_config; th->ext_config = (struct rb_ext_config){0}; } static void -ext_config_pop(rb_thread_t *th, struct rb_ext_config *prev) +ext_config_pop(rb_thread_t *th, volatile struct rb_ext_config *prev) { th->ext_config = *prev; } @@ -1407,7 +1407,7 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa VALUE realpaths = get_loaded_features_realpaths(vm_ns); VALUE realpath_map = get_loaded_features_realpath_map(vm_ns); volatile bool reset_ext_config = false; - struct rb_ext_config prev_ext_config; + volatile struct rb_ext_config prev_ext_config; path = rb_str_encode_ospath(fname); RUBY_DTRACE_HOOK(REQUIRE_ENTRY, RSTRING_PTR(fname)); From b7f65f01eea8810ac8be64865e3415e634c3633a Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 1 Aug 2025 09:19:18 -0500 Subject: [PATCH 007/157] [DOC] Tweaks for String#grapheme_clusters --- doc/string/grapheme_clusters.rdoc | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/string/grapheme_clusters.rdoc b/doc/string/grapheme_clusters.rdoc index 8c7f5a7259..07ea1e318b 100644 --- a/doc/string/grapheme_clusters.rdoc +++ b/doc/string/grapheme_clusters.rdoc @@ -1,6 +1,19 @@ Returns an array of the grapheme clusters in +self+ (see {Unicode Grapheme Cluster Boundaries}[https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries]): - s = "\u0061\u0308-pqr-\u0062\u0308-xyz-\u0063\u0308" # => "ä-pqr-b̈-xyz-c̈" + s = "ä-pqr-b̈-xyz-c̈" + s.size # => 16 + s.bytesize # => 19 + s.grapheme_clusters.size # => 13 s.grapheme_clusters # => ["ä", "-", "p", "q", "r", "-", "b̈", "-", "x", "y", "z", "-", "c̈"] + +Details: + + s = "ä" + s.grapheme_clusters # => ["ä"] # One grapheme cluster. + s.bytes # => [97, 204, 136] # Three bytes. + s.chars # => ["a", "̈"] # Two characters. + s.chars.map {|char| char.ord } # => [97, 776] # Their values. + +Related: see {Converting to Non-String}[rdoc-ref:String@Converting+to+Non--5CString]. From 409da39afbcd927577801be1626193e719f04005 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 5 Aug 2025 09:06:47 -0500 Subject: [PATCH 008/157] [DOC] Tweaks for String#gsub --- string.c | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/string.c b/string.c index 68c4f5f1d7..54e719623e 100644 --- a/string.c +++ b/string.c @@ -6601,14 +6601,41 @@ rb_str_gsub_bang(int argc, VALUE *argv, VALUE str) * gsub(pattern) {|match| ... } -> new_string * gsub(pattern) -> enumerator * - * Returns a copy of +self+ with all occurrences of the given +pattern+ replaced. + * Returns a copy of +self+ with zero or more substrings replaced. * - * See {Substitution Methods}[rdoc-ref:String@Substitution+Methods]. + * Argument +pattern+ may be a string or a Regexp; + * argument +replacement+ may be a string or a Hash. + * Varying types for the argument values makes this method very versatile. * - * Returns an Enumerator if no +replacement+ and no block given. + * Below are some simple examples; + * for many more examples, see {Substitution Methods}[rdoc-ref:String@Substitution+Methods]. * - * Related: String#sub, String#sub!, String#gsub!. + * With arguments +pattern+ and string +replacement+ given, + * replaces each matching substring with the given +replacement+ string: * + * s = 'abracadabra' + * s.gsub('ab', 'AB') # => "ABracadABra" + * s.gsub(/[a-c]/, 'X') # => "XXrXXXdXXrX" + * + * With arguments +pattern+ and hash +replacement+ given, + * replaces each matching substring with a value from the given +replacement+ hash, + * or removes it: + * + * h = {'a' => 'A', 'b' => 'B', 'c' => 'C'} + * s.gsub(/[a-c]/, h) # => "ABrACAdABrA" # 'a', 'b', 'c' replaced. + * s.gsub(/[a-d]/, h) # => "ABrACAABrA" # 'd' removed. + * + * With argument +pattern+ and a block given, + * calls the block with each matching substring; + * replaces that substring with the block's return value: + * + * s.gsub(/[a-d]/) {|substring| substring.upcase } + * # => "ABrACADABrA" + * + * With argument +pattern+ and no block given, + * returns a new Enumerator. + * + * Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. */ static VALUE From 72b8bb4cafe2512c8c5273b4614eba2028a4c350 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sun, 3 Aug 2025 09:13:44 -0500 Subject: [PATCH 009/157] [DOC] Tweaks for String#gsub! --- string.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/string.c b/string.c index 54e719623e..fe848d6a4a 100644 --- a/string.c +++ b/string.c @@ -6576,15 +6576,12 @@ str_gsub(int argc, VALUE *argv, VALUE str, int bang) * gsub!(pattern) {|match| ... } -> self or nil * gsub!(pattern) -> an_enumerator * - * Performs the specified substring replacement(s) on +self+; - * returns +self+ if any replacement occurred, +nil+ otherwise. + * Like String#gsub, except that: * - * See {Substitution Methods}[rdoc-ref:String@Substitution+Methods]. - * - * Returns an Enumerator if no +replacement+ and no block given. - * - * Related: String#sub, String#gsub, String#sub!. + * - Performs substitutions in +self+ (not in a copy of +self+). + * - Returns +self+ if any characters are removed, +nil+ otherwise. * + * Related: see {Modifying}[rdoc-ref:String@Modifying]. */ static VALUE From 8e9ea4c202fb104d7c17ad1f3cc59d697120501a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 5 Aug 2025 17:07:37 +0900 Subject: [PATCH 010/157] Convert `PKG_CONFIG_PATH` to msys/cygwin path --- test/mkmf/test_pkg_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mkmf/test_pkg_config.rb b/test/mkmf/test_pkg_config.rb index adf5fa6e92..d0a2dc130a 100644 --- a/test/mkmf/test_pkg_config.rb +++ b/test/mkmf/test_pkg_config.rb @@ -26,7 +26,7 @@ class TestMkmfPkgConfig < TestMkmf Cflags: -I${includedir}/cflags-I --cflags-other EOF - @pkg_config_path, ENV["PKG_CONFIG_PATH"] = ENV["PKG_CONFIG_PATH"], @fixtures_dir + @pkg_config_path, ENV["PKG_CONFIG_PATH"] = ENV["PKG_CONFIG_PATH"], mkintpath(@fixtures_dir) end end From 79d8a3159f60d32396c8281fe438e86ab97e3daa Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 5 Aug 2025 21:34:03 +0900 Subject: [PATCH 011/157] Check if the found pkg-config is usable actually --- test/mkmf/test_pkg_config.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/mkmf/test_pkg_config.rb b/test/mkmf/test_pkg_config.rb index d0a2dc130a..abeaf548f8 100644 --- a/test/mkmf/test_pkg_config.rb +++ b/test/mkmf/test_pkg_config.rb @@ -3,7 +3,9 @@ require_relative 'base' require 'shellwords' class TestMkmfPkgConfig < TestMkmf - PKG_CONFIG = config_string("PKG_CONFIG") {|path| find_executable0(path)} + PKG_CONFIG = config_string("PKG_CONFIG") do |path| + find_executable0(path, "--version") {$?.success?} + end def setup super From 4cfe5baf3d988425ad5e50984f6388a1165f92e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 1 Aug 2025 11:15:21 +0200 Subject: [PATCH 012/157] Use snprintf instead of deprecated sprintf When compiling with -fsanitize=address on macOS, the deprecation of sprintf is effective and prevents compiling yjit.c. More details: https://openradar.appspot.com/FB11761475. --- yjit.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yjit.c b/yjit.c index f83a330bd6..44788eaf2c 100644 --- a/yjit.c +++ b/yjit.c @@ -622,8 +622,9 @@ rb_yjit_iseq_inspect(const rb_iseq_t *iseq) const char *path = RSTRING_PTR(rb_iseq_path(iseq)); int lineno = iseq->body->location.code_location.beg_pos.lineno; - char *buf = ZALLOC_N(char, strlen(label) + strlen(path) + num_digits(lineno) + 3); - sprintf(buf, "%s@%s:%d", label, path, lineno); + const size_t size = strlen(label) + strlen(path) + num_digits(lineno) + 3; + char *buf = ZALLOC_N(char, size); + snprintf(buf, size, "%s@%s:%d", label, path, lineno); return buf; } From 0e33256c8e921e67682d6475634771576ae14748 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 6 Aug 2025 00:22:43 +0900 Subject: [PATCH 013/157] CI: Use `\e` instead of `\033` [ci skip] --- .github/workflows/mingw.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 72656fa766..7f8d05a634 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -76,19 +76,19 @@ jobs: # show where result=true for e in gcc.exe ragel.exe make.exe libcrypto-3-x64.dll libssl-3-x64.dll; do - echo ::group::$'\033[93m'$e$'\033[m' + echo ::group::$'\e[93m'$e$'\e[m' where $e || result=false echo ::endgroup:: done # show version for e in gcc ragel make "openssl version"; do case "$e" in *" "*) ;; *) e="$e --version";; esac - echo ::group::$'\033[93m'$e$'\033[m' + echo ::group::$'\e[93m'$e$'\e[m' $e || result=false echo ::endgroup:: done # show packages - echo ::group::$'\033[93m'Packages$'\033[m' + echo ::group::$'\e[93m'Packages$'\e[m' pacman -Qs mingw-w64-ucrt-x86_64-* | sed -n "s,local/mingw-w64-ucrt-x86_64-,,p" echo ::endgroup:: $result From 95320f1ddfd0d17ddad3c0a20b43636601b6bb55 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 5 Aug 2025 11:05:23 -0400 Subject: [PATCH 014/157] Fix RUBY_FREE_AT_EXIT for static symbols Since static symbols allocate memory, we should deallocate them at shutdown to prevent memory leaks from being reported with RUBY_FREE_AT_EXIT. --- internal/symbol.h | 1 + symbol.c | 14 ++++++++++++++ vm.c | 1 + 3 files changed, 16 insertions(+) diff --git a/internal/symbol.h b/internal/symbol.h index 131cddef90..8571c00289 100644 --- a/internal/symbol.h +++ b/internal/symbol.h @@ -35,6 +35,7 @@ bool rb_obj_is_symbol_table(VALUE obj); void rb_sym_global_symbol_table_foreach_weak_reference(int (*callback)(VALUE *key, void *data), void *data); void rb_gc_free_dsymbol(VALUE); int rb_static_id_valid_p(ID id); +void rb_free_global_symbol_table(void); #if __has_builtin(__builtin_constant_p) #define rb_sym_intern_ascii_cstr(ptr) \ diff --git a/symbol.c b/symbol.c index 43ab0ffa32..abb2c76dc2 100644 --- a/symbol.c +++ b/symbol.c @@ -386,6 +386,20 @@ rb_sym_global_symbols_update_references(void) symbols->ids = rb_gc_location(symbols->ids); } +static int +rb_free_global_symbol_table_i(VALUE *sym_ptr, void *data) +{ + sym_set_free(*sym_ptr); + + return ST_DELETE; +} + +void +rb_free_global_symbol_table(void) +{ + rb_concurrent_set_foreach_with_replace(ruby_global_symbols.sym_set, rb_free_global_symbol_table_i, NULL); +} + WARN_UNUSED_RESULT(static ID lookup_str_id(VALUE str)); WARN_UNUSED_RESULT(static VALUE lookup_id_str(ID id)); diff --git a/vm.c b/vm.c index 9284a2ce69..4223c2d2ac 100644 --- a/vm.c +++ b/vm.c @@ -3146,6 +3146,7 @@ ruby_vm_destruct(rb_vm_t *vm) rb_free_encoded_insn_data(); rb_free_global_enc_table(); rb_free_loaded_builtin_table(); + rb_free_global_symbol_table(); rb_free_shared_fiber_pool(); rb_free_transcoder_table(); From 3ef8d833ab6e803cdff714ee454d7a4d47ee1c47 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 1 Aug 2025 18:17:33 +0200 Subject: [PATCH 015/157] rb_gc_impl_mark_and_move: avoid needless writes Assuming not all objects are moved during compaction, it is preferable to avoid rewriting references that haven't moved as to avoid invalidating potentially shared memory pages. --- gc/default/default.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gc/default/default.c b/gc/default/default.c index 47cfe3fb3b..9038a01e4e 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -4420,7 +4420,10 @@ rb_gc_impl_mark_and_move(void *objspace_ptr, VALUE *ptr) GC_ASSERT(objspace->flags.during_compacting); GC_ASSERT(during_gc); - *ptr = rb_gc_impl_location(objspace, *ptr); + VALUE destination = rb_gc_impl_location(objspace, *ptr); + if (destination != *ptr) { + *ptr = destination; + } } else { gc_mark(objspace, *ptr); From 18e37ac430e02d89738406c52d1faaaa08c2e0cf Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 6 May 2025 08:57:30 -0400 Subject: [PATCH 016/157] [ruby/prism] Optimize context_terminator with a lookup table https://github.com/ruby/prism/commit/483aa89234 --- prism/config.yml | 71 ++++++++++++------------ prism/prism.c | 137 ++++++++++++++++++++--------------------------- 2 files changed, 96 insertions(+), 112 deletions(-) diff --git a/prism/config.yml b/prism/config.yml index 257bd389ed..b37b98cbdf 100644 --- a/prism/config.yml +++ b/prism/config.yml @@ -322,13 +322,42 @@ warnings: - UNUSED_LOCAL_VARIABLE - VOID_STATEMENT tokens: + # The order of the tokens at the beginning is important, because we use them + # for a lookup table. - name: EOF value: 1 comment: final token in the file - - name: MISSING - comment: "a token that was expected but not found" - - name: NOT_PROVIDED - comment: "a token that was not present but it is okay" + - name: BRACE_RIGHT + comment: "}" + - name: COMMA + comment: "," + - name: EMBEXPR_END + comment: "}" + - name: KEYWORD_DO + comment: "do" + - name: KEYWORD_ELSE + comment: "else" + - name: KEYWORD_ELSIF + comment: "elsif" + - name: KEYWORD_END + comment: "end" + - name: KEYWORD_ENSURE + comment: "ensure" + - name: KEYWORD_IN + comment: "in" + - name: KEYWORD_RESCUE + comment: "rescue" + - name: KEYWORD_THEN + comment: "then" + - name: KEYWORD_WHEN + comment: "when" + - name: NEWLINE + comment: "a newline character outside of other tokens" + - name: PARENTHESIS_RIGHT + comment: ")" + - name: SEMICOLON + comment: ";" + # Tokens from here on are not used for lookup, and can be in any order. - name: AMPERSAND comment: "&" - name: AMPERSAND_AMPERSAND @@ -351,8 +380,6 @@ tokens: comment: "!~" - name: BRACE_LEFT comment: "{" - - name: BRACE_RIGHT - comment: "}" - name: BRACKET_LEFT comment: "[" - name: BRACKET_LEFT_ARRAY @@ -375,8 +402,6 @@ tokens: comment: ":" - name: COLON_COLON comment: "::" - - name: COMMA - comment: "," - name: COMMENT comment: "a comment" - name: CONSTANT @@ -395,8 +420,6 @@ tokens: comment: "a line inside of embedded documentation" - name: EMBEXPR_BEGIN comment: "#{" - - name: EMBEXPR_END - comment: "}" - name: EMBVAR comment: "#" - name: EQUAL @@ -463,20 +486,10 @@ tokens: comment: "def" - name: KEYWORD_DEFINED comment: "defined?" - - name: KEYWORD_DO - comment: "do" - name: KEYWORD_DO_LOOP comment: "do keyword for a predicate in a while, until, or for loop" - - name: KEYWORD_ELSE - comment: "else" - - name: KEYWORD_ELSIF - comment: "elsif" - - name: KEYWORD_END - comment: "end" - name: KEYWORD_END_UPCASE comment: "END" - - name: KEYWORD_ENSURE - comment: "ensure" - name: KEYWORD_FALSE comment: "false" - name: KEYWORD_FOR @@ -485,8 +498,6 @@ tokens: comment: "if" - name: KEYWORD_IF_MODIFIER comment: "if in the modifier form" - - name: KEYWORD_IN - comment: "in" - name: KEYWORD_MODULE comment: "module" - name: KEYWORD_NEXT @@ -499,8 +510,6 @@ tokens: comment: "or" - name: KEYWORD_REDO comment: "redo" - - name: KEYWORD_RESCUE - comment: "rescue" - name: KEYWORD_RESCUE_MODIFIER comment: "rescue in the modifier form" - name: KEYWORD_RETRY @@ -511,8 +520,6 @@ tokens: comment: "self" - name: KEYWORD_SUPER comment: "super" - - name: KEYWORD_THEN - comment: "then" - name: KEYWORD_TRUE comment: "true" - name: KEYWORD_UNDEF @@ -525,8 +532,6 @@ tokens: comment: "until" - name: KEYWORD_UNTIL_MODIFIER comment: "until in the modifier form" - - name: KEYWORD_WHEN - comment: "when" - name: KEYWORD_WHILE comment: "while" - name: KEYWORD_WHILE_MODIFIER @@ -563,16 +568,12 @@ tokens: comment: "-=" - name: MINUS_GREATER comment: "->" - - name: NEWLINE - comment: "a newline character outside of other tokens" - name: NUMBERED_REFERENCE comment: "a numbered reference to a capture group in the previous regular expression match" - name: PARENTHESIS_LEFT comment: "(" - name: PARENTHESIS_LEFT_PARENTHESES comment: "( for a parentheses node" - - name: PARENTHESIS_RIGHT - comment: ")" - name: PERCENT comment: "%" - name: PERCENT_EQUAL @@ -605,8 +606,6 @@ tokens: comment: "the beginning of a regular expression" - name: REGEXP_END comment: "the end of a regular expression" - - name: SEMICOLON - comment: ";" - name: SLASH comment: "/" - name: SLASH_EQUAL @@ -651,6 +650,10 @@ tokens: comment: "a separator between words in a list" - name: __END__ comment: "marker for the point in the file at which the parser should stop" + - name: MISSING + comment: "a token that was expected but not found" + - name: NOT_PROVIDED + comment: "a token that was not present but it is okay" flags: - name: ArgumentsNodeFlags values: diff --git a/prism/prism.c b/prism/prism.c index d01c2a0766..9d2598e746 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -8586,85 +8586,66 @@ parser_lex_magic_comment(pm_parser_t *parser, bool semantic_token_seen) { /* Context manipulations */ /******************************************************************************/ -static bool -context_terminator(pm_context_t context, pm_token_t *token) { - switch (context) { - case PM_CONTEXT_MAIN: - case PM_CONTEXT_DEF_PARAMS: - case PM_CONTEXT_DEFINED: - case PM_CONTEXT_MULTI_TARGET: - case PM_CONTEXT_TERNARY: - case PM_CONTEXT_RESCUE_MODIFIER: - return token->type == PM_TOKEN_EOF; - case PM_CONTEXT_DEFAULT_PARAMS: - return token->type == PM_TOKEN_COMMA || token->type == PM_TOKEN_PARENTHESIS_RIGHT; - case PM_CONTEXT_PREEXE: - case PM_CONTEXT_POSTEXE: - return token->type == PM_TOKEN_BRACE_RIGHT; - case PM_CONTEXT_MODULE: - case PM_CONTEXT_CLASS: - case PM_CONTEXT_SCLASS: - case PM_CONTEXT_LAMBDA_DO_END: - case PM_CONTEXT_DEF: - case PM_CONTEXT_BLOCK_KEYWORDS: - return token->type == PM_TOKEN_KEYWORD_END || token->type == PM_TOKEN_KEYWORD_RESCUE || token->type == PM_TOKEN_KEYWORD_ENSURE; - case PM_CONTEXT_WHILE: - case PM_CONTEXT_UNTIL: - case PM_CONTEXT_ELSE: - case PM_CONTEXT_FOR: - case PM_CONTEXT_BEGIN_ENSURE: - case PM_CONTEXT_BLOCK_ENSURE: - case PM_CONTEXT_CLASS_ENSURE: - case PM_CONTEXT_DEF_ENSURE: - case PM_CONTEXT_LAMBDA_ENSURE: - case PM_CONTEXT_MODULE_ENSURE: - case PM_CONTEXT_SCLASS_ENSURE: - return token->type == PM_TOKEN_KEYWORD_END; - case PM_CONTEXT_LOOP_PREDICATE: - return token->type == PM_TOKEN_KEYWORD_DO || token->type == PM_TOKEN_KEYWORD_THEN; - case PM_CONTEXT_FOR_INDEX: - return token->type == PM_TOKEN_KEYWORD_IN; - case PM_CONTEXT_CASE_WHEN: - return token->type == PM_TOKEN_KEYWORD_WHEN || token->type == PM_TOKEN_KEYWORD_END || token->type == PM_TOKEN_KEYWORD_ELSE; - case PM_CONTEXT_CASE_IN: - return token->type == PM_TOKEN_KEYWORD_IN || token->type == PM_TOKEN_KEYWORD_END || token->type == PM_TOKEN_KEYWORD_ELSE; - case PM_CONTEXT_IF: - case PM_CONTEXT_ELSIF: - return token->type == PM_TOKEN_KEYWORD_ELSE || token->type == PM_TOKEN_KEYWORD_ELSIF || token->type == PM_TOKEN_KEYWORD_END; - case PM_CONTEXT_UNLESS: - return token->type == PM_TOKEN_KEYWORD_ELSE || token->type == PM_TOKEN_KEYWORD_END; - case PM_CONTEXT_EMBEXPR: - return token->type == PM_TOKEN_EMBEXPR_END; - case PM_CONTEXT_BLOCK_BRACES: - return token->type == PM_TOKEN_BRACE_RIGHT; - case PM_CONTEXT_PARENS: - return token->type == PM_TOKEN_PARENTHESIS_RIGHT; - case PM_CONTEXT_BEGIN: - case PM_CONTEXT_BEGIN_RESCUE: - case PM_CONTEXT_BLOCK_RESCUE: - case PM_CONTEXT_CLASS_RESCUE: - case PM_CONTEXT_DEF_RESCUE: - case PM_CONTEXT_LAMBDA_RESCUE: - case PM_CONTEXT_MODULE_RESCUE: - case PM_CONTEXT_SCLASS_RESCUE: - return token->type == PM_TOKEN_KEYWORD_ENSURE || token->type == PM_TOKEN_KEYWORD_RESCUE || token->type == PM_TOKEN_KEYWORD_ELSE || token->type == PM_TOKEN_KEYWORD_END; - case PM_CONTEXT_BEGIN_ELSE: - case PM_CONTEXT_BLOCK_ELSE: - case PM_CONTEXT_CLASS_ELSE: - case PM_CONTEXT_DEF_ELSE: - case PM_CONTEXT_LAMBDA_ELSE: - case PM_CONTEXT_MODULE_ELSE: - case PM_CONTEXT_SCLASS_ELSE: - return token->type == PM_TOKEN_KEYWORD_ENSURE || token->type == PM_TOKEN_KEYWORD_END; - case PM_CONTEXT_LAMBDA_BRACES: - return token->type == PM_TOKEN_BRACE_RIGHT; - case PM_CONTEXT_PREDICATE: - return token->type == PM_TOKEN_KEYWORD_THEN || token->type == PM_TOKEN_NEWLINE || token->type == PM_TOKEN_SEMICOLON; - case PM_CONTEXT_NONE: - return false; - } +static const uint32_t context_terminators[] = { + [PM_CONTEXT_NONE] = 0, + [PM_CONTEXT_BEGIN] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BEGIN_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BEGIN_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BEGIN_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BLOCK_BRACES] = (1 << PM_TOKEN_BRACE_RIGHT), + [PM_CONTEXT_BLOCK_KEYWORDS] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_BLOCK_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BLOCK_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_BLOCK_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_CASE_WHEN] = (1 << PM_TOKEN_KEYWORD_WHEN) | (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_ELSE), + [PM_CONTEXT_CASE_IN] = (1 << PM_TOKEN_KEYWORD_IN) | (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_ELSE), + [PM_CONTEXT_CLASS] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_CLASS_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_CLASS_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_CLASS_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_DEF] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_DEF_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_DEF_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_DEF_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_DEF_PARAMS] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_DEFINED] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_DEFAULT_PARAMS] = (1 << PM_TOKEN_COMMA) | (1 << PM_TOKEN_PARENTHESIS_RIGHT), + [PM_CONTEXT_ELSE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_ELSIF] = (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_ELSIF) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_EMBEXPR] = (1 << PM_TOKEN_EMBEXPR_END), + [PM_CONTEXT_FOR] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_FOR_INDEX] = (1 << PM_TOKEN_KEYWORD_IN), + [PM_CONTEXT_IF] = (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_ELSIF) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_LAMBDA_BRACES] = (1 << PM_TOKEN_BRACE_RIGHT), + [PM_CONTEXT_LAMBDA_DO_END] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_LAMBDA_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_LAMBDA_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_LAMBDA_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_LOOP_PREDICATE] = (1 << PM_TOKEN_KEYWORD_DO) | (1 << PM_TOKEN_KEYWORD_THEN), + [PM_CONTEXT_MAIN] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_MODULE] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_MODULE_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_MODULE_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_MODULE_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_MULTI_TARGET] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_PARENS] = (1 << PM_TOKEN_PARENTHESIS_RIGHT), + [PM_CONTEXT_POSTEXE] = (1 << PM_TOKEN_BRACE_RIGHT), + [PM_CONTEXT_PREDICATE] = (1 << PM_TOKEN_KEYWORD_THEN) | (1 << PM_TOKEN_NEWLINE) | (1 << PM_TOKEN_SEMICOLON), + [PM_CONTEXT_PREEXE] = (1 << PM_TOKEN_BRACE_RIGHT), + [PM_CONTEXT_RESCUE_MODIFIER] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_SCLASS] = (1 << PM_TOKEN_KEYWORD_END) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ENSURE), + [PM_CONTEXT_SCLASS_ENSURE] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_SCLASS_ELSE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_SCLASS_RESCUE] = (1 << PM_TOKEN_KEYWORD_ENSURE) | (1 << PM_TOKEN_KEYWORD_RESCUE) | (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_TERNARY] = (1 << PM_TOKEN_EOF), + [PM_CONTEXT_UNLESS] = (1 << PM_TOKEN_KEYWORD_ELSE) | (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_UNTIL] = (1 << PM_TOKEN_KEYWORD_END), + [PM_CONTEXT_WHILE] = (1 << PM_TOKEN_KEYWORD_END), +}; - return false; +static inline bool +context_terminator(pm_context_t context, pm_token_t *token) { + return token->type < 32 && (context_terminators[context] & (1 << token->type)); } /** From f814a777550ee39865f8fe26d0061cba8b715509 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 13 Jun 2025 12:44:08 +0900 Subject: [PATCH 017/157] [ruby/prism] Reject `true && not true` A command-call-like `not true` must be rejected after `&&` and `||`. https://bugs.ruby-lang.org/issues/21337 https://github.com/ruby/prism/commit/0513cf22ad --- prism/templates/src/diagnostic.c.erb | 3 +-- test/prism/errors/command_calls_31.txt | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/prism/templates/src/diagnostic.c.erb b/prism/templates/src/diagnostic.c.erb index 9a30a57e3b..389b1dc484 100644 --- a/prism/templates/src/diagnostic.c.erb +++ b/prism/templates/src/diagnostic.c.erb @@ -184,8 +184,7 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = { [PM_ERR_EXPECT_FOR_DELIMITER] = { "unexpected %s; expected a 'do', newline, or ';' after the 'for' loop collection", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_IDENT_REQ_PARAMETER] = { "expected an identifier for the required parameter", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_IN_DELIMITER] = { "expected a delimiter after the patterns of an `in` clause", PM_ERROR_LEVEL_SYNTAX }, - [PM_ERR_EXPECT_LPAREN_AFTER_NOT_LPAREN] = { "expected a `(` immediately after `not`", PM_ERROR_LEVEL_SYNTAX }, - [PM_ERR_EXPECT_LPAREN_AFTER_NOT_OTHER] = { "expected a `(` after `not`", PM_ERROR_LEVEL_SYNTAX }, + [PM_ERR_EXPECT_LPAREN_AFTER_NOT] = { "expected a `(` after `not`", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_LPAREN_REQ_PARAMETER] = { "expected a `(` to start a required parameter", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_MESSAGE] = { "unexpected %s; expecting a message to send to the receiver", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_RBRACKET] = { "expected a matching `]`", PM_ERROR_LEVEL_SYNTAX }, diff --git a/test/prism/errors/command_calls_31.txt b/test/prism/errors/command_calls_31.txt index e662b25444..72d5fc588f 100644 --- a/test/prism/errors/command_calls_31.txt +++ b/test/prism/errors/command_calls_31.txt @@ -7,11 +7,6 @@ true || not true ^~~~ unexpected 'true', expecting end-of-input true && not (true) - ^ expected a `(` immediately after `not` + ^ expected a `(` after `not` ^ unexpected '(', expecting end-of-input -true && not -true -^~~~ expected a `(` after `not` -^~~~ unexpected 'true', expecting end-of-input - From 087190fcd21973eb34b600fa82e6567189f4bbd7 Mon Sep 17 00:00:00 2001 From: ydah Date: Mon, 7 Jul 2025 19:06:26 +0900 Subject: [PATCH 018/157] [ruby/prism] Improve error handling for missing parentheses after 'not' in command calls https://github.com/ruby/prism/commit/d9151b8a82 --- prism/templates/src/diagnostic.c.erb | 3 ++- test/prism/errors/command_calls_31.txt | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/prism/templates/src/diagnostic.c.erb b/prism/templates/src/diagnostic.c.erb index 389b1dc484..9a30a57e3b 100644 --- a/prism/templates/src/diagnostic.c.erb +++ b/prism/templates/src/diagnostic.c.erb @@ -184,7 +184,8 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = { [PM_ERR_EXPECT_FOR_DELIMITER] = { "unexpected %s; expected a 'do', newline, or ';' after the 'for' loop collection", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_IDENT_REQ_PARAMETER] = { "expected an identifier for the required parameter", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_IN_DELIMITER] = { "expected a delimiter after the patterns of an `in` clause", PM_ERROR_LEVEL_SYNTAX }, - [PM_ERR_EXPECT_LPAREN_AFTER_NOT] = { "expected a `(` after `not`", PM_ERROR_LEVEL_SYNTAX }, + [PM_ERR_EXPECT_LPAREN_AFTER_NOT_LPAREN] = { "expected a `(` immediately after `not`", PM_ERROR_LEVEL_SYNTAX }, + [PM_ERR_EXPECT_LPAREN_AFTER_NOT_OTHER] = { "expected a `(` after `not`", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_LPAREN_REQ_PARAMETER] = { "expected a `(` to start a required parameter", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_MESSAGE] = { "unexpected %s; expecting a message to send to the receiver", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_EXPECT_RBRACKET] = { "expected a matching `]`", PM_ERROR_LEVEL_SYNTAX }, diff --git a/test/prism/errors/command_calls_31.txt b/test/prism/errors/command_calls_31.txt index 72d5fc588f..e662b25444 100644 --- a/test/prism/errors/command_calls_31.txt +++ b/test/prism/errors/command_calls_31.txt @@ -7,6 +7,11 @@ true || not true ^~~~ unexpected 'true', expecting end-of-input true && not (true) - ^ expected a `(` after `not` + ^ expected a `(` immediately after `not` ^ unexpected '(', expecting end-of-input +true && not +true +^~~~ expected a `(` after `not` +^~~~ unexpected 'true', expecting end-of-input + From 2936da902cadc3e9c5737469892df9c116f24b77 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 2 Jun 2025 22:21:25 -0400 Subject: [PATCH 019/157] [ruby/prism] Handle new ractor stuff https://github.com/ruby/prism/commit/f5ded5104d --- test/prism/ractor_test.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/prism/ractor_test.rb b/test/prism/ractor_test.rb index fba10dbfe2..6169940beb 100644 --- a/test/prism/ractor_test.rb +++ b/test/prism/ractor_test.rb @@ -62,7 +62,11 @@ module Prism if reader reader.gets.chomp else - puts(ignore_warnings { Ractor.new(*arguments, &block) }.value) + ractor = ignore_warnings { Ractor.new(*arguments, &block) } + + # Somewhere in the Ruby 3.5.* series, Ractor#take was removed and + # Ractor#value was added. + puts(ractor.respond_to?(:value) ? ractor.value : ractor.take) end end end From 2e672fdee0a81d21b877b7561a2f24f5d57c234d Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 21 Apr 2025 16:07:03 -0400 Subject: [PATCH 020/157] [ruby/prism] Bump JRuby version https://github.com/ruby/prism/commit/27d284bbb8 --- test/prism/ruby/parameters_signature_test.rb | 2 +- test/prism/ruby/parser_test.rb | 12 +----------- test/prism/ruby/ripper_test.rb | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/test/prism/ruby/parameters_signature_test.rb b/test/prism/ruby/parameters_signature_test.rb index af5b54ed91..ea1eea106b 100644 --- a/test/prism/ruby/parameters_signature_test.rb +++ b/test/prism/ruby/parameters_signature_test.rb @@ -54,7 +54,7 @@ module Prism assert_parameters([[:keyrest, :**]], "**") end - if RUBY_ENGINE != "truffleruby" + if RUBY_ENGINE == "ruby" def test_key_ordering assert_parameters([[:keyreq, :a], [:keyreq, :b], [:key, :c], [:key, :d]], "a:, c: 1, b:, d: 2") end diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb index 2396f4186c..156e8f9e9f 100644 --- a/test/prism/ruby/parser_test.rb +++ b/test/prism/ruby/parser_test.rb @@ -99,16 +99,6 @@ module Prism "seattlerb/regexp_esc_C_slash.txt", ] - # These files are either failing to parse or failing to translate, so we'll - # skip them for now. - skip_all = skip_incorrect | [ - ] - - # Not sure why these files are failing on JRuby, but skipping them for now. - if RUBY_ENGINE == "jruby" - skip_all.push("emoji_method_calls.txt", "symbols.txt") - end - # These files are failing to translate their lexer output into the lexer # output expected by the parser gem, so we'll skip them for now. skip_tokens = [ @@ -147,7 +137,7 @@ module Prism define_method(fixture.test_name) do assert_equal_parses( fixture, - compare_asts: !skip_all.include?(fixture.path), + compare_asts: !skip_incorrect.include?(fixture.path), compare_tokens: !skip_tokens.include?(fixture.path), compare_comments: fixture.path != "embdoc_no_newline_at_end.txt" ) diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index d4b278c28e..5c37178889 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return if RUBY_VERSION < "3.3" || RUBY_ENGINE == "truffleruby" +return if RUBY_VERSION < "3.3" || RUBY_ENGINE != "ruby" require_relative "../test_helper" From 6e2b139d6ac1bcbae26c06a4e3022e8b2be8307e Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 6 May 2025 09:30:03 -0400 Subject: [PATCH 021/157] [ruby/prism] Ensure context terminators terminate expressions https://github.com/ruby/prism/commit/915f6b3ae9 --- prism/prism.c | 6 ++++++ test/prism/fixtures/case_in_hash_key.txt | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 test/prism/fixtures/case_in_hash_key.txt diff --git a/prism/prism.c b/prism/prism.c index 9d2598e746..4d2c372d5d 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -22176,6 +22176,12 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc ) { node = parse_expression_infix(parser, node, binding_power, current_binding_powers.right, accepts_command_call, (uint16_t) (depth + 1)); + if (context_terminator(parser->current_context->context, &parser->current)) { + // If this token terminates the current context, then we need to + // stop parsing the expression, as it has become a statement. + return node; + } + switch (PM_NODE_TYPE(node)) { case PM_MULTI_WRITE_NODE: // Multi-write nodes are statements, and cannot be followed by diff --git a/test/prism/fixtures/case_in_hash_key.txt b/test/prism/fixtures/case_in_hash_key.txt new file mode 100644 index 0000000000..75ac8a846f --- /dev/null +++ b/test/prism/fixtures/case_in_hash_key.txt @@ -0,0 +1,6 @@ +case 1 +in 2 + A.print message: +in 3 + A.print message: +end From b482e3d7cd77c688ed0e38e1c95c1f0b2b205cd6 Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Mon, 21 Jul 2025 11:00:33 +0900 Subject: [PATCH 022/157] [ruby/prism] Make `it = it` assign `nil` to match parse.y behavior [Bug #21139] Currently Prism returns `42` for code like this: ```ruby 42.tap { it = it; p it } # => 42 ``` But parse.y returns `nil`: ```ruby 42.tap { it = it; p it } # => nil ``` In parse.y, it on the right-hand side is parsed as a local variable. In Prism, it was parsed as the implicit block parameter it, which caused this inconsistent behavior. This change makes the right-hand side it to be parsed as a local variable, aligning with parse.y's behavior. Bug ticket: https://bugs.ruby-lang.org/issues/21139 https://github.com/ruby/prism/commit/cf3bbf9d2c --- prism/prism.c | 9 ++++++++- test/prism/fixtures/it_assignment.txt | 1 + test/prism/ruby/parser_test.rb | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/prism/fixtures/it_assignment.txt diff --git a/prism/prism.c b/prism/prism.c index 4d2c372d5d..85098c52d8 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -16459,7 +16459,14 @@ parse_variable(pm_parser_t *parser) { pm_node_list_append(¤t_scope->implicit_parameters, node); return node; - } else if ((parser->version >= PM_OPTIONS_VERSION_CRUBY_3_4) && pm_token_is_it(parser->previous.start, parser->previous.end)) { + } else if ((parser->version != PM_OPTIONS_VERSION_CRUBY_3_3) && pm_token_is_it(parser->previous.start, parser->previous.end)) { + if (match1(parser, PM_TOKEN_EQUAL)) { + pm_constant_id_t name_id = pm_parser_local_add_location(parser, parser->previous.start, parser->previous.end, 0); + pm_node_t *node = (pm_node_t *) pm_local_variable_read_node_create_constant_id(parser, &parser->previous, name_id, 0, false); + + return node; + } + pm_node_t *node = (pm_node_t *) pm_it_local_variable_read_node_create(parser, &parser->previous); pm_node_list_append(¤t_scope->implicit_parameters, node); diff --git a/test/prism/fixtures/it_assignment.txt b/test/prism/fixtures/it_assignment.txt new file mode 100644 index 0000000000..523b0ffe1e --- /dev/null +++ b/test/prism/fixtures/it_assignment.txt @@ -0,0 +1 @@ +42.tap { it = it; p it } diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb index 156e8f9e9f..7bf2e59a86 100644 --- a/test/prism/ruby/parser_test.rb +++ b/test/prism/ruby/parser_test.rb @@ -183,6 +183,25 @@ module Prism assert_equal(it_block_parameter_sexp, actual_ast.to_sexp) end + def test_it_assignment_syntax + it_assignment_fixture_path = Pathname(__dir__).join('../../../test/prism/fixtures/it_assignment.txt') + + buffer = Parser::Source::Buffer.new(it_assignment_fixture_path) + buffer.source = it_assignment_fixture_path.read + actual_ast = Prism::Translation::Parser34.new.tokenize(buffer)[0] + + it_assignment_sexp = parse_sexp { + s(:block, + s(:send, s(:int, 42), :tap), + s(:args), + s(:begin, + s(:lvasgn, :it, s(:lvar, :it)), + s(:send, nil, :p, s(:lvar, :it)))) + } + + assert_equal(it_assignment_sexp, actual_ast.to_sexp) + end + private def assert_equal_parses(fixture, compare_asts: true, compare_tokens: true, compare_comments: true) From 02200ac81cd6712759995d920e354a0c3e96488c Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Wed, 23 Jul 2025 21:23:47 +0900 Subject: [PATCH 023/157] [ruby/prism] Add it read and assignment test https://github.com/ruby/prism/commit/659d769621 --- test/prism/fixtures/it_read_and_assignment.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/prism/fixtures/it_read_and_assignment.txt diff --git a/test/prism/fixtures/it_read_and_assignment.txt b/test/prism/fixtures/it_read_and_assignment.txt new file mode 100644 index 0000000000..2cceeb2a54 --- /dev/null +++ b/test/prism/fixtures/it_read_and_assignment.txt @@ -0,0 +1 @@ +42.tap { p it; it = it; p it } From a12e0c1db1d33525b904e2c0c801a22d42658d31 Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Wed, 23 Jul 2025 21:20:50 +0900 Subject: [PATCH 024/157] [ruby/prism] Remove uneeded test https://github.com/ruby/prism/commit/a6b448b10f --- test/prism/ruby/parser_test.rb | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb index 7bf2e59a86..156e8f9e9f 100644 --- a/test/prism/ruby/parser_test.rb +++ b/test/prism/ruby/parser_test.rb @@ -183,25 +183,6 @@ module Prism assert_equal(it_block_parameter_sexp, actual_ast.to_sexp) end - def test_it_assignment_syntax - it_assignment_fixture_path = Pathname(__dir__).join('../../../test/prism/fixtures/it_assignment.txt') - - buffer = Parser::Source::Buffer.new(it_assignment_fixture_path) - buffer.source = it_assignment_fixture_path.read - actual_ast = Prism::Translation::Parser34.new.tokenize(buffer)[0] - - it_assignment_sexp = parse_sexp { - s(:block, - s(:send, s(:int, 42), :tap), - s(:args), - s(:begin, - s(:lvasgn, :it, s(:lvar, :it)), - s(:send, nil, :p, s(:lvar, :it)))) - } - - assert_equal(it_assignment_sexp, actual_ast.to_sexp) - end - private def assert_equal_parses(fixture, compare_asts: true, compare_tokens: true, compare_comments: true) From 6bc07f142233759e747f04db82115593014af1fe Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Mon, 28 Jul 2025 23:25:49 +0900 Subject: [PATCH 025/157] [ruby/prism] Convert implicit parameter `it` to local variable in `parse_expression_infix` function https://github.com/ruby/prism/commit/fb136c6eb5 --- prism/prism.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index 85098c52d8..afd767b84c 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -16459,14 +16459,7 @@ parse_variable(pm_parser_t *parser) { pm_node_list_append(¤t_scope->implicit_parameters, node); return node; - } else if ((parser->version != PM_OPTIONS_VERSION_CRUBY_3_3) && pm_token_is_it(parser->previous.start, parser->previous.end)) { - if (match1(parser, PM_TOKEN_EQUAL)) { - pm_constant_id_t name_id = pm_parser_local_add_location(parser, parser->previous.start, parser->previous.end, 0); - pm_node_t *node = (pm_node_t *) pm_local_variable_read_node_create_constant_id(parser, &parser->previous, name_id, 0, false); - - return node; - } - + } else if ((parser->version >= PM_OPTIONS_VERSION_CRUBY_3_4) && pm_token_is_it(parser->previous.start, parser->previous.end)) { pm_node_t *node = (pm_node_t *) pm_it_local_variable_read_node_create(parser, &parser->previous); pm_node_list_append(¤t_scope->implicit_parameters, node); @@ -21190,6 +21183,13 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t } PRISM_FALLTHROUGH case PM_CASE_WRITABLE: { + // When we have `it = value`, we need to add `it` as a local + // variable before parsing the value, in case the value + // references the variable. + if (PM_NODE_TYPE_P(node, PM_IT_LOCAL_VARIABLE_READ_NODE)) { + pm_parser_local_add_location(parser, node->location.start, node->location.end, 0); + } + parser_lex(parser); pm_node_t *value = parse_assignment_values(parser, previous_binding_power, PM_NODE_TYPE_P(node, PM_MULTI_TARGET_NODE) ? PM_BINDING_POWER_MULTI_ASSIGNMENT + 1 : binding_power, accepts_command_call, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL, (uint16_t) (depth + 1)); From 52312d53ca6da5eb61e3a1efa534eb221f5772d7 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 5 Aug 2025 14:58:00 -0500 Subject: [PATCH 026/157] [DOC] Tweaks for GC.start (#14093) --- gc.rb | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/gc.rb b/gc.rb index d3bb8db036..1298e30056 100644 --- a/gc.rb +++ b/gc.rb @@ -7,31 +7,39 @@ # You may obtain information about the operation of the \GC through GC::Profiler. module GC - # Initiates garbage collection, even if manually disabled. + # Initiates garbage collection, even if explicitly disabled by GC.disable. # - # The +full_mark+ keyword argument determines whether or not to perform a - # major garbage collection cycle. When set to +true+, a major garbage - # collection cycle is run, meaning all objects are marked. When set to - # +false+, a minor garbage collection cycle is run, meaning only young - # objects are marked. + # Keyword arguments: # - # The +immediate_mark+ keyword argument determines whether or not to perform - # incremental marking. When set to +true+, marking is completed during the - # call to this method. When set to +false+, marking is performed in steps - # that are interleaved with future Ruby code execution, so marking might not - # be completed during this method call. Note that if +full_mark+ is +false+, - # then marking will always be immediate, regardless of the value of - # +immediate_mark+. + # - +full_mark+: + # its boolean value determines whether to perform a major garbage collection cycle: # - # The +immediate_sweep+ keyword argument determines whether or not to defer - # sweeping (using lazy sweep). When set to +false+, sweeping is performed in - # steps that are interleaved with future Ruby code execution, so sweeping might - # not be completed during this method call. When set to +true+, sweeping is - # completed during the call to this method. + # - +true+: initiates a major garbage collection cycle, + # meaning all objects (old and new) are marked. + # - +false+: initiates a minor garbage collection cycle, + # meaning only young objects are marked. # - # Note: These keyword arguments are implementation and version-dependent. They - # are not guaranteed to be future-compatible and may be ignored if the - # underlying implementation does not support them. + # - +immediate_mark+: + # its boolean value determines whether to perform incremental marking: + # + # - +true+: marking is completed before the method returns. + # - +false+: marking is performed by parts, + # interleaved with program execution both before the method returns and afterward; + # therefore marking may not be completed before the return. + # Note that if +full_mark+ is +false+, marking will always be immediate, + # regardless of the value of +immediate_mark+. + # + # - +immediate_sweep+: + # its boolean value determines whether to defer sweeping (using lazy sweep): + # + # - +true+: sweeping is completed before the method returns. + # - +false+: sweeping is performed by parts, + # interleaved with program execution both before the method returns and afterward; + # therefore sweeping may not be completed before the return. + # + # Note that these keword arguments are implementation- and version-dependent, + # are not guaranteed to be future-compatible, + # and may be ignored in some implementations. def self.start full_mark: true, immediate_mark: true, immediate_sweep: true Primitive.gc_start_internal full_mark, immediate_mark, immediate_sweep, false end From ef95e5ba3de65d42fe0e1d41519dcf05db11a4e8 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 5 Aug 2025 13:56:04 -0700 Subject: [PATCH 027/157] ZJIT: Profile type+shape distributions (#13901) ZJIT uses the interpreter to take type profiles of what objects pass through the code. It stores a compressed record of the history per opcode for the opcodes we select. Before this change, we re-used the HIR Type data-structure, a shallow type lattice, to store historical type information. This was quick for bringup but is quite lossy as profiles go: we get one bit per built-in type seen, and if we see a non-built-in type in addition, we end up with BasicObject. Not very helpful. Additionally, it does not give us any notion of cardinality: how many of each type did we see? This change brings with it a much more interesting slice of type history: a histogram. A Distribution holds a record of the top-N (where N is fixed at Ruby compile-time) `(Class, ShapeId)` pairs and their counts. It also holds an *other* count in case we see more than N pairs. Using this distribution, we can make more informed decisions about when we should use type information. We can determine if we are strictly monomorphic, very nearly monomorphic, or something else. Maybe the call-site is polymorphic, so we should have a polymorphic inline cache. Exciting stuff. I also plumb this new distribution into the HIR part of the compilation pipeline. --- zjit.c | 11 ++ zjit/bindgen/src/main.rs | 3 + zjit/src/cruby.rs | 10 +- zjit/src/cruby_bindings.inc.rs | 4 + zjit/src/distribution.rs | 266 +++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 114 ++++++++------ zjit/src/hir_type/mod.rs | 31 +++- zjit/src/lib.rs | 1 + zjit/src/profile.rs | 114 +++++++++++--- 9 files changed, 482 insertions(+), 72 deletions(-) create mode 100644 zjit/src/distribution.rs diff --git a/zjit.c b/zjit.c index abe7422540..09ab128ae3 100644 --- a/zjit.c +++ b/zjit.c @@ -346,6 +346,17 @@ rb_zjit_shape_obj_too_complex_p(VALUE obj) return rb_shape_obj_too_complex_p(obj); } +enum { + RB_SPECIAL_CONST_SHAPE_ID = SPECIAL_CONST_SHAPE_ID, + RB_INVALID_SHAPE_ID = INVALID_SHAPE_ID, +}; + +bool +rb_zjit_singleton_class_p(VALUE klass) +{ + return RCLASS_SINGLETON_P(klass); +} + // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_stats(rb_execution_context_t *ec, VALUE self); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index f67d8e91d3..77299c2657 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -351,8 +351,11 @@ fn main() { .allowlist_function("rb_optimized_call") .allowlist_function("rb_zjit_icache_invalidate") .allowlist_function("rb_zjit_print_exception") + .allowlist_function("rb_zjit_singleton_class_p") .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") + .allowlist_var("RB_SPECIAL_CONST_SHAPE_ID") + .allowlist_var("RB_INVALID_SHAPE_ID") // From jit.c .allowlist_function("rb_assert_holding_vm_lock") diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index afa3ddfb49..095a2988f8 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -265,6 +265,12 @@ pub struct ID(pub ::std::os::raw::c_ulong); /// Pointer to an ISEQ pub type IseqPtr = *const rb_iseq_t; +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ShapeId(pub u32); + +pub const SPECIAL_CONST_SHAPE_ID: ShapeId = ShapeId(RB_SPECIAL_CONST_SHAPE_ID); +pub const INVALID_SHAPE_ID: ShapeId = ShapeId(RB_INVALID_SHAPE_ID); + // Given an ISEQ pointer, convert PC to insn_idx pub fn iseq_pc_to_insn_idx(iseq: IseqPtr, pc: *mut VALUE) -> Option { let pc_zero = unsafe { rb_iseq_pc_at_idx(iseq, 0) }; @@ -487,8 +493,8 @@ impl VALUE { unsafe { rb_zjit_shape_obj_too_complex_p(self) } } - pub fn shape_id_of(self) -> u32 { - unsafe { rb_obj_shape_id(self) } + pub fn shape_id_of(self) -> ShapeId { + ShapeId(unsafe { rb_obj_shape_id(self) }) } pub fn embedded_p(self) -> bool { diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 7fe1a0406a..5c939fabe7 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -719,6 +719,9 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; +pub const RB_SPECIAL_CONST_SHAPE_ID: _bindgen_ty_38 = 33554432; +pub const RB_INVALID_SHAPE_ID: _bindgen_ty_38 = 4294967295; +pub type _bindgen_ty_38 = u32; pub type rb_iseq_param_keyword_struct = rb_iseq_constant_body__bindgen_ty_1_rb_iseq_param_keyword; unsafe extern "C" { pub fn ruby_xfree(ptr: *mut ::std::os::raw::c_void); @@ -938,6 +941,7 @@ unsafe extern "C" { pub fn rb_iseq_set_zjit_payload(iseq: *const rb_iseq_t, payload: *mut ::std::os::raw::c_void); pub fn rb_zjit_print_exception(); pub fn rb_zjit_shape_obj_too_complex_p(obj: VALUE) -> bool; + pub fn rb_zjit_singleton_class_p(klass: VALUE) -> bool; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; diff --git a/zjit/src/distribution.rs b/zjit/src/distribution.rs new file mode 100644 index 0000000000..5927ffa5c9 --- /dev/null +++ b/zjit/src/distribution.rs @@ -0,0 +1,266 @@ +/// This implementation was inspired by the type feedback module from Google's S6, which was +/// written in C++ for use with Python. This is a new implementation in Rust created for use with +/// Ruby instead of Python. +#[derive(Debug, Clone)] +pub struct Distribution { + /// buckets and counts have the same length + /// buckets[0] is always the most common item + buckets: [T; N], + counts: [usize; N], + /// if there is no more room, increment the fallback + other: usize, + // TODO(max): Add count disparity, which can help determine when to reset the distribution +} + +impl Distribution { + pub fn new() -> Self { + Self { buckets: [Default::default(); N], counts: [0; N], other: 0 } + } + + pub fn observe(&mut self, item: T) { + for (bucket, count) in self.buckets.iter_mut().zip(self.counts.iter_mut()) { + if *bucket == item || *count == 0 { + *bucket = item; + *count += 1; + // Keep the most frequent item at the front + self.bubble_up(); + return; + } + } + self.other += 1; + } + + /// Keep the highest counted bucket at index 0 + fn bubble_up(&mut self) { + if N == 0 { return; } + let max_index = self.counts.into_iter().enumerate().max_by_key(|(_, val)| *val).unwrap().0; + if max_index != 0 { + self.counts.swap(0, max_index); + self.buckets.swap(0, max_index); + } + } + + pub fn each_item(&self) -> impl Iterator + '_ { + self.buckets.iter().zip(self.counts.iter()) + .filter_map(|(&bucket, &count)| if count > 0 { Some(bucket) } else { None }) + } + + pub fn each_item_mut(&mut self) -> impl Iterator + '_ { + self.buckets.iter_mut().zip(self.counts.iter()) + .filter_map(|(bucket, &count)| if count > 0 { Some(bucket) } else { None }) + } +} + +#[derive(PartialEq, Debug, Clone, Copy)] +enum DistributionKind { + /// No types seen + Empty, + /// One type seen + Monomorphic, + /// Between 2 and (fixed) N types seen + Polymorphic, + /// Polymorphic, but with a significant skew towards one type + SkewedPolymorphic, + /// More than N types seen with no clear winner + Megamorphic, + /// Megamorphic, but with a significant skew towards one type + SkewedMegamorphic, +} + +#[derive(Debug)] +pub struct DistributionSummary { + kind: DistributionKind, + buckets: [T; N], + // TODO(max): Determine if we need some notion of stability +} + +const SKEW_THRESHOLD: f64 = 0.75; + +impl DistributionSummary { + pub fn new(dist: &Distribution) -> Self { + #[cfg(debug_assertions)] + { + let first_count = dist.counts[0]; + for &count in &dist.counts[1..] { + assert!(first_count >= count, "First count should be the largest"); + } + } + let num_seen = dist.counts.iter().sum::() + dist.other; + let kind = if dist.other == 0 { + // Seen <= N types total + if dist.counts[0] == 0 { + DistributionKind::Empty + } else if dist.counts[1] == 0 { + DistributionKind::Monomorphic + } else if (dist.counts[0] as f64)/(num_seen as f64) >= SKEW_THRESHOLD { + DistributionKind::SkewedPolymorphic + } else { + DistributionKind::Polymorphic + } + } else { + // Seen > N types total; considered megamorphic + if (dist.counts[0] as f64)/(num_seen as f64) >= SKEW_THRESHOLD { + DistributionKind::SkewedMegamorphic + } else { + DistributionKind::Megamorphic + } + }; + Self { kind, buckets: dist.buckets.clone() } + } + + pub fn is_monomorphic(&self) -> bool { + self.kind == DistributionKind::Monomorphic + } + + pub fn is_skewed_polymorphic(&self) -> bool { + self.kind == DistributionKind::SkewedPolymorphic + } + + pub fn is_skewed_megamorphic(&self) -> bool { + self.kind == DistributionKind::SkewedMegamorphic + } + + pub fn bucket(&self, idx: usize) -> T { + assert!(idx < N, "index {idx} out of bounds for buckets[{N}]"); + self.buckets[idx] + } +} + +#[cfg(test)] +mod distribution_tests { + use super::*; + + #[test] + fn start_empty() { + let dist = Distribution::::new(); + assert_eq!(dist.other, 0); + assert!(dist.counts.iter().all(|&b| b == 0)); + } + + #[test] + fn observe_adds_record() { + let mut dist = Distribution::::new(); + dist.observe(10); + assert_eq!(dist.buckets[0], 10); + assert_eq!(dist.counts[0], 1); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_increments_record() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + assert_eq!(dist.buckets[0], 10); + assert_eq!(dist.counts[0], 2); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_two() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(11); + assert_eq!(dist.buckets[0], 11); + assert_eq!(dist.counts[0], 3); + assert_eq!(dist.buckets[1], 10); + assert_eq!(dist.counts[1], 2); + assert_eq!(dist.other, 0); + } + + #[test] + fn observe_with_max_increments_other() { + let mut dist = Distribution::::new(); + dist.observe(10); + assert!(dist.buckets.is_empty()); + assert!(dist.counts.is_empty()); + assert_eq!(dist.other, 1); + } + + #[test] + fn empty_distribution_returns_empty_summary() { + let dist = Distribution::::new(); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Empty); + } + + #[test] + fn monomorphic_distribution_returns_monomorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(10); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Monomorphic); + assert_eq!(summary.buckets[0], 10); + } + + #[test] + fn polymorphic_distribution_returns_polymorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Polymorphic); + assert_eq!(summary.buckets[0], 11); + assert_eq!(summary.buckets[1], 10); + } + + #[test] + fn skewed_polymorphic_distribution_returns_skewed_polymorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::SkewedPolymorphic); + assert_eq!(summary.buckets[0], 11); + assert_eq!(summary.buckets[1], 10); + } + + #[test] + fn megamorphic_distribution_returns_megamorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(12); + dist.observe(13); + dist.observe(14); + dist.observe(11); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::Megamorphic); + assert_eq!(summary.buckets[0], 11); + } + + #[test] + fn skewed_megamorphic_distribution_returns_skewed_megamorphic_summary() { + let mut dist = Distribution::::new(); + dist.observe(10); + dist.observe(11); + dist.observe(11); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(12); + dist.observe(13); + dist.observe(14); + let summary = DistributionSummary::new(&dist); + assert_eq!(summary.kind, DistributionKind::SkewedMegamorphic); + assert_eq!(summary.buckets[0], 12); + } +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 976580c85b..203be0661e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -11,6 +11,7 @@ use std::{ }; use crate::hir_type::{Type, types}; use crate::bitset::BitSet; +use crate::profile::{TypeDistributionSummary, ProfiledType}; /// An index of an [`Insn`] in a [`Function`]. This is a popular /// type since this effectively acts as a pointer to an [`Insn`]. @@ -1357,19 +1358,23 @@ impl Function { /// Return the interpreter-profiled type of the HIR instruction at the given ISEQ instruction /// index, if it is known. This historical type record is not a guarantee and must be checked /// with a GuardType or similar. - fn profiled_type_of_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option { + fn profiled_type_of_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option { let Some(ref profiles) = self.profiles else { return None }; let Some(entries) = profiles.types.get(&iseq_insn_idx) else { return None }; - for &(entry_insn, entry_type) in entries { - if self.union_find.borrow().find_const(entry_insn) == self.union_find.borrow().find_const(insn) { - return Some(entry_type); + for (entry_insn, entry_type_summary) in entries { + if self.union_find.borrow().find_const(*entry_insn) == self.union_find.borrow().find_const(insn) { + if entry_type_summary.is_monomorphic() || entry_type_summary.is_skewed_polymorphic() { + return Some(entry_type_summary.bucket(0)); + } else { + return None; + } } } None } - fn likely_is_fixnum(&self, val: InsnId, profiled_type: Type) -> bool { - return self.is_a(val, types::Fixnum) || profiled_type.is_subtype(types::Fixnum); + fn likely_is_fixnum(&self, val: InsnId, profiled_type: ProfiledType) -> bool { + return self.is_a(val, types::Fixnum) || profiled_type.is_fixnum(); } fn coerce_to_fixnum(&mut self, block: BlockId, val: InsnId, state: InsnId) -> InsnId { @@ -1380,8 +1385,8 @@ impl Function { fn arguments_likely_fixnums(&mut self, left: InsnId, right: InsnId, state: InsnId) -> bool { let frame_state = self.frame_state(state); let iseq_insn_idx = frame_state.insn_idx as usize; - let left_profiled_type = self.profiled_type_of_at(left, iseq_insn_idx).unwrap_or(types::BasicObject); - let right_profiled_type = self.profiled_type_of_at(right, iseq_insn_idx).unwrap_or(types::BasicObject); + let left_profiled_type = self.profiled_type_of_at(left, iseq_insn_idx).unwrap_or(ProfiledType::empty()); + let right_profiled_type = self.profiled_type_of_at(right, iseq_insn_idx).unwrap_or(ProfiledType::empty()); self.likely_is_fixnum(left, left_profiled_type) && self.likely_is_fixnum(right, right_profiled_type) } @@ -1510,15 +1515,16 @@ impl Function { self.try_rewrite_aref(block, insn_id, self_val, args[0], state), Insn::SendWithoutBlock { mut self_val, cd, args, state } => { let frame_state = self.frame_state(state); - let (klass, guard_equal_to) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() { + let (klass, profiled_type) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() { // If we know the class statically, use it to fold the lookup at compile-time. (klass, None) } else { - // If we know that self is top-self from profile information, guard and use it to fold the lookup at compile-time. - match self.profiled_type_of_at(self_val, frame_state.insn_idx) { - Some(self_type) if self_type.is_top_self() => (self_type.exact_ruby_class().unwrap(), self_type.ruby_object()), - _ => { self.push_insn_id(block, insn_id); continue; } - } + // If we know that self is reasonably monomorphic from profile information, guard and use it to fold the lookup at compile-time. + // TODO(max): Figure out how to handle top self? + let Some(recv_type) = self.profiled_type_of_at(self_val, frame_state.insn_idx) else { + self.push_insn_id(block, insn_id); continue; + }; + (recv_type.class(), Some(recv_type)) }; let ci = unsafe { get_call_data_ci(cd) }; // info about the call site let mid = unsafe { vm_ci_mid(ci) }; @@ -1542,8 +1548,8 @@ impl Function { self.push_insn_id(block, insn_id); continue; } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); - if let Some(expected) = guard_equal_to { - self_val = self.push_insn(block, Insn::GuardBitEquals { val: self_val, expected, state }); + if let Some(profiled_type) = profiled_type { + self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); } let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, cd, cme, iseq, args, state }); self.make_equal_to(insn_id, send_direct); @@ -1611,17 +1617,12 @@ impl Function { let method_id = unsafe { rb_vm_ci_mid(call_info) }; // If we have info about the class of the receiver - // - // TODO(alan): there was a seemingly a miscomp here if you swap with - // `inexact_ruby_class`. Theoretically it can call a method too general - // for the receiver. Confirm and add a test. - let (recv_class, guard_type) = if let Some(klass) = self_type.runtime_exact_ruby_class() { - (klass, None) + let (recv_class, profiled_type) = if let Some(class) = self_type.runtime_exact_ruby_class() { + (class, None) } else { let iseq_insn_idx = fun.frame_state(state).insn_idx; let Some(recv_type) = fun.profiled_type_of_at(self_val, iseq_insn_idx) else { return Err(()) }; - let Some(recv_class) = recv_type.runtime_exact_ruby_class() else { return Err(()) }; - (recv_class, Some(recv_type.unspecialized())) + (recv_type.class(), Some(recv_type)) }; // Do method lookup @@ -1661,9 +1662,9 @@ impl Function { if ci_flags & VM_CALL_ARGS_SIMPLE != 0 { // Commit to the replacement. Put PatchPoint. fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass: recv_class, method: method_id, cme: method }, state }); - if let Some(guard_type) = guard_type { + if let Some(profiled_type) = profiled_type { // Guard receiver class - self_val = fun.push_insn(block, Insn::GuardType { val: self_val, guard_type, state }); + self_val = fun.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); } let cfun = unsafe { get_mct_func(cfunc) }.cast(); let mut cfunc_args = vec![self_val]; @@ -2506,7 +2507,7 @@ struct ProfileOracle { /// instruction index. At a given ISEQ instruction, the interpreter has profiled the stack /// operands to a given ISEQ instruction, and this list of pairs of (InsnId, Type) map that /// profiling information into HIR instructions. - types: HashMap>, + types: HashMap>, } impl ProfileOracle { @@ -2521,9 +2522,9 @@ impl ProfileOracle { let entry = self.types.entry(iseq_insn_idx).or_insert_with(|| vec![]); // operand_types is always going to be <= stack size (otherwise it would have an underflow // at run-time) so use that to drive iteration. - for (idx, &insn_type) in operand_types.iter().rev().enumerate() { + for (idx, insn_type_distribution) in operand_types.iter().rev().enumerate() { let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling"); - entry.push((insn, insn_type)) + entry.push((insn, TypeDistributionSummary::new(insn_type_distribution))) } } } @@ -5548,8 +5549,8 @@ mod opt_tests { fn test@:5: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } @@ -5588,8 +5589,8 @@ mod opt_tests { fn test@:6: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } @@ -5607,8 +5608,8 @@ mod opt_tests { bb0(v0:BasicObject): v2:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) - v7:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1040), v2 + v7:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1038), v2 Return v8 "#]]); } @@ -5629,8 +5630,8 @@ mod opt_tests { v2:Fixnum[1] = Const Value(1) v3:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1040), v2, v3 + v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038), v2, v3 Return v9 "#]]); } @@ -5652,11 +5653,11 @@ mod opt_tests { fn test@:7: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1040) - PatchPoint MethodRedefined(Object@0x1000, bar@0x1048, cme:0x1050) - v11:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1040) + v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038) + PatchPoint MethodRedefined(Object@0x1000, bar@0x1040, cme:0x1048) + v11:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1038) Return v12 "#]]); } @@ -6438,6 +6439,31 @@ mod opt_tests { "#]]); } + #[test] + fn test_send_direct_to_instance_method() { + eval(" + class C + def foo + 3 + end + end + + def test(c) = c.foo + c = C.new + test c + test c + "); + + assert_optimized_method_hir("test", expect![[r#" + fn test@:8: + bb0(v0:BasicObject, v1:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) + v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v8:BasicObject = SendWithoutBlockDirect v7, :foo (0x1038) + Return v8 + "#]]); + } + #[test] fn dont_specialize_call_to_iseq_with_opt() { eval(" @@ -7385,8 +7411,8 @@ mod opt_tests { fn test@:3: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[VALUE(0x1038)] = GuardBitEquals v0, VALUE(0x1038) - v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1040) + v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); } diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 9ad0bdc649..84679c419d 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -1,12 +1,13 @@ #![allow(non_upper_case_globals)] use crate::cruby::{Qfalse, Qnil, Qtrue, VALUE, RUBY_T_ARRAY, RUBY_T_STRING, RUBY_T_HASH, RUBY_T_CLASS, RUBY_T_MODULE}; -use crate::cruby::{rb_cInteger, rb_cFloat, rb_cArray, rb_cHash, rb_cString, rb_cSymbol, rb_cObject, rb_cTrueClass, rb_cFalseClass, rb_cNilClass, rb_cRange, rb_cSet, rb_cRegexp, rb_cClass, rb_cModule}; +use crate::cruby::{rb_cInteger, rb_cFloat, rb_cArray, rb_cHash, rb_cString, rb_cSymbol, rb_cObject, rb_cTrueClass, rb_cFalseClass, rb_cNilClass, rb_cRange, rb_cSet, rb_cRegexp, rb_cClass, rb_cModule, rb_zjit_singleton_class_p}; use crate::cruby::ClassRelationship; use crate::cruby::get_class_name; use crate::cruby::ruby_sym_to_rust_string; use crate::cruby::rb_mRubyVMFrozenCore; use crate::cruby::rb_obj_class; use crate::hir::PtrPrintMap; +use crate::profile::ProfiledType; #[derive(Copy, Clone, Debug, PartialEq)] /// Specialization of the type. If we know additional information about the object, we put it here. @@ -74,8 +75,14 @@ fn write_spec(f: &mut std::fmt::Formatter, printer: &TypePrinter) -> std::fmt::R Specialization::Object(val) if val == unsafe { rb_mRubyVMFrozenCore } => write!(f, "[VMFrozenCore]"), Specialization::Object(val) if ty.is_subtype(types::Symbol) => write!(f, "[:{}]", ruby_sym_to_rust_string(val)), Specialization::Object(val) => write!(f, "[{}]", val.print(printer.ptr_map)), + // TODO(max): Ensure singleton classes never have Type specialization + Specialization::Type(val) if unsafe { rb_zjit_singleton_class_p(val) } => + write!(f, "[class*:{}@{}]", get_class_name(val), val.print(printer.ptr_map)), Specialization::Type(val) => write!(f, "[class:{}]", get_class_name(val)), - Specialization::TypeExact(val) => write!(f, "[class_exact:{}]", get_class_name(val)), + Specialization::TypeExact(val) if unsafe { rb_zjit_singleton_class_p(val) } => + write!(f, "[class_exact*:{}@{}]", get_class_name(val), val.print(printer.ptr_map)), + Specialization::TypeExact(val) => + write!(f, "[class_exact:{}]", get_class_name(val)), Specialization::Int(val) if ty.is_subtype(types::CBool) => write!(f, "[{}]", val != 0), Specialization::Int(val) if ty.is_subtype(types::CInt8) => write!(f, "[{}]", (val as i64) >> 56), Specialization::Int(val) if ty.is_subtype(types::CInt16) => write!(f, "[{}]", (val as i64) >> 48), @@ -231,6 +238,20 @@ impl Type { } } + pub fn from_profiled_type(val: ProfiledType) -> Type { + if val.is_fixnum() { types::Fixnum } + else if val.is_flonum() { types::Flonum } + else if val.is_static_symbol() { types::StaticSymbol } + else if val.is_nil() { types::NilClass } + else if val.is_true() { types::TrueClass } + else if val.is_false() { types::FalseClass } + else if val.class() == unsafe { rb_cString } { types::StringExact } + else { + // TODO(max): Add more cases for inferring type bits from built-in types + Type { bits: bits::BasicObject, spec: Specialization::TypeExact(val.class()) } + } + } + /// Private. Only for creating type globals. const fn from_bits(bits: u64) -> Type { Type { @@ -274,12 +295,6 @@ impl Type { self.is_subtype(types::NilClass) || self.is_subtype(types::FalseClass) } - /// Top self is the Ruby global object, where top-level method definitions go. Return true if - /// this Type has a Ruby object specialization that is the top-level self. - pub fn is_top_self(&self) -> bool { - self.ruby_object() == Some(unsafe { crate::cruby::rb_vm_top_self() }) - } - /// Return the object specialization, if any. pub fn ruby_object(&self) -> Option { match self.spec { diff --git a/zjit/src/lib.rs b/zjit/src/lib.rs index d5ca2b74ba..b36bf6515e 100644 --- a/zjit/src/lib.rs +++ b/zjit/src/lib.rs @@ -6,6 +6,7 @@ pub use std; mod state; +mod distribution; mod cruby; mod cruby_methods; mod hir; diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 7db8e44c7a..a99229604b 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -1,7 +1,8 @@ // We use the YARV bytecode constants which have a CRuby-style name #![allow(non_upper_case_globals)] -use crate::{cruby::*, gc::get_or_create_iseq_payload, hir_type::{types::{Empty}, Type}, options::get_option}; +use crate::{cruby::*, gc::get_or_create_iseq_payload, options::get_option}; +use crate::distribution::{Distribution, DistributionSummary}; /// Ephemeral state for profiling runtime information struct Profiler { @@ -79,25 +80,100 @@ fn profile_insn(profiler: &mut Profiler, bare_opcode: ruby_vminsn_type) { } } +const DISTRIBUTION_SIZE: usize = 4; + +pub type TypeDistribution = Distribution; + +pub type TypeDistributionSummary = DistributionSummary; + /// Profile the Type of top-`n` stack operands fn profile_operands(profiler: &mut Profiler, profile: &mut IseqProfile, n: usize) { let types = &mut profile.opnd_types[profiler.insn_idx]; - if types.len() <= n { - types.resize(n, Empty); + if types.is_empty() { + types.resize(n, TypeDistribution::new()); } for i in 0..n { - let opnd_type = Type::from_value(profiler.peek_at_stack((n - i - 1) as isize)); - types[i] = types[i].union(opnd_type); - if let Some(object) = types[i].gc_object() { - unsafe { rb_gc_writebarrier(profiler.iseq.into(), object) }; - } + let obj = profiler.peek_at_stack((n - i - 1) as isize); + // TODO(max): Handle GC-hidden classes like Array, Hash, etc and make them look normal or + // drop them or something + let ty = ProfiledType::new(obj.class_of(), obj.shape_id_of()); + unsafe { rb_gc_writebarrier(profiler.iseq.into(), ty.class()) }; + types[i].observe(ty); + } +} + +/// opt_send_without_block/opt_plus/... should store: +/// * the class of the receiver, so we can do method lookup +/// * the shape of the receiver, so we can optimize ivar lookup +/// with those two, pieces of information, we can also determine when an object is an immediate: +/// * Integer + SPECIAL_CONST_SHAPE_ID == Fixnum +/// * Float + SPECIAL_CONST_SHAPE_ID == Flonum +/// * Symbol + SPECIAL_CONST_SHAPE_ID == StaticSymbol +/// * NilClass == Nil +/// * TrueClass == True +/// * FalseClass == False +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProfiledType { + class: VALUE, + shape: ShapeId, +} + +impl Default for ProfiledType { + fn default() -> Self { + Self::empty() + } +} + +impl ProfiledType { + fn new(class: VALUE, shape: ShapeId) -> Self { + Self { class, shape } + } + + pub fn empty() -> Self { + Self { class: VALUE(0), shape: INVALID_SHAPE_ID } + } + + pub fn is_empty(&self) -> bool { + self.class == VALUE(0) + } + + pub fn class(&self) -> VALUE { + self.class + } + + pub fn shape(&self) -> ShapeId { + self.shape + } + + pub fn is_fixnum(&self) -> bool { + self.class == unsafe { rb_cInteger } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_flonum(&self) -> bool { + self.class == unsafe { rb_cFloat } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_static_symbol(&self) -> bool { + self.class == unsafe { rb_cSymbol } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_nil(&self) -> bool { + self.class == unsafe { rb_cNilClass } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_true(&self) -> bool { + self.class == unsafe { rb_cTrueClass } && self.shape == SPECIAL_CONST_SHAPE_ID + } + + pub fn is_false(&self) -> bool { + self.class == unsafe { rb_cFalseClass } && self.shape == SPECIAL_CONST_SHAPE_ID } } #[derive(Debug)] pub struct IseqProfile { /// Type information of YARV instruction operands, indexed by the instruction index - opnd_types: Vec>, + opnd_types: Vec>, /// Number of profiled executions for each YARV instruction, indexed by the instruction index num_profiles: Vec, @@ -112,16 +188,17 @@ impl IseqProfile { } /// Get profiled operand types for a given instruction index - pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[Type]> { + pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[TypeDistribution]> { self.opnd_types.get(insn_idx).map(|v| &**v) } /// Run a given callback with every object in IseqProfile pub fn each_object(&self, callback: impl Fn(VALUE)) { - for types in &self.opnd_types { - for opnd_type in types { - if let Some(object) = opnd_type.gc_object() { - callback(object); + for operands in &self.opnd_types { + for distribution in operands { + for profiled_type in distribution.each_item() { + // If the type is a GC object, call the callback + callback(profiled_type.class); } } } @@ -129,10 +206,11 @@ impl IseqProfile { /// Run a given callback with a mutable reference to every object in IseqProfile pub fn each_object_mut(&mut self, callback: impl Fn(&mut VALUE)) { - for types in self.opnd_types.iter_mut() { - for opnd_type in types.iter_mut() { - if let Some(object) = opnd_type.gc_object_mut() { - callback(object); + for operands in &mut self.opnd_types { + for distribution in operands { + for ref mut profiled_type in distribution.each_item_mut() { + // If the type is a GC object, call the callback + callback(&mut profiled_type.class); } } } From 19336a639252ea75204eda040b9e916d1c7188bf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:26:39 +0900 Subject: [PATCH 028/157] [rubygems/rubygems] Removed compatibility.rb that file is for Ruby 1.9. https://github.com/rubygems/rubygems/commit/120c174e7f --- lib/rubygems.rb | 3 --- lib/rubygems/compatibility.rb | 41 ----------------------------------- 2 files changed, 44 deletions(-) delete mode 100644 lib/rubygems/compatibility.rb diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 0c40f8482f..d4e88579e8 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -12,9 +12,6 @@ module Gem VERSION = "3.8.0.dev" end -# Must be first since it unloads the prelude from 1.9.2 -require_relative "rubygems/compatibility" - require_relative "rubygems/defaults" require_relative "rubygems/deprecate" require_relative "rubygems/errors" diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb deleted file mode 100644 index 0d9df56f8a..0000000000 --- a/lib/rubygems/compatibility.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -#-- -# This file contains all sorts of little compatibility hacks that we've -# had to introduce over the years. Quarantining them into one file helps -# us know when we can get rid of them. -# -# Ruby 1.9.x has introduced some things that are awkward, and we need to -# support them, so we define some constants to use later. -# -# TODO remove at RubyGems 4 -#++ - -module Gem - # :stopdoc: - - RubyGemsVersion = VERSION - deprecate_constant(:RubyGemsVersion) - - RbConfigPriorities = %w[ - MAJOR - MINOR - TEENY - EXEEXT RUBY_SO_NAME arch bindir datadir libdir ruby_install_name - ruby_version rubylibprefix sitedir sitelibdir vendordir vendorlibdir - rubylibdir - ].freeze - - if defined?(ConfigMap) - RbConfigPriorities.each do |key| - ConfigMap[key.to_sym] = RbConfig::CONFIG[key] - end - else - ## - # Configuration settings from ::RbConfig - ConfigMap = Hash.new do |cm, key| - cm[key] = RbConfig::CONFIG[key.to_s] - end - deprecate_constant(:ConfigMap) - end -end From 51f88f9922ef0a8c670eb541de726ae0d11a1706 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:39:18 +0900 Subject: [PATCH 029/157] [rubygems/rubygems] Added ability for changing deprecated version from only next major https://github.com/rubygems/rubygems/commit/15177de84e --- lib/rubygems/deprecate.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 7d24f9cbfc..e9acb5a02b 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -126,7 +126,7 @@ module Gem # telling the user of +repl+ (unless +repl+ is :none) and the # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none) + def rubygems_deprecate(name, replacement=:none, version=Gem::Deprecate.next_rubygems_major_version) class_eval do old = "_deprecated_#{name}" alias_method old, name @@ -136,7 +136,7 @@ module Gem msg = [ "NOTE: #{target}#{name} is deprecated", replacement == :none ? " with no replacement" : "; use #{replacement} instead", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", + ". It will be removed in Rubygems #{version}", "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", ] warn "#{msg.join}." unless Gem::Deprecate.skip From 052b38a5d9ca4d0ab2ad872bf896fe256a0186f5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 18:45:25 +0900 Subject: [PATCH 030/157] [rubygems/rubygems] Deprecate Gem::Specification#datadir and will remove it at RG 4.1 https://github.com/rubygems/rubygems/commit/e99cdab171 --- lib/rubygems/basic_specification.rb | 3 +++ test/rubygems/test_config.rb | 7 ------- test/rubygems/test_gem.rb | 29 ----------------------------- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index a0b552f63c..591b555725 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -199,6 +199,9 @@ class Gem::BasicSpecification File.expand_path(File.join(gems_dir, full_name, "data", name)) end + extend Gem::Deprecate + rubygems_deprecate :datadir, :none, "4.1" + ## # Full path of the target library file. # If the file is not in this gem, return nil. diff --git a/test/rubygems/test_config.rb b/test/rubygems/test_config.rb index 657624d526..822b57b0dc 100644 --- a/test/rubygems/test_config.rb +++ b/test/rubygems/test_config.rb @@ -5,13 +5,6 @@ require "rubygems" require "shellwords" class TestGemConfig < Gem::TestCase - def test_datadir - util_make_gems - spec = Gem::Specification.find_by_name("a") - spec.activate - assert_equal "#{spec.full_gem_path}/data/a", spec.datadir - end - def test_good_rake_path_is_escaped path = Gem::TestCase.class_variable_get(:@@good_rake) ruby, rake = path.shellsplit diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index cdc3479e37..49e81fcedb 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -527,35 +527,6 @@ class TestGem < Gem::TestCase assert_equal expected, Gem.configuration end - def test_self_datadir - foo = nil - - Dir.chdir @tempdir do - FileUtils.mkdir_p "data" - File.open File.join("data", "foo.txt"), "w" do |fp| - fp.puts "blah" - end - - foo = util_spec "foo" do |s| - s.files = %w[data/foo.txt] - end - - install_gem foo - end - - gem "foo" - - expected = File.join @gemhome, "gems", foo.full_name, "data", "foo" - - assert_equal expected, Gem::Specification.find_by_name("foo").datadir - end - - def test_self_datadir_nonexistent_package - assert_raise(Gem::MissingSpecError) do - Gem::Specification.find_by_name("xyzzy").datadir - end - end - def test_self_default_exec_format ruby_install_name "ruby" do assert_equal "%s", Gem.default_exec_format From 4d26ccd2afaf33a813464d1abe4cf518950b2f2e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 5 Aug 2025 20:20:49 +0900 Subject: [PATCH 031/157] [rubygems/rubygems] Allow to use Gem::Deprecate#rubygems_deprecate and rubygems_deprecate_command without rubygems.rb https://github.com/rubygems/rubygems/commit/4925403686 --- lib/rubygems/deprecate.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index e9acb5a02b..a20649cbda 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -126,13 +126,14 @@ module Gem # telling the user of +repl+ (unless +repl+ is :none) and the # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none, version=Gem::Deprecate.next_rubygems_major_version) + def rubygems_deprecate(name, replacement=:none, version=nil) class_eval do old = "_deprecated_#{name}" alias_method old, name define_method name do |*args, &block| klass = is_a? Module target = klass ? "#{self}." : "#{self.class}#" + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "NOTE: #{target}#{name} is deprecated", replacement == :none ? " with no replacement" : "; use #{replacement} instead", @@ -147,13 +148,14 @@ module Gem end # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) + def rubygems_deprecate_command(version = nil) class_eval do define_method "deprecated?" do true end define_method "deprecation_warning" do + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "#{command} command is deprecated", ". It will be removed in Rubygems #{version}.\n", From 9c0ebff2cded1b60d5d7c922d7cf8dbaa54ecfe2 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 6 Aug 2025 01:00:04 +0100 Subject: [PATCH 032/157] ZJIT: Avoid matching built-in iseq's HIR line numbers in tests (#14124) ZJIT: Avoid matching built-in ISEQs' HIR line numbers in tests Co-authored-by: Author: Takashi Kokubun --- zjit/src/hir.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 203be0661e..635120eb80 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2242,6 +2242,12 @@ impl<'a> std::fmt::Display for FunctionPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let fun = &self.fun; let iseq_name = iseq_get_location(fun.iseq, 0); + // In tests, strip the line number for builtin ISEQs to make tests stable across line changes + let iseq_name = if cfg!(test) && iseq_name.contains("@:197: + fn Float@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject): v6:Flonum = InvokeBuiltin rb_f_float, v0, v1, v2 Jump bb1(v0, v1, v2, v3, v6) @@ -5015,7 +5021,7 @@ mod tests { #[test] fn test_invokebuiltin_cexpr_annotated() { assert_method_hir_with_opcode("class", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#" - fn class@:20: + fn class@: bb0(v0:BasicObject): v3:Class = InvokeBuiltin _bi20, v0 Jump bb1(v0, v3) @@ -5031,7 +5037,7 @@ mod tests { assert!(iseq_contains_opcode(iseq, YARVINSN_opt_invokebuiltin_delegate), "iseq Dir.open does not contain invokebuiltin"); let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, expect![[r#" - fn open@:184: + fn open@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject): v5:NilClass = Const Value(nil) v8:BasicObject = InvokeBuiltin dir_s_open, v0, v1, v2 @@ -5045,7 +5051,7 @@ mod tests { assert!(iseq_contains_opcode(iseq, YARVINSN_opt_invokebuiltin_delegate_leave), "iseq GC.enable does not contain invokebuiltin"); let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, expect![[r#" - fn enable@:55: + fn enable@: bb0(v0:BasicObject): v3:BasicObject = InvokeBuiltin gc_enable, v0 Jump bb1(v0, v3) @@ -5060,7 +5066,7 @@ mod tests { assert!(iseq_contains_opcode(iseq, YARVINSN_invokebuiltin), "iseq GC.start does not contain invokebuiltin"); let function = iseq_to_hir(iseq).unwrap(); assert_function_hir(function, expect![[r#" - fn start@:36: + fn start@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject): v6:FalseClass = Const Value(false) v8:BasicObject = InvokeBuiltin gc_start_internal, v0, v1, v2, v3, v6 From 8691a4ada14de39a607f96ea128184da40168b90 Mon Sep 17 00:00:00 2001 From: Frank Olbricht Date: Sat, 1 Jun 2024 10:57:06 +0200 Subject: [PATCH 033/157] [rubygems/rubygems] Use IMDSv2 for S3 instance credentials https://github.com/rubygems/rubygems/commit/fa1c51ef59 --- lib/rubygems/s3_uri_signer.rb | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index 0d8e9e8285..bdd272a77a 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -146,18 +146,20 @@ class Gem::S3URISigner require_relative "request" require_relative "request/connection_pools" require "json" - - iam_info = ec2_metadata_request(EC2_IAM_INFO) + token = ec2_metadata_token + iam_info = ec2_metadata_request(EC2_IAM_INFO, token) # Expected format: arn:aws:iam:::instance-profile/ role_name = iam_info["InstanceProfileArn"].split("/").last - ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name) + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token) end - def ec2_metadata_request(url) + def ec2_metadata_request(url, token) uri = Gem::URI(url) @request_pool ||= create_request_pool(uri) request = Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, @request_pool) - response = request.fetch + response = request.fetch do |req| + req.add_field "X-aws-ec2-metadata-token", token + end case response when Gem::Net::HTTPOK then @@ -167,6 +169,22 @@ class Gem::S3URISigner end end + def ec2_metadata_token + uri = Gem::URI(EC2_IAM_TOKEN) + @request_pool ||= create_request_pool(uri) + request = Gem::Request.new(uri, Gem::Net::HTTP::Put, nil, @request_pool) + response = request.fetch do |req| + req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60 + end + + case response + when Gem::Net::HTTPOK then + response.body + else + raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}") + end + end + def create_request_pool(uri) proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme)) certs = Gem::Request.get_cert_files @@ -174,6 +192,7 @@ class Gem::S3URISigner end BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze + EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token" EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info" EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" end From 720ae3285e26ba09b173b9f9fe0fab47fd508ff5 Mon Sep 17 00:00:00 2001 From: pjsk Date: Tue, 27 May 2025 17:42:03 -0700 Subject: [PATCH 034/157] [rubygems/rubygems] make things a bit more testable https://github.com/rubygems/rubygems/commit/29c085f5f5 --- lib/rubygems/s3_uri_signer.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index bdd272a77a..41c25bc77e 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -147,6 +147,7 @@ class Gem::S3URISigner require_relative "request/connection_pools" require "json" token = ec2_metadata_token + iam_info = ec2_metadata_request(EC2_IAM_INFO, token) # Expected format: arn:aws:iam:::instance-profile/ role_name = iam_info["InstanceProfileArn"].split("/").last @@ -154,9 +155,8 @@ class Gem::S3URISigner end def ec2_metadata_request(url, token) - uri = Gem::URI(url) - @request_pool ||= create_request_pool(uri) - request = Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, @request_pool) + request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get) + response = request.fetch do |req| req.add_field "X-aws-ec2-metadata-token", token end @@ -170,9 +170,8 @@ class Gem::S3URISigner end def ec2_metadata_token - uri = Gem::URI(EC2_IAM_TOKEN) - @request_pool ||= create_request_pool(uri) - request = Gem::Request.new(uri, Gem::Net::HTTP::Put, nil, @request_pool) + request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put) + response = request.fetch do |req| req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60 end @@ -185,6 +184,14 @@ class Gem::S3URISigner end end + def ec2_iam_request(uri, verb) + @request_pool ||= {} + @request_pool[uri] ||= create_request_pool(uri) + pool = @request_pool[uri] + + Gem::Request.new(uri, verb, nil, pool) + end + def create_request_pool(uri) proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme)) certs = Gem::Request.get_cert_files From 23b34517bd8f62e50e2e5f7f22039a713aa32cc8 Mon Sep 17 00:00:00 2001 From: pjsk Date: Tue, 27 May 2025 17:43:37 -0700 Subject: [PATCH 035/157] [rubygems/rubygems] Surgery on test code to make fallback to imdsv1 easier to test https://github.com/rubygems/rubygems/commit/5b4eece722 --- test/rubygems/test_gem_remote_fetcher_s3.rb | 274 ++++++++++++++++---- 1 file changed, 227 insertions(+), 47 deletions(-) diff --git a/test/rubygems/test_gem_remote_fetcher_s3.rb b/test/rubygems/test_gem_remote_fetcher_s3.rb index e3aaa7a691..6c6f847b09 100644 --- a/test/rubygems/test_gem_remote_fetcher_s3.rb +++ b/test/rubygems/test_gem_remote_fetcher_s3.rb @@ -8,6 +8,103 @@ require "rubygems/package" class TestGemRemoteFetcherS3 < Gem::TestCase include Gem::DefaultUserInteraction + class FakeGemRequest < Gem::Request + + attr_reader :last_request, :uri + + # Override perform_request to stub things + def perform_request(request) + @last_request = request + @response + end + + def set_response(response) + @response = response + end + end + + class FakeS3URISigner < Gem::S3URISigner + # Convenience method to output the recent aws iam queries made in tests + # this outputs the verb, path, and any non-generic headers + def recent_aws_query_logs + sreqs = @aws_iam_calls.map do |c| + r = c.last_request + s = +"#{r.method} #{c.uri}\n" + r.each_header do |key, v| + # Only include headers that start with x- + next unless key.start_with?("x-") + s << " #{key}=#{v}\n" + end + s + end + + sreqs.join("") + end + + def initialize(uri, method) + @aws_iam_calls = [] + super + end + + def ec2_iam_request(uri, verb) + fake_s3_request = FakeGemRequest.new(uri, verb, nil, nil) + @aws_iam_calls << fake_s3_request + + case uri.to_s + when "http://169.254.169.254/latest/api/token" + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body + "mysecrettoken" + end + fake_s3_request.set_response(res) + + when "http://169.254.169.254/latest/meta-data/iam/info" + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body + <<~JSON + { + "Code": "Success", + "LastUpdated": "2023-05-27:05:05", + "InstanceProfileArn": "arn:aws:iam::somesecretid:instance-profile/TestRole", + "InstanceProfileId": "SOMEPROFILEID" + } + JSON + end + fake_s3_request.set_response(res) + + when "http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole" + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body + $instance_profile + end + fake_s3_request.set_response(res) + + else + raise "Unexpected request to #{uri}" + end + + fake_s3_request + end + end + + class FakeGemFetcher < Gem::RemoteFetcher + + attr_reader :fetched_uri, :last_s3_uri_signer + + def request(uri, request_class, last_modified = nil) + @fetched_uri = uri + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body + "success" + end + res + end + + def s3_uri_signer(uri, method) + @last_s3_uri_signer = FakeS3URISigner.new(uri, method) + end + end + def setup super @@ -18,43 +115,56 @@ class TestGemRemoteFetcherS3 < Gem::TestCase @a1.loaded_from = File.join(@gemhome, "specifications", @a1.full_name) end - def assert_fetch_s3(url, signature, token=nil, region="us-east-1", instance_profile_json=nil, method="GET") - fetcher = Gem::RemoteFetcher.new nil - @fetcher = fetcher - $fetched_uri = nil + def assert_fetched_s3_with_imds_v2 + # Three API requests: + # 1. Get the token + # 2. Lookup profile details + # 3. Query the credentials + expected = <<~TEXT + PUT http://169.254.169.254/latest/api/token + x-aws-ec2-metadata-token-ttl-seconds=60 + GET http://169.254.169.254/latest/meta-data/iam/info + x-aws-ec2-metadata-token=mysecrettoken + GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole + x-aws-ec2-metadata-token=mysecrettoken + TEXT + recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs + assert_equal(expected.strip, recent_aws_query_logs.strip) + end + + def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, method: "GET") + @fetcher = FakeGemFetcher.new nil $instance_profile = instance_profile_json - - def fetcher.request(uri, request_class, last_modified = nil) - $fetched_uri = uri - res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body - "success" - end - res - end - - def fetcher.s3_uri_signer(uri, method) - require "json" - s3_uri_signer = Gem::S3URISigner.new(uri, method) - def s3_uri_signer.ec2_metadata_credentials_json - JSON.parse($instance_profile) - end - # Running sign operation to make sure uri.query is not mutated - s3_uri_signer.sign - raise "URI query is not empty: #{uri.query}" unless uri.query.nil? - s3_uri_signer - end - res = fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") - assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", $fetched_uri.to_s + assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s if method == "HEAD" assert_equal 200, res.code else assert_equal "success", res end + + # Validation for EC2 IAM signing + if Gem.configuration[:s3_source]&.dig("my-bucket", :provider) == "instance_profile" + # Three API requests: + # 1. Get the token + # 2. Lookup profile details + # 3. Query the credentials + expected = <<~TEXT + PUT http://169.254.169.254/latest/api/token + x-aws-ec2-metadata-token-ttl-seconds=60 + GET http://169.254.169.254/latest/meta-data/iam/info + x-aws-ec2-metadata-token=mysecrettoken + GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole + x-aws-ec2-metadata-token=mysecrettoken + TEXT + recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs + assert_equal(expected.strip, recent_aws_query_logs.strip) + else + assert_equal("", @fetcher.last_s3_uri_signer.recent_aws_query_logs) + end ensure - $fetched_uri = nil + $instance_profile = nil end def test_fetch_s3_config_creds @@ -63,7 +173,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + ) end ensure Gem.configuration[:s3_source] = nil @@ -79,7 +192,15 @@ class TestGemRemoteFetcherS3 < Gem::TestCase region = "us-east-1" instance_profile_json = nil method = "HEAD" - assert_fetch_s3 url, "a3c6cf9a2db62e85f4e57f8fc8ac8b5ff5c1fdd4aeef55935d05e05174d9c885", token, region, instance_profile_json, method + + assert_fetch_s3( + url: url, + signature: "a3c6cf9a2db62e85f4e57f8fc8ac8b5ff5c1fdd4aeef55935d05e05174d9c885", + token: token, + region: region, + instance_profile_json: instance_profile_json, + method: method + ) end ensure Gem.configuration[:s3_source] = nil @@ -91,7 +212,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", nil, "us-west-2" + assert_fetch_s3( + url: url, + signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + region: "us-west-2" + ) end ensure Gem.configuration[:s3_source] = nil @@ -103,7 +228,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", "testtoken" + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken" + ) end ensure Gem.configuration[:s3_source] = nil @@ -118,7 +247,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -134,7 +266,12 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", nil, "us-west-2" + assert_fetch_s3( + url: url, + signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + token: nil, + region: "us-west-2" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -150,7 +287,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", "testtoken" + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -160,7 +301,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase def test_fetch_s3_url_creds url = "s3://testuser:testpass@my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + ) end end @@ -171,8 +315,13 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", nil, "us-east-1", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + ) + assert_fetched_s3_with_imds_v2 end ensure Gem.configuration[:s3_source] = nil @@ -185,8 +334,13 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", nil, "us-west-2", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + assert_fetch_s3( + url: url, + signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + region: "us-west-2", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + ) + assert_fetched_s3_with_imds_v2 end ensure Gem.configuration[:s3_source] = nil @@ -199,14 +353,40 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", "testtoken", "us-east-1", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken", + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + ) + assert_fetched_s3_with_imds_v2 end ensure Gem.configuration[:s3_source] = nil end - def refute_fetch_s3(url, expected_message) + def test_fetch_s3_instance_profile_creds_with_fallback + Gem.configuration[:s3_source] = { + "my-bucket" => { provider: "instance_profile" }, + } + + url = "s3://my-bucket/gems/specs.4.8.gz" + Time.stub :now, Time.at(1_561_353_581) do + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken", + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + ) + assert_fetched_s3_with_imds_v2 + end + ensure + Gem.configuration[:s3_source] = nil + end + + def refute_fetch_s3(url:, expected_message:) fetcher = Gem::RemoteFetcher.new nil @fetcher = fetcher @@ -219,7 +399,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase def test_fetch_s3_no_source_key url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "no s3_source key exists in .gemrc" + refute_fetch_s3(url: url, expected_message: "no s3_source key exists in .gemrc") end def test_fetch_s3_no_host @@ -228,7 +408,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://other-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "no key for host other-bucket in s3_source in .gemrc" + refute_fetch_s3(url: url, expected_message: "no key for host other-bucket in s3_source in .gemrc") ensure Gem.configuration[:s3_source] = nil end @@ -237,7 +417,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Gem.configuration[:s3_source] = { "my-bucket" => { secret: "testpass" } } url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "s3_source for my-bucket missing id or secret" + refute_fetch_s3(url: url, expected_message: "s3_source for my-bucket missing id or secret") ensure Gem.configuration[:s3_source] = nil end @@ -246,7 +426,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Gem.configuration[:s3_source] = { "my-bucket" => { id: "testuser" } } url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "s3_source for my-bucket missing id or secret" + refute_fetch_s3(url: url, expected_message: "s3_source for my-bucket missing id or secret") ensure Gem.configuration[:s3_source] = nil end From 01ae9e4fb095db753291d65ae6d56411d17386a7 Mon Sep 17 00:00:00 2001 From: pjsk Date: Wed, 28 May 2025 16:56:54 -0700 Subject: [PATCH 036/157] [rubygems/rubygems] implement fallback https://github.com/rubygems/rubygems/commit/e09a6ec815 --- lib/rubygems/s3_uri_signer.rb | 45 +++++++++---- test/rubygems/test_gem_remote_fetcher_s3.rb | 72 +++++++++++++-------- 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index 41c25bc77e..148cba38c4 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true require_relative "openssl" +require_relative "user_interaction" ## # S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems # More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html class Gem::S3URISigner + include Gem::UserInteraction + class ConfigurationError < Gem::Exception def initialize(message) super message @@ -146,19 +149,40 @@ class Gem::S3URISigner require_relative "request" require_relative "request/connection_pools" require "json" - token = ec2_metadata_token - iam_info = ec2_metadata_request(EC2_IAM_INFO, token) - # Expected format: arn:aws:iam:::instance-profile/ - role_name = iam_info["InstanceProfileArn"].split("/").last - ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token) + # First try V2 fallback to V1 + res = nil + begin + res = ec2_metadata_credentials_imds_v2 + rescue InstanceProfileError + alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1" + res = ec2_metadata_credentials_imds_v1 + end + res end - def ec2_metadata_request(url, token) + def ec2_metadata_credentials_imds_v2 + token = ec2_metadata_token + iam_info = ec2_metadata_request(EC2_IAM_INFO, token:) + # Expected format: arn:aws:iam:::instance-profile/ + role_name = iam_info["InstanceProfileArn"].split("/").last + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:) + end + + def ec2_metadata_credentials_imds_v1 + iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil) + # Expected format: arn:aws:iam:::instance-profile/ + role_name = iam_info["InstanceProfileArn"].split("/").last + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil) + end + + def ec2_metadata_request(url, token:) request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get) response = request.fetch do |req| - req.add_field "X-aws-ec2-metadata-token", token + if token + req.add_field "X-aws-ec2-metadata-token", token + end end case response @@ -185,11 +209,8 @@ class Gem::S3URISigner end def ec2_iam_request(uri, verb) - @request_pool ||= {} - @request_pool[uri] ||= create_request_pool(uri) - pool = @request_pool[uri] - - Gem::Request.new(uri, verb, nil, pool) + @request_pool ||= create_request_pool(uri) + Gem::Request.new(uri, verb, nil, @request_pool) end def create_request_pool(uri) diff --git a/test/rubygems/test_gem_remote_fetcher_s3.rb b/test/rubygems/test_gem_remote_fetcher_s3.rb index 6c6f847b09..664facd3de 100644 --- a/test/rubygems/test_gem_remote_fetcher_s3.rb +++ b/test/rubygems/test_gem_remote_fetcher_s3.rb @@ -9,7 +9,6 @@ class TestGemRemoteFetcherS3 < Gem::TestCase include Gem::DefaultUserInteraction class FakeGemRequest < Gem::Request - attr_reader :last_request, :uri # Override perform_request to stub things @@ -52,12 +51,13 @@ class TestGemRemoteFetcherS3 < Gem::TestCase case uri.to_s when "http://169.254.169.254/latest/api/token" - res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body - "mysecrettoken" + if $imdsv2_token_failure + res = Gem::Net::HTTPUnauthorized.new nil, 401, nil + def res.body = "you got a 401! panic!" + else + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body = "mysecrettoken" end - fake_s3_request.set_response(res) - when "http://169.254.169.254/latest/meta-data/iam/info" res = Gem::Net::HTTPOK.new nil, 200, nil def res.body @@ -70,33 +70,26 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } JSON end - fake_s3_request.set_response(res) when "http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole" res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body - $instance_profile - end - fake_s3_request.set_response(res) - + def res.body = $instance_profile else raise "Unexpected request to #{uri}" end + fake_s3_request.set_response(res) fake_s3_request end end class FakeGemFetcher < Gem::RemoteFetcher - attr_reader :fetched_uri, :last_s3_uri_signer def request(uri, request_class, last_modified = nil) @fetched_uri = uri res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body - "success" - end + def res.body = "success" res end @@ -132,10 +125,33 @@ class TestGemRemoteFetcherS3 < Gem::TestCase assert_equal(expected.strip, recent_aws_query_logs.strip) end - def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, method: "GET") - @fetcher = FakeGemFetcher.new nil + def assert_fetched_s3_with_imds_v1 + # Three API requests: + # 1. Get the token (which fails) + # 2. Lookup profile details without token + # 3. Query the credentials without token + expected = <<~TEXT + PUT http://169.254.169.254/latest/api/token + x-aws-ec2-metadata-token-ttl-seconds=60 + GET http://169.254.169.254/latest/meta-data/iam/info + GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole + TEXT + recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs + assert_equal(expected.strip, recent_aws_query_logs.strip) + end + + def with_imds_v2_failure + $imdsv2_token_failure = true + yield(fetcher) + ensure + $imdsv2_token_failure = nil + end + + def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, fetcher: nil, method: "GET") + @fetcher = fetcher || FakeGemFetcher.new(nil) $instance_profile = instance_profile_json - res = fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") + res = @fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") + $imdsv2_token_failure ||= nil assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s if method == "HEAD" @@ -373,14 +389,16 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3( - url: url, - signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", - token: "testtoken", - region: "us-east-1", - instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' - ) - assert_fetched_s3_with_imds_v2 + with_imds_v2_failure do + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken", + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + ) + assert_fetched_s3_with_imds_v1 + end end ensure Gem.configuration[:s3_source] = nil From 374f7dbcbbf797cb3e5a1460140981d811bc7c10 Mon Sep 17 00:00:00 2001 From: pjsk Date: Mon, 16 Jun 2025 17:08:13 -0700 Subject: [PATCH 037/157] [rubygems/rubygems] removed global variables https://github.com/rubygems/rubygems/commit/42c5947dbe --- test/rubygems/test_gem_remote_fetcher_s3.rb | 40 +++++++-------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/test/rubygems/test_gem_remote_fetcher_s3.rb b/test/rubygems/test_gem_remote_fetcher_s3.rb index 664facd3de..8c9d363f65 100644 --- a/test/rubygems/test_gem_remote_fetcher_s3.rb +++ b/test/rubygems/test_gem_remote_fetcher_s3.rb @@ -23,6 +23,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase end class FakeS3URISigner < Gem::S3URISigner + class << self + attr_accessor :should_fail, :instance_profile + end + # Convenience method to output the recent aws iam queries made in tests # this outputs the verb, path, and any non-generic headers def recent_aws_query_logs @@ -51,7 +55,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase case uri.to_s when "http://169.254.169.254/latest/api/token" - if $imdsv2_token_failure + if FakeS3URISigner.should_fail res = Gem::Net::HTTPUnauthorized.new nil, 401, nil def res.body = "you got a 401! panic!" else @@ -73,7 +77,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase when "http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole" res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body = $instance_profile + def res.body = FakeS3URISigner.instance_profile else raise "Unexpected request to #{uri}" end @@ -141,46 +145,26 @@ class TestGemRemoteFetcherS3 < Gem::TestCase end def with_imds_v2_failure - $imdsv2_token_failure = true + FakeS3URISigner.should_fail = true yield(fetcher) ensure - $imdsv2_token_failure = nil + FakeS3URISigner.should_fail = false end def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, fetcher: nil, method: "GET") + FakeS3URISigner.instance_profile = instance_profile_json + @fetcher = fetcher || FakeGemFetcher.new(nil) - $instance_profile = instance_profile_json res = @fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") - $imdsv2_token_failure ||= nil - + assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s if method == "HEAD" assert_equal 200, res.code else assert_equal "success", res end - - # Validation for EC2 IAM signing - if Gem.configuration[:s3_source]&.dig("my-bucket", :provider) == "instance_profile" - # Three API requests: - # 1. Get the token - # 2. Lookup profile details - # 3. Query the credentials - expected = <<~TEXT - PUT http://169.254.169.254/latest/api/token - x-aws-ec2-metadata-token-ttl-seconds=60 - GET http://169.254.169.254/latest/meta-data/iam/info - x-aws-ec2-metadata-token=mysecrettoken - GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole - x-aws-ec2-metadata-token=mysecrettoken - TEXT - recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs - assert_equal(expected.strip, recent_aws_query_logs.strip) - else - assert_equal("", @fetcher.last_s3_uri_signer.recent_aws_query_logs) - end ensure - $instance_profile = nil + FakeS3URISigner.instance_profile = nil end def test_fetch_s3_config_creds From fe3ed3e7f3486e9bb8b1583ecb4e41efc882e4d3 Mon Sep 17 00:00:00 2001 From: pjsk Date: Mon, 16 Jun 2025 18:21:47 -0700 Subject: [PATCH 038/157] [rubygems/rubygems] Update tests to respect token for where v2 and v1 are invoked https://github.com/rubygems/rubygems/commit/261315e399 --- test/rubygems/test_gem_remote_fetcher_s3.rb | 60 +++++++++++---------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/test/rubygems/test_gem_remote_fetcher_s3.rb b/test/rubygems/test_gem_remote_fetcher_s3.rb index 8c9d363f65..4a5acc5a86 100644 --- a/test/rubygems/test_gem_remote_fetcher_s3.rb +++ b/test/rubygems/test_gem_remote_fetcher_s3.rb @@ -24,7 +24,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase class FakeS3URISigner < Gem::S3URISigner class << self - attr_accessor :should_fail, :instance_profile + attr_accessor :return_token, :instance_profile end # Convenience method to output the recent aws iam queries made in tests @@ -55,12 +55,12 @@ class TestGemRemoteFetcherS3 < Gem::TestCase case uri.to_s when "http://169.254.169.254/latest/api/token" - if FakeS3URISigner.should_fail + if FakeS3URISigner.return_token.nil? res = Gem::Net::HTTPUnauthorized.new nil, 401, nil def res.body = "you got a 401! panic!" else res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body = "mysecrettoken" + def res.body = FakeS3URISigner.return_token end when "http://169.254.169.254/latest/meta-data/iam/info" res = Gem::Net::HTTPOK.new nil, 200, nil @@ -112,7 +112,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase @a1.loaded_from = File.join(@gemhome, "specifications", @a1.full_name) end - def assert_fetched_s3_with_imds_v2 + def assert_fetched_s3_with_imds_v2(expected_token) # Three API requests: # 1. Get the token # 2. Lookup profile details @@ -121,9 +121,9 @@ class TestGemRemoteFetcherS3 < Gem::TestCase PUT http://169.254.169.254/latest/api/token x-aws-ec2-metadata-token-ttl-seconds=60 GET http://169.254.169.254/latest/meta-data/iam/info - x-aws-ec2-metadata-token=mysecrettoken + x-aws-ec2-metadata-token=#{expected_token} GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole - x-aws-ec2-metadata-token=mysecrettoken + x-aws-ec2-metadata-token=#{expected_token} TEXT recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs assert_equal(expected.strip, recent_aws_query_logs.strip) @@ -153,10 +153,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, fetcher: nil, method: "GET") FakeS3URISigner.instance_profile = instance_profile_json - + FakeS3URISigner.return_token = token + @fetcher = fetcher || FakeGemFetcher.new(nil) res = @fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") - + assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s if method == "HEAD" assert_equal 200, res.code @@ -165,6 +166,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase end ensure FakeS3URISigner.instance_profile = nil + FakeS3URISigner.return_token = nil end def test_fetch_s3_config_creds @@ -175,7 +177,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Time.stub :now, Time.at(1_561_353_581) do assert_fetch_s3( url: url, - signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", ) end ensure @@ -195,9 +197,9 @@ class TestGemRemoteFetcherS3 < Gem::TestCase assert_fetch_s3( url: url, - signature: "a3c6cf9a2db62e85f4e57f8fc8ac8b5ff5c1fdd4aeef55935d05e05174d9c885", - token: token, - region: region, + signature: "a3c6cf9a2db62e85f4e57f8fc8ac8b5ff5c1fdd4aeef55935d05e05174d9c885", + token: token, + region: region, instance_profile_json: instance_profile_json, method: method ) @@ -317,11 +319,12 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Time.stub :now, Time.at(1_561_353_581) do assert_fetch_s3( url: url, - signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + signature: "da82e098bdaed0d3087047670efc98eaadc20559a473b5eac8d70190d2a9e8fd", region: "us-east-1", - instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + token: "mysecrettoken", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "mysecrettoken"}' ) - assert_fetched_s3_with_imds_v2 + assert_fetched_s3_with_imds_v2("mysecrettoken") end ensure Gem.configuration[:s3_source] = nil @@ -336,11 +339,12 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Time.stub :now, Time.at(1_561_353_581) do assert_fetch_s3( url: url, - signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + signature: "532960594dbfe31d1bbfc0e8e7a666c3cbdd8b00a143774da51b7f920704afd2", region: "us-west-2", - instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + token: "mysecrettoken", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "mysecrettoken"}' ) - assert_fetched_s3_with_imds_v2 + assert_fetched_s3_with_imds_v2("mysecrettoken") end ensure Gem.configuration[:s3_source] = nil @@ -360,7 +364,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase region: "us-east-1", instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' ) - assert_fetched_s3_with_imds_v2 + assert_fetched_s3_with_imds_v2("testtoken") end ensure Gem.configuration[:s3_source] = nil @@ -373,16 +377,14 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - with_imds_v2_failure do - assert_fetch_s3( - url: url, - signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", - token: "testtoken", - region: "us-east-1", - instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' - ) - assert_fetched_s3_with_imds_v1 - end + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + token: nil, + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + ) + assert_fetched_s3_with_imds_v1 end ensure Gem.configuration[:s3_source] = nil From e60e1952a4bed328983b15918da5354246bcf320 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 6 Aug 2025 03:52:59 +0100 Subject: [PATCH 039/157] ZJIT: Fix `Kernel#Float`'s annotation (#14123) As pointed out in https://github.com/ruby/ruby/pull/14078#discussion_r2255427676, the return type should be `Float` instead. --- zjit/src/cruby_methods.rs | 2 +- zjit/src/hir.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 8d1548f92b..c9ebcebc86 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -174,7 +174,7 @@ pub fn init() -> Annotations { annotate!(rb_cNilClass, "nil?", types::TrueClass, no_gc, leaf, elidable); annotate!(rb_mKernel, "nil?", types::FalseClass, no_gc, leaf, elidable); - annotate_builtin!(rb_mKernel, "Float", types::Flonum); + annotate_builtin!(rb_mKernel, "Float", types::Float); annotate_builtin!(rb_mKernel, "Integer", types::Integer); annotate_builtin!(rb_mKernel, "class", types::Class, leaf); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 635120eb80..1a67037ed3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5011,9 +5011,9 @@ mod tests { assert_method_hir_with_opcode("Float", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#" fn Float@: bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject): - v6:Flonum = InvokeBuiltin rb_f_float, v0, v1, v2 + v6:Float = InvokeBuiltin rb_f_float, v0, v1, v2 Jump bb1(v0, v1, v2, v3, v6) - bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject, v12:Flonum): + bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject, v12:Float): Return v12 "#]]); } From 92688f7d570c9c37ccb05b80577e1032aae908b7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 5 Aug 2025 13:43:25 +0200 Subject: [PATCH 040/157] variable.c: refactor accesses to the generic_fields_tbl All accesses to `generic_fields_tbl_` are now encapsulated inside: - `rb_obj_fields` - `rb_obj_set_fields` - `rb_obj_replace_fields` --- internal/variable.h | 1 - ractor.c | 3 +- variable.c | 270 +++++++++++++++++--------------------------- variable.h | 8 +- vm_insnhelper.c | 19 ++-- 5 files changed, 122 insertions(+), 179 deletions(-) diff --git a/internal/variable.h b/internal/variable.h index bbf3243fe9..0a474d6669 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -46,7 +46,6 @@ void rb_gvar_namespace_ready(const char *name); */ VALUE rb_mod_set_temporary_name(VALUE, VALUE); -int rb_gen_fields_tbl_get(VALUE obj, ID id, VALUE *fields_obj); void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table); void rb_obj_init_too_complex(VALUE obj, st_table *table); void rb_evict_ivars_to_hash(VALUE obj); diff --git a/ractor.c b/ractor.c index a46eb00685..91542b940b 100644 --- a/ractor.c +++ b/ractor.c @@ -1679,8 +1679,7 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) } while (0) if (UNLIKELY(rb_obj_exivar_p(obj))) { - VALUE fields_obj; - rb_ivar_generic_fields_tbl_lookup(obj, &fields_obj); + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); if (UNLIKELY(rb_shape_obj_too_complex_p(obj))) { struct obj_traverse_replace_callback_data d = { diff --git a/variable.c b/variable.c index 5ae2d3e3b0..f504cc57f5 100644 --- a/variable.c +++ b/variable.c @@ -1185,25 +1185,24 @@ IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(ID id) rb_raise(rb_eRactorIsolationError, "can not access class variables from non-main Ractors"); \ } -static inline struct st_table * -generic_fields_tbl(VALUE obj, ID id, bool force_check_ractor) +static inline void +ivar_ractor_check(VALUE obj, ID id) { - ASSERT_vm_locking(); - - if ((force_check_ractor || LIKELY(rb_is_instance_id(id)) /* not internal ID */ ) && + if (LIKELY(rb_is_instance_id(id)) /* not internal ID */ && !RB_OBJ_FROZEN_RAW(obj) && UNLIKELY(!rb_ractor_main_p()) && UNLIKELY(rb_ractor_shareable_p(obj))) { rb_raise(rb_eRactorIsolationError, "can not access instance variables of shareable objects from non-main Ractors"); } - return generic_fields_tbl_; } static inline struct st_table * -generic_fields_tbl_no_ractor_check(VALUE obj) +generic_fields_tbl_no_ractor_check(void) { - return generic_fields_tbl(obj, 0, false); + ASSERT_vm_locking(); + + return generic_fields_tbl_; } struct st_table * @@ -1212,62 +1211,33 @@ rb_generic_fields_tbl_get(void) return generic_fields_tbl_; } -static inline VALUE -generic_fields_lookup(VALUE obj, ID id, bool force_check_ractor) -{ - VALUE fields_obj = Qfalse; - RB_VM_LOCKING() { - st_table *generic_tbl = generic_fields_tbl(obj, id, false); - st_lookup(generic_tbl, obj, (st_data_t *)&fields_obj); - } - return fields_obj; -} - -static inline void -generic_fields_insert(VALUE obj, VALUE fields_obj) -{ - RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - - RB_VM_LOCKING() { - st_table *generic_tbl = generic_fields_tbl_no_ractor_check(obj); - st_insert(generic_tbl, obj, fields_obj); - } - RB_OBJ_WRITTEN(obj, Qundef, fields_obj); -} - -int -rb_gen_fields_tbl_get(VALUE obj, ID id, VALUE *fields_obj) -{ - RUBY_ASSERT(!RB_TYPE_P(obj, T_ICLASS)); - - st_data_t data; - int r = 0; - - RB_VM_LOCKING() { - if (st_lookup(generic_fields_tbl(obj, id, false), (st_data_t)obj, &data)) { - *fields_obj = (VALUE)data; - r = 1; - } - } - - return r; -} - -int -rb_ivar_generic_fields_tbl_lookup(VALUE obj, VALUE *fields_obj) -{ - return rb_gen_fields_tbl_get(obj, 0, fields_obj); -} - void rb_mark_generic_ivar(VALUE obj) { VALUE data; - if (st_lookup(generic_fields_tbl_no_ractor_check(obj), (st_data_t)obj, (st_data_t *)&data)) { + // Bypass ASSERT_vm_locking() check because marking may happen concurrently with mmtk + if (st_lookup(generic_fields_tbl_, (st_data_t)obj, (st_data_t *)&data)) { rb_gc_mark_movable(data); } } +VALUE +rb_obj_fields(VALUE obj, ID field_name) +{ + RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO)); + ivar_ractor_check(obj, field_name); + + VALUE fields_obj = 0; + if (rb_obj_exivar_p(obj)) { + RB_VM_LOCKING() { + if (!st_lookup(generic_fields_tbl_, (st_data_t)obj, (st_data_t *)&fields_obj)) { + rb_bug("Object is missing entry in generic_fields_tbl"); + } + } + } + return fields_obj; +} + void rb_free_generic_ivar(VALUE obj) { @@ -1275,57 +1245,70 @@ rb_free_generic_ivar(VALUE obj) st_data_t key = (st_data_t)obj, value; RB_VM_LOCKING() { - st_delete(generic_fields_tbl_no_ractor_check(obj), &key, &value); + st_delete(generic_fields_tbl_no_ractor_check(), &key, &value); + RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); } } } +void +rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fields_obj) +{ + ivar_ractor_check(obj, field_name); + + RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); + RUBY_ASSERT(!original_fields_obj || IMEMO_TYPE_P(original_fields_obj, imemo_fields)); + + if (fields_obj != original_fields_obj) { + RB_VM_LOCKING() { + st_insert(generic_fields_tbl_, (st_data_t)obj, (st_data_t)fields_obj); + } + + RB_OBJ_WRITTEN(obj, original_fields_obj, fields_obj); + + if (original_fields_obj) { + // Clear root shape to avoid triggering cleanup such as free_object_id. + rb_imemo_fields_clear(original_fields_obj); + } + } + + RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); +} + +void +rb_obj_replace_fields(VALUE obj, VALUE fields_obj, ID field_name) +{ + RB_VM_LOCKING() { + VALUE original_fields_obj = rb_obj_fields(obj, field_name); + rb_obj_set_fields(obj, fields_obj, field_name, original_fields_obj); + } +} + VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id) { RUBY_ASSERT(!SPECIAL_CONST_P(obj)); RUBY_ASSERT(RSHAPE_TYPE_P(target_shape_id, SHAPE_IVAR) || RSHAPE_TYPE_P(target_shape_id, SHAPE_OBJ_ID)); - if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { - ASSERT_vm_locking(); - VALUE field_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - if (field_obj) { - return rb_obj_field_get(field_obj, target_shape_id); - } - return Qundef; - } - if (rb_shape_too_complex_p(target_shape_id)) { st_table *fields_hash; switch (BUILTIN_TYPE(obj)) { case T_CLASS: case T_MODULE: - rb_bug("Unreachable"); + fields_hash = rb_imemo_fields_complex_tbl(RCLASS_WRITABLE_FIELDS_OBJ(obj)); break; case T_OBJECT: fields_hash = ROBJECT_FIELDS_HASH(obj); break; case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); fields_hash = rb_imemo_fields_complex_tbl(obj); break; default: - RUBY_ASSERT(rb_obj_exivar_p(obj)); - VALUE fields_obj = 0; - rb_ivar_generic_fields_tbl_lookup(obj, &fields_obj); - RUBY_ASSERT(fields_obj); - fields_hash = rb_imemo_fields_complex_tbl(fields_obj); + fields_hash = rb_imemo_fields_complex_tbl(rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id))); break; } VALUE value = Qundef; st_lookup(fields_hash, RSHAPE_EDGE_NAME(target_shape_id), &value); - -#if RUBY_DEBUG - if (UNDEF_P(value)) { - rb_bug("Object's shape includes object_id, but it's missing %s", rb_obj_info(obj)); - } -#endif - RUBY_ASSERT(!UNDEF_P(value)); return value; } @@ -1335,21 +1318,16 @@ rb_obj_field_get(VALUE obj, shape_id_t target_shape_id) switch (BUILTIN_TYPE(obj)) { case T_CLASS: case T_MODULE: - rb_bug("Unreachable"); + fields = rb_imemo_fields_ptr(RCLASS_WRITABLE_FIELDS_OBJ(obj)); break; case T_OBJECT: fields = ROBJECT_FIELDS(obj); break; case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); fields = rb_imemo_fields_ptr(obj); break; default: - RUBY_ASSERT(rb_obj_exivar_p(obj)); - VALUE fields_obj = 0; - rb_ivar_generic_fields_tbl_lookup(obj, &fields_obj); - RUBY_ASSERT(fields_obj); - fields = rb_imemo_fields_ptr(fields_obj); + fields = rb_imemo_fields_ptr(rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id))); break; } return fields[attr_index]; @@ -1422,28 +1400,26 @@ rb_ivar_lookup(VALUE obj, ID id, VALUE undef) break; } default: - shape_id = RBASIC_SHAPE_ID(obj); - if (rb_obj_exivar_p(obj)) { - VALUE fields_obj = 0; - rb_gen_fields_tbl_get(obj, id, &fields_obj); - - RUBY_ASSERT(fields_obj); - - if (rb_shape_obj_too_complex_p(fields_obj)) { - VALUE val; - if (rb_st_lookup(rb_imemo_fields_complex_tbl(fields_obj), (st_data_t)id, (st_data_t *)&val)) { - return val; - } - else { - return undef; + { + shape_id = RBASIC_SHAPE_ID(obj); + VALUE fields_obj = rb_obj_fields(obj, id); + if (fields_obj) { + if (rb_shape_obj_too_complex_p(fields_obj)) { + VALUE val; + if (rb_st_lookup(rb_imemo_fields_complex_tbl(fields_obj), (st_data_t)id, (st_data_t *)&val)) { + return val; + } + else { + return undef; + } } + ivar_list = rb_imemo_fields_ptr(fields_obj); } - ivar_list = rb_imemo_fields_ptr(fields_obj); + else { + return undef; + } + break; } - else { - return undef; - } - break; } attr_index_t index = 0; @@ -1524,8 +1500,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) fields = ROBJECT_FIELDS(obj); break; default: { - VALUE fields_obj; - rb_gen_fields_tbl_get(obj, id, &fields_obj); + VALUE fields_obj = rb_obj_fields(obj, id); fields = rb_imemo_fields_ptr(fields_obj); break; } @@ -1579,10 +1554,8 @@ too_complex: break; default: { - VALUE fields_obj; - if (rb_gen_fields_tbl_get(obj, 0, &fields_obj)) { - table = rb_imemo_fields_complex_tbl(fields_obj); - } + VALUE fields_obj = rb_obj_fields(obj, id); + table = rb_imemo_fields_complex_tbl(fields_obj); break; } } @@ -1603,8 +1576,6 @@ rb_attr_delete(VALUE obj, ID id) return rb_ivar_delete(obj, id, Qnil); } -static inline void generic_update_fields_obj(VALUE obj, VALUE fields_obj, const VALUE original_fields_obj); - static shape_id_t obj_transition_too_complex(VALUE obj, st_table *table) { @@ -1637,12 +1608,7 @@ obj_transition_too_complex(VALUE obj, st_table *table) { VALUE fields_obj = rb_imemo_fields_new_complex_tbl(rb_obj_class(obj), table); RBASIC_SET_SHAPE_ID(fields_obj, shape_id); - - RB_VM_LOCKING() { - const VALUE original_fields_obj = generic_fields_lookup(obj, 0, false); - generic_update_fields_obj(obj, fields_obj, original_fields_obj); - } - RBASIC_SET_SHAPE_ID(obj, shape_id); + rb_obj_replace_fields(obj, fields_obj, 0); } } @@ -1839,19 +1805,6 @@ general_field_set(VALUE obj, shape_id_t target_shape_id, VALUE val, void *data, } } -static inline void -generic_update_fields_obj(VALUE obj, VALUE fields_obj, const VALUE original_fields_obj) -{ - if (fields_obj != original_fields_obj) { - if (original_fields_obj) { - // Clear root shape to avoid triggering cleanup such as free_object_id. - rb_imemo_fields_clear(original_fields_obj); - } - - generic_fields_insert(obj, fields_obj); - } -} - static VALUE imemo_fields_set(VALUE klass, VALUE fields_obj, shape_id_t target_shape_id, ID field_name, VALUE val, bool concurrent) { @@ -1904,16 +1857,10 @@ generic_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE va RUBY_ASSERT(field_name); } - const VALUE original_fields_obj = generic_fields_lookup(obj, field_name, false); + const VALUE original_fields_obj = rb_obj_fields(obj, field_name); VALUE fields_obj = imemo_fields_set(rb_obj_class(obj), original_fields_obj, target_shape_id, field_name, val, false); - generic_update_fields_obj(obj, fields_obj, original_fields_obj); - - if (RBASIC_SHAPE_ID(fields_obj) == target_shape_id) { - RBASIC_SET_SHAPE_ID(obj, target_shape_id); - } - - RUBY_ASSERT(RBASIC_SHAPE_ID(obj) == RBASIC_SHAPE_ID(fields_obj)); + rb_obj_set_fields(obj, fields_obj, field_name, original_fields_obj); } static shape_id_t @@ -2162,10 +2109,8 @@ ivar_defined0(VALUE obj, ID id) break; default: { - VALUE fields_obj; - if (rb_gen_fields_tbl_get(obj, 0, &fields_obj)) { - table = rb_imemo_fields_complex_tbl(fields_obj); - } + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); // defined? doesn't require ractor checks + table = rb_imemo_fields_complex_tbl(fields_obj); } } @@ -2306,7 +2251,6 @@ imemo_fields_each(VALUE fields_obj, rb_ivar_foreach_callback_func *func, st_data void rb_copy_generic_ivar(VALUE dest, VALUE obj) { - VALUE fields_obj; VALUE new_fields_obj; rb_check_frozen(dest); @@ -2317,7 +2261,8 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) shape_id_t src_shape_id = rb_obj_shape_id(obj); - if (rb_gen_fields_tbl_get(obj, 0, &fields_obj)) { + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); + if (fields_obj) { unsigned long src_num_ivs = rb_ivar_count(fields_obj); if (!src_num_ivs) { goto clear; @@ -2355,8 +2300,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) RBASIC_SET_SHAPE_ID(new_fields_obj, dest_shape_id); RB_VM_LOCKING() { - generic_fields_tbl_no_ractor_check(dest); - st_insert(generic_fields_tbl_no_ractor_check(obj), (st_data_t)dest, (st_data_t)new_fields_obj); + st_insert(generic_fields_tbl_no_ractor_check(), (st_data_t)dest, (st_data_t)new_fields_obj); RB_OBJ_WRITTEN(dest, Qundef, new_fields_obj); } @@ -2407,11 +2351,11 @@ rb_field_foreach(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg, } break; default: - if (rb_obj_exivar_p(obj)) { - VALUE fields_obj = 0; - if (!rb_gen_fields_tbl_get(obj, 0, &fields_obj)) return; - - imemo_fields_each(fields_obj, func, arg, ivar_only); + { + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); + if (fields_obj) { + imemo_fields_each(fields_obj, func, arg, ivar_only); + } } break; } @@ -2462,17 +2406,15 @@ rb_ivar_count(VALUE obj) break; default: - if (rb_obj_exivar_p(obj)) { - - if (rb_shape_obj_too_complex_p(obj)) { - VALUE fields_obj; - - if (rb_gen_fields_tbl_get(obj, 0, &fields_obj)) { - iv_count = rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); + { + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); + if (fields_obj) { + if (rb_shape_obj_too_complex_p(fields_obj)) { + rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); + } + else { + iv_count = RBASIC_FIELDS_COUNT(obj); } - } - else { - iv_count = RBASIC_FIELDS_COUNT(obj); } } break; diff --git a/variable.h b/variable.h index 82a79c63ce..f2afead9d3 100644 --- a/variable.h +++ b/variable.h @@ -12,8 +12,14 @@ #include "shape.h" -int rb_ivar_generic_fields_tbl_lookup(VALUE obj, VALUE *); void rb_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table); +VALUE rb_obj_fields(VALUE obj, ID field_name); + +static inline VALUE +rb_obj_fields_no_ractor_check(VALUE obj) +{ + return rb_obj_fields(obj, 0); +} void rb_free_rb_global_tbl(void); void rb_free_generic_fields_tbl_(void); diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 7842d7657a..8ce7db1a80 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1267,8 +1267,8 @@ vm_getivar(VALUE obj, ID id, const rb_iseq_t *iseq, IVC ic, const struct rb_call } default: if (rb_obj_exivar_p(obj)) { - VALUE fields_obj = 0; - if (!rb_gen_fields_tbl_get(obj, id, &fields_obj)) { + VALUE fields_obj = rb_obj_fields(obj, id); + if (!fields_obj) { return default_value; } ivar_list = rb_imemo_fields_ptr(fields_obj); @@ -1343,10 +1343,8 @@ vm_getivar(VALUE obj, ID id, const rb_iseq_t *iseq, IVC ic, const struct rb_call break; default: { - VALUE fields_obj; - if (rb_gen_fields_tbl_get(obj, 0, &fields_obj)) { - table = rb_imemo_fields_complex_tbl(fields_obj); - } + VALUE fields_obj = rb_obj_fields(obj, id); + table = rb_imemo_fields_complex_tbl(fields_obj); break; } } @@ -1466,8 +1464,6 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, shape_id_t dest_shape_id, attr_i { shape_id_t shape_id = RBASIC_SHAPE_ID(obj); - VALUE fields_obj = 0; - // Cache hit case if (shape_id == dest_shape_id) { RUBY_ASSERT(dest_shape_id != INVALID_SHAPE_ID && shape_id != INVALID_SHAPE_ID); @@ -1484,14 +1480,15 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, shape_id_t dest_shape_id, attr_i return Qundef; } - rb_gen_fields_tbl_get(obj, 0, &fields_obj); + VALUE fields_obj = rb_obj_fields(obj, id); + RUBY_ASSERT(fields_obj); + RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[index], val); if (shape_id != dest_shape_id) { RBASIC_SET_SHAPE_ID(obj, dest_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } - RB_OBJ_WRITE(obj, &rb_imemo_fields_ptr(fields_obj)[index], val); - RB_DEBUG_COUNTER_INC(ivar_set_ic_hit); return val; From 06312377adc62ea1c82b5574749ee3704a1e4e9f Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 31 Jul 2025 14:38:16 -0400 Subject: [PATCH 041/157] Make Ractor::Selector write-barrier protected --- ractor_sync.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ractor_sync.c b/ractor_sync.c index a3ed38295b..057448c5f5 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -1273,7 +1273,7 @@ static const rb_data_type_t ractor_selector_data_type = { ractor_selector_memsize, NULL, // update }, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; static struct ractor_selector * @@ -1318,6 +1318,8 @@ ractor_selector_add(VALUE selv, VALUE rpv) } st_insert(s->ports, (st_data_t)rpv, (st_data_t)rp); + RB_OBJ_WRITTEN(selv, Qundef, rpv); + return selv; } From 9b3ad3449be62c46fbacdb59f43aa526f7da2f79 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 6 Aug 2025 15:23:29 +0200 Subject: [PATCH 042/157] Mark `cross_ractor_require_data_type` as embeddable Nothing prevents it, so might as well. --- ractor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ractor.c b/ractor.c index 91542b940b..dc83ccc1b4 100644 --- a/ractor.c +++ b/ractor.c @@ -2302,7 +2302,7 @@ static const rb_data_type_t cross_ractor_require_data_type = { NULL, // memsize NULL, // compact }, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_DECL_MARKING + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_DECL_MARKING | RUBY_TYPED_EMBEDDABLE }; static VALUE From f3206cc79bec2fd852e81ec56de59f0a67ab32b7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 6 Aug 2025 14:46:36 +0200 Subject: [PATCH 043/157] Struct: keep direct reference to IMEMO/fields when space allows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's not rare for structs to have additional ivars, hence are one of the most common, if not the most common type in the `gen_fields_tbl`. This can cause Ractor contention, but even in single ractor mode means having to do a hash lookup to access the ivars, and increase GC work. Instead, unless the struct is perfectly right sized, we can store a reference to the associated IMEMO/fields object right after the last struct member. ``` compare-ruby: ruby 3.5.0dev (2025-08-06T12:50:36Z struct-ivar-fields-2 9a30d141a1) +PRISM [arm64-darwin24] built-ruby: ruby 3.5.0dev (2025-08-06T12:57:59Z struct-ivar-fields-2 2ff3ec237f) +PRISM [arm64-darwin24] warming up..... | |compare-ruby|built-ruby| |:---------------------|-----------:|---------:| |member_reader | 590.317k| 579.246k| | | 1.02x| -| |member_writer | 543.963k| 527.104k| | | 1.03x| -| |member_reader_method | 213.540k| 213.004k| | | 1.00x| -| |member_writer_method | 192.657k| 191.491k| | | 1.01x| -| |ivar_reader | 403.993k| 569.915k| | | -| 1.41x| ``` Co-Authored-By: Étienne Barrié --- benchmark/struct_accessor.yml | 12 +++++++ depend | 8 +++++ gc.c | 13 +++++++ internal/struct.h | 41 ++++++++++++++++++++++ struct.c | 11 +++++- test/ruby/test_object_id.rb | 49 ++++++++++++++++++++++++++ test/ruby/test_ractor.rb | 18 ++++++++++ variable.c | 65 +++++++++++++++++++++++------------ 8 files changed, 194 insertions(+), 23 deletions(-) diff --git a/benchmark/struct_accessor.yml b/benchmark/struct_accessor.yml index 61176cfdd4..d95240e2dd 100644 --- a/benchmark/struct_accessor.yml +++ b/benchmark/struct_accessor.yml @@ -1,5 +1,12 @@ prelude: | C = Struct.new(:x) do + def initialize(...) + super + @ivar = 42 + end + + attr_accessor :ivar + class_eval <<-END def r #{'x;'*256} @@ -15,11 +22,16 @@ prelude: | m = method(:x=) #{'m.call(nil);'*256} end + def r_ivar + #{'ivar;'*256} + end END end + C.new(nil) # ensure common shape is known obj = C.new(nil) benchmark: member_reader: "obj.r" member_writer: "obj.w" member_reader_method: "obj.rm" member_writer_method: "obj.wm" + ivar_reader: "obj.r_ivar" diff --git a/depend b/depend index ec8c2771c9..ecaf33b1c7 100644 --- a/depend +++ b/depend @@ -6065,6 +6065,7 @@ hash.$(OBJEXT): $(top_srcdir)/internal/set_table.h hash.$(OBJEXT): $(top_srcdir)/internal/st.h hash.$(OBJEXT): $(top_srcdir)/internal/static_assert.h hash.$(OBJEXT): $(top_srcdir)/internal/string.h +hash.$(OBJEXT): $(top_srcdir)/internal/struct.h hash.$(OBJEXT): $(top_srcdir)/internal/symbol.h hash.$(OBJEXT): $(top_srcdir)/internal/thread.h hash.$(OBJEXT): $(top_srcdir)/internal/time.h @@ -6288,6 +6289,7 @@ hash.$(OBJEXT): {$(VPATH)}symbol.h hash.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h hash.$(OBJEXT): {$(VPATH)}thread_native.h hash.$(OBJEXT): {$(VPATH)}util.h +hash.$(OBJEXT): {$(VPATH)}variable.h hash.$(OBJEXT): {$(VPATH)}vm_core.h hash.$(OBJEXT): {$(VPATH)}vm_debug.h hash.$(OBJEXT): {$(VPATH)}vm_opts.h @@ -12926,6 +12928,7 @@ range.$(OBJEXT): $(top_srcdir)/internal/enumerator.h range.$(OBJEXT): $(top_srcdir)/internal/error.h range.$(OBJEXT): $(top_srcdir)/internal/fixnum.h range.$(OBJEXT): $(top_srcdir)/internal/gc.h +range.$(OBJEXT): $(top_srcdir)/internal/imemo.h range.$(OBJEXT): $(top_srcdir)/internal/numeric.h range.$(OBJEXT): $(top_srcdir)/internal/range.h range.$(OBJEXT): $(top_srcdir)/internal/serial.h @@ -12948,6 +12951,7 @@ range.$(OBJEXT): {$(VPATH)}config.h range.$(OBJEXT): {$(VPATH)}defines.h range.$(OBJEXT): {$(VPATH)}encoding.h range.$(OBJEXT): {$(VPATH)}id.h +range.$(OBJEXT): {$(VPATH)}id_table.h range.$(OBJEXT): {$(VPATH)}intern.h range.$(OBJEXT): {$(VPATH)}internal.h range.$(OBJEXT): {$(VPATH)}internal/abi.h @@ -15580,6 +15584,7 @@ shape.$(OBJEXT): $(top_srcdir)/internal/serial.h shape.$(OBJEXT): $(top_srcdir)/internal/set_table.h shape.$(OBJEXT): $(top_srcdir)/internal/static_assert.h shape.$(OBJEXT): $(top_srcdir)/internal/string.h +shape.$(OBJEXT): $(top_srcdir)/internal/struct.h shape.$(OBJEXT): $(top_srcdir)/internal/symbol.h shape.$(OBJEXT): $(top_srcdir)/internal/variable.h shape.$(OBJEXT): $(top_srcdir)/internal/vm.h @@ -16569,6 +16574,7 @@ string.$(OBJEXT): $(top_srcdir)/internal/serial.h string.$(OBJEXT): $(top_srcdir)/internal/set_table.h string.$(OBJEXT): $(top_srcdir)/internal/static_assert.h string.$(OBJEXT): $(top_srcdir)/internal/string.h +string.$(OBJEXT): $(top_srcdir)/internal/struct.h string.$(OBJEXT): $(top_srcdir)/internal/transcode.h string.$(OBJEXT): $(top_srcdir)/internal/variable.h string.$(OBJEXT): $(top_srcdir)/internal/vm.h @@ -16766,6 +16772,7 @@ string.$(OBJEXT): {$(VPATH)}thread.h string.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h string.$(OBJEXT): {$(VPATH)}thread_native.h string.$(OBJEXT): {$(VPATH)}util.h +string.$(OBJEXT): {$(VPATH)}variable.h string.$(OBJEXT): {$(VPATH)}vm_core.h string.$(OBJEXT): {$(VPATH)}vm_debug.h string.$(OBJEXT): {$(VPATH)}vm_opts.h @@ -18103,6 +18110,7 @@ variable.$(OBJEXT): $(top_srcdir)/internal/serial.h variable.$(OBJEXT): $(top_srcdir)/internal/set_table.h variable.$(OBJEXT): $(top_srcdir)/internal/static_assert.h variable.$(OBJEXT): $(top_srcdir)/internal/string.h +variable.$(OBJEXT): $(top_srcdir)/internal/struct.h variable.$(OBJEXT): $(top_srcdir)/internal/symbol.h variable.$(OBJEXT): $(top_srcdir)/internal/thread.h variable.$(OBJEXT): $(top_srcdir)/internal/variable.h diff --git a/gc.c b/gc.c index 4af43edcc4..64a22cd1b7 100644 --- a/gc.c +++ b/gc.c @@ -3260,6 +3260,10 @@ rb_gc_mark_children(void *objspace, VALUE obj) gc_mark_internal(ptr[i]); } + if (!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS)) { + gc_mark_internal(RSTRUCT_FIELDS_OBJ(obj)); + } + break; } @@ -4188,6 +4192,15 @@ rb_gc_update_object_references(void *objspace, VALUE obj) for (i = 0; i < len; i++) { UPDATE_IF_MOVED(objspace, ptr[i]); } + + if (RSTRUCT_EMBED_LEN(obj)) { + if (!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS)) { + UPDATE_IF_MOVED(objspace, ptr[len]); + } + } + else { + UPDATE_IF_MOVED(objspace, RSTRUCT(obj)->as.heap.fields_obj); + } } break; default: diff --git a/internal/struct.h b/internal/struct.h index a8c773b730..337f96a336 100644 --- a/internal/struct.h +++ b/internal/struct.h @@ -11,10 +11,23 @@ #include "ruby/internal/stdbool.h" /* for bool */ #include "ruby/ruby.h" /* for struct RBasic */ +/* Flags of RStruct + * + * 1-7: RSTRUCT_EMBED_LEN + * If non-zero, the struct is embedded (its contents follow the + * header, rather than being on a separately allocated buffer) and + * these bits are the length of the Struct. + * 8: RSTRUCT_GEN_FIELDS + * The struct is embedded and has no space left to store the + * IMEMO/fields reference. Any ivar this struct may have will be in + * the generic_fields_tbl. This flag doesn't imply the struct has + * ivars. + */ enum { RSTRUCT_EMBED_LEN_MASK = RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 | RUBY_FL_USER2 | RUBY_FL_USER1, RSTRUCT_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+1), + RSTRUCT_GEN_FIELDS = RUBY_FL_USER8, }; struct RStruct { @@ -23,6 +36,7 @@ struct RStruct { struct { long len; const VALUE *ptr; + VALUE fields_obj; } heap; /* This is a length 1 array because: * 1. GCC has a bug that does not optimize C flexible array members @@ -116,4 +130,31 @@ RSTRUCT_GET(VALUE st, long k) return RSTRUCT_CONST_PTR(st)[k]; } +static inline VALUE +RSTRUCT_FIELDS_OBJ(VALUE st) +{ + const long embed_len = RSTRUCT_EMBED_LEN(st); + VALUE fields_obj; + if (embed_len) { + RUBY_ASSERT(!FL_TEST_RAW(st, RSTRUCT_GEN_FIELDS)); + fields_obj = RSTRUCT_GET(st, embed_len); + } + else { + fields_obj = RSTRUCT(st)->as.heap.fields_obj; + } + return fields_obj; +} + +static inline void +RSTRUCT_SET_FIELDS_OBJ(VALUE st, VALUE fields_obj) +{ + const long embed_len = RSTRUCT_EMBED_LEN(st); + if (embed_len) { + RUBY_ASSERT(!FL_TEST_RAW(st, RSTRUCT_GEN_FIELDS)); + RSTRUCT_SET(st, embed_len, fields_obj); + } + else { + RB_OBJ_WRITE(st, &RSTRUCT(st)->as.heap.fields_obj, fields_obj); + } +} #endif /* INTERNAL_STRUCT_H */ diff --git a/struct.c b/struct.c index 74ca9369a6..c53e68b3da 100644 --- a/struct.c +++ b/struct.c @@ -811,13 +811,22 @@ struct_alloc(VALUE klass) { long n = num_members(klass); size_t embedded_size = offsetof(struct RStruct, as.ary) + (sizeof(VALUE) * n); + if (RCLASS_MAX_IV_COUNT(klass) > 0) { + embedded_size += sizeof(VALUE); + } + VALUE flags = T_STRUCT | (RGENGC_WB_PROTECTED_STRUCT ? FL_WB_PROTECTED : 0); if (n > 0 && rb_gc_size_allocatable_p(embedded_size)) { flags |= n << RSTRUCT_EMBED_LEN_SHIFT; NEWOBJ_OF(st, struct RStruct, klass, flags, embedded_size, 0); - + if (RCLASS_MAX_IV_COUNT(klass) == 0 && embedded_size == rb_gc_obj_slot_size((VALUE)st)) { + FL_SET_RAW((VALUE)st, RSTRUCT_GEN_FIELDS); + } + else { + RSTRUCT_SET_FIELDS_OBJ((VALUE)st, 0); + } rb_mem_clear((VALUE *)st->as.ary, n); return (VALUE)st; diff --git a/test/ruby/test_object_id.rb b/test/ruby/test_object_id.rb index 24434f8aba..adb819febc 100644 --- a/test/ruby/test_object_id.rb +++ b/test/ruby/test_object_id.rb @@ -252,3 +252,52 @@ class TestObjectIdRactor < Test::Unit::TestCase end; end end + +class TestObjectIdStruct < TestObjectId + EmbeddedStruct = Struct.new(:embedded_field) + + def setup + @obj = EmbeddedStruct.new + end +end + +class TestObjectIdStructGenIvar < TestObjectId + GenIvarStruct = Struct.new(:a, :b, :c) + + def setup + @obj = GenIvarStruct.new + end +end + +class TestObjectIdStructNotEmbed < TestObjectId + MANY_IVS = 80 + + StructNotEmbed = Struct.new(*MANY_IVS.times.map { |i| :"field_#{i}" }) + + def setup + @obj = StructNotEmbed.new + end +end + +class TestObjectIdStructTooComplex < TestObjectId + StructTooComplex = Struct.new(:a) do + def initialize + @too_complex_obj_id_test = 1 + end + end + + def setup + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) + assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS + end + 8.times do |i| + StructTooComplex.new.instance_variable_set("@TestObjectIdStructTooComplex#{i}", 1) + end + @obj = StructTooComplex.new + @obj.instance_variable_set("@a#{rand(10_000)}", 1) + + if defined?(RubyVM::Shape) + assert_predicate(RubyVM::Shape.of(@obj), :too_complex?) + end + end +end diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 97af7e7413..0a456a1d0f 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -99,6 +99,24 @@ class TestRactor < Test::Unit::TestCase RUBY end + def test_struct_instance_variables + assert_ractor(<<~'RUBY') + StructIvar = Struct.new(:member) do + def initialize(*) + super + @ivar = "ivar" + end + attr_reader :ivar + end + obj = StructIvar.new("member") + obj_copy = Ractor.new { Ractor.receive }.send(obj).value + assert_equal obj.ivar, obj_copy.ivar + refute_same obj.ivar, obj_copy.ivar + assert_equal obj.member, obj_copy.member + refute_same obj.member, obj_copy.member + RUBY + end + def test_fork_raise_isolation_error assert_ractor(<<~'RUBY') ractor = Ractor.new do diff --git a/variable.c b/variable.c index f504cc57f5..76b16b04cb 100644 --- a/variable.c +++ b/variable.c @@ -29,6 +29,7 @@ #include "internal/object.h" #include "internal/gc.h" #include "internal/re.h" +#include "internal/struct.h" #include "internal/symbol.h" #include "internal/thread.h" #include "internal/variable.h" @@ -1228,10 +1229,19 @@ rb_obj_fields(VALUE obj, ID field_name) ivar_ractor_check(obj, field_name); VALUE fields_obj = 0; - if (rb_obj_exivar_p(obj)) { - RB_VM_LOCKING() { - if (!st_lookup(generic_fields_tbl_, (st_data_t)obj, (st_data_t *)&fields_obj)) { - rb_bug("Object is missing entry in generic_fields_tbl"); + if (rb_shape_obj_has_fields(obj)) { + switch (BUILTIN_TYPE(obj)) { + case T_STRUCT: + if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { + fields_obj = RSTRUCT_FIELDS_OBJ(obj); + break; + } + // fall through + default: + RB_VM_LOCKING() { + if (!st_lookup(generic_fields_tbl_, (st_data_t)obj, (st_data_t *)&fields_obj)) { + rb_bug("Object is missing entry in generic_fields_tbl"); + } } } } @@ -1243,11 +1253,19 @@ rb_free_generic_ivar(VALUE obj) { if (rb_obj_exivar_p(obj)) { st_data_t key = (st_data_t)obj, value; - - RB_VM_LOCKING() { - st_delete(generic_fields_tbl_no_ractor_check(), &key, &value); - RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); + switch (BUILTIN_TYPE(obj)) { + case T_STRUCT: + if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { + RSTRUCT_SET_FIELDS_OBJ(obj, 0); + break; + } + // fall through + default: + RB_VM_LOCKING() { + st_delete(generic_fields_tbl_no_ractor_check(), &key, &value); + } } + RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); } } @@ -1260,12 +1278,20 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie RUBY_ASSERT(!original_fields_obj || IMEMO_TYPE_P(original_fields_obj, imemo_fields)); if (fields_obj != original_fields_obj) { - RB_VM_LOCKING() { - st_insert(generic_fields_tbl_, (st_data_t)obj, (st_data_t)fields_obj); + switch (BUILTIN_TYPE(obj)) { + case T_STRUCT: + if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { + RSTRUCT_SET_FIELDS_OBJ(obj, fields_obj); + break; + } + // fall through + default: + RB_VM_LOCKING() { + st_insert(generic_fields_tbl_, (st_data_t)obj, (st_data_t)fields_obj); + } + RB_OBJ_WRITTEN(obj, original_fields_obj, fields_obj); } - RB_OBJ_WRITTEN(obj, original_fields_obj, fields_obj); - if (original_fields_obj) { // Clear root shape to avoid triggering cleanup such as free_object_id. rb_imemo_fields_clear(original_fields_obj); @@ -1276,11 +1302,11 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie } void -rb_obj_replace_fields(VALUE obj, VALUE fields_obj, ID field_name) +rb_obj_replace_fields(VALUE obj, VALUE fields_obj) { RB_VM_LOCKING() { - VALUE original_fields_obj = rb_obj_fields(obj, field_name); - rb_obj_set_fields(obj, fields_obj, field_name, original_fields_obj); + VALUE original_fields_obj = rb_obj_fields_no_ractor_check(obj); + rb_obj_set_fields(obj, fields_obj, 0, original_fields_obj); } } @@ -1608,7 +1634,7 @@ obj_transition_too_complex(VALUE obj, st_table *table) { VALUE fields_obj = rb_imemo_fields_new_complex_tbl(rb_obj_class(obj), table); RBASIC_SET_SHAPE_ID(fields_obj, shape_id); - rb_obj_replace_fields(obj, fields_obj, 0); + rb_obj_replace_fields(obj, fields_obj); } } @@ -2299,12 +2325,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) rb_shape_copy_fields(new_fields_obj, dest_buf, dest_shape_id, src_buf, src_shape_id); RBASIC_SET_SHAPE_ID(new_fields_obj, dest_shape_id); - RB_VM_LOCKING() { - st_insert(generic_fields_tbl_no_ractor_check(), (st_data_t)dest, (st_data_t)new_fields_obj); - RB_OBJ_WRITTEN(dest, Qundef, new_fields_obj); - } - - RBASIC_SET_SHAPE_ID(dest, dest_shape_id); + rb_obj_replace_fields(dest, new_fields_obj); } return; From 7aa0e09bfe9cbb9f9abe8111fa3673a85d86ca9b Mon Sep 17 00:00:00 2001 From: ydah Date: Tue, 5 Aug 2025 21:44:13 +0900 Subject: [PATCH 044/157] Fix typo in comment regarding symlink flags in autogen.sh --- autogen.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen.sh b/autogen.sh index f11a471a08..6cbc5dddab 100755 --- a/autogen.sh +++ b/autogen.sh @@ -10,7 +10,7 @@ case "$0" in * ) srcdir="";; # Otherwise esac -# If install-only is explicitly requested, disbale symlink flags +# If install-only is explicitly requested, disable symlink flags case " $* " in *" -i "* | *" --install"* ) symlink_flags="" ;; * ) symlink_flags="--install --symlink" ;; From bcd21053f733a93a82a34c6f5c23d6af2a8010ed Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Sat, 26 Jul 2025 10:31:23 +0900 Subject: [PATCH 045/157] Add MODULE NODE locations Add `keyword_module` amd `keyword_end` locations to struct `RNode_MODULE`. memo: ``` >ruby --dump=parsetree -e 'module A end' @ ProgramNode (location: (1,0)-(1,12)) +-- locals: [] +-- statements: @ StatementsNode (location: (1,0)-(1,12)) +-- body: (length: 1) +-- @ ModuleNode (location: (1,0)-(1,12)) +-- locals: [] +-- module_keyword_loc: (1,0)-(1,6) = "module" +-- constant_path: | @ ConstantReadNode (location: (1,7)-(1,8)) | +-- name: :A +-- body: nil +-- end_keyword_loc: (1,9)-(1,12) = "end" +-- name: :A ``` --- ast.c | 5 +++++ node_dump.c | 4 +++- parse.y | 10 ++++++---- rubyparser.h | 2 ++ test/ruby/test_ast.rb | 5 +++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ast.c b/ast.c index 04f2d1384c..bc2adeacd6 100644 --- a/ast.c +++ b/ast.c @@ -866,6 +866,11 @@ node_locations(VALUE ast_value, const NODE *node) location_new(&RNODE_IF(node)->if_keyword_loc), location_new(&RNODE_IF(node)->then_keyword_loc), location_new(&RNODE_IF(node)->end_keyword_loc)); + case NODE_MODULE: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_MODULE(node)->module_keyword_loc), + location_new(&RNODE_MODULE(node)->end_keyword_loc)); case NODE_NEXT: return rb_ary_new_from_args(2, location_new(nd_code_loc(node)), diff --git a/node_dump.c b/node_dump.c index 9822ae5fc2..c318baeeed 100644 --- a/node_dump.c +++ b/node_dump.c @@ -1009,8 +1009,10 @@ dump_node(VALUE buf, VALUE indent, int comment, const NODE * node) ANN("format: module [nd_cpath]; [nd_body]; end"); ANN("example: module M; ..; end"); F_NODE(nd_cpath, RNODE_MODULE, "module path"); - LAST_NODE; F_NODE(nd_body, RNODE_MODULE, "module definition"); + F_LOC(module_keyword_loc, RNODE_MODULE); + LAST_NODE; + F_LOC(end_keyword_loc, RNODE_MODULE); return; case NODE_SCLASS: diff --git a/parse.y b/parse.y index dbe21332b4..e77dc790bc 100644 --- a/parse.y +++ b/parse.y @@ -1145,7 +1145,7 @@ static rb_node_alias_t *rb_node_alias_new(struct parser_params *p, NODE *nd_1st, static rb_node_valias_t *rb_node_valias_new(struct parser_params *p, ID nd_alias, ID nd_orig, const YYLTYPE *loc, const YYLTYPE *keyword_loc); static rb_node_undef_t *rb_node_undef_new(struct parser_params *p, NODE *nd_undef, const YYLTYPE *loc); static rb_node_class_t *rb_node_class_new(struct parser_params *p, NODE *nd_cpath, NODE *nd_body, NODE *nd_super, const YYLTYPE *loc, const YYLTYPE *class_keyword_loc, const YYLTYPE *inheritance_operator_loc, const YYLTYPE *end_keyword_loc); -static rb_node_module_t *rb_node_module_new(struct parser_params *p, NODE *nd_cpath, NODE *nd_body, const YYLTYPE *loc); +static rb_node_module_t *rb_node_module_new(struct parser_params *p, NODE *nd_cpath, NODE *nd_body, const YYLTYPE *loc, const YYLTYPE *module_keyword_loc, const YYLTYPE *end_keyword_loc); static rb_node_sclass_t *rb_node_sclass_new(struct parser_params *p, NODE *nd_recv, NODE *nd_body, const YYLTYPE *loc); static rb_node_colon2_t *rb_node_colon2_new(struct parser_params *p, NODE *nd_head, ID nd_mid, const YYLTYPE *loc, const YYLTYPE *delimiter_loc, const YYLTYPE *name_loc); static rb_node_colon3_t *rb_node_colon3_new(struct parser_params *p, ID nd_mid, const YYLTYPE *loc, const YYLTYPE *delimiter_loc, const YYLTYPE *name_loc); @@ -1253,7 +1253,7 @@ static rb_node_error_t *rb_node_error_new(struct parser_params *p, const YYLTYPE #define NEW_VALIAS(n,o,loc,k_loc) (NODE *)rb_node_valias_new(p,n,o,loc,k_loc) #define NEW_UNDEF(i,loc) (NODE *)rb_node_undef_new(p,i,loc) #define NEW_CLASS(n,b,s,loc,ck_loc,io_loc,ek_loc) (NODE *)rb_node_class_new(p,n,b,s,loc,ck_loc,io_loc,ek_loc) -#define NEW_MODULE(n,b,loc) (NODE *)rb_node_module_new(p,n,b,loc) +#define NEW_MODULE(n,b,loc,mk_loc,ek_loc) (NODE *)rb_node_module_new(p,n,b,loc,mk_loc,ek_loc) #define NEW_SCLASS(r,b,loc) (NODE *)rb_node_sclass_new(p,r,b,loc) #define NEW_COLON2(c,i,loc,d_loc,n_loc) (NODE *)rb_node_colon2_new(p,c,i,loc,d_loc,n_loc) #define NEW_COLON3(i,loc,d_loc,n_loc) (NODE *)rb_node_colon3_new(p,i,loc,d_loc,n_loc) @@ -4621,7 +4621,7 @@ primary : inline_primary bodystmt k_end { - $$ = NEW_MODULE($cpath, $bodystmt, &@$); + $$ = NEW_MODULE($cpath, $bodystmt, &@$, &@k_module, &@k_end); nd_set_line(RNODE_MODULE($$)->nd_body, @k_end.end_pos.lineno); set_line_body($bodystmt, @cpath.end_pos.lineno); nd_set_line($$, @cpath.end_pos.lineno); @@ -11438,13 +11438,15 @@ rb_node_sclass_new(struct parser_params *p, NODE *nd_recv, NODE *nd_body, const } static rb_node_module_t * -rb_node_module_new(struct parser_params *p, NODE *nd_cpath, NODE *nd_body, const YYLTYPE *loc) +rb_node_module_new(struct parser_params *p, NODE *nd_cpath, NODE *nd_body, const YYLTYPE *loc, const YYLTYPE *module_keyword_loc, const YYLTYPE *end_keyword_loc) { /* Keep the order of node creation */ NODE *scope = NEW_SCOPE(0, nd_body, loc); rb_node_module_t *n = NODE_NEWNODE(NODE_MODULE, rb_node_module_t, loc); n->nd_cpath = nd_cpath; n->nd_body = scope; + n->module_keyword_loc = *module_keyword_loc; + n->end_keyword_loc = *end_keyword_loc; return n; } diff --git a/rubyparser.h b/rubyparser.h index 9fd6906ca6..e436d1c404 100644 --- a/rubyparser.h +++ b/rubyparser.h @@ -901,6 +901,8 @@ typedef struct RNode_MODULE { struct RNode *nd_cpath; struct RNode *nd_body; + rb_code_location_t module_keyword_loc; + rb_code_location_t end_keyword_loc; } rb_node_module_t; typedef struct RNode_SCLASS { diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 5524fa7146..6372b0d34e 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -1491,6 +1491,11 @@ dummy assert_locations(node.children[-1].locations, [[1, 0, 1, 20], [1, 0, 1, 2], [1, 10, 1, 12], [1, 17, 1, 20]]) end + def test_module_locations + node = ast_parse('module A end') + assert_locations(node.children[-1].locations, [[1, 0, 1, 12], [1, 0, 1, 6], [1, 9, 1, 12]]) + end + def test_if_locations node = ast_parse("if cond then 1 else 2 end") assert_locations(node.children[-1].locations, [[1, 0, 1, 25], [1, 0, 1, 2], [1, 8, 1, 12], [1, 22, 1, 25]]) From ebb775be8d30e42da66597997bccf57c0d33f58d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 6 Aug 2025 10:05:20 -0700 Subject: [PATCH 046/157] ZJIT: Fix "immediate value too large" on cmp for x86_64 (#14125) Co-authored-by: Alan Wu --- test/ruby/test_zjit.rb | 8 ++++++++ zjit/src/backend/x86_64/mod.rs | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 6db57e18ba..e3ef6f6569 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -283,6 +283,14 @@ class TestZJIT < Test::Unit::TestCase }, insns: [:opt_eq], call_threshold: 2 end + def test_opt_eq_with_minus_one + assert_compiles '[false, true]', %q{ + def test(a) = a == -1 + test(1) # profile opt_eq + [test(0), test(-1)] + }, insns: [:opt_eq], call_threshold: 2 + end + def test_opt_neq_dynamic # TODO(max): Don't split this test; instead, run all tests with and without # profiling. diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index d21c7ee09c..f7aa1acc4d 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -381,7 +381,7 @@ impl Assembler mov(cb, Assembler::SCRATCH0, opnd.into()); Assembler::SCRATCH0 } else { - opnd.into() + imm_opnd(*value as i64) } }, _ => opnd.into() @@ -963,7 +963,9 @@ mod tests { asm.cmp(Opnd::Reg(RAX_REG), Opnd::UImm(0xFF)); asm.compile_with_num_regs(&mut cb, 0); - assert_eq!(format!("{:x}", cb), "4881f8ff000000"); + assert_disasm!(cb, "4881f8ff000000", " + 0x0: cmp rax, 0xff + "); } #[test] @@ -973,7 +975,22 @@ mod tests { asm.cmp(Opnd::Reg(RAX_REG), Opnd::UImm(0xFFFF_FFFF_FFFF)); asm.compile_with_num_regs(&mut cb, 0); - assert_eq!(format!("{:x}", cb), "49bbffffffffffff00004c39d8"); + assert_disasm!(cb, "49bbffffffffffff00004c39d8", " + 0x0: movabs r11, 0xffffffffffff + 0xa: cmp rax, r11 + "); + } + + #[test] + fn test_emit_cmp_64_bits() { + let (mut asm, mut cb) = setup_asm(); + + asm.cmp(Opnd::Reg(RAX_REG), Opnd::UImm(0xFFFF_FFFF_FFFF_FFFF)); + asm.compile_with_num_regs(&mut cb, 0); + + assert_disasm!(cb, "4883f8ff", " + 0x0: cmp rax, -1 + "); } #[test] @@ -1051,7 +1068,9 @@ mod tests { asm.test(Opnd::Reg(RAX_REG), Opnd::UImm(0xFF)); asm.compile_with_num_regs(&mut cb, 0); - assert_eq!(format!("{:x}", cb), "f6c0ff"); + assert_disasm!(cb, "48f7c0ff000000", " + 0x0: test rax, 0xff + "); } #[test] From 71b46938a7dd61304f012025e06891e8fe2e37fb Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 5 Aug 2025 15:53:02 -0400 Subject: [PATCH 047/157] Fix off-by-one in symbol next_id Symbol last_id was changed to next_id, but it remained to be set to tNEXT_ID - 1 initially, causing the initial static symbol to overlap with the last built-in symbol in id.def. --- symbol.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/symbol.c b/symbol.c index abb2c76dc2..c337cc288e 100644 --- a/symbol.c +++ b/symbol.c @@ -99,7 +99,9 @@ typedef struct { VALUE ids; } rb_symbols_t; -rb_symbols_t ruby_global_symbols = {tNEXT_ID-1}; +rb_symbols_t ruby_global_symbols = { + .next_id = tNEXT_ID, +}; struct sym_set_static_sym_entry { VALUE sym; From 21cb1c9e929e9aaad24a9b64576aceb54b8e4e7a Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 6 Aug 2025 01:25:27 -0400 Subject: [PATCH 048/157] ZJIT: x86: split: Fix live ranges index-out-of-range panic Previously we crashed panicked due to index bounds check running test_fixnum.rb. On ARM and in other places in the x86 backend, this isn't a problem because they inspect the output of instructions which is never replaced. --- zjit/src/backend/x86_64/mod.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index f7aa1acc4d..8027c74b18 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -148,6 +148,15 @@ impl Assembler }; } + // When we split an operand, we can create a new VReg not in `live_ranges`. + // So when we see a VReg with out-of-range index, it's created from splitting + // from the loop above and we know it doesn't outlive the current instruction. + let vreg_outlives_insn = |vreg_idx| { + live_ranges + .get(vreg_idx) + .map_or(false, |live_range: &LiveRange| live_range.end() > index) + }; + // We are replacing instructions here so we know they are already // being used. It is okay not to use their output here. #[allow(unused_must_use)] @@ -183,7 +192,7 @@ impl Assembler }, // Instruction output whose live range spans beyond this instruction (Opnd::VReg { idx, .. }, _) => { - if live_ranges[idx].end() > index { + if vreg_outlives_insn(idx) { *left = asm.load(*left); } }, @@ -248,7 +257,7 @@ impl Assembler match opnd { // Instruction output whose live range spans beyond this instruction Opnd::VReg { idx, .. } => { - if live_ranges[*idx].end() > index { + if vreg_outlives_insn(*idx) { *opnd = asm.load(*opnd); } }, @@ -272,7 +281,7 @@ impl Assembler // If we have an instruction output whose live range // spans beyond this instruction, we have to load it. Opnd::VReg { idx, .. } => { - if live_ranges[idx].end() > index { + if vreg_outlives_insn(idx) { *truthy = asm.load(*truthy); } }, @@ -307,7 +316,7 @@ impl Assembler // If we have an instruction output whose live range // spans beyond this instruction, we have to load it. Opnd::VReg { idx, .. } => { - if live_ranges[idx].end() > index { + if vreg_outlives_insn(idx) { *opnd = asm.load(*opnd); } }, From e378a21a32bb0ca26f206f975073760a4e1a8bef Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 6 Aug 2025 01:27:16 -0400 Subject: [PATCH 049/157] ZJIT: Run TestFixnum --- test/.excludes-zjit/TestFixnum.rb | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 test/.excludes-zjit/TestFixnum.rb diff --git a/test/.excludes-zjit/TestFixnum.rb b/test/.excludes-zjit/TestFixnum.rb deleted file mode 100644 index aaf8760f2f..0000000000 --- a/test/.excludes-zjit/TestFixnum.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Issue: https://github.com/Shopify/ruby/issues/646 -exclude(/test_/, 'Tests make ZJIT panic on Ubuntu') From 4a70f946a7131da871ca2aef8256a8b94299eba6 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 6 Aug 2025 21:51:41 +0100 Subject: [PATCH 050/157] ZJIT: Implement SingleRactorMode invalidation (#14121) * ZJIT: Implement SingleRactorMode invalidation * ZJIT: Add macro for compiling jumps * ZJIT: Fix typo in comment * YJIT: Fix typo in comment * ZJIT: Avoid using unexported types in zjit.h `enum ruby_vminsn_type` is declared in `insns.inc` and is not exported. Using it in `zjit.h` would cause build errors when the file including it doesn't include `insns.inc`. --- depend | 1 + ractor.c | 2 ++ test/ruby/test_zjit.rb | 20 +++++++++++ yjit/src/invariants.rs | 2 +- zjit.h | 8 +++-- zjit/src/codegen.rs | 8 ++--- zjit/src/invariants.rs | 75 ++++++++++++++++++++++++++++-------------- zjit/src/profile.rs | 4 +-- 8 files changed, 85 insertions(+), 35 deletions(-) diff --git a/depend b/depend index ecaf33b1c7..ea2486e9e8 100644 --- a/depend +++ b/depend @@ -12702,6 +12702,7 @@ ractor.$(OBJEXT): {$(VPATH)}vm_debug.h ractor.$(OBJEXT): {$(VPATH)}vm_opts.h ractor.$(OBJEXT): {$(VPATH)}vm_sync.h ractor.$(OBJEXT): {$(VPATH)}yjit.h +ractor.$(OBJEXT): {$(VPATH)}zjit.h random.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h random.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h random.$(OBJEXT): $(CCAN_DIR)/list/list.h diff --git a/ractor.c b/ractor.c index dc83ccc1b4..096bda5df6 100644 --- a/ractor.c +++ b/ractor.c @@ -19,6 +19,7 @@ #include "internal/thread.h" #include "variable.h" #include "yjit.h" +#include "zjit.h" VALUE rb_cRactor; static VALUE rb_cRactorSelector; @@ -511,6 +512,7 @@ ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VAL r->debug = cr->debug; rb_yjit_before_ractor_spawn(); + rb_zjit_before_ractor_spawn(); rb_thread_create_ractor(r, args, block); RB_GC_GUARD(rv); diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index e3ef6f6569..ca6293ce04 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -950,6 +950,26 @@ class TestZJIT < Test::Unit::TestCase RUBY end + def test_single_ractor_mode_invalidation + # Without invalidating the single-ractor mode, the test would crash + assert_compiles '"errored but not crashed"', <<~RUBY, call_threshold: 2, insns: [:opt_getconstant_path] + C = Object.new + + def test + C + rescue Ractor::IsolationError + "errored but not crashed" + end + + test + test + + Ractor.new { + test + }.value + RUBY + end + def test_dupn assert_compiles '[[1], [1, 1], :rhs, [nil, :rhs]]', <<~RUBY, insns: [:dupn] def test(array) = (array[1, 2] ||= :rhs) diff --git a/yjit/src/invariants.rs b/yjit/src/invariants.rs index a1a7d300aa..6ae1342ce3 100644 --- a/yjit/src/invariants.rs +++ b/yjit/src/invariants.rs @@ -303,7 +303,7 @@ pub extern "C" fn rb_yjit_cme_invalidate(callee_cme: *const rb_callable_method_e }); } -/// Callback for then Ruby is about to spawn a ractor. In that case we need to +/// Callback for when Ruby is about to spawn a ractor. In that case we need to /// invalidate every block that is assuming single ractor mode. #[no_mangle] pub extern "C" fn rb_yjit_before_ractor_spawn() { diff --git a/zjit.h b/zjit.h index 5ce2826d06..adf47046f8 100644 --- a/zjit.h +++ b/zjit.h @@ -14,7 +14,7 @@ extern bool rb_zjit_enabled_p; extern uint64_t rb_zjit_call_threshold; extern uint64_t rb_zjit_profile_threshold; void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); -void rb_zjit_profile_insn(enum ruby_vminsn_type insn, rb_execution_context_t *ec); +void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec); void rb_zjit_profile_enable(const rb_iseq_t *iseq); void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); void rb_zjit_cme_invalidate(const rb_callable_method_entry_t *cme); @@ -22,15 +22,17 @@ void rb_zjit_invalidate_ep_is_bp(const rb_iseq_t *iseq); void rb_zjit_constant_state_changed(ID id); void rb_zjit_iseq_mark(void *payload); void rb_zjit_iseq_update_references(void *payload); +void rb_zjit_before_ractor_spawn(void); #else #define rb_zjit_enabled_p false static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {} -static inline void rb_zjit_profile_insn(enum ruby_vminsn_type insn, rb_execution_context_t *ec) {} +static inline void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec) {} static inline void rb_zjit_profile_enable(const rb_iseq_t *iseq) {} static inline void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} static inline void rb_zjit_cme_invalidate(const rb_callable_method_entry_t *cme) {} static inline void rb_zjit_invalidate_ep_is_bp(const rb_iseq_t *iseq) {} static inline void rb_zjit_constant_state_changed(ID id) {} -#endif // #if USE_YJIT +static inline void rb_zjit_before_ractor_spawn(void) {} +#endif // #if USE_ZJIT #endif // #ifndef ZJIT_H diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 0db4d6b781..b05aaca682 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -4,7 +4,7 @@ use std::ffi::{c_int, c_void}; use crate::asm::Label; use crate::backend::current::{Reg, ALLOC_REGS}; -use crate::invariants::{track_bop_assumption, track_cme_assumption, track_stable_constant_names_assumption}; +use crate::invariants::{track_bop_assumption, track_cme_assumption, track_single_ractor_assumption, track_stable_constant_names_assumption}; use crate::gc::{get_or_create_iseq_payload, append_gc_offsets}; use crate::state::ZJITState; use crate::stats::{counter_ptr, Counter}; @@ -542,9 +542,9 @@ fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, invariant: &Invarian let side_exit_ptr = cb.resolve_label(label); track_stable_constant_names_assumption(idlist, code_ptr, side_exit_ptr); } - _ => { - debug!("ZJIT: gen_patch_point: unimplemented invariant {invariant:?}"); - return; + Invariant::SingleRactorMode => { + let side_exit_ptr = cb.resolve_label(label); + track_single_ractor_assumption(code_ptr, side_exit_ptr); } } }); diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 25cffb970e..c8c91dc45b 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -1,7 +1,20 @@ -use std::{collections::{HashMap, HashSet}}; +use std::{collections::{HashMap, HashSet}, mem}; use crate::{backend::lir::{asm_comment, Assembler}, cruby::{rb_callable_method_entry_t, ruby_basic_operators, src_loc, with_vm_lock, IseqPtr, RedefinitionFlag, ID}, hir::Invariant, options::debug, state::{zjit_enabled_p, ZJITState}, virtualmem::CodePtr}; +macro_rules! compile_jumps { + ($cb:expr, $jumps:expr, $($comment_args:tt)*) => { + for jump in $jumps { + $cb.with_write_ptr(jump.from, |cb| { + let mut asm = Assembler::new(); + asm_comment!(asm, $($comment_args)*); + asm.jmp(jump.to.into()); + asm.compile(cb).expect("can write existing code"); + }); + } + }; +} + #[derive(Debug, Eq, Hash, PartialEq)] struct Jump { from: CodePtr, @@ -26,6 +39,9 @@ pub struct Invariants { /// Map from constant ID to patch points that assume the constant hasn't been redefined constant_state_patch_points: HashMap>, + + /// Set of patch points that assume that the interpreter is running with only one ractor + single_ractor_patch_points: HashSet, } /// Called when a basic operator is redefined. Note that all the blocks assuming @@ -46,14 +62,7 @@ pub extern "C" fn rb_zjit_bop_redefined(klass: RedefinitionFlag, bop: ruby_basic debug!("BOP is redefined: {}", bop); // Invalidate all patch points for this BOP - for jump in jumps { - cb.with_write_ptr(jump.from, |cb| { - let mut asm = Assembler::new(); - asm_comment!(asm, "BOP is redefined: {}", bop); - asm.jmp(jump.to.into()); - asm.compile(cb).expect("can write existing code"); - }); - } + compile_jumps!(cb, jumps, "BOP is redefined: {}", bop); cb.mark_all_executable(); } @@ -159,14 +168,8 @@ pub extern "C" fn rb_zjit_cme_invalidate(cme: *const rb_callable_method_entry_t) debug!("CME is invalidated: {:?}", cme); // Invalidate all patch points for this CME - for jump in jumps { - cb.with_write_ptr(jump.from, |cb| { - let mut asm = Assembler::new(); - asm_comment!(asm, "CME is invalidated: {:?}", cme); - asm.jmp(jump.to.into()); - asm.compile(cb).expect("can write existing code"); - }); - } + compile_jumps!(cb, jumps, "CME is invalidated: {:?}", cme); + cb.mark_all_executable(); } }); @@ -187,16 +190,38 @@ pub extern "C" fn rb_zjit_constant_state_changed(id: ID) { debug!("Constant state changed: {:?}", id); // Invalidate all patch points for this constant ID - for jump in jumps { - cb.with_write_ptr(jump.from, |cb| { - let mut asm = Assembler::new(); - asm_comment!(asm, "Constant state changed: {:?}", id); - asm.jmp(jump.to.into()); - asm.compile(cb).expect("can write existing code"); - }); - } + compile_jumps!(cb, jumps, "Constant state changed: {:?}", id); cb.mark_all_executable(); } }); } + +/// Track the JIT code that assumes that the interpreter is running with only one ractor +pub fn track_single_ractor_assumption(patch_point_ptr: CodePtr, side_exit_ptr: CodePtr) { + let invariants = ZJITState::get_invariants(); + invariants.single_ractor_patch_points.insert(Jump { + from: patch_point_ptr, + to: side_exit_ptr, + }); +} + +/// Callback for when Ruby is about to spawn a ractor. In that case we need to +/// invalidate every block that is assuming single ractor mode. +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_before_ractor_spawn() { + // If ZJIT isn't enabled, do nothing + if !zjit_enabled_p() { + return; + } + + with_vm_lock(src_loc!(), || { + let cb = ZJITState::get_code_block(); + let jumps = mem::take(&mut ZJITState::get_invariants().single_ractor_patch_points); + + // Invalidate all patch points for single ractor mode + compile_jumps!(cb, jumps, "Another ractor spawned, invalidating single ractor mode assumption"); + + cb.mark_all_executable(); + }); +} diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index a99229604b..12b10b98ee 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -39,10 +39,10 @@ impl Profiler { /// API called from zjit_* instruction. opcode is the bare (non-zjit_*) instruction. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { +pub extern "C" fn rb_zjit_profile_insn(bare_opcode: u32, ec: EcPtr) { with_vm_lock(src_loc!(), || { let mut profiler = Profiler::new(ec); - profile_insn(&mut profiler, bare_opcode); + profile_insn(&mut profiler, bare_opcode as ruby_vminsn_type); }); } From ba4a36e226d9a4203b0721f456e894efe4629bd0 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 6 Aug 2025 13:56:01 -0700 Subject: [PATCH 051/157] ZJIT: Inline attr_reader/attr_accessor (#14126) We can rewrite SendWithoutBlock to GetIvar. --- test/ruby/test_zjit.rb | 32 +++++++++++ zjit/src/hir.rs | 125 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 144 insertions(+), 13 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index ca6293ce04..8115a60166 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -887,6 +887,38 @@ class TestZJIT < Test::Unit::TestCase } end + def test_attr_reader + assert_compiles '[4, 4]', %q{ + class C + attr_reader :foo + + def initialize + @foo = 4 + end + end + + def test(c) = c.foo + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_attr_accessor + assert_compiles '[4, 4]', %q{ + class C + attr_accessor :foo + + def initialize + @foo = 4 + end + end + + def test(c) = c.foo + c = C.new + [test(c), test(c)] + }, call_threshold: 2, insns: [:opt_send_without_block] + end + def test_uncached_getconstant_path assert_compiles RUBY_COPYRIGHT.dump, %q{ def test = RUBY_COPYRIGHT diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1a67037ed3..7e92f59329 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1537,22 +1537,31 @@ impl Function { // It allows you to use a faster ISEQ if possible. cme = unsafe { rb_check_overloaded_cme(cme, ci) }; let def_type = unsafe { get_cme_def_type(cme) }; - if def_type != VM_METHOD_TYPE_ISEQ { + if def_type == VM_METHOD_TYPE_ISEQ { // TODO(max): Allow non-iseq; cache cme + // Only specialize positional-positional calls + // TODO(max): Handle other kinds of parameter passing + let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; + if !can_direct_send(iseq) { + self.push_insn_id(block, insn_id); continue; + } + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); + if let Some(profiled_type) = profiled_type { + self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); + } + let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, cd, cme, iseq, args, state }); + self.make_equal_to(insn_id, send_direct); + } else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() { + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); + if let Some(profiled_type) = profiled_type { + self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); + } + let id = unsafe { get_cme_def_body_attr_id(cme) }; + let getivar = self.push_insn(block, Insn::GetIvar { self_val, id, state }); + self.make_equal_to(insn_id, getivar); + } else { self.push_insn_id(block, insn_id); continue; } - // Only specialize positional-positional calls - // TODO(max): Handle other kinds of parameter passing - let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; - if !can_direct_send(iseq) { - self.push_insn_id(block, insn_id); continue; - } - self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); - if let Some(profiled_type) = profiled_type { - self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); - } - let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, cd, cme, iseq, args, state }); - self.make_equal_to(insn_id, send_direct); } Insn::GetConstantPath { ic, state, .. } => { let idlist: *const ID = unsafe { (*ic).segments }; @@ -7422,4 +7431,94 @@ mod opt_tests { Return v7 "#]]); } + + #[test] + fn test_inline_attr_reader_constant() { + eval(" + class C + attr_reader :foo + end + + O = C.new + def test = O.foo + test + test + "); + assert_optimized_method_hir("test", expect![[r#" + fn test@:7: + bb0(v0:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, O) + v9:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(C@0x1010, foo@0x1018, cme:0x1020) + v11:BasicObject = GetIvar v9, :@foo + Return v11 + "#]]); + } + + #[test] + fn test_inline_attr_accessor_constant() { + eval(" + class C + attr_accessor :foo + end + + O = C.new + def test = O.foo + test + test + "); + assert_optimized_method_hir("test", expect![[r#" + fn test@:7: + bb0(v0:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, O) + v9:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(C@0x1010, foo@0x1018, cme:0x1020) + v11:BasicObject = GetIvar v9, :@foo + Return v11 + "#]]); + } + + #[test] + fn test_inline_attr_reader() { + eval(" + class C + attr_reader :foo + end + + def test(o) = o.foo + test C.new + test C.new + "); + assert_optimized_method_hir("test", expect![[r#" + fn test@:6: + bb0(v0:BasicObject, v1:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) + v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v8:BasicObject = GetIvar v7, :@foo + Return v8 + "#]]); + } + + #[test] + fn test_inline_attr_accessor() { + eval(" + class C + attr_accessor :foo + end + + def test(o) = o.foo + test C.new + test C.new + "); + assert_optimized_method_hir("test", expect![[r#" + fn test@:6: + bb0(v0:BasicObject, v1:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) + v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v8:BasicObject = GetIvar v7, :@foo + Return v8 + "#]]); + } } From a9f6fe0914cb65f16cdf82390fa070ad76e54a8d Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 6 Aug 2025 14:05:03 -0700 Subject: [PATCH 052/157] Avoid marking CC children after invalidation Once klass becomes Qundef, it's disconnected and won't be invalidated when the CME is. So once that happens we must not mark or attempt to move the cme_ field. --- imemo.c | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/imemo.c b/imemo.c index 7298d78d65..2c721ca911 100644 --- a/imemo.c +++ b/imemo.c @@ -337,28 +337,37 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) * cc->klass (klass) should not be marked because if the klass is * free'ed, the cc->klass will be cleared by `vm_cc_invalidate()`. * - * cc->cme (cme) should not be marked because if cc is invalidated - * when cme is free'ed. + * For "normal" CCs cc->cme (cme) should not be marked because the cc is + * invalidated through the klass when the cme is free'd. * - klass marks cme if klass uses cme. - * - caller classe's ccs->cme marks cc->cme. - * - if cc is invalidated (klass doesn't refer the cc), - * cc is invalidated by `vm_cc_invalidate()` and cc->cme is - * not be accessed. - * - On the multi-Ractors, cme will be collected with global GC + * - caller class's ccs->cme marks cc->cme. + * - if cc is invalidated (klass doesn't refer the cc), cc is + * invalidated by `vm_cc_invalidate()` after which cc->cme must not + * be accessed. + * - With multi-Ractors, cme will be collected with global GC * so that it is safe if GC is not interleaving while accessing * cc and cme. - * - However, cc_type_super and cc_type_refinement are not chained - * from ccs so cc->cme should be marked; the cme might be - * reachable only through cc in these cases. + * + * However cc_type_super and cc_type_refinement are not chained + * from ccs so cc->cme should be marked as long as the cc is valid; + * the cme might be reachable only through cc in these cases. */ struct rb_callcache *cc = (struct rb_callcache *)obj; - if (reference_updating) { + if (UNDEF_P(cc->klass)) { + /* If it's invalidated, we must not mark anything. + * All fields should are considered invalid + */ + } + else if (reference_updating) { if (moved_or_living_object_strictly_p((VALUE)cc->cme_)) { *((VALUE *)&cc->klass) = rb_gc_location(cc->klass); *((struct rb_callable_method_entry_struct **)&cc->cme_) = (struct rb_callable_method_entry_struct *)rb_gc_location((VALUE)cc->cme_); + + RUBY_ASSERT(RB_TYPE_P(cc->klass, T_CLASS) || RB_TYPE_P(cc->klass, T_ICLASS)); + RUBY_ASSERT(IMEMO_TYPE_P((VALUE)cc->cme_, imemo_ment)); } - else if (vm_cc_valid(cc)) { + else { vm_cc_invalidate(cc); } } From 640a2f1dc77c0ecf226dbd71cf7a1eb876a1f037 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 6 Aug 2025 13:44:46 -0700 Subject: [PATCH 053/157] Ensure ObjectSpace.dump won't call cc_cme on invalidated CC --- ext/objspace/objspace_dump.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index f90ad89b5a..94a9d43f98 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -451,13 +451,16 @@ dump_object(VALUE obj, struct dump_config *dc) break; case imemo_callcache: - mid = vm_cc_cme((const struct rb_callcache *)obj)->called_id; - if (mid != 0) { - dump_append(dc, ", \"called_id\":"); - dump_append_id(dc, mid); - + { VALUE klass = ((const struct rb_callcache *)obj)->klass; - if (klass != 0) { + if (klass != Qundef) { + mid = vm_cc_cme((const struct rb_callcache *)obj)->called_id; + if (mid != 0) { + dump_append(dc, ", \"called_id\":"); + dump_append_id(dc, mid); + + } + dump_append(dc, ", \"receiver_class\":"); dump_append_ref(dc, klass); } From fccd96cc6c3cc9f500dc87ae9be65aaa212b02fa Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 6 Aug 2025 13:53:00 -0700 Subject: [PATCH 054/157] Add stricter assertions on CC access --- vm_callinfo.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vm_callinfo.h b/vm_callinfo.h index 3b6880e320..79ccbfa7ab 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -418,6 +418,8 @@ static inline const struct rb_callable_method_entry_struct * vm_cc_cme(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc)); + VM_ASSERT(cc_check_class(cc->klass)); VM_ASSERT(cc->call_ == NULL || // not initialized yet !vm_cc_markable(cc) || cc->cme_ != NULL); @@ -430,6 +432,8 @@ vm_cc_call(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); VM_ASSERT(cc->call_ != NULL); + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc)); + VM_ASSERT(cc_check_class(cc->klass)); return cc->call_; } From 24d0b458cd6ccde1e052c1db15af120def349693 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 6 Aug 2025 11:09:51 -0400 Subject: [PATCH 055/157] Make Random write-barrier protected --- random.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/random.c b/random.c index 1611c3db97..85d72057cd 100644 --- a/random.c +++ b/random.c @@ -263,7 +263,7 @@ const rb_data_type_t rb_random_data_type = { random_free, random_memsize, }, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; #define random_mt_mark rb_random_mark @@ -284,7 +284,7 @@ static const rb_data_type_t random_mt_type = { }, &rb_random_data_type, (void *)&random_mt_if, - RUBY_TYPED_FREE_IMMEDIATELY + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static rb_random_t * @@ -422,10 +422,10 @@ random_init(int argc, VALUE *argv, VALUE obj) argc = rb_check_arity(argc, 0, 1); rb_check_frozen(obj); if (argc == 0) { - rnd->seed = rand_init_default(rng, rnd); + RB_OBJ_WRITE(obj, &rnd->seed, rand_init_default(rng, rnd)); } else { - rnd->seed = rand_init(rng, rnd, rb_to_int(argv[0])); + RB_OBJ_WRITE(obj, &rnd->seed, rand_init(rng, rnd, rb_to_int(argv[0]))); } return obj; } From a260bbc550adf2a685e40ea492334a1d1f3a0222 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 11:28:10 -0700 Subject: [PATCH 056/157] ZJIT: Set PC before StringCopy (#14141) ZJIT: Set PC before StringCopy This function allocates. --- zjit/src/codegen.rs | 5 ++-- zjit/src/hir.rs | 64 +++++++++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b05aaca682..a5439bf2ca 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -329,7 +329,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)), Insn::NewRange { low, high, flag, state } => gen_new_range(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), - Insn::StringCopy { val, chilled } => gen_string_copy(asm, opnd!(val), *chilled), + Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), Insn::Param { idx } => unreachable!("block.insns should not have Insn::Param({idx})"), Insn::Snapshot { .. } => return Some(()), // we don't need to do anything for this instruction at the moment Insn::Jump(branch) => return gen_jump(jit, asm, branch), @@ -871,8 +871,9 @@ fn gen_send_without_block_direct( } /// Compile a string resurrection -fn gen_string_copy(asm: &mut Assembler, recv: Opnd, chilled: bool) -> Opnd { +fn gen_string_copy(asm: &mut Assembler, recv: Opnd, chilled: bool, state: &FrameState) -> Opnd { // TODO: split rb_ec_str_resurrect into separate functions + gen_prepare_call_with_gc(asm, state); let chilled = if chilled { Opnd::Imm(1) } else { Opnd::Imm(0) }; asm_ccall!(asm, rb_ec_str_resurrect, EC, recv, chilled) } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 7e92f59329..f4dae3f0ec 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -443,7 +443,7 @@ pub enum Insn { /// SSA block parameter. Also used for function parameters in the function's entry block. Param { idx: usize }, - StringCopy { val: InsnId, chilled: bool }, + StringCopy { val: InsnId, chilled: bool, state: InsnId }, StringIntern { val: InsnId }, /// Put special object (VMCORE, CBASE, etc.) based on value_type @@ -1116,7 +1116,7 @@ impl Function { }, &Return { val } => Return { val: find!(val) }, &Throw { throw_state, val } => Throw { throw_state, val: find!(val) }, - &StringCopy { val, chilled } => StringCopy { val: find!(val), chilled }, + &StringCopy { val, chilled, state } => StringCopy { val: find!(val), chilled, state }, &StringIntern { val } => StringIntern { val: find!(val) }, &Test { val } => Test { val: find!(val) }, &IsNil { val } => IsNil { val: find!(val) }, @@ -1870,7 +1870,6 @@ impl Function { worklist.push_back(high); worklist.push_back(state); } - &Insn::StringCopy { val, .. } | &Insn::StringIntern { val } | &Insn::Return { val } | &Insn::Throw { val, .. } @@ -1880,6 +1879,7 @@ impl Function { | &Insn::IsNil { val } => worklist.push_back(val), &Insn::SetGlobal { val, state, .. } + | &Insn::StringCopy { val, state, .. } | &Insn::GuardType { val, state, .. } | &Insn::GuardBitEquals { val, state, .. } | &Insn::ToArray { val, state } @@ -2667,12 +2667,14 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } YARVINSN_putstring => { let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); - let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: false }); + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: false, state: exit_id }); state.stack_push(insn_id); } YARVINSN_putchilledstring => { let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) }); - let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: true }); + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: true, state: exit_id }); state.stack_push(insn_id); } YARVINSN_putself => { state.stack_push(self_param); } @@ -3862,8 +3864,8 @@ mod tests { fn test@:1: bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v3:StringExact = StringCopy v2 - Return v3 + v4:StringExact = StringCopy v2 + Return v4 "#]]); } @@ -4390,11 +4392,11 @@ mod tests { v5:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v7:ArrayExact = ArrayDup v5 v8:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) - v9:StringExact = StringCopy v8 - v10:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) - v11:StringExact = StringCopy v10 - v13:BasicObject = SendWithoutBlock v0, :unknown_method, v4, v7, v9, v11 - Return v13 + v10:StringExact = StringCopy v8 + v11:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) + v13:StringExact = StringCopy v11 + v15:BasicObject = SendWithoutBlock v0, :unknown_method, v4, v7, v10, v13 + Return v15 "#]]); } @@ -4640,7 +4642,7 @@ mod tests { v4:NilClass = Const Value(nil) v7:BasicObject = SendWithoutBlock v1, :+, v2 v8:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v9:StringExact = StringCopy v8 + v10:StringExact = StringCopy v8 SideExit UnknownNewarraySend(PACK) "#]]); } @@ -5943,8 +5945,8 @@ mod opt_tests { assert_optimized_method_hir("test", expect![[r#" fn test@:3: bb0(v0:BasicObject): - v5:Fixnum[5] = Const Value(5) - Return v5 + v6:Fixnum[5] = Const Value(5) + Return v6 "#]]); } @@ -6572,10 +6574,10 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v3:StringExact = StringCopy v2 + v4:StringExact = StringCopy v2 PatchPoint MethodRedefined(String@0x1008, bytesize@0x1010, cme:0x1018) - v8:Fixnum = CCall bytesize@0x1040, v3 - Return v8 + v9:Fixnum = CCall bytesize@0x1040, v4 + Return v9 "#]]); } @@ -6918,10 +6920,10 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v3:StringExact = StringCopy v2 - v5:BasicObject = SendWithoutBlock v3, :dup - v7:BasicObject = SendWithoutBlock v5, :freeze - Return v7 + v4:StringExact = StringCopy v2 + v6:BasicObject = SendWithoutBlock v4, :dup + v8:BasicObject = SendWithoutBlock v6, :freeze + Return v8 "#]]); } @@ -6934,10 +6936,10 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v3:StringExact = StringCopy v2 - v4:NilClass = Const Value(nil) - v6:BasicObject = SendWithoutBlock v3, :freeze, v4 - Return v6 + v4:StringExact = StringCopy v2 + v5:NilClass = Const Value(nil) + v7:BasicObject = SendWithoutBlock v4, :freeze, v5 + Return v7 "#]]); } @@ -6979,10 +6981,10 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) - v3:StringExact = StringCopy v2 - v5:BasicObject = SendWithoutBlock v3, :dup - v7:BasicObject = SendWithoutBlock v5, :-@ - Return v7 + v4:StringExact = StringCopy v2 + v6:BasicObject = SendWithoutBlock v4, :dup + v8:BasicObject = SendWithoutBlock v6, :-@ + Return v8 "#]]); } @@ -6996,7 +6998,7 @@ mod opt_tests { bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v4:StringExact = StringCopy v3 + v5:StringExact = StringCopy v3 SideExit UnknownOpcode(concatstrings) "#]]); } From 846b5eec57ad2c030b7136d89193215055529c4a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:38:19 +0200 Subject: [PATCH 057/157] Convert `marshal_compat_table` to use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- marshal.c | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/marshal.c b/marshal.c index 7db4bfc6d9..f7474ca60e 100644 --- a/marshal.c +++ b/marshal.c @@ -2572,19 +2572,19 @@ Init_marshal(void) } static int -marshal_compat_table_mark_i(st_data_t key, st_data_t value, st_data_t _) +marshal_compat_table_mark_and_move_i(st_data_t key, st_data_t value, st_data_t _) { marshal_compat_t *p = (marshal_compat_t *)value; - rb_gc_mark_movable(p->newclass); - rb_gc_mark_movable(p->oldclass); + rb_gc_mark_and_move(&p->newclass); + rb_gc_mark_and_move(&p->oldclass); return ST_CONTINUE; } static void -marshal_compat_table_mark(void *tbl) +marshal_compat_table_mark_and_move(void *tbl) { if (!tbl) return; - st_foreach(tbl, marshal_compat_table_mark_i, 0); + st_foreach(tbl, marshal_compat_table_mark_and_move_i, 0); } static int @@ -2607,29 +2607,13 @@ marshal_compat_table_memsize(const void *data) return st_memsize(data) + sizeof(marshal_compat_t) * st_table_size(data); } -static int -marshal_compat_table_compact_i(st_data_t key, st_data_t value, st_data_t _) -{ - marshal_compat_t *p = (marshal_compat_t *)value; - p->newclass = rb_gc_location(p->newclass); - p->oldclass = rb_gc_location(p->oldclass); - return ST_CONTINUE; -} - -static void -marshal_compat_table_compact(void *tbl) -{ - if (!tbl) return; - st_foreach(tbl, marshal_compat_table_compact_i, 0); -} - static const rb_data_type_t marshal_compat_type = { .wrap_struct_name = "marshal_compat_table", .function = { - .dmark = marshal_compat_table_mark, + .dmark = marshal_compat_table_mark_and_move, .dfree = marshal_compat_table_free, .dsize = marshal_compat_table_memsize, - .dcompact = marshal_compat_table_compact, + .dcompact = marshal_compat_table_mark_and_move, }, .flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY, }; From aee8e65c702283af8a70cb1fdac0797aa7c75001 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:41:21 +0200 Subject: [PATCH 058/157] Convert `Enumerator` types to use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- enumerator.c | 99 ++++++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 72 deletions(-) diff --git a/enumerator.c b/enumerator.c index ab03a273d5..8d02c836e9 100644 --- a/enumerator.c +++ b/enumerator.c @@ -280,28 +280,20 @@ enumerator_ptr(VALUE obj) } static void -proc_entry_mark(void *p) +proc_entry_mark_and_move(void *p) { struct proc_entry *ptr = p; - rb_gc_mark_movable(ptr->proc); - rb_gc_mark_movable(ptr->memo); -} - -static void -proc_entry_compact(void *p) -{ - struct proc_entry *ptr = p; - ptr->proc = rb_gc_location(ptr->proc); - ptr->memo = rb_gc_location(ptr->memo); + rb_gc_mark_and_move(&ptr->proc); + rb_gc_mark_and_move(&ptr->memo); } static const rb_data_type_t proc_entry_data_type = { "proc_entry", { - proc_entry_mark, + proc_entry_mark_and_move, RUBY_TYPED_DEFAULT_FREE, NULL, // Nothing allocated externally, so don't need a memsize function - proc_entry_compact, + proc_entry_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; @@ -1280,26 +1272,19 @@ enumerator_size(VALUE obj) * Yielder */ static void -yielder_mark(void *p) +yielder_mark_and_move(void *p) { struct yielder *ptr = p; - rb_gc_mark_movable(ptr->proc); -} - -static void -yielder_compact(void *p) -{ - struct yielder *ptr = p; - ptr->proc = rb_gc_location(ptr->proc); + rb_gc_mark_and_move(&ptr->proc); } static const rb_data_type_t yielder_data_type = { "yielder", { - yielder_mark, + yielder_mark_and_move, RUBY_TYPED_DEFAULT_FREE, NULL, - yielder_compact, + yielder_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; @@ -1410,28 +1395,20 @@ yielder_new(void) * Generator */ static void -generator_mark(void *p) +generator_mark_and_move(void *p) { struct generator *ptr = p; - rb_gc_mark_movable(ptr->proc); - rb_gc_mark_movable(ptr->obj); -} - -static void -generator_compact(void *p) -{ - struct generator *ptr = p; - ptr->proc = rb_gc_location(ptr->proc); - ptr->obj = rb_gc_location(ptr->obj); + rb_gc_mark_and_move(&ptr->proc); + rb_gc_mark_and_move(&ptr->obj); } static const rb_data_type_t generator_data_type = { "generator", { - generator_mark, + generator_mark_and_move, RUBY_TYPED_DEFAULT_FREE, NULL, - generator_compact, + generator_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; @@ -2894,19 +2871,11 @@ stop_result(VALUE self) */ static void -producer_mark(void *p) +producer_mark_and_move(void *p) { struct producer *ptr = p; - rb_gc_mark_movable(ptr->init); - rb_gc_mark_movable(ptr->proc); -} - -static void -producer_compact(void *p) -{ - struct producer *ptr = p; - ptr->init = rb_gc_location(ptr->init); - ptr->proc = rb_gc_location(ptr->proc); + rb_gc_mark_and_move(&ptr->init); + rb_gc_mark_and_move(&ptr->proc); } #define producer_free RUBY_TYPED_DEFAULT_FREE @@ -2920,10 +2889,10 @@ producer_memsize(const void *p) static const rb_data_type_t producer_data_type = { "producer", { - producer_mark, + producer_mark_and_move, producer_free, producer_memsize, - producer_compact, + producer_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; @@ -3082,17 +3051,10 @@ enumerator_s_produce(int argc, VALUE *argv, VALUE klass) */ static void -enum_chain_mark(void *p) +enum_chain_mark_and_move(void *p) { struct enum_chain *ptr = p; - rb_gc_mark_movable(ptr->enums); -} - -static void -enum_chain_compact(void *p) -{ - struct enum_chain *ptr = p; - ptr->enums = rb_gc_location(ptr->enums); + rb_gc_mark_and_move(&ptr->enums); } #define enum_chain_free RUBY_TYPED_DEFAULT_FREE @@ -3106,10 +3068,10 @@ enum_chain_memsize(const void *p) static const rb_data_type_t enum_chain_data_type = { "chain", { - enum_chain_mark, + enum_chain_mark_and_move, enum_chain_free, enum_chain_memsize, - enum_chain_compact, + enum_chain_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; @@ -3404,17 +3366,10 @@ enumerator_plus(VALUE obj, VALUE eobj) */ static void -enum_product_mark(void *p) +enum_product_mark_and_move(void *p) { struct enum_product *ptr = p; - rb_gc_mark_movable(ptr->enums); -} - -static void -enum_product_compact(void *p) -{ - struct enum_product *ptr = p; - ptr->enums = rb_gc_location(ptr->enums); + rb_gc_mark_and_move(&ptr->enums); } #define enum_product_free RUBY_TYPED_DEFAULT_FREE @@ -3428,10 +3383,10 @@ enum_product_memsize(const void *p) static const rb_data_type_t enum_product_data_type = { "product", { - enum_product_mark, + enum_product_mark_and_move, enum_product_free, enum_product_memsize, - enum_product_compact, + enum_product_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; From bc9781c264b9e37c2482dba429b560436104a1f5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:42:14 +0200 Subject: [PATCH 059/157] Convert `name_err_mesg` to use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- error.c | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/error.c b/error.c index 9a758a7dd7..e07c99e6df 100644 --- a/error.c +++ b/error.c @@ -2517,30 +2517,21 @@ typedef struct name_error_message_struct { } name_error_message_t; static void -name_err_mesg_mark(void *p) +name_err_mesg_mark_and_move(void *p) { name_error_message_t *ptr = (name_error_message_t *)p; - rb_gc_mark_movable(ptr->mesg); - rb_gc_mark_movable(ptr->recv); - rb_gc_mark_movable(ptr->name); -} - -static void -name_err_mesg_update(void *p) -{ - name_error_message_t *ptr = (name_error_message_t *)p; - ptr->mesg = rb_gc_location(ptr->mesg); - ptr->recv = rb_gc_location(ptr->recv); - ptr->name = rb_gc_location(ptr->name); + rb_gc_mark_and_move(&ptr->mesg); + rb_gc_mark_and_move(&ptr->recv); + rb_gc_mark_and_move(&ptr->name); } static const rb_data_type_t name_err_mesg_data_type = { "name_err_mesg", { - name_err_mesg_mark, + name_err_mesg_mark_and_move, RUBY_TYPED_DEFAULT_FREE, NULL, // No external memory to report, - name_err_mesg_update, + name_err_mesg_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; From 559d9e1f67cb6c19630b20b609af315ed7fdafca Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:43:18 +0200 Subject: [PATCH 060/157] Convert `VM/shape_tree` to use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- shape.c | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/shape.c b/shape.c index 6e1b49352f..d6d05e12d5 100644 --- a/shape.c +++ b/shape.c @@ -296,26 +296,13 @@ rb_shape_get_root_shape(void) } static void -shape_tree_mark(void *data) +shape_tree_mark_and_move(void *data) { rb_shape_t *cursor = rb_shape_get_root_shape(); rb_shape_t *end = RSHAPE(rb_shape_tree.next_shape_id - 1); while (cursor <= end) { if (cursor->edges && !SINGLE_CHILD_P(cursor->edges)) { - rb_gc_mark_movable(cursor->edges); - } - cursor++; - } -} - -static void -shape_tree_compact(void *data) -{ - rb_shape_t *cursor = rb_shape_get_root_shape(); - rb_shape_t *end = RSHAPE(rb_shape_tree.next_shape_id - 1); - while (cursor <= end) { - if (cursor->edges && !SINGLE_CHILD_P(cursor->edges)) { - cursor->edges = rb_gc_location(cursor->edges); + rb_gc_mark_and_move(&cursor->edges); } cursor++; } @@ -330,10 +317,10 @@ shape_tree_memsize(const void *data) static const rb_data_type_t shape_tree_type = { .wrap_struct_name = "VM/shape_tree", .function = { - .dmark = shape_tree_mark, + .dmark = shape_tree_mark_and_move, .dfree = NULL, // Nothing to free, done at VM exit in rb_shape_free_all, .dsize = shape_tree_memsize, - .dcompact = shape_tree_compact, + .dcompact = shape_tree_mark_and_move, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; From 5bcfc53d6fa5518a194f2d0771830eb961180991 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:44:48 +0200 Subject: [PATCH 061/157] set.c: use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- set.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/set.c b/set.c index f83fb0880c..c589fb4523 100644 --- a/set.c +++ b/set.c @@ -172,9 +172,7 @@ set_foreach_replace(st_data_t key, st_data_t argp, int error) static int set_replace_ref(st_data_t *key, st_data_t argp, int existing) { - if (rb_gc_location((VALUE)*key) != (VALUE)*key) { - *key = rb_gc_location((VALUE)*key); - } + rb_gc_mark_and_move((VALUE *)key); return ST_CONTINUE; } From 1986d775cdae6a0ab40a2528e2ec9b50e06eba70 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:47:37 +0200 Subject: [PATCH 062/157] symbol.c: use `rb_gc_mark_and_move` over `rb_gc_location` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- gc.c | 4 ++-- internal/symbol.h | 3 +-- symbol.c | 15 +++------------ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/gc.c b/gc.c index 64a22cd1b7..90f2b29bfa 100644 --- a/gc.c +++ b/gc.c @@ -3028,7 +3028,7 @@ rb_gc_mark_roots(void *objspace, const char **categoryp) mark_current_machine_context(ec); MARK_CHECKPOINT("global_symbols"); - rb_sym_global_symbols_mark(); + rb_sym_global_symbols_mark_and_move(); MARK_CHECKPOINT("finish"); @@ -4045,7 +4045,7 @@ rb_gc_update_vm_references(void *objspace) rb_vm_update_references(vm); rb_gc_update_global_tbl(); - rb_sym_global_symbols_update_references(); + rb_sym_global_symbols_mark_and_move(); #if USE_YJIT void rb_yjit_root_update_references(void); // in Rust diff --git a/internal/symbol.h b/internal/symbol.h index 8571c00289..b9109b1347 100644 --- a/internal/symbol.h +++ b/internal/symbol.h @@ -17,8 +17,7 @@ #endif /* symbol.c */ -void rb_sym_global_symbols_mark(void); -void rb_sym_global_symbols_update_references(void); +void rb_sym_global_symbols_mark_and_move(void); VALUE rb_to_symbol_type(VALUE obj); VALUE rb_sym_intern(const char *ptr, long len, rb_encoding *enc); VALUE rb_sym_intern_ascii(const char *ptr, long len); diff --git a/symbol.c b/symbol.c index c337cc288e..ddb0f1556b 100644 --- a/symbol.c +++ b/symbol.c @@ -371,21 +371,12 @@ Init_sym(void) } void -rb_sym_global_symbols_mark(void) +rb_sym_global_symbols_mark_and_move(void) { rb_symbols_t *symbols = &ruby_global_symbols; - rb_gc_mark_movable(symbols->sym_set); - rb_gc_mark_movable(symbols->ids); -} - -void -rb_sym_global_symbols_update_references(void) -{ - rb_symbols_t *symbols = &ruby_global_symbols; - - symbols->sym_set = rb_gc_location(symbols->sym_set); - symbols->ids = rb_gc_location(symbols->ids); + rb_gc_mark_and_move(&symbols->sym_set); + rb_gc_mark_and_move(&symbols->ids); } static int From 1aabd2cb365ff0a236609fe1c4ab193d16027757 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 14:48:30 +0200 Subject: [PATCH 063/157] Convert `time` to use `rb_gc_mark_and_move` The `p->field = rb_gc_location(p->field)` isn't ideal because it means all references are rewritten on compaction, regardless of whether the referenced object has moved. This isn't good for caches nor for Copy-on-Write. `rb_gc_mark_and_move` avoid needless writes, and most of the time allow to have a single function for both marking and updating references. --- time.c | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/time.c b/time.c index 4c2b15d90e..7159a93098 100644 --- a/time.c +++ b/time.c @@ -1888,39 +1888,25 @@ force_make_tm(VALUE time, struct time_object *tobj) } static void -time_mark(void *ptr) +time_mark_and_move(void *ptr) { struct time_object *tobj = ptr; if (!FIXWV_P(tobj->timew)) { - rb_gc_mark_movable(w2v(tobj->timew)); + rb_gc_mark_and_move(&WIDEVAL_GET(tobj->timew)); } - rb_gc_mark_movable(tobj->vtm.year); - rb_gc_mark_movable(tobj->vtm.subsecx); - rb_gc_mark_movable(tobj->vtm.utc_offset); - rb_gc_mark_movable(tobj->vtm.zone); -} - -static void -time_compact(void *ptr) -{ - struct time_object *tobj = ptr; - if (!FIXWV_P(tobj->timew)) { - WIDEVAL_GET(tobj->timew) = WIDEVAL_WRAP(rb_gc_location(w2v(tobj->timew))); - } - - tobj->vtm.year = rb_gc_location(tobj->vtm.year); - tobj->vtm.subsecx = rb_gc_location(tobj->vtm.subsecx); - tobj->vtm.utc_offset = rb_gc_location(tobj->vtm.utc_offset); - tobj->vtm.zone = rb_gc_location(tobj->vtm.zone); + rb_gc_mark_and_move(&tobj->vtm.year); + rb_gc_mark_and_move(&tobj->vtm.subsecx); + rb_gc_mark_and_move(&tobj->vtm.utc_offset); + rb_gc_mark_and_move(&tobj->vtm.zone); } static const rb_data_type_t time_data_type = { .wrap_struct_name = "time", .function = { - .dmark = time_mark, + .dmark = time_mark_and_move, .dfree = RUBY_TYPED_DEFAULT_FREE, .dsize = NULL, - .dcompact = time_compact, + .dcompact = time_mark_and_move, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, }; From 363ad0ad17c8ce36035c64ee3267ec680423bc68 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 12:11:55 -0700 Subject: [PATCH 064/157] ZJIT: Create HeapObject Type (#14140) This is a counterpoint to the Immediate type and it represents all BasicObject subclasses except for the several immediate objects. If we know something is a HeapObject, we know we can treat it as an RBasic pointer. --- zjit/src/hir.rs | 20 ++++++++++---------- zjit/src/hir_type/gen_hir_type.rb | 2 ++ zjit/src/hir_type/hir_type.inc.rs | 5 ++++- zjit/src/hir_type/mod.rs | 29 ++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f4dae3f0ec..f98aa846cd 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5566,7 +5566,7 @@ mod opt_tests { fn test@:5: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v6:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); @@ -5606,7 +5606,7 @@ mod opt_tests { fn test@:6: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v6:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); @@ -5625,7 +5625,7 @@ mod opt_tests { bb0(v0:BasicObject): v2:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) - v7:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v7:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1038), v2 Return v8 "#]]); @@ -5647,7 +5647,7 @@ mod opt_tests { v2:Fixnum[1] = Const Value(1) v3:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v8:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038), v2, v3 Return v9 "#]]); @@ -5670,10 +5670,10 @@ mod opt_tests { fn test@:7: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v8:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v8:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1038) PatchPoint MethodRedefined(Object@0x1000, bar@0x1040, cme:0x1048) - v11:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v11:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1038) Return v12 "#]]); @@ -6475,7 +6475,7 @@ mod opt_tests { fn test@:8: bb0(v0:BasicObject, v1:BasicObject): PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) - v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v7:HeapObject[class_exact:C] = GuardType v1, HeapObject[class_exact:C] v8:BasicObject = SendWithoutBlockDirect v7, :foo (0x1038) Return v8 "#]]); @@ -7428,7 +7428,7 @@ mod opt_tests { fn test@:3: bb0(v0:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v6:BasicObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, BasicObject[class_exact*:Object@VALUE(0x1000)] + v6:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v0, HeapObject[class_exact*:Object@VALUE(0x1000)] v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1038) Return v7 "#]]); @@ -7497,7 +7497,7 @@ mod opt_tests { fn test@:6: bb0(v0:BasicObject, v1:BasicObject): PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) - v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v7:HeapObject[class_exact:C] = GuardType v1, HeapObject[class_exact:C] v8:BasicObject = GetIvar v7, :@foo Return v8 "#]]); @@ -7518,7 +7518,7 @@ mod opt_tests { fn test@:6: bb0(v0:BasicObject, v1:BasicObject): PatchPoint MethodRedefined(C@0x1000, foo@0x1008, cme:0x1010) - v7:BasicObject[class_exact:C] = GuardType v1, BasicObject[class_exact:C] + v7:HeapObject[class_exact:C] = GuardType v1, HeapObject[class_exact:C] v8:BasicObject = GetIvar v7, :@foo Return v8 "#]]); diff --git a/zjit/src/hir_type/gen_hir_type.rb b/zjit/src/hir_type/gen_hir_type.rb index 6857678982..15aa68a600 100644 --- a/zjit/src/hir_type/gen_hir_type.rb +++ b/zjit/src/hir_type/gen_hir_type.rb @@ -156,6 +156,8 @@ add_union "BuiltinExact", $builtin_exact add_union "Subclass", $subclass add_union "BoolExact", [true_exact.name, false_exact.name] add_union "Immediate", [fixnum.name, flonum.name, static_sym.name, nil_exact.name, true_exact.name, false_exact.name, undef_.name] +$bits["HeapObject"] = ["BasicObject & !Immediate"] +$numeric_bits["HeapObject"] = $numeric_bits["BasicObject"] & ~$numeric_bits["Immediate"] # ===== Finished generating the DAG; write Rust code ===== diff --git a/zjit/src/hir_type/hir_type.inc.rs b/zjit/src/hir_type/hir_type.inc.rs index 68039c7f53..5850874080 100644 --- a/zjit/src/hir_type/hir_type.inc.rs +++ b/zjit/src/hir_type/hir_type.inc.rs @@ -38,6 +38,7 @@ mod bits { pub const HashExact: u64 = 1u64 << 23; pub const HashSubclass: u64 = 1u64 << 24; pub const HeapFloat: u64 = 1u64 << 25; + pub const HeapObject: u64 = BasicObject & !Immediate; pub const Immediate: u64 = FalseClass | Fixnum | Flonum | NilClass | StaticSymbol | TrueClass | Undef; pub const Integer: u64 = Bignum | Fixnum; pub const Module: u64 = Class | ModuleExact | ModuleSubclass; @@ -65,7 +66,7 @@ mod bits { pub const Symbol: u64 = DynamicSymbol | StaticSymbol; pub const TrueClass: u64 = 1u64 << 40; pub const Undef: u64 = 1u64 << 41; - pub const AllBitPatterns: [(&'static str, u64); 65] = [ + pub const AllBitPatterns: [(&'static str, u64); 66] = [ ("Any", Any), ("RubyValue", RubyValue), ("Immediate", Immediate), @@ -75,6 +76,7 @@ mod bits { ("BuiltinExact", BuiltinExact), ("BoolExact", BoolExact), ("TrueClass", TrueClass), + ("HeapObject", HeapObject), ("String", String), ("Subclass", Subclass), ("StringSubclass", StringSubclass), @@ -174,6 +176,7 @@ pub mod types { pub const HashExact: Type = Type::from_bits(bits::HashExact); pub const HashSubclass: Type = Type::from_bits(bits::HashSubclass); pub const HeapFloat: Type = Type::from_bits(bits::HeapFloat); + pub const HeapObject: Type = Type::from_bits(bits::HeapObject); pub const Immediate: Type = Type::from_bits(bits::Immediate); pub const Integer: Type = Type::from_bits(bits::Integer); pub const Module: Type = Type::from_bits(bits::Module); diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 84679c419d..c5e7aa87c6 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -248,7 +248,7 @@ impl Type { else if val.class() == unsafe { rb_cString } { types::StringExact } else { // TODO(max): Add more cases for inferring type bits from built-in types - Type { bits: bits::BasicObject, spec: Specialization::TypeExact(val.class()) } + Type { bits: bits::HeapObject, spec: Specialization::TypeExact(val.class()) } } } @@ -583,6 +583,7 @@ mod tests { assert_subtype(Type::fixnum(123), types::Immediate); assert_subtype(types::Fixnum, types::Immediate); assert_not_subtype(types::Bignum, types::Immediate); + assert_not_subtype(types::Integer, types::Immediate); assert_subtype(types::NilClass, types::Immediate); assert_subtype(types::TrueClass, types::Immediate); assert_subtype(types::FalseClass, types::Immediate); @@ -592,6 +593,32 @@ mod tests { assert_not_subtype(types::HeapFloat, types::Immediate); } + #[test] + fn heap_object() { + assert_not_subtype(Type::fixnum(123), types::HeapObject); + assert_not_subtype(types::Fixnum, types::HeapObject); + assert_subtype(types::Bignum, types::HeapObject); + assert_not_subtype(types::Integer, types::HeapObject); + assert_not_subtype(types::NilClass, types::HeapObject); + assert_not_subtype(types::TrueClass, types::HeapObject); + assert_not_subtype(types::FalseClass, types::HeapObject); + assert_not_subtype(types::StaticSymbol, types::HeapObject); + assert_subtype(types::DynamicSymbol, types::HeapObject); + assert_not_subtype(types::Flonum, types::HeapObject); + assert_subtype(types::HeapFloat, types::HeapObject); + assert_not_subtype(types::BasicObject, types::HeapObject); + assert_not_subtype(types::Object, types::HeapObject); + assert_not_subtype(types::Immediate, types::HeapObject); + assert_not_subtype(types::HeapObject, types::Immediate); + crate::cruby::with_rubyvm(|| { + let left = Type::from_value(rust_str_to_ruby("hello")); + let right = Type::from_value(rust_str_to_ruby("world")); + assert_subtype(left, types::HeapObject); + assert_subtype(right, types::HeapObject); + assert_subtype(left.union(right), types::HeapObject); + }); + } + #[test] fn fixnum_has_ruby_object() { assert_eq!(Type::fixnum(3).ruby_object(), Some(VALUE::fixnum_from_usize(3))); From 96c9e1e93a77e0aeedfc717a4abf03b3bfcd5169 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 7 Aug 2025 15:30:02 -0700 Subject: [PATCH 065/157] ZJIT: Remove GC offsets overwritten by invalidation (#14102) ZJIT: Remove GC offsts overwritten by invalidation --- zjit/src/asm/mod.rs | 7 +++- zjit/src/codegen.rs | 11 +++--- zjit/src/gc.rs | 81 +++++++++++++++++++++++++------------- zjit/src/invariants.rs | 89 ++++++++++++++++++++++++------------------ 4 files changed, 118 insertions(+), 70 deletions(-) diff --git a/zjit/src/asm/mod.rs b/zjit/src/asm/mod.rs index 6c3e955463..9bf11dfc4a 100644 --- a/zjit/src/asm/mod.rs +++ b/zjit/src/asm/mod.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::fmt; +use std::ops::Range; use std::rc::Rc; use std::cell::RefCell; use std::mem; @@ -124,7 +125,7 @@ impl CodeBlock { } /// Invoke a callback with write_ptr temporarily adjusted to a given address - pub fn with_write_ptr(&mut self, code_ptr: CodePtr, callback: impl Fn(&mut CodeBlock)) { + pub fn with_write_ptr(&mut self, code_ptr: CodePtr, callback: impl Fn(&mut CodeBlock)) -> Range { // Temporarily update the write_pos. Ignore the dropped_bytes flag at the old address. let old_write_pos = self.write_pos; let old_dropped_bytes = self.dropped_bytes; @@ -134,9 +135,13 @@ impl CodeBlock { // Invoke the callback callback(self); + // Build a code range modified by the callback + let ret = code_ptr..self.get_write_ptr(); + // Restore the original write_pos and dropped_bytes flag. self.dropped_bytes = old_dropped_bytes; self.write_pos = old_write_pos; + ret } /// Get a (possibly dangling) direct pointer into the executable memory block diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a5439bf2ca..78debc326d 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -5,7 +5,7 @@ use std::ffi::{c_int, c_void}; use crate::asm::Label; use crate::backend::current::{Reg, ALLOC_REGS}; use crate::invariants::{track_bop_assumption, track_cme_assumption, track_single_ractor_assumption, track_stable_constant_names_assumption}; -use crate::gc::{get_or_create_iseq_payload, append_gc_offsets}; +use crate::gc::{append_gc_offsets, get_or_create_iseq_payload, get_or_create_iseq_payload_ptr}; use crate::state::ZJITState; use crate::stats::{counter_ptr, Counter}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; @@ -521,6 +521,7 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf /// Record a patch point that should be invalidated on a given invariant fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, invariant: &Invariant, state: &FrameState) -> Option<()> { + let payload_ptr = get_or_create_iseq_payload_ptr(jit.iseq); let label = asm.new_label("patch_point").unwrap_label(); let invariant = invariant.clone(); @@ -532,19 +533,19 @@ fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, invariant: &Invarian match invariant { Invariant::BOPRedefined { klass, bop } => { let side_exit_ptr = cb.resolve_label(label); - track_bop_assumption(klass, bop, code_ptr, side_exit_ptr); + track_bop_assumption(klass, bop, code_ptr, side_exit_ptr, payload_ptr); } Invariant::MethodRedefined { klass: _, method: _, cme } => { let side_exit_ptr = cb.resolve_label(label); - track_cme_assumption(cme, code_ptr, side_exit_ptr); + track_cme_assumption(cme, code_ptr, side_exit_ptr, payload_ptr); } Invariant::StableConstantNames { idlist } => { let side_exit_ptr = cb.resolve_label(label); - track_stable_constant_names_assumption(idlist, code_ptr, side_exit_ptr); + track_stable_constant_names_assumption(idlist, code_ptr, side_exit_ptr, payload_ptr); } Invariant::SingleRactorMode => { let side_exit_ptr = cb.resolve_label(label); - track_single_ractor_assumption(code_ptr, side_exit_ptr); + track_single_ractor_assumption(code_ptr, side_exit_ptr, payload_ptr); } } }); diff --git a/zjit/src/gc.rs b/zjit/src/gc.rs index 01bcc9fe5d..d94d86036b 100644 --- a/zjit/src/gc.rs +++ b/zjit/src/gc.rs @@ -1,6 +1,6 @@ // This module is responsible for marking/moving objects on GC. -use std::ffi::c_void; +use std::{ffi::c_void, ops::Range}; use crate::{cruby::*, profile::IseqProfile, state::ZJITState, virtualmem::CodePtr}; /// This is all the data ZJIT stores on an ISEQ. We mark objects in this struct on GC. @@ -26,16 +26,16 @@ impl IseqPayload { } } -/// Get the payload object associated with an iseq. Create one if none exists. -pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { +/// Get a pointer to the payload object associated with an ISEQ. Create one if none exists. +pub fn get_or_create_iseq_payload_ptr(iseq: IseqPtr) -> *mut IseqPayload { type VoidPtr = *mut c_void; - let payload_non_null = unsafe { + unsafe { let payload = rb_iseq_get_zjit_payload(iseq); if payload.is_null() { // Allocate a new payload with Box and transfer ownership to the GC. - // We drop the payload with Box::from_raw when the GC frees the iseq and calls us. - // NOTE(alan): Sometimes we read from an iseq without ever writing to it. + // We drop the payload with Box::from_raw when the GC frees the ISEQ and calls us. + // NOTE(alan): Sometimes we read from an ISEQ without ever writing to it. // We allocate in those cases anyways. let iseq_size = get_iseq_encoded_size(iseq); let new_payload = IseqPayload::new(iseq_size); @@ -46,13 +46,23 @@ pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { } else { payload as *mut IseqPayload } - }; + } +} +/// Get the payload object associated with an ISEQ. Create one if none exists. +pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { + let payload_non_null = get_or_create_iseq_payload_ptr(iseq); + payload_ptr_as_mut(payload_non_null) +} + +/// Convert an IseqPayload pointer to a mutable reference. Only one reference +/// should be kept at a time. +fn payload_ptr_as_mut(payload_ptr: *mut IseqPayload) -> &'static mut IseqPayload { // SAFETY: we should have the VM lock and all other Ruby threads should be asleep. So we have // exclusive mutable access. // Hmm, nothing seems to stop calling this on the same // iseq twice, though, which violates aliasing rules. - unsafe { payload_non_null.as_mut() }.unwrap() + unsafe { payload_ptr.as_mut() }.unwrap() } #[unsafe(no_mangle)] @@ -90,29 +100,12 @@ pub extern "C" fn rb_zjit_iseq_mark(payload: *mut c_void) { } } -/// Append a set of gc_offsets to the iseq's payload -pub fn append_gc_offsets(iseq: IseqPtr, offsets: &Vec) { - let payload = get_or_create_iseq_payload(iseq); - payload.gc_offsets.extend(offsets); - - // Call writebarrier on each newly added value - let cb = ZJITState::get_code_block(); - for &offset in offsets.iter() { - let value_ptr: *const u8 = offset.raw_ptr(cb); - let value_ptr = value_ptr as *const VALUE; - unsafe { - let object = value_ptr.read_unaligned(); - rb_gc_writebarrier(iseq.into(), object); - } - } -} - -/// GC callback for updating GC objects in the per-iseq payload. +/// GC callback for updating GC objects in the per-ISEQ payload. /// This is a mirror of [rb_zjit_iseq_mark]. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_iseq_update_references(payload: *mut c_void) { let payload = if payload.is_null() { - return; // nothing to mark + return; // nothing to update } else { // SAFETY: The GC takes the VM lock while marking, which // we assert, so we should be synchronized and data race free. @@ -150,3 +143,37 @@ pub extern "C" fn rb_zjit_iseq_update_references(payload: *mut c_void) { } cb.mark_all_executable(); } + +/// Append a set of gc_offsets to the iseq's payload +pub fn append_gc_offsets(iseq: IseqPtr, offsets: &Vec) { + let payload = get_or_create_iseq_payload(iseq); + payload.gc_offsets.extend(offsets); + + // Call writebarrier on each newly added value + let cb = ZJITState::get_code_block(); + for &offset in offsets.iter() { + let value_ptr: *const u8 = offset.raw_ptr(cb); + let value_ptr = value_ptr as *const VALUE; + unsafe { + let object = value_ptr.read_unaligned(); + rb_gc_writebarrier(iseq.into(), object); + } + } +} + +/// Remove GC offsets that overlap with a given removed_range. +/// We do this when invalidation rewrites some code with a jump instruction +/// and GC offsets are corrupted by the rewrite, assuming no on-stack code +/// will step into the instruction with the GC offsets after invalidation. +pub fn remove_gc_offsets(payload_ptr: *mut IseqPayload, removed_range: &Range) { + let payload = payload_ptr_as_mut(payload_ptr); + payload.gc_offsets.retain(|&gc_offset| { + let offset_range = gc_offset..(gc_offset.add_bytes(SIZEOF_VALUE)); + !ranges_overlap(&offset_range, removed_range) + }); +} + +/// Return true if given Range ranges overlap with each other +fn ranges_overlap(left: &Range, right: &Range) -> bool where T: PartialOrd { + left.start < right.end && right.start < left.end +} diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index c8c91dc45b..85bc04fc71 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -1,24 +1,32 @@ use std::{collections::{HashMap, HashSet}, mem}; -use crate::{backend::lir::{asm_comment, Assembler}, cruby::{rb_callable_method_entry_t, ruby_basic_operators, src_loc, with_vm_lock, IseqPtr, RedefinitionFlag, ID}, hir::Invariant, options::debug, state::{zjit_enabled_p, ZJITState}, virtualmem::CodePtr}; +use crate::{backend::lir::{asm_comment, Assembler}, cruby::{rb_callable_method_entry_t, ruby_basic_operators, src_loc, with_vm_lock, IseqPtr, RedefinitionFlag, ID}, gc::IseqPayload, hir::Invariant, options::debug, state::{zjit_enabled_p, ZJITState}, virtualmem::CodePtr}; +use crate::gc::remove_gc_offsets; -macro_rules! compile_jumps { - ($cb:expr, $jumps:expr, $($comment_args:tt)*) => { - for jump in $jumps { - $cb.with_write_ptr(jump.from, |cb| { +macro_rules! compile_patch_points { + ($cb:expr, $patch_points:expr, $($comment_args:tt)*) => { + for patch_point in $patch_points { + let written_range = $cb.with_write_ptr(patch_point.patch_point_ptr, |cb| { let mut asm = Assembler::new(); asm_comment!(asm, $($comment_args)*); - asm.jmp(jump.to.into()); + asm.jmp(patch_point.side_exit_ptr.into()); asm.compile(cb).expect("can write existing code"); }); + // Stop marking GC offsets corrupted by the jump instruction + remove_gc_offsets(patch_point.payload_ptr, &written_range); } }; } +/// When a PatchPoint is invalidated, it generates a jump instruction from `from` to `to`. #[derive(Debug, Eq, Hash, PartialEq)] -struct Jump { - from: CodePtr, - to: CodePtr, +struct PatchPoint { + /// Code pointer to be invalidated + patch_point_ptr: CodePtr, + /// Code pointer to a side exit + side_exit_ptr: CodePtr, + /// Raw pointer to the ISEQ payload + payload_ptr: *mut IseqPayload, } /// Used to track all of the various block references that contain assumptions @@ -32,16 +40,16 @@ pub struct Invariants { no_ep_escape_iseqs: HashSet, /// Map from a class and its associated basic operator to a set of patch points - bop_patch_points: HashMap<(RedefinitionFlag, ruby_basic_operators), HashSet>, + bop_patch_points: HashMap<(RedefinitionFlag, ruby_basic_operators), HashSet>, /// Map from CME to patch points that assume the method hasn't been redefined - cme_patch_points: HashMap<*const rb_callable_method_entry_t, HashSet>, + cme_patch_points: HashMap<*const rb_callable_method_entry_t, HashSet>, /// Map from constant ID to patch points that assume the constant hasn't been redefined - constant_state_patch_points: HashMap>, + constant_state_patch_points: HashMap>, /// Set of patch points that assume that the interpreter is running with only one ractor - single_ractor_patch_points: HashSet, + single_ractor_patch_points: HashSet, } /// Called when a basic operator is redefined. Note that all the blocks assuming @@ -56,13 +64,13 @@ pub extern "C" fn rb_zjit_bop_redefined(klass: RedefinitionFlag, bop: ruby_basic with_vm_lock(src_loc!(), || { let invariants = ZJITState::get_invariants(); - if let Some(jumps) = invariants.bop_patch_points.get(&(klass, bop)) { + if let Some(patch_points) = invariants.bop_patch_points.get(&(klass, bop)) { let cb = ZJITState::get_code_block(); let bop = Invariant::BOPRedefined { klass, bop }; debug!("BOP is redefined: {}", bop); // Invalidate all patch points for this BOP - compile_jumps!(cb, jumps, "BOP is redefined: {}", bop); + compile_patch_points!(cb, patch_points, "BOP is redefined: {}", bop); cb.mark_all_executable(); } @@ -106,12 +114,14 @@ pub fn track_bop_assumption( klass: RedefinitionFlag, bop: ruby_basic_operators, patch_point_ptr: CodePtr, - side_exit_ptr: CodePtr + side_exit_ptr: CodePtr, + payload_ptr: *mut IseqPayload, ) { let invariants = ZJITState::get_invariants(); - invariants.bop_patch_points.entry((klass, bop)).or_default().insert(Jump { - from: patch_point_ptr, - to: side_exit_ptr, + invariants.bop_patch_points.entry((klass, bop)).or_default().insert(PatchPoint { + patch_point_ptr, + side_exit_ptr, + payload_ptr, }); } @@ -119,12 +129,14 @@ pub fn track_bop_assumption( pub fn track_cme_assumption( cme: *const rb_callable_method_entry_t, patch_point_ptr: CodePtr, - side_exit_ptr: CodePtr + side_exit_ptr: CodePtr, + payload_ptr: *mut IseqPayload, ) { let invariants = ZJITState::get_invariants(); - invariants.cme_patch_points.entry(cme).or_default().insert(Jump { - from: patch_point_ptr, - to: side_exit_ptr, + invariants.cme_patch_points.entry(cme).or_default().insert(PatchPoint { + patch_point_ptr, + side_exit_ptr, + payload_ptr, }); } @@ -132,7 +144,8 @@ pub fn track_cme_assumption( pub fn track_stable_constant_names_assumption( idlist: *const ID, patch_point_ptr: CodePtr, - side_exit_ptr: CodePtr + side_exit_ptr: CodePtr, + payload_ptr: *mut IseqPayload, ) { let invariants = ZJITState::get_invariants(); @@ -143,9 +156,10 @@ pub fn track_stable_constant_names_assumption( break; } - invariants.constant_state_patch_points.entry(id).or_default().insert(Jump { - from: patch_point_ptr, - to: side_exit_ptr, + invariants.constant_state_patch_points.entry(id).or_default().insert(PatchPoint { + patch_point_ptr, + side_exit_ptr, + payload_ptr, }); idx += 1; @@ -163,12 +177,12 @@ pub extern "C" fn rb_zjit_cme_invalidate(cme: *const rb_callable_method_entry_t) with_vm_lock(src_loc!(), || { let invariants = ZJITState::get_invariants(); // Get the CMD's jumps and remove the entry from the map as it has been invalidated - if let Some(jumps) = invariants.cme_patch_points.remove(&cme) { + if let Some(patch_points) = invariants.cme_patch_points.remove(&cme) { let cb = ZJITState::get_code_block(); debug!("CME is invalidated: {:?}", cme); // Invalidate all patch points for this CME - compile_jumps!(cb, jumps, "CME is invalidated: {:?}", cme); + compile_patch_points!(cb, patch_points, "CME is invalidated: {:?}", cme); cb.mark_all_executable(); } @@ -185,12 +199,12 @@ pub extern "C" fn rb_zjit_constant_state_changed(id: ID) { with_vm_lock(src_loc!(), || { let invariants = ZJITState::get_invariants(); - if let Some(jumps) = invariants.constant_state_patch_points.get(&id) { + if let Some(patch_points) = invariants.constant_state_patch_points.get(&id) { let cb = ZJITState::get_code_block(); debug!("Constant state changed: {:?}", id); // Invalidate all patch points for this constant ID - compile_jumps!(cb, jumps, "Constant state changed: {:?}", id); + compile_patch_points!(cb, patch_points, "Constant state changed: {:?}", id); cb.mark_all_executable(); } @@ -198,11 +212,12 @@ pub extern "C" fn rb_zjit_constant_state_changed(id: ID) { } /// Track the JIT code that assumes that the interpreter is running with only one ractor -pub fn track_single_ractor_assumption(patch_point_ptr: CodePtr, side_exit_ptr: CodePtr) { +pub fn track_single_ractor_assumption(patch_point_ptr: CodePtr, side_exit_ptr: CodePtr, payload_ptr: *mut IseqPayload) { let invariants = ZJITState::get_invariants(); - invariants.single_ractor_patch_points.insert(Jump { - from: patch_point_ptr, - to: side_exit_ptr, + invariants.single_ractor_patch_points.insert(PatchPoint { + patch_point_ptr, + side_exit_ptr, + payload_ptr, }); } @@ -217,10 +232,10 @@ pub extern "C" fn rb_zjit_before_ractor_spawn() { with_vm_lock(src_loc!(), || { let cb = ZJITState::get_code_block(); - let jumps = mem::take(&mut ZJITState::get_invariants().single_ractor_patch_points); + let patch_points = mem::take(&mut ZJITState::get_invariants().single_ractor_patch_points); // Invalidate all patch points for single ractor mode - compile_jumps!(cb, jumps, "Another ractor spawned, invalidating single ractor mode assumption"); + compile_patch_points!(cb, patch_points, "Another ractor spawned, invalidating single ractor mode assumption"); cb.mark_all_executable(); }); From d25eb1eb5c05ac1cadd9cc03ddd37f8eda57d7f1 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 7 Aug 2025 23:38:02 +0100 Subject: [PATCH 066/157] ZJIT: Optimize class guards by directly reading klass field (#14136) Replace `rb_yarv_class_of` call with: - a constant check for special constants (nil, fixnums, symbols, etc) - a check for false - direct memory read at offset 8 for regular heap objects for the class check --- test/ruby/test_zjit.rb | 24 ++++++++++++++++++++++++ zjit/src/codegen.rs | 24 ++++++++++++++++++------ zjit/src/hir_type/mod.rs | 2 +- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 8115a60166..5c4875e3dd 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1432,6 +1432,30 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2, insns: [:opt_nil_p] end + def test_basic_object_guard_works_with_immediate + assert_compiles 'NilClass', %q{ + class Foo; end + + def test(val) = val.class + + test(Foo.new) + test(Foo.new) + test(nil) + }, call_threshold: 2 + end + + def test_basic_object_guard_works_with_false + assert_compiles 'FalseClass', %q{ + class Foo; end + + def test(val) = val.class + + test(Foo.new) + test(Foo.new) + test(false) + }, call_threshold: 2 + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 78debc326d..233ef02048 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1069,17 +1069,29 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard asm.cmp(val, Qtrue.into()); asm.jne(side_exit(jit, state, GuardType(guard_type))?); } else if guard_type.is_subtype(types::FalseClass) { - assert!(Qfalse.as_i64() == 0); - asm.test(val, val); + asm.cmp(val, Qfalse.into()); asm.jne(side_exit(jit, state, GuardType(guard_type))?); + } else if guard_type.is_immediate() { + // All immediate types' guard should have been handled above + panic!("unexpected immediate guard type: {guard_type}"); } else if let Some(expected_class) = guard_type.runtime_exact_ruby_class() { - asm_comment!(asm, "guard exact class"); + asm_comment!(asm, "guard exact class for non-immediate types"); - // Get the class of the value - let klass = asm.ccall(rb_yarv_class_of as *const u8, vec![val]); + let side_exit = side_exit(jit, state, GuardType(guard_type))?; + + // Check if it's a special constant + asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); + asm.jnz(side_exit.clone()); + + // Check if it's false + asm.cmp(val, Qfalse.into()); + asm.je(side_exit.clone()); + + // Load the class from the object's klass field + let klass = asm.load(Opnd::mem(64, val, RUBY_OFFSET_RBASIC_KLASS)); asm.cmp(klass, Opnd::Value(expected_class)); - asm.jne(side_exit(jit, state, GuardType(guard_type))?); + asm.jne(side_exit); } else { unimplemented!("unsupported type: {guard_type}"); } diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index c5e7aa87c6..607ccbde84 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -497,7 +497,7 @@ impl Type { } } - fn is_immediate(&self) -> bool { + pub fn is_immediate(&self) -> bool { self.is_subtype(types::Immediate) } From c41c323f1a001ab62c69add4bf01c6a887641f2a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 7 Aug 2025 15:39:45 -0700 Subject: [PATCH 067/157] Invalidate CCs when cme is invalidated in marking * Skip assertion when cc->klass is Qundef * Invalidate CCs when cme is invalidated in marking * Add additional assertions that CC references stay valid Co-authored-by: Peter Zhu --- imemo.c | 3 +++ vm_method.c | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/imemo.c b/imemo.c index 2c721ca911..30dae8d583 100644 --- a/imemo.c +++ b/imemo.c @@ -372,6 +372,9 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) } } else { + RUBY_ASSERT(RB_TYPE_P(cc->klass, T_CLASS) || RB_TYPE_P(cc->klass, T_ICLASS)); + RUBY_ASSERT(IMEMO_TYPE_P((VALUE)cc->cme_, imemo_ment)); + rb_gc_mark_weak((VALUE *)&cc->klass); if ((vm_cc_super_p(cc) || vm_cc_refinement_p(cc))) { rb_gc_mark_movable((VALUE)cc->cme_); diff --git a/vm_method.c b/vm_method.c index 76b1c97d04..c1793c102c 100644 --- a/vm_method.c +++ b/vm_method.c @@ -30,16 +30,27 @@ mark_cc_entry_i(VALUE ccs_ptr, void *data) VM_ASSERT(vm_ccs_p(ccs)); if (METHOD_ENTRY_INVALIDATED(ccs->cme)) { + /* Before detaching the CCs from this class, we need to invalidate the cc + * since we will no longer be marking the cme on their behalf. + */ + for (int i = 0; i < ccs->len; i++) { + const struct rb_callcache *cc = ccs->entries[i].cc; + if (cc->klass == Qundef) continue; // already invalidated + VM_ASSERT(cc->klass == Qundef || vm_cc_check_cme(cc, ccs->cme)); + VM_ASSERT(!vm_cc_super_p(cc) && !vm_cc_refinement_p(cc)); + vm_cc_invalidate(cc); + } ruby_xfree(ccs); return ID_TABLE_DELETE; } else { rb_gc_mark_movable((VALUE)ccs->cme); - for (int i=0; ilen; i++) { - VM_ASSERT(vm_cc_check_cme(ccs->entries[i].cc, ccs->cme)); + for (int i = 0; i < ccs->len; i++) { + const struct rb_callcache *cc = ccs->entries[i].cc; + VM_ASSERT(cc->klass == Qundef || vm_cc_check_cme(cc, ccs->cme)); - rb_gc_mark_movable((VALUE)ccs->entries[i].cc); + rb_gc_mark_movable((VALUE)cc); } return ID_TABLE_CONTINUE; } From 2edc944702ef8aa04e10f7832ba36f410fbfa513 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 7 Aug 2025 23:41:05 +0100 Subject: [PATCH 068/157] ZJIT: Implement `defined?` codegen for non-yield calls (#14101) --- test/ruby/test_zjit.rb | 20 ++++++++++++++++++++ zjit/src/codegen.rs | 16 +++++++++++++--- zjit/src/hir.rs | 19 ++++++++++--------- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 5c4875e3dd..9eca30c787 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1043,6 +1043,26 @@ class TestZJIT < Test::Unit::TestCase } end + def test_defined_with_defined_values + assert_compiles '["constant", "method", "global-variable"]', %q{ + class Foo; end + def bar; end + $ruby = 1 + + def test = return defined?(Foo), defined?(bar), defined?($ruby) + + test + }, insns: [:defined] + end + + def test_defined_with_undefined_values + assert_compiles '[nil, nil, nil]', %q{ + def test = return defined?(Foo), defined?(bar), defined?($ruby) + + test + }, insns: [:defined] + end + def test_defined_yield assert_compiles "nil", "defined?(yield)" assert_compiles '[nil, nil, "yield"]', %q{ diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 233ef02048..5f56e05c8b 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -369,7 +369,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::SideExit { state, reason } => return gen_side_exit(jit, asm, reason, &function.frame_state(*state)), Insn::PutSpecialObject { value_type } => gen_putspecialobject(asm, *value_type), Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state))?, - Insn::Defined { op_type, obj, pushval, v } => gen_defined(jit, asm, *op_type, *obj, *pushval, opnd!(v))?, + Insn::Defined { op_type, obj, pushval, v, state } => gen_defined(jit, asm, *op_type, *obj, *pushval, opnd!(v), &function.frame_state(*state))?, &Insn::IncrCounter(counter) => return Some(gen_incr_counter(asm, counter)), Insn::ArrayExtend { .. } | Insn::ArrayMax { .. } @@ -438,7 +438,7 @@ fn gen_get_ep(asm: &mut Assembler, level: u32) -> Opnd { ep_opnd } -fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, _obj: VALUE, pushval: VALUE, _tested_value: Opnd) -> Option { +fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, obj: VALUE, pushval: VALUE, tested_value: Opnd, state: &FrameState) -> Option { match op_type as defined_type { DEFINED_YIELD => { // `yield` goes to the block handler stowed in the "local" iseq which is @@ -455,7 +455,17 @@ fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, _obj: VALUE, Some(Qnil.into()) } } - _ => None + _ => { + // Save the PC and SP because the callee may allocate or call #respond_to? + gen_prepare_non_leaf_call(jit, asm, state)?; + + // TODO: Inline the cases for each op_type + // Call vm_defined(ec, reg_cfp, op_type, obj, v) + let def_result = asm_ccall!(asm, rb_vm_defined, EC, CFP, op_type.into(), obj.into(), tested_value); + + asm.cmp(def_result.with_num_bits(8).unwrap(), 0.into()); + Some(asm.csel_ne(pushval.into(), Qnil.into())) + } } } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f98aa846cd..55041945d4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -472,7 +472,7 @@ pub enum Insn { Test { val: InsnId }, /// Return C `true` if `val` is `Qnil`, else `false`. IsNil { val: InsnId }, - Defined { op_type: usize, obj: VALUE, pushval: VALUE, v: InsnId }, + Defined { op_type: usize, obj: VALUE, pushval: VALUE, v: InsnId, state: InsnId }, GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId }, /// Get a global variable named `id` @@ -1173,7 +1173,7 @@ impl Function { &ArrayDup { val, state } => ArrayDup { val: find!(val), state }, &HashDup { val, state } => HashDup { val: find!(val), state }, &CCall { cfun, ref args, name, return_type, elidable } => CCall { cfun, args: find_vec!(args), name, return_type, elidable }, - &Defined { op_type, obj, pushval, v } => Defined { op_type, obj, pushval, v: find!(v) }, + &Defined { op_type, obj, pushval, v, state } => Defined { op_type, obj, pushval, v: find!(v), state: find!(state) }, &DefinedIvar { self_val, pushval, id, state } => DefinedIvar { self_val: find!(self_val), pushval, id, state }, &NewArray { ref elements, state } => NewArray { elements: find_vec!(elements), state: find!(state) }, &NewHash { ref elements, state } => { @@ -2788,7 +2788,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let obj = get_arg(pc, 1); let pushval = get_arg(pc, 2); let v = state.stack_pop()?; - state.stack_push(fun.push_insn(block, Insn::Defined { op_type, obj, pushval, v })); + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + state.stack_push(fun.push_insn(block, Insn::Defined { op_type, obj, pushval, v, state: exit_id })); } YARVINSN_definedivar => { // (ID id, IVC ic, VALUE pushval) @@ -4061,12 +4062,12 @@ mod tests { fn test@:2: bb0(v0:BasicObject): v2:NilClass = Const Value(nil) - v3:BasicObject = Defined constant, v2 - v4:BasicObject = Defined func, v0 - v5:NilClass = Const Value(nil) - v6:BasicObject = Defined global-variable, v5 - v8:ArrayExact = NewArray v3, v4, v6 - Return v8 + v4:BasicObject = Defined constant, v2 + v6:BasicObject = Defined func, v0 + v7:NilClass = Const Value(nil) + v9:BasicObject = Defined global-variable, v7 + v11:ArrayExact = NewArray v4, v6, v9 + Return v11 "#]]); } From 4fef87588a4748055a916a7968c949c2a7e65430 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 7 Aug 2025 16:56:27 -0700 Subject: [PATCH 069/157] ZJIT: Remove the need for unwrap() on with_num_bits() (#14144) * ZJIT: Remove the need for unwrap() on with_num_bits() * Fix arm64 tests * Track the caller of with_num_bits Co-authored-by: Alan Wu --------- Co-authored-by: Alan Wu --- zjit/src/backend/arm64/mod.rs | 16 ++++++++-------- zjit/src/backend/lir.rs | 18 +++++++++++++++--- zjit/src/codegen.rs | 5 +++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 148d01ea86..0c7e6883c2 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -256,7 +256,7 @@ impl Assembler // Many Arm insns support only 32-bit or 64-bit operands. asm.load with fewer // bits zero-extends the value, so it's safe to recognize it as a 32-bit value. if out_opnd.rm_num_bits() < 32 { - out_opnd.with_num_bits(32).unwrap() + out_opnd.with_num_bits(32) } else { out_opnd } @@ -282,7 +282,7 @@ impl Assembler BitmaskImmediate::new_32b_reg(imm as u32).is_ok()) { Opnd::UImm(imm as u64) } else { - asm.load(opnd).with_num_bits(dest_num_bits).unwrap() + asm.load(opnd).with_num_bits(dest_num_bits) } }, Opnd::UImm(uimm) => { @@ -292,7 +292,7 @@ impl Assembler BitmaskImmediate::new_32b_reg(uimm as u32).is_ok()) { opnd } else { - asm.load(opnd).with_num_bits(dest_num_bits).unwrap() + asm.load(opnd).with_num_bits(dest_num_bits) } }, Opnd::None | Opnd::Value(_) => unreachable!() @@ -360,8 +360,8 @@ impl Assembler match opnd0 { Opnd::Reg(_) | Opnd::VReg { .. } => { match opnd0.rm_num_bits() { - 8 => asm.and(opnd0.with_num_bits(64).unwrap(), Opnd::UImm(0xff)), - 16 => asm.and(opnd0.with_num_bits(64).unwrap(), Opnd::UImm(0xffff)), + 8 => asm.and(opnd0.with_num_bits(64), Opnd::UImm(0xff)), + 16 => asm.and(opnd0.with_num_bits(64), Opnd::UImm(0xffff)), 32 | 64 => opnd0, bits => unreachable!("Invalid number of bits. {}", bits) } @@ -505,7 +505,7 @@ impl Assembler let split_right = split_shifted_immediate(asm, *right); let opnd1 = match split_right { Opnd::VReg { .. } if opnd0.num_bits() != split_right.num_bits() => { - split_right.with_num_bits(opnd0.num_bits().unwrap()).unwrap() + split_right.with_num_bits(opnd0.num_bits().unwrap()) }, _ => split_right }; @@ -1823,7 +1823,7 @@ mod tests { #[test] fn test_emit_test_32b_reg_not_bitmask_imm() { let (mut asm, mut cb) = setup_asm(); - let w0 = Opnd::Reg(X0_REG).with_num_bits(32).unwrap(); + let w0 = Opnd::Reg(X0_REG).with_num_bits(32); asm.test(w0, Opnd::UImm(u32::MAX.into())); // All ones is not encodable with a bitmask immediate, // so this needs one register @@ -1833,7 +1833,7 @@ mod tests { #[test] fn test_emit_test_32b_reg_bitmask_imm() { let (mut asm, mut cb) = setup_asm(); - let w0 = Opnd::Reg(X0_REG).with_num_bits(32).unwrap(); + let w0 = Opnd::Reg(X0_REG).with_num_bits(32); asm.test(w0, Opnd::UImm(0x80000001)); asm.compile_with_num_regs(&mut cb, 0); } diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index b910052dae..1bed45cba5 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -146,17 +146,29 @@ impl Opnd } } - pub fn with_num_bits(&self, num_bits: u8) -> Option { + /// Return Some(Opnd) with a given num_bits if self has num_bits. + /// None if self doesn't have a num_bits field. + pub fn try_num_bits(&self, num_bits: u8) -> Option { assert!(num_bits == 8 || num_bits == 16 || num_bits == 32 || num_bits == 64); match *self { Opnd::Reg(reg) => Some(Opnd::Reg(reg.with_num_bits(num_bits))), Opnd::Mem(Mem { base, disp, .. }) => Some(Opnd::Mem(Mem { base, disp, num_bits })), Opnd::VReg { idx, .. } => Some(Opnd::VReg { idx, num_bits }), - //Opnd::Stack { idx, stack_size, num_locals, sp_offset, reg_mapping, .. } => Some(Opnd::Stack { idx, num_bits, stack_size, num_locals, sp_offset, reg_mapping }), _ => None, } } + /// Return Opnd with a given num_bits if self has num_bits. + /// Panic otherwise. This should be used only when you know which Opnd self is. + #[track_caller] + pub fn with_num_bits(&self, num_bits: u8) -> Opnd { + if let Some(opnd) = self.try_num_bits(num_bits) { + opnd + } else { + unreachable!("with_num_bits should not be used on: {self:?}"); + } + } + /// Get the size in bits for register/memory operands. pub fn rm_num_bits(&self) -> u8 { self.num_bits().unwrap() @@ -1720,7 +1732,7 @@ impl Assembler while let Some(opnd) = opnd_iter.next() { match *opnd { Opnd::VReg { idx, num_bits } => { - *opnd = Opnd::Reg(reg_mapping[idx].unwrap()).with_num_bits(num_bits).unwrap(); + *opnd = Opnd::Reg(reg_mapping[idx].unwrap()).with_num_bits(num_bits); }, Opnd::Mem(Mem { base: MemBase::VReg(idx), disp, num_bits }) => { let base = MemBase::Reg(reg_mapping[idx].unwrap().reg_no); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 5f56e05c8b..3e6bbfa605 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -463,7 +463,7 @@ fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, obj: VALUE, // Call vm_defined(ec, reg_cfp, op_type, obj, v) let def_result = asm_ccall!(asm, rb_vm_defined, EC, CFP, op_type.into(), obj.into(), tested_value); - asm.cmp(def_result.with_num_bits(8).unwrap(), 0.into()); + asm.cmp(def_result.with_num_bits(8), 0.into()); Some(asm.csel_ne(pushval.into(), Qnil.into())) } } @@ -1070,7 +1070,8 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard } else if guard_type.is_subtype(types::StaticSymbol) { // Static symbols have (val & 0xff) == RUBY_SYMBOL_FLAG // Use 8-bit comparison like YJIT does - asm.cmp(val.with_num_bits(8).unwrap(), Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); + debug_assert!(val.try_num_bits(8).is_some(), "GuardType should not be used for a known constant, but val was: {val:?}"); + asm.cmp(val.try_num_bits(8)?, Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); asm.jne(side_exit(jit, state, GuardType(guard_type))?); } else if guard_type.is_subtype(types::NilClass) { asm.cmp(val, Qnil.into()); From 3ad26d050137c27dbb5df2c72c5f40d1352f5007 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 02:26:46 +0000 Subject: [PATCH 070/157] Bump actions/cache in /.github/actions/setup/directories Bumps [actions/cache](https://github.com/actions/cache) from 4.2.3 to 4.2.4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/5a3ec84eff668545956fd18022155c47e93e2684...0400d5f644dc74513175e3cd8d07132dd4860809) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 4.2.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/actions/setup/directories/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup/directories/action.yml b/.github/actions/setup/directories/action.yml index 7c1e4e9b0e..728e082189 100644 --- a/.github/actions/setup/directories/action.yml +++ b/.github/actions/setup/directories/action.yml @@ -93,7 +93,7 @@ runs: path: ${{ inputs.srcdir }} fetch-depth: ${{ inputs.fetch-depth }} - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ inputs.srcdir }}/.downloaded-cache key: ${{ runner.os }}-${{ runner.arch }}-downloaded-cache From f76ce9fd28bd0e94c7b2b904bf27958a6db1aed9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 8 Aug 2025 19:33:00 +0900 Subject: [PATCH 071/157] [ruby/optparse] Use `~/.config` only if `$XDG_CONFIG_HOME` is unset or empty https://github.com/ruby/optparse/commit/2f9c7500a3 --- lib/optparse.rb | 12 +++++++++--- test/optparse/test_load.rb | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/optparse.rb b/lib/optparse.rb index 23b4844b2c..06e33db1f5 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -2049,10 +2049,16 @@ XXX basename = File.basename($0, '.*') return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil basename << ".options" + if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty? + # https://specifications.freedesktop.org/basedir-spec/latest/#variables + # + # If $XDG_CONFIG_HOME is either not set or empty, a default + # equal to $HOME/.config should be used. + xdg = ['~/.config', true] + end return [ - # XDG - ENV['XDG_CONFIG_HOME'], - ['~/.config', true], + xdg, + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), # Haiku diff --git a/test/optparse/test_load.rb b/test/optparse/test_load.rb index 8c835032ba..f664cfbf72 100644 --- a/test/optparse/test_load.rb +++ b/test/optparse/test_load.rb @@ -47,7 +47,7 @@ class TestOptionParserLoad < Test::Unit::TestCase begin yield dir, optdir ensure - File.unlink(file) + File.unlink(file) rescue nil Dir.rmdir(optdir) rescue nil end else @@ -101,7 +101,7 @@ class TestOptionParserLoad < Test::Unit::TestCase end def test_load_xdg_config_home - result, = setup_options_xdg_config_home + result, dir = setup_options_xdg_config_home assert_load(result) setup_options_home_config do @@ -115,6 +115,11 @@ class TestOptionParserLoad < Test::Unit::TestCase setup_options_home_config_settings do assert_load(result) end + + File.unlink("#{dir}/#{@basename}.options") + setup_options_home_config do + assert_load_nothing + end end def test_load_home_config @@ -128,6 +133,11 @@ class TestOptionParserLoad < Test::Unit::TestCase setup_options_home_config_settings do assert_load(result) end + + setup_options_xdg_config_home do |_, dir| + File.unlink("#{dir}/#{@basename}.options") + assert_load_nothing + end end def test_load_xdg_config_dirs From a15cf61ba6768453430eecd03d3b563cfce92903 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 8 Aug 2025 20:17:58 +0900 Subject: [PATCH 072/157] Revert "Check if the found pkg-config is usable actually" This reverts commit 79d8a3159f60d32396c8281fe438e86ab97e3daa. The second argument of `find_executable0` in mkmf is `path`, not arguments to the program like as `EnvUtil.find_executable`. --- test/mkmf/test_pkg_config.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/mkmf/test_pkg_config.rb b/test/mkmf/test_pkg_config.rb index abeaf548f8..d0a2dc130a 100644 --- a/test/mkmf/test_pkg_config.rb +++ b/test/mkmf/test_pkg_config.rb @@ -3,9 +3,7 @@ require_relative 'base' require 'shellwords' class TestMkmfPkgConfig < TestMkmf - PKG_CONFIG = config_string("PKG_CONFIG") do |path| - find_executable0(path, "--version") {$?.success?} - end + PKG_CONFIG = config_string("PKG_CONFIG") {|path| find_executable0(path)} def setup super From 0b30cf3b40a363576e11d52ae56e5345f77a761f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 15:13:07 -0400 Subject: [PATCH 073/157] ZJIT: Print out command to repro in bisect script --- tool/zjit_bisect.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 472a60e66c..ff54d5e550 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -93,5 +93,7 @@ result = run_bisect(command, jit_list) File.open("jitlist.txt", "w") do |file| file.puts(result) end +puts "Run:" +puts "#{RUBY} --zjit-allowed-iseqs=jitlist.txt #{OPTIONS}" puts "Reduced JIT list (available in jitlist.txt):" puts result From fd6d6a45cd3d1d27224c6f02b37884c14edab2c4 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 15:31:58 -0400 Subject: [PATCH 074/157] ZJIT: Use shellwords in bisect script --- tool/zjit_bisect.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index ff54d5e550..39a302b2bd 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -1,11 +1,13 @@ #!/usr/bin/env ruby require 'logger' require 'open3' +require 'shellwords' require 'tempfile' require 'timeout' RUBY = ARGV[0] || raise("Usage: ruby jit_bisect.rb ") -OPTIONS = ARGV[1] || raise("Usage: ruby jit_bisect.rb ") +OPTIONS = ARGV[1..] +raise("Usage: ruby jit_bisect.rb -- ") if OPTIONS.empty? TIMEOUT_SEC = 5 LOGGER = Logger.new($stdout) @@ -65,7 +67,8 @@ def run_with_jit_list(ruby, options, jit_list) temp_file.flush temp_file.close # Run the JIT with the temporary file - Open3.capture3("#{ruby} --zjit-allowed-iseqs=#{temp_file.path} #{options}") + command = Shellwords.join [ruby, "--zjit-allowed-iseqs=#{temp_file.path}", *options] + Open3.capture3(command) end end @@ -77,7 +80,8 @@ end # Collect the JIT list from the failing Ruby process jit_list = nil Tempfile.create "jit_list" do |temp_file| - Open3.capture3("#{RUBY} --zjit-log-compiled-iseqs=#{temp_file.path} #{OPTIONS}") + command = Shellwords.join [RUBY, "--zjit-log-compiled-iseqs=#{temp_file.path}", *OPTIONS] + Open3.capture3(command) jit_list = File.readlines(temp_file.path).map(&:strip).reject(&:empty?) end LOGGER.info("Starting with JIT list of #{jit_list.length} items.") @@ -94,6 +98,7 @@ File.open("jitlist.txt", "w") do |file| file.puts(result) end puts "Run:" -puts "#{RUBY} --zjit-allowed-iseqs=jitlist.txt #{OPTIONS}" +command = Shellwords.join [RUBY, "--zjit-allowed-iseqs=jitlist.txt", *OPTIONS] +puts command puts "Reduced JIT list (available in jitlist.txt):" puts result From 0782bd2826eccd1b0d831dfa171bc45fa7687168 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 15:33:08 -0400 Subject: [PATCH 075/157] ZJIT: Use OptionParser in bisect script --- tool/zjit_bisect.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 39a302b2bd..fdf204b9f5 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -1,14 +1,26 @@ #!/usr/bin/env ruby require 'logger' require 'open3' +require 'optparse' require 'shellwords' require 'tempfile' require 'timeout' -RUBY = ARGV[0] || raise("Usage: ruby jit_bisect.rb ") +ARGS = {:timeout => 5} +OptionParser.new do |opts| + opts.banner += " -- " + opts.on("--timeout=TIMEOUT_SEC", "Seconds until child process is killed") do |timeout| + ARGS[:timeout] = Integer(timeout) + end + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end +end.parse! + +RUBY = ARGV[0] || raise("Usage: ruby jit_bisect.rb -- ") OPTIONS = ARGV[1..] raise("Usage: ruby jit_bisect.rb -- ") if OPTIONS.empty? -TIMEOUT_SEC = 5 LOGGER = Logger.new($stdout) # From https://github.com/tekknolagi/omegastar @@ -87,7 +99,7 @@ end LOGGER.info("Starting with JIT list of #{jit_list.length} items.") # Now narrow it down command = lambda do |items| - status = Timeout.timeout(TIMEOUT_SEC) do + status = Timeout.timeout(ARGS[:timeout]) do _, _, status = run_with_jit_list(RUBY, OPTIONS, items) status end From 180469a3a78793d0bd128c11e08f14f38d751095 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 7 Aug 2025 15:55:54 -0400 Subject: [PATCH 076/157] ZJIT: Actually kill timed-out process in bisect --- tool/zjit_bisect.rb | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index fdf204b9f5..021feb36de 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -72,6 +72,29 @@ def run_bisect(command, items) bisect_impl(command, [], items) end +def run_ruby *cmd + stdout_data = nil + stderr_data = nil + status = nil + Open3.popen3(Shellwords.join cmd) do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid + begin + Timeout.timeout(ARGS[:timeout]) do + stdout_data = stdout.read + stderr_data = stderr.read + status = wait_thr.value + end + rescue Timeout::Error + Process.kill("KILL", pid) + stderr_data = "(killed due to timeout)" + # Wait for the process to be reaped + wait_thr.value + status = 1 + end + end + [stdout_data, stderr_data, status] +end + def run_with_jit_list(ruby, options, jit_list) # Make a new temporary file containing the JIT list Tempfile.create("jit_list") do |temp_file| @@ -79,31 +102,26 @@ def run_with_jit_list(ruby, options, jit_list) temp_file.flush temp_file.close # Run the JIT with the temporary file - command = Shellwords.join [ruby, "--zjit-allowed-iseqs=#{temp_file.path}", *options] - Open3.capture3(command) + run_ruby ruby, "--zjit-allowed-iseqs=#{temp_file.path}", *options end end # Try running with no JIT list to get a stable baseline -_, stderr, status = run_with_jit_list(RUBY, OPTIONS, []) -if !status.success? +_, stderr, exitcode = run_with_jit_list(RUBY, OPTIONS, []) +if exitcode != 0 raise "Command failed with empty JIT list: #{stderr}" end # Collect the JIT list from the failing Ruby process jit_list = nil Tempfile.create "jit_list" do |temp_file| - command = Shellwords.join [RUBY, "--zjit-log-compiled-iseqs=#{temp_file.path}", *OPTIONS] - Open3.capture3(command) + run_ruby RUBY, "--zjit-log-compiled-iseqs=#{temp_file.path}", *OPTIONS jit_list = File.readlines(temp_file.path).map(&:strip).reject(&:empty?) end LOGGER.info("Starting with JIT list of #{jit_list.length} items.") # Now narrow it down command = lambda do |items| - status = Timeout.timeout(ARGS[:timeout]) do - _, _, status = run_with_jit_list(RUBY, OPTIONS, items) - status - end - status.success? + _, _, exitcode = run_with_jit_list(RUBY, OPTIONS, items) + exitcode == 0 end result = run_bisect(command, jit_list) File.open("jitlist.txt", "w") do |file| From c6a27a02533fb28888a49a78ef3ffb9b0be4a4d4 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 8 Aug 2025 10:40:55 -0400 Subject: [PATCH 077/157] ZJII: Address review feedback --- tool/zjit_bisect.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 021feb36de..a4280a4ec2 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -6,7 +6,7 @@ require 'shellwords' require 'tempfile' require 'timeout' -ARGS = {:timeout => 5} +ARGS = {timeout: 5} OptionParser.new do |opts| opts.banner += " -- " opts.on("--timeout=TIMEOUT_SEC", "Seconds until child process is killed") do |timeout| @@ -76,7 +76,7 @@ def run_ruby *cmd stdout_data = nil stderr_data = nil status = nil - Open3.popen3(Shellwords.join cmd) do |stdin, stdout, stderr, wait_thr| + Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| pid = wait_thr.pid begin Timeout.timeout(ARGS[:timeout]) do @@ -128,7 +128,7 @@ File.open("jitlist.txt", "w") do |file| file.puts(result) end puts "Run:" -command = Shellwords.join [RUBY, "--zjit-allowed-iseqs=jitlist.txt", *OPTIONS] +command = [RUBY, "--zjit-allowed-iseqs=jitlist.txt", *OPTIONS].shelljoin puts command puts "Reduced JIT list (available in jitlist.txt):" puts result From 7b5cd5ce15c0e0f5828b3e9ea07e59c306600066 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 01:01:47 +0900 Subject: [PATCH 078/157] Revert "Convert `PKG_CONFIG_PATH` to msys/cygwin path" This reverts commit 8e9ea4c202fb104d7c17ad1f3cc59d697120501a. The environment variable is converted internally. --- test/mkmf/test_pkg_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mkmf/test_pkg_config.rb b/test/mkmf/test_pkg_config.rb index d0a2dc130a..adf5fa6e92 100644 --- a/test/mkmf/test_pkg_config.rb +++ b/test/mkmf/test_pkg_config.rb @@ -26,7 +26,7 @@ class TestMkmfPkgConfig < TestMkmf Cflags: -I${includedir}/cflags-I --cflags-other EOF - @pkg_config_path, ENV["PKG_CONFIG_PATH"] = ENV["PKG_CONFIG_PATH"], mkintpath(@fixtures_dir) + @pkg_config_path, ENV["PKG_CONFIG_PATH"] = ENV["PKG_CONFIG_PATH"], @fixtures_dir end end From e4b386e090f00a6690511f2d14aff5fa7b80328d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 8 Aug 2025 20:52:16 +0900 Subject: [PATCH 079/157] CI: Remove Strawberry Perl pkg-config --- .github/workflows/mingw.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 7f8d05a634..ce53e33aae 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -70,6 +70,19 @@ jobs: with: ruby-version: '3.2' + - name: Remove Strawberry Perl pkg-config + working-directory: + # `pkg-config.bat` included in Strawberry Perl is written in + # Perl and doesn't work when another msys2 `perl` precede its + # own `perl`. + # + # ``` + # Can't find C:\Strawberry\perl\bin\pkg-config.bat on PATH, '.' not in PATH. + # ``` + run: | + Get-Command pkg-config.bat | % { ren $_.path ($_.path + "~") } + shell: pwsh + - name: Misc system & package info working-directory: run: | From 057d7c1c58eeaad9dbba4f07e9a390dcb6cafd8f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 8 Aug 2025 17:24:58 +0200 Subject: [PATCH 080/157] object_id_to_ref: complete incremental GC before iterating Otherwise dealing with garbage objects is tricky. --- gc.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gc.c b/gc.c index 90f2b29bfa..3d0935ad1a 100644 --- a/gc.c +++ b/gc.c @@ -1755,7 +1755,6 @@ rb_gc_pointer_to_heap_p(VALUE obj) #define LAST_OBJECT_ID() (object_id_counter * OBJ_ID_INCREMENT) static VALUE id2ref_value = 0; static st_table *id2ref_tbl = NULL; -static bool id2ref_tbl_built = false; #if SIZEOF_SIZE_T == SIZEOF_LONG_LONG static size_t object_id_counter = 1; @@ -1947,6 +1946,7 @@ build_id2ref_i(VALUE obj, void *data) case T_CLASS: case T_MODULE: if (RCLASS(obj)->object_id) { + RUBY_ASSERT(!rb_objspace_garbage_object_p(obj)); st_insert(id2ref_tbl, RCLASS(obj)->object_id, obj); } break; @@ -1955,6 +1955,7 @@ build_id2ref_i(VALUE obj, void *data) break; default: if (rb_shape_obj_has_id(obj)) { + RUBY_ASSERT(!rb_objspace_garbage_object_p(obj)); st_insert(id2ref_tbl, rb_obj_id(obj), obj); } break; @@ -1979,12 +1980,12 @@ object_id_to_ref(void *objspace_ptr, VALUE object_id) // build_id2ref_i will most certainly malloc, which could trigger GC and sweep // objects we just added to the table. - bool gc_disabled = RTEST(rb_gc_disable_no_rest()); + // By calling rb_gc_disable() we also save having to handle potentially garbage objects. + bool gc_disabled = RTEST(rb_gc_disable()); { rb_gc_impl_each_object(objspace, build_id2ref_i, (void *)id2ref_tbl); } if (!gc_disabled) rb_gc_enable(); - id2ref_tbl_built = true; } VALUE obj; @@ -2036,10 +2037,9 @@ obj_free_object_id(VALUE obj) RUBY_ASSERT(FIXNUM_P(obj_id) || RB_TYPE_P(obj_id, T_BIGNUM)); if (!st_delete(id2ref_tbl, (st_data_t *)&obj_id, NULL)) { - // If we're currently building the table then it's not a bug. // The the object is a T_IMEMO/fields, then it's possible the actual object // has been garbage collected already. - if (id2ref_tbl_built && !RB_TYPE_P(obj, T_IMEMO)) { + if (!RB_TYPE_P(obj, T_IMEMO)) { rb_bug("Object ID seen, but not in _id2ref table: object_id=%llu object=%s", NUM2ULL(obj_id), rb_obj_info(obj)); } } From 8eb26ebf918e6ffbb9d8f3e586ed1749b6c51f15 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 8 Aug 2025 10:56:19 -0700 Subject: [PATCH 081/157] ZJIT: Add a graphviz dumper for HIR (#14117) This is moderately useful just in stdout (copy and paste into a renderer) but potentially more useful alongside a tool that parses stdout looking for `digraph G { ... }` and renders those automatically. --- zjit/src/hir.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++ zjit/src/options.rs | 4 + 2 files changed, 180 insertions(+) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 55041945d4..f2f990afde 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -843,6 +843,22 @@ impl<'a> FunctionPrinter<'a> { } } +/// Pretty printer for [`Function`]. +pub struct FunctionGraphvizPrinter<'a> { + fun: &'a Function, + ptr_map: PtrPrintMap, +} + +impl<'a> FunctionGraphvizPrinter<'a> { + pub fn new(fun: &'a Function) -> Self { + let mut ptr_map = PtrPrintMap::identity(); + if cfg!(test) { + ptr_map.map_ptrs = true; + } + Self { fun, ptr_map } + } +} + /// Union-Find (Disjoint-Set) is a data structure for managing disjoint sets that has an interface /// of two operations: /// @@ -2115,6 +2131,10 @@ impl Function { Some(DumpHIR::Debug) => println!("Optimized HIR:\n{:#?}", &self), None => {}, } + + if get_option!(dump_hir_graphviz) { + println!("{}", FunctionGraphvizPrinter::new(&self)); + } } @@ -2293,6 +2313,87 @@ impl<'a> std::fmt::Display for FunctionPrinter<'a> { } } +struct HtmlEncoder<'a, 'b> { + formatter: &'a mut std::fmt::Formatter<'b>, +} + +impl<'a, 'b> std::fmt::Write for HtmlEncoder<'a, 'b> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for ch in s.chars() { + match ch { + '<' => self.formatter.write_str("<")?, + '>' => self.formatter.write_str(">")?, + '&' => self.formatter.write_str("&")?, + '"' => self.formatter.write_str(""")?, + '\'' => self.formatter.write_str("'")?, + _ => self.formatter.write_char(ch)?, + } + } + Ok(()) + } +} + +impl<'a> std::fmt::Display for FunctionGraphvizPrinter<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + macro_rules! write_encoded { + ($f:ident, $($arg:tt)*) => { + HtmlEncoder { formatter: $f }.write_fmt(format_args!($($arg)*)) + }; + } + use std::fmt::Write; + let fun = &self.fun; + let iseq_name = iseq_get_location(fun.iseq, 0); + write!(f, "digraph G {{ # ")?; + write_encoded!(f, "{iseq_name}")?; + write!(f, "\n")?; + writeln!(f, "node [shape=plaintext];")?; + writeln!(f, "mode=hier; overlap=false; splines=true;")?; + for block_id in fun.rpo() { + writeln!(f, r#" {block_id} [label=<"#)?; + write!(f, r#"")?; + for insn_id in &fun.blocks[block_id.0].insns { + let insn_id = fun.union_find.borrow().find_const(*insn_id); + let insn = fun.find(insn_id); + if matches!(insn, Insn::Snapshot {..}) { + continue; + } + write!(f, r#"")?; + } + writeln!(f, "
{block_id}("#)?; + if !fun.blocks[block_id.0].params.is_empty() { + let mut sep = ""; + for param in &fun.blocks[block_id.0].params { + write_encoded!(f, "{sep}{param}")?; + let insn_type = fun.type_of(*param); + if !insn_type.is_subtype(types::Empty) { + write_encoded!(f, ":{}", insn_type.print(&self.ptr_map))?; + } + sep = ", "; + } + } + let mut edges = vec![]; + writeln!(f, ") 
"#)?; + if insn.has_output() { + let insn_type = fun.type_of(insn_id); + if insn_type.is_subtype(types::Empty) { + write_encoded!(f, "{insn_id} = ")?; + } else { + write_encoded!(f, "{insn_id}:{} = ", insn_type.print(&self.ptr_map))?; + } + } + if let Insn::Jump(ref target) | Insn::IfTrue { ref target, .. } | Insn::IfFalse { ref target, .. } = insn { + edges.push((insn_id, target.target)); + } + write_encoded!(f, "{}", insn.print(&self.ptr_map))?; + writeln!(f, " 
>];")?; + for (src, dst) in edges { + writeln!(f, " {block_id}:{src} -> {dst}:params;")?; + } + } + writeln!(f, "}}") + } +} + #[derive(Debug, Clone, PartialEq)] pub struct FrameState { iseq: IseqPtr, @@ -5145,6 +5246,81 @@ mod tests { } } +#[cfg(test)] +mod graphviz_tests { + use super::*; + use expect_test::{expect, Expect}; + + #[track_caller] + fn assert_optimized_graphviz(method: &str, expected: Expect) { + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); + unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; + let mut function = iseq_to_hir(iseq).unwrap(); + function.optimize(); + function.validate().unwrap(); + let actual = format!("{}", FunctionGraphvizPrinter::new(&function)); + expected.assert_eq(&actual); + } + + #[test] + fn test_guard_fixnum_or_fixnum() { + eval(r#" + def test(x, y) = x | y + + test(1, 2) + "#); + assert_optimized_graphviz("test", expect![[r#" + digraph G { # test@<compiled>:2 + node [shape=plaintext]; + mode=hier; overlap=false; splines=true; + bb0 [label=< + + + + + + +
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject) 
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, 29) 
v8:Fixnum = GuardType v1, Fixnum 
v9:Fixnum = GuardType v2, Fixnum 
v10:Fixnum = FixnumOr v8, v9 
Return v10 
>]; + } + "#]]); + } + + #[test] + fn test_multiple_blocks() { + eval(r#" + def test(c) + if c + 3 + else + 4 + end + end + + test(1) + test("x") + "#); + assert_optimized_graphviz("test", expect![[r#" + digraph G { # test@<compiled>:3 + node [shape=plaintext]; + mode=hier; overlap=false; splines=true; + bb0 [label=< + + + + + +
bb0(v0:BasicObject, v1:BasicObject) 
v3:CBool = Test v1 
IfFalse v3, bb1(v0, v1) 
v5:Fixnum[3] = Const Value(3) 
Return v5 
>]; + bb0:v4 -> bb1:params; + bb1 [label=< + + + +
bb1(v7:BasicObject, v8:BasicObject) 
v10:Fixnum[4] = Const Value(4) 
Return v10 
>]; + } + "#]]); + } +} + #[cfg(test)] mod opt_tests { use super::*; diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 340812f089..92f56b8916 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -37,6 +37,8 @@ pub struct Options { /// Dump High-level IR after optimization, right before codegen. pub dump_hir_opt: Option, + pub dump_hir_graphviz: bool, + /// Dump low-level IR pub dump_lir: bool, @@ -61,6 +63,7 @@ impl Default for Options { debug: false, dump_hir_init: None, dump_hir_opt: None, + dump_hir_graphviz: false, dump_lir: false, dump_disasm: false, perf: false, @@ -186,6 +189,7 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { ("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot), ("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All), ("dump-hir" | "dump-hir-opt", "debug") => options.dump_hir_opt = Some(DumpHIR::Debug), + ("dump-hir-graphviz", "") => options.dump_hir_graphviz = true, ("dump-hir-init", "") => options.dump_hir_init = Some(DumpHIR::WithoutSnapshot), ("dump-hir-init", "all") => options.dump_hir_init = Some(DumpHIR::All), From eb931a09c53568abc7773e38bcc9f68f523db193 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 8 Aug 2025 11:24:39 -0700 Subject: [PATCH 082/157] ZJIT: Fix "memory operand with non-register base" (#14153) --- test/ruby/test_zjit.rb | 12 ++++++++++++ zjit/src/backend/lir.rs | 2 +- zjit/src/codegen.rs | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 9eca30c787..e040463bbf 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -148,6 +148,18 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2 end + def test_send_on_heap_object_in_spilled_arg + # This leads to a register spill, so not using `assert_compiles` + assert_runs 'Hash', %q{ + def entry(a1, a2, a3, a4, a5, a6, a7, a8, a9) + a9.itself.class + end + + entry(1, 2, 3, 4, 5, 6, 7, 8, {}) # profile + entry(1, 2, 3, 4, 5, 6, 7, 8, {}) + }, call_threshold: 2 + end + def test_invokebuiltin omit 'Test fails at the moment due to not handling optional parameters' assert_compiles '["."]', %q{ diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 1bed45cba5..3263392cf6 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -111,7 +111,7 @@ impl Opnd }) }, - _ => unreachable!("memory operand with non-register base") + _ => unreachable!("memory operand with non-register base: {base:?}") } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 3e6bbfa605..1d6901bac4 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1088,9 +1088,15 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard } else if let Some(expected_class) = guard_type.runtime_exact_ruby_class() { asm_comment!(asm, "guard exact class for non-immediate types"); - let side_exit = side_exit(jit, state, GuardType(guard_type))?; + // If val isn't in a register, load it to use it as the base of Opnd::mem later. + // TODO: Max thinks codegen should not care about the shapes of the operands except to create them. (Shopify/ruby#685) + let val = match val { + Opnd::Reg(_) | Opnd::VReg { .. } => val, + _ => asm.load(val), + }; // Check if it's a special constant + let side_exit = side_exit(jit, state, GuardType(guard_type))?; asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); asm.jnz(side_exit.clone()); From 0ba488d7f51c8b52811445245c87cb824e564069 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 8 Aug 2025 14:54:53 -0400 Subject: [PATCH 083/157] ZJIT: Avoid compiling and direct sends to forwardable ISEQs These `...` ISEQs have a special calling convention in the interpreter and our stubs and JIT calling convention don't deal well. Reject for now. Debugged with help from `@tekknolagi` and `tool/zjit_bisect.rb`. Merely avoiding direct sends is enough to pass the attached test, but also avoid compiling ISEQs with `...` parameter to limit exposure for now. `SendWithoutBlock`, which does dynamic dispatch using interpreter code, seems to handle calling into forwardable ISEQs correctly, so they are fine -- we can't predict where these dynamic sends land anyways. --- test/ruby/test_zjit.rb | 9 +++++++++ zjit/src/hir.rs | 30 +++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index e040463bbf..c86ac62a9f 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -70,6 +70,15 @@ class TestZJIT < Test::Unit::TestCase } end + def test_call_a_forwardable_method + assert_runs '[]', %q{ + def test_root = forwardable + def forwardable(...) = Array.[](...) + test_root + test_root + }, call_threshold: 2 + end + def test_setlocal_on_eval_with_spill assert_compiles '1', %q{ @b = binding diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f2f990afde..87d2a613d0 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -969,6 +969,7 @@ fn can_direct_send(iseq: *const rb_iseq_t) -> bool { else if unsafe { rb_get_iseq_flags_has_kw(iseq) } { false } else if unsafe { rb_get_iseq_flags_has_kwrest(iseq) } { false } else if unsafe { rb_get_iseq_flags_has_block(iseq) } { false } + else if unsafe { rb_get_iseq_flags_forwardable(iseq) } { false } else { true } } @@ -2581,6 +2582,9 @@ pub enum CallType { #[derive(Debug, PartialEq)] pub enum ParameterType { Optional, + /// For example, `foo(...)`. Interaction of JIT + /// calling convention and side exits currently unsolved. + Forwardable, } #[derive(Debug, PartialEq)] @@ -2650,6 +2654,7 @@ pub const SELF_PARAM_IDX: usize = 0; fn filter_unknown_parameter_type(iseq: *const rb_iseq_t) -> Result<(), ParseError> { if unsafe { rb_get_iseq_body_param_opt_num(iseq) } != 0 { return Err(ParseError::UnknownParameterType(ParameterType::Optional)); } + if unsafe { rb_get_iseq_flags_forwardable(iseq) } { return Err(ParseError::UnknownParameterType(ParameterType::Forwardable)); } Ok(()) } @@ -4583,11 +4588,13 @@ mod tests { eval(" def test(...) = super(...) "); - assert_method_hir("test", expect![[r#" - fn test@:2: - bb0(v0:BasicObject, v1:BasicObject): - SideExit UnknownOpcode(invokesuperforward) - "#]]); + assert_compile_fails("test", ParseError::UnknownParameterType(ParameterType::Forwardable)); + } + + #[test] + fn test_cant_compile_forwardable() { + eval("def forwardable(...) = nil"); + assert_compile_fails("forwardable", ParseError::UnknownParameterType(ParameterType::Forwardable)); } // TODO(max): Figure out how to generate a call with OPT_SEND flag @@ -4631,11 +4638,7 @@ mod tests { eval(" def test(...) = foo(...) "); - assert_method_hir("test", expect![[r#" - fn test@:2: - bb0(v0:BasicObject, v1:BasicObject): - SideExit UnknownOpcode(sendforward) - "#]]); + assert_compile_fails("test", ParseError::UnknownParameterType(ParameterType::Forwardable)); } #[test] @@ -5691,7 +5694,6 @@ mod opt_tests { def kw_rest(**k) = k def post(*rest, post) = post def block(&b) = nil - def forwardable(...) = nil "); assert_optimized_method_hir("rest", expect![[r#" @@ -5721,12 +5723,6 @@ mod opt_tests { bb0(v0:BasicObject, v1:ArrayExact, v2:BasicObject): Return v2 "#]]); - assert_optimized_method_hir("forwardable", expect![[r#" - fn forwardable@:7: - bb0(v0:BasicObject, v1:BasicObject): - v3:NilClass = Const Value(nil) - Return v3 - "#]]); } #[test] From e639e5fd1af51e2462879d6db862ee5320914ba7 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 8 Aug 2025 15:04:48 -0400 Subject: [PATCH 084/157] Make rb_gc_impl_writebarrier_remember Ractor-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rb_gc_impl_writebarrier_remember is not Ractor safe because it writes to bitmaps and also pushes onto the mark stack during incremental marking. We should acquire the VM lock to prevent race conditions. In the case that the object is not old, there is no performance impact. However, we can see a performance impact in this microbenchmark where the object is old: 4.times.map do Ractor.new do ary = [] 3.times { GC.start } 10_000_000.times do |i| ary.push(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) ary.clear end end end.map(&:value) Before: Time (mean ± σ): 682.4 ms ± 5.1 ms [User: 2564.8 ms, System: 16.0 ms] After: Time (mean ± σ): 5.522 s ± 0.096 s [User: 8.237 s, System: 7.931 s] Co-Authored-By: Luke Gruber Co-Authored-By: John Hawthorn --- gc/default/default.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 9038a01e4e..d4e34b9d03 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -6110,15 +6110,19 @@ rb_gc_impl_writebarrier_remember(void *objspace_ptr, VALUE obj) gc_report(1, objspace, "rb_gc_writebarrier_remember: %s\n", rb_obj_info(obj)); - if (is_incremental_marking(objspace)) { - if (RVALUE_BLACK_P(objspace, obj)) { - gc_grey(objspace, obj); - } - } - else { - if (RVALUE_OLD_P(objspace, obj)) { - rgengc_remember(objspace, obj); + if (is_incremental_marking(objspace) || RVALUE_OLD_P(objspace, obj)) { + int lev = RB_GC_VM_LOCK_NO_BARRIER(); + { + if (is_incremental_marking(objspace)) { + if (RVALUE_BLACK_P(objspace, obj)) { + gc_grey(objspace, obj); + } + } + else if (RVALUE_OLD_P(objspace, obj)) { + rgengc_remember(objspace, obj); + } } + RB_GC_VM_UNLOCK_NO_BARRIER(lev); } } From 07878ebe787843f510be460738ff02dd883bf9ad Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Mon, 23 Jun 2025 14:33:52 -0400 Subject: [PATCH 085/157] Fix lock ordering issue for rb_ractor_sched_wait() and rb_ractor_sched_wakeup() In rb_ractor_sched_wait() (ex: Ractor.receive), we acquire RACTOR_LOCK(cr) and then thread_sched_lock(cur_th). However, on wakeup if we're a dnt, in thread_sched_wait_running_turn() we acquire thread_sched_lock(cur_th) after condvar wakeup and then RACTOR_LOCK(cr). This lock inversion can cause a deadlock with rb_ractor_wakeup_all() (ex: port.send(obj)), where we acquire RACTOR_LOCK(other_r) and then thread_sched_lock(other_th). So, the error happens: nt 1: Ractor.receive rb_ractor_sched_wait() after condvar wakeup in thread_sched_wait_running_turn(): - thread_sched_lock(cur_th) (condvar) # acquires lock - rb_ractor_lock_self(cr) # deadlock here: tries to acquire, HANGS nt 2: port.send ractor_wakeup_all() - RACTOR_LOCK(port_r) # acquires lock - thread_sched_lock # tries to acquire, HANGS To fix it, we now unlock the thread_sched_lock before acquiring the ractor_lock in rb_ractor_sched_wait(). Script that reproduces issue: ```ruby require "async" class RactorWrapper def initialize @ractor = Ractor.new do Ractor.recv # Ractor doesn't start until explicitly told to # Do some calculations fib = ->(x) { x < 2 ? 1 : fib.call(x - 1) + fib.call(x - 2) } fib.call(20) end end def take_async @ractor.send(nil) Thread.new { @ractor.value }.value end end Async do |task| 10_000.times do |i| task.async do RactorWrapper.new.take_async puts i end end end exit 0 ``` Fixes [Bug #21398] Co-authored-by: John Hawthorn --- test/ruby/test_ractor.rb | 39 +++++++++++++++++++++++++++++++++++++++ thread_pthread.c | 13 +++++-------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 0a456a1d0f..74de2bf9cd 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -162,6 +162,45 @@ class TestRactor < Test::Unit::TestCase RUBY end + # [Bug #21398] + def test_port_receive_dnt_with_port_send + assert_ractor(<<~'RUBY', timeout: 30) + THREADS = 10 + JOBS_PER_THREAD = 50 + ARRAY_SIZE = 20_000 + def ractor_job(job_count, array_size) + port = Ractor::Port.new + workers = (1..4).map do |i| + Ractor.new(port) do |job_port| + while job = Ractor.receive + result = job.map { |x| x * 2 }.sum + job_port.send result + end + end + end + jobs = Array.new(job_count) { Array.new(array_size) { rand(1000) } } + jobs.each_with_index do |job, i| + w_idx = i % 4 + workers[w_idx].send(job) + end + results = [] + jobs.size.times do + result = port.receive # dnt receive + results << result + end + results + end + threads = [] + # creates 40 ractors (THREADSx4) + THREADS.times do + threads << Thread.new do + ractor_job(JOBS_PER_THREAD, ARRAY_SIZE) + end + end + threads.each(&:join) + RUBY + end + def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj) diff --git a/thread_pthread.c b/thread_pthread.c index 377e1d9f64..730ecb5416 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1351,6 +1351,7 @@ rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_fun } thread_sched_lock(sched, th); + rb_ractor_unlock_self(cr); { // setup sleep bool can_direct_transfer = !th_has_dedicated_nt(th); @@ -1358,16 +1359,12 @@ rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_fun th->status = THREAD_STOPPED_FOREVER; RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th); thread_sched_wakeup_next_thread(sched, th, can_direct_transfer); - - rb_ractor_unlock_self(cr); - { - // sleep - thread_sched_wait_running_turn(sched, th, can_direct_transfer); - th->status = THREAD_RUNNABLE; - } - rb_ractor_lock_self(cr); + // sleep + thread_sched_wait_running_turn(sched, th, can_direct_transfer); + th->status = THREAD_RUNNABLE; } thread_sched_unlock(sched, th); + rb_ractor_lock_self(cr); ubf_clear(th); From d80c03d22a5e92a5423a18da1d6494c484392c87 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 8 Aug 2025 16:31:29 -0700 Subject: [PATCH 086/157] Fix id2ref table build when GC in progress Previously, if GC was in progress when we're initially building the id2ref table, it could see the empty table and then crash when trying to remove ids from it. This commit fixes the bug by only publishing the table after GC is done. Co-authored-by: Aaron Patterson --- gc.c | 7 +++++-- test/ruby/test_objectspace.rb | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/gc.c b/gc.c index 3d0935ad1a..4c8a042c1e 100644 --- a/gc.c +++ b/gc.c @@ -1975,14 +1975,17 @@ object_id_to_ref(void *objspace_ptr, VALUE object_id) // GC Must not trigger while we build the table, otherwise if we end // up freeing an object that had an ID, we might try to delete it from // the table even though it wasn't inserted yet. - id2ref_tbl = st_init_table(&object_id_hash_type); - id2ref_value = TypedData_Wrap_Struct(0, &id2ref_tbl_type, id2ref_tbl); + st_table *tmp_id2ref_tbl = st_init_table(&object_id_hash_type); + VALUE tmp_id2ref_value = TypedData_Wrap_Struct(0, &id2ref_tbl_type, tmp_id2ref_tbl); // build_id2ref_i will most certainly malloc, which could trigger GC and sweep // objects we just added to the table. // By calling rb_gc_disable() we also save having to handle potentially garbage objects. bool gc_disabled = RTEST(rb_gc_disable()); { + id2ref_tbl = tmp_id2ref_tbl; + id2ref_value = tmp_id2ref_value; + rb_gc_impl_each_object(objspace, build_id2ref_i, (void *)id2ref_tbl); } if (!gc_disabled) rb_gc_enable(); diff --git a/test/ruby/test_objectspace.rb b/test/ruby/test_objectspace.rb index f27f586ab7..a479547599 100644 --- a/test/ruby/test_objectspace.rb +++ b/test/ruby/test_objectspace.rb @@ -284,6 +284,21 @@ End end; end + def test_id2ref_table_build + assert_separately([], <<-End) + 10.times do + Object.new.object_id + end + + GC.start(immediate_mark: false) + + obj = Object.new + EnvUtil.suppress_warning do + assert_equal obj, ObjectSpace._id2ref(obj.object_id) + end + End + end + def test_each_object_singleton_class assert_separately([], <<-End) class C From 22fe80f275f5e8a66d1e38daab1eb663eab79850 Mon Sep 17 00:00:00 2001 From: Carl Zulauf Date: Mon, 4 Aug 2025 08:52:03 -0600 Subject: [PATCH 087/157] Fix Typo in Regular Expressions docs (_regexp.rdoc) Small fix for a typo in the regular expression docs. The line of code above this change does not produce the output shown in the docs. With this change the docs will show the correct output for this example of using regex quantifiers. --- doc/_regexp.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/_regexp.rdoc b/doc/_regexp.rdoc index 45a307fad6..c9f3742241 100644 --- a/doc/_regexp.rdoc +++ b/doc/_regexp.rdoc @@ -502,7 +502,7 @@ An added _quantifier_ specifies how many matches are required or allowed: /\w*/.match('x') # => # /\w*/.match('xyz') - # => # + # => # - + - Matches one or more times: From 4209ebb1e4c30f6cb70047238c858c99e773e964 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 5 Aug 2025 11:12:13 -0700 Subject: [PATCH 088/157] [DOC] Array#fill fix to indicate return is self doc currently indicates the return value as `new_array` but then in the first sentence explains "always returns +self+ (never a new array)". --- array.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/array.c b/array.c index f485223e34..1afd52fb3d 100644 --- a/array.c +++ b/array.c @@ -4755,10 +4755,10 @@ rb_ary_clear(VALUE ary) /* * call-seq: - * fill(object, start = nil, count = nil) -> new_array - * fill(object, range) -> new_array - * fill(start = nil, count = nil) {|element| ... } -> new_array - * fill(range) {|element| ... } -> new_array + * fill(object, start = nil, count = nil) -> self + * fill(object, range) -> self + * fill(start = nil, count = nil) {|element| ... } -> self + * fill(range) {|element| ... } -> self * * Replaces selected elements in +self+; * may add elements to +self+; From 60ca525fce71b702ea8e5893c976044170a56d75 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 5 Aug 2025 11:42:05 -0700 Subject: [PATCH 089/157] [DOC] Array#map! fix to indicate return is self --- array.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/array.c b/array.c index 1afd52fb3d..9f13b1bf51 100644 --- a/array.c +++ b/array.c @@ -3659,9 +3659,9 @@ rb_ary_collect(VALUE ary) /* * call-seq: - * collect! {|element| ... } -> new_array + * collect! {|element| ... } -> self * collect! -> new_enumerator - * map! {|element| ... } -> new_array + * map! {|element| ... } -> self * map! -> new_enumerator * * With a block given, calls the block with each element of +self+ From 23c0113932407abccddbc6ee5b297d38d2d2bb9c Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:30:17 +0900 Subject: [PATCH 090/157] [ruby/stringio] fix: prevent segfault in StringIO#seek with SEEK_END on null device (https://github.com/ruby/stringio/pull/137) Fixes segmentation fault when calling `seek` with `SEEK_END` on null device StringIO created by `StringIO.new(nil)`. ```bash ruby -e "require 'stringio'; StringIO.new(nil).seek(0, IO::SEEK_END)" ``` I tested with below versions. ```bash [koh@Kohs-MacBook-Pro] ~ % ruby -v;gem info stringio;sw_vers ruby 3.4.5 (2025-07-16 revision https://github.com/ruby/stringio/commit/20cda200d3) +PRISM [arm64-darwin24] *** LOCAL GEMS *** stringio (3.1.2) Authors: Nobu Nakada, Charles Oliver Nutter Homepage: https://github.com/ruby/stringio Licenses: Ruby, BSD-2-Clause Installed at (default): /Users/koh/.local/share/mise/installs/ruby/3.4.5/lib/ruby/gems/3.4.0 Pseudo IO on String ProductName: macOS ProductVersion: 15.5 BuildVersion: 24F74 [koh@Kohs-MacBook-Pro] ~ % ``` https://github.com/ruby/stringio/commit/9399747bf9 --- ext/stringio/stringio.c | 6 +++++- test/stringio/test_stringio.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 3003939e10..d9beb25434 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -837,7 +837,11 @@ strio_seek(int argc, VALUE *argv, VALUE self) offset = ptr->pos; break; case 2: - offset = RSTRING_LEN(ptr->string); + if (NIL_P(ptr->string)) { + offset = 0; + } else { + offset = RSTRING_LEN(ptr->string); + } break; default: error_inval("invalid whence"); diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index 002b946b6f..4c9cf37425 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -70,6 +70,16 @@ class TestStringIO < Test::Unit::TestCase assert_nil io.getc end + def test_seek_null + io = StringIO.new(nil) + assert_equal(0, io.seek(0, IO::SEEK_SET)) + assert_equal(0, io.pos) + assert_equal(0, io.seek(0, IO::SEEK_CUR)) + assert_equal(0, io.pos) + assert_equal(0, io.seek(0, IO::SEEK_END)) # This should not segfault + assert_equal(0, io.pos) + end + def test_truncate io = StringIO.new("") io.puts "abc" From 31f2d8990dcebf84cbbd3fedf838babaa59554a3 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 19:44:26 +0900 Subject: [PATCH 091/157] [ruby/stringio] Fix SEGV at read/pread on null StringIO https://github.com/ruby/stringio/commit/113dd5a55e --- ext/stringio/stringio.c | 28 +++++++++++++++++++--------- test/stringio/test_stringio.rb | 10 ++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index d9beb25434..1e6310d292 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -203,6 +203,18 @@ check_modifiable(struct StringIO *ptr) } } +static inline bool +outside_p(struct StringIO *ptr, long pos) +{ + return NIL_P(ptr->string) || pos >= RSTRING_LEN(ptr->string); +} + +static inline bool +eos_p(struct StringIO *ptr) +{ + return outside_p(ptr, ptr->pos); +} + static VALUE strio_s_allocate(VALUE klass) { @@ -628,9 +640,8 @@ static struct StringIO * strio_to_read(VALUE self) { struct StringIO *ptr = readable(self); - if (NIL_P(ptr->string)) return NULL; - if (ptr->pos < RSTRING_LEN(ptr->string)) return ptr; - return NULL; + if (eos_p(ptr)) return NULL; + return ptr; } /* @@ -910,7 +921,7 @@ strio_getc(VALUE self) int len; char *p; - if (NIL_P(str) || pos >= RSTRING_LEN(str)) { + if (eos_p(ptr)) { return Qnil; } p = RSTRING_PTR(str)+pos; @@ -931,7 +942,7 @@ strio_getbyte(VALUE self) { struct StringIO *ptr = readable(self); int c; - if (NIL_P(ptr->string) || ptr->pos >= RSTRING_LEN(ptr->string)) { + if (eos_p(ptr)) { return Qnil; } c = RSTRING_PTR(ptr->string)[ptr->pos++]; @@ -1609,10 +1620,9 @@ strio_read(int argc, VALUE *argv, VALUE self) if (len < 0) { rb_raise(rb_eArgError, "negative length %ld given", len); } - if (len > 0 && - (NIL_P(ptr->string) || ptr->pos >= RSTRING_LEN(ptr->string))) { + if (eos_p(ptr)) { if (!NIL_P(str)) rb_str_resize(str, 0); - return Qnil; + return len > 0 ? Qnil : rb_str_new(0, 0); } binary = 1; break; @@ -1688,7 +1698,7 @@ strio_pread(int argc, VALUE *argv, VALUE self) struct StringIO *ptr = readable(self); - if (offset >= RSTRING_LEN(ptr->string)) { + if (outside_p(ptr, offset)) { rb_eof_error(); } diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index 4c9cf37425..8b5ab37657 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -70,6 +70,16 @@ class TestStringIO < Test::Unit::TestCase assert_nil io.getc end + def test_pread_null + io = StringIO.new(nil) + assert_raise(EOFError) { io.pread(1, 0) } + end + + def test_read_null + io = StringIO.new(nil) + assert_equal "", io.read(0) + end + def test_seek_null io = StringIO.new(nil) assert_equal(0, io.seek(0, IO::SEEK_SET)) From b4d5ebcd12418642dec9f1e2e73ac87495666262 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 19:45:25 +0900 Subject: [PATCH 092/157] [ruby/stringio] Fix SEGV at eof? on null StringIO https://github.com/ruby/stringio/commit/29b9133332 --- test/stringio/test_stringio.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index 8b5ab37657..5215a6d312 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -70,6 +70,11 @@ class TestStringIO < Test::Unit::TestCase assert_nil io.getc end + def test_pread_eof + io = StringIO.new(nil) + assert_predicate io, :eof? + end + def test_pread_null io = StringIO.new(nil) assert_raise(EOFError) { io.pread(1, 0) } From 77b3495e979ceb597e86d255d3d647b8d5b43a9e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 20:15:53 +0900 Subject: [PATCH 093/157] [ruby/stringio] Adjust indent [ci skip] https://github.com/ruby/stringio/commit/ac6292c17f --- ext/stringio/stringio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 1e6310d292..0493c8cd50 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1935,7 +1935,7 @@ Init_stringio(void) #undef rb_intern #ifdef HAVE_RB_EXT_RACTOR_SAFE - rb_ext_ractor_safe(true); + rb_ext_ractor_safe(true); #endif VALUE StringIO = rb_define_class("StringIO", rb_cObject); From 2a6345e957c01f4495323723c7a3d7ac0d4ac339 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 9 Aug 2025 10:54:22 +0200 Subject: [PATCH 094/157] time.c: fix time_mark_and_move when WIDEVALUE_IS_WIDER In such case the pointer need to be casted. --- time.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time.c b/time.c index 7159a93098..95df85db93 100644 --- a/time.c +++ b/time.c @@ -1891,8 +1891,8 @@ static void time_mark_and_move(void *ptr) { struct time_object *tobj = ptr; - if (!FIXWV_P(tobj->timew)) { - rb_gc_mark_and_move(&WIDEVAL_GET(tobj->timew)); + if (!WIDEVALUE_IS_WIDER || !FIXWV_P(tobj->timew)) { + rb_gc_mark_and_move((VALUE *)&WIDEVAL_GET(tobj->timew)); } rb_gc_mark_and_move(&tobj->vtm.year); rb_gc_mark_and_move(&tobj->vtm.subsecx); From 90f81994125200d0640cc3bcf7340700144f804b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 10 Aug 2025 00:55:34 +0900 Subject: [PATCH 095/157] CI: mingw: Tweak misc system & package info --- .github/workflows/mingw.yml | 50 +++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index ce53e33aae..d74bf062b9 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -86,25 +86,37 @@ jobs: - name: Misc system & package info working-directory: run: | - # show where - result=true - for e in gcc.exe ragel.exe make.exe libcrypto-3-x64.dll libssl-3-x64.dll; do - echo ::group::$'\e[93m'$e$'\e[m' - where $e || result=false - echo ::endgroup:: - done - # show version - for e in gcc ragel make "openssl version"; do - case "$e" in *" "*) ;; *) e="$e --version";; esac - echo ::group::$'\e[93m'$e$'\e[m' - $e || result=false - echo ::endgroup:: - done - # show packages - echo ::group::$'\e[93m'Packages$'\e[m' - pacman -Qs mingw-w64-ucrt-x86_64-* | sed -n "s,local/mingw-w64-ucrt-x86_64-,,p" - echo ::endgroup:: - $result + group() { echo ::group::$'\e[94;1m'"$*"$'\e[m'; } + endgroup() { echo ::endgroup::; } + + group Path + cygpath -wa / . $(type -p cygpath bash sh) + endgroup + + I() { + group $1 + run Where type -pa $1 && { [ $# -eq 1 ] || run Version "$@"; } || + failed+=($1) + endgroup + } + run() { local w m=$1; shift; w="$("$@")" && show "$m" && indent "$w"; } + indent() { [ -z "$1" ] || echo "$1" | /bin/sed '/^$/!s/^/ /'; } + show() { echo $'\e[96m'"$*"$'\e[m'; } + + failed=() + + I gcc.exe --version + I ragel.exe --version + I make.exe --version + I openssl.exe version + I libcrypto-3-x64.dll + I libssl-3-x64.dll + + group Packages + pacman -Qs mingw-w64-ucrt-x86_64-* | /bin/sed -n "s,local/mingw-w64-ucrt-x86_64-,,p" + endgroup + + [ ${#failed[@]} -eq 0 ] - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: From c1f9f0a7ef9cfb4c57fb90b0a04f8f0274856386 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 10 Aug 2025 15:41:35 +0900 Subject: [PATCH 096/157] CI: windows: Windows-2019 or earlier no longer used --- .github/workflows/windows.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 053c37ec5d..d7c88393be 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -39,7 +39,7 @@ jobs: test_task: test-bundled-gems fail-fast: false - runs-on: windows-${{ matrix.os < 2022 && '2019' || matrix.os }} + runs-on: windows-${{ matrix.os }} if: >- ${{!(false @@ -94,7 +94,7 @@ jobs: - name: Install libraries with vcpkg run: | - vcpkg install --vcpkg-root=C:\Users\runneradmin\scoop\apps\vcpkg\current + vcpkg install --vcpkg-root=%USERPROFILE%\scoop\apps\vcpkg\current working-directory: src - name: Save vcpkg artifact @@ -184,7 +184,7 @@ jobs: - name: Set up Launchable uses: ./.github/actions/launchable/setup with: - os: windows-${{ matrix.os < 2022 && '2019' || matrix.os }} + os: windows-${{ matrix.os }} launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} builddir: build srcdir: src From cc4eba000b18558dc65fb349dce2b92aa7d1760f Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 10 Aug 2025 01:07:40 +0900 Subject: [PATCH 097/157] Win: Use `@` instead of `echo off` in `vssetup.cmd` `echo off` affects the batch files called from this file as well. --- win32/vssetup.cmd | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/win32/vssetup.cmd b/win32/vssetup.cmd index be77c87b29..1ff0a7d10a 100755 --- a/win32/vssetup.cmd +++ b/win32/vssetup.cmd @@ -1,27 +1,27 @@ -@echo off -setlocal ENABLEEXTENSIONS +@setlocal ENABLEEXTENSIONS +::- do not `echo off` that affects the called batch files ::- check for vswhere -set vswhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe -if not exist "%vswhere%" ( +@set vswhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe +@if not exist "%vswhere%" ( echo 1>&2 vswhere.exe not found exit /b 1 ) ::- find the latest build tool and its setup batch file. -set VSDEVCMD= -for /f "delims=" %%I in ('"%vswhere%" -products * -latest -property installationPath') do ( +@set VSDEVCMD= +@for /f "delims=" %%I in ('"%vswhere%" -products * -latest -property installationPath') do @( set VSDEVCMD=%%I\Common7\Tools\VsDevCmd.bat ) -if not defined VSDEVCMD ( +@if not defined VSDEVCMD ( echo 1>&2 Visual Studio not found exit /b 1 ) ::- default to the current processor. -set arch=%PROCESSOR_ARCHITECTURE% +@set arch=%PROCESSOR_ARCHITECTURE% ::- `vsdevcmd.bat` requires arch names to be lowercase -for %%i in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do @( +@for %%i in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do @( call set arch=%%arch:%%i=%%i%% ) -echo on && endlocal && "%VSDEVCMD%" -arch=%arch% -host_arch=%arch% %* +@(endlocal && "%VSDEVCMD%" -arch=%arch% -host_arch=%arch% %*) From a443cd012a352a7660ee22f9468082031f354d47 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 20:26:19 +0900 Subject: [PATCH 098/157] CI: mingw: Set `cmd` as the default shell It is used in more steps than `sh`. --- .github/workflows/mingw.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index d74bf062b9..ca3d6a2859 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -117,6 +117,7 @@ jobs: endgroup [ ${#failed[@]} -eq 0 ] + shell: sh - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -135,6 +136,7 @@ jobs: run: > ../src/configure --disable-install-doc --prefix=/. --build=$CHOST --host=$CHOST --target=$CHOST + shell: sh - name: make all timeout-minutes: 30 @@ -157,7 +159,6 @@ jobs: - name: test timeout-minutes: 30 run: make test - shell: cmd env: GNUMAKEFLAGS: '' RUBY_TESTOPTS: '-v --tty=no' @@ -165,7 +166,6 @@ jobs: - name: test-all timeout-minutes: 45 - shell: cmd run: | make ${{ StartsWith(matrix.test_task, 'test/') && matrix.test_task || 'test-all' }} env: @@ -180,7 +180,6 @@ jobs: timeout-minutes: 10 run: | make ${{ StartsWith(matrix.test_task, 'spec/') && matrix.test_task || 'test-spec' }} - shell: cmd if: ${{ matrix.test_task == 'check' || matrix.test_task == 'test-spec' || StartsWith(matrix.test_task, 'spec/') }} - uses: ./src/.github/actions/slack @@ -192,4 +191,4 @@ jobs: defaults: run: working-directory: build - shell: sh + shell: cmd From 4adb6f6969d3f58d3987708201acf4f44b870cc9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 9 Aug 2025 21:37:08 +0900 Subject: [PATCH 099/157] CI: mingw: Set up msys2 environment variables --- .github/workflows/mingw.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index ca3d6a2859..3e889c56c0 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -70,6 +70,24 @@ jobs: with: ruby-version: '3.2' + - name: Set up env + id: setup-env + working-directory: + run: | + $msys2 = ${env:MSYS2_LOCATION} + echo $msys2\usr\bin $msys2\ucrt64\bin | + Tee-Object ${env:GITHUB_PATH} -Append -Encoding utf-8 + + # Use the fast device for the temporary directory. + # %TEMP% is inconsistent with %TMP% and test-all expects they are consistent. + # https://github.com/actions/virtual-environments/issues/712#issuecomment-613004302 + $tmp = ${env:RUNNER_TEMP} + echo HOME=$home TMP=$tmp TEMP=$tmp TMPDIR=$tmp | + Tee-Object ${env:GITHUB_ENV} -Append -Encoding utf-8 + shell: pwsh # cmd.exe does not strip spaces before `|`. + env: + MSYS2_LOCATION: ${{ env.RI_DEVKIT }} + - name: Remove Strawberry Perl pkg-config working-directory: # `pkg-config.bat` included in Strawberry Perl is written in From b0a4e2399b34701d4fc05f9543142b0c73d1fb9b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 24 Jul 2025 14:01:02 +0900 Subject: [PATCH 100/157] CI: mingw: Use the official actions for msys2 Because ruby/setup-ruby is affected to test result. --- .github/workflows/mingw.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 3e889c56c0..1c4cb533d3 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -65,10 +65,21 @@ jobs: )}} steps: - - name: Set up Ruby & MSYS2 - uses: ruby/setup-ruby@d8d83c3960843afb664e821fed6be52f37da5267 # v1.231.0 + - uses: msys2/setup-msys2@40677d36a502eb2cf0fb808cc9dec31bf6152638 # v2.28.0 + id: msys2 with: - ruby-version: '3.2' + msystem: UCRT64 + update: true + install: >- + git + make + ruby + autoconf + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-ragel + mingw-w64-ucrt-x86_64-openssl + mingw-w64-ucrt-x86_64-libyaml + mingw-w64-ucrt-x86_64-libffi - name: Set up env id: setup-env @@ -86,7 +97,7 @@ jobs: Tee-Object ${env:GITHUB_ENV} -Append -Encoding utf-8 shell: pwsh # cmd.exe does not strip spaces before `|`. env: - MSYS2_LOCATION: ${{ env.RI_DEVKIT }} + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} - name: Remove Strawberry Perl pkg-config working-directory: From df11c073f37d6bd17be249a7e3ff0e596fb3f42b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 10 Aug 2025 15:45:49 +0900 Subject: [PATCH 101/157] [DOC] Fix `vssetup.cmd` arguments --- doc/windows.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/windows.md b/doc/windows.md index 13c797875e..4ea03d0507 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -99,16 +99,18 @@ sh ../../ruby/configure -C --disable-install-doc --with-opt-dir=C:\Users\usernam To cross build arm64 binary: ``` - cmd /k win32\vssetup.cmd -arch arm64 + cmd /k win32\vssetup.cmd -arch=arm64 ``` To cross build x64 binary: ``` - cmd /k win32\vssetup.cmd -arch x64 + cmd /k win32\vssetup.cmd -arch=x64 ``` - See `win32\vssetup.cmd -help` for other command line options. + This batch file is a wrapper of `vsdevcmd.bat` and options are + passed to it as-is. `win32\vssetup.cmd -help` for other command + line options. **Note** building ruby requires following commands. From 5e324ac11c2c9c6712e2cdff37f212367f71e094 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Sun, 10 Aug 2025 13:50:43 +0100 Subject: [PATCH 102/157] Optimize `str_casecmp` length check using pointer end --- benchmark/string_casecmp.yml | 2 ++ string.c | 6 +++--- test/ruby/test_string.rb | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/benchmark/string_casecmp.yml b/benchmark/string_casecmp.yml index 2354040a04..cc75bc803e 100644 --- a/benchmark/string_casecmp.yml +++ b/benchmark/string_casecmp.yml @@ -20,7 +20,9 @@ benchmark: casecmp-10: lstr10.casecmp(ustr10) casecmp-100: lstr100.casecmp(ustr100) casecmp-1000: lstr1000.casecmp(ustr1000) + casecmp-1000vs10: lstr1000.casecmp(ustr10) casecmp-nonascii1: lnonascii1.casecmp(unonascii1) casecmp-nonascii10: lnonascii10.casecmp(unonascii10) casecmp-nonascii100: lnonascii100.casecmp(unonascii100) casecmp-nonascii1000: lnonascii1000.casecmp(unonascii1000) + casecmp-nonascii1000vs10: lnonascii1000.casecmp(unonascii10) \ No newline at end of file diff --git a/string.c b/string.c index fe848d6a4a..96a9f96bd3 100644 --- a/string.c +++ b/string.c @@ -4381,9 +4381,9 @@ str_casecmp(VALUE str1, VALUE str2) p2 += l2; } } - if (RSTRING_LEN(str1) == RSTRING_LEN(str2)) return INT2FIX(0); - if (RSTRING_LEN(str1) > RSTRING_LEN(str2)) return INT2FIX(1); - return INT2FIX(-1); + if (p1 == p1end && p2 == p2end) return INT2FIX(0); + if (p1 == p1end) return INT2FIX(-1); + return INT2FIX(1); } /* diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 8fb57bd58e..811785bf7e 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2832,9 +2832,12 @@ CODE def test_casecmp assert_equal(0, S("FoO").casecmp("fOO")) assert_equal(1, S("FoO").casecmp("BaR")) + assert_equal(-1, S("foo").casecmp("FOOBAR")) assert_equal(-1, S("baR").casecmp("FoO")) assert_equal(1, S("\u3042B").casecmp("\u3042a")) assert_equal(-1, S("foo").casecmp("foo\0")) + assert_equal(1, S("FOOBAR").casecmp("foo")) + assert_equal(0, S("foo\0bar").casecmp("FOO\0BAR")) assert_nil(S("foo").casecmp(:foo)) assert_nil(S("foo").casecmp(Object.new)) @@ -2842,6 +2845,16 @@ CODE o = Object.new def o.to_str; "fOO"; end assert_equal(0, S("FoO").casecmp(o)) + + assert_equal(0, S("#" * 128 + "A" * 256 + "b").casecmp("#" * 128 + "a" * 256 + "B")) + assert_equal(0, S("a" * 256 + "B").casecmp("A" * 256 + "b")) + + assert_equal(-1, S("@").casecmp("`")) + assert_equal(0, S("hello\u00E9X").casecmp("HELLO\u00E9x")) + + s1 = S("\xff".force_encoding("UTF-8")) + s2 = S("\xff".force_encoding("ISO-2022-JP")) + assert_nil(s1.casecmp(s2)) end def test_casecmp? @@ -2857,6 +2870,10 @@ CODE o = Object.new def o.to_str; "fOO"; end assert_equal(true, S("FoO").casecmp?(o)) + + s1 = S("\xff".force_encoding("UTF-8")) + s2 = S("\xff".force_encoding("ISO-2022-JP")) + assert_nil(s1.casecmp?(s2)) end def test_upcase2 From 09d6cfc55c9048dfe248be0ca71eb5b8968e6b28 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Sun, 10 Aug 2025 14:08:53 +0100 Subject: [PATCH 103/157] Update test_string.rb --- test/ruby/test_string.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 811785bf7e..c7e4b0c1ec 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2842,6 +2842,9 @@ CODE assert_nil(S("foo").casecmp(:foo)) assert_nil(S("foo").casecmp(Object.new)) + assert_nil(S("foo").casecmp(0)) + assert_nil(S("foo").casecmp(5.00)) + o = Object.new def o.to_str; "fOO"; end assert_equal(0, S("FoO").casecmp(o)) @@ -2867,6 +2870,9 @@ CODE assert_nil(S("foo").casecmp?(:foo)) assert_nil(S("foo").casecmp?(Object.new)) + assert_nil(S("foo").casecmp(0)) + assert_nil(S("foo").casecmp(5.00)) + o = Object.new def o.to_str; "fOO"; end assert_equal(true, S("FoO").casecmp?(o)) From c914389ae83d0a5b8ddc00d2c1577c937de72743 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Sun, 10 Aug 2025 15:36:04 +0100 Subject: [PATCH 104/157] Update string_casecmp.yml --- benchmark/string_casecmp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/string_casecmp.yml b/benchmark/string_casecmp.yml index cc75bc803e..88a3555c8a 100644 --- a/benchmark/string_casecmp.yml +++ b/benchmark/string_casecmp.yml @@ -25,4 +25,4 @@ benchmark: casecmp-nonascii10: lnonascii10.casecmp(unonascii10) casecmp-nonascii100: lnonascii100.casecmp(unonascii100) casecmp-nonascii1000: lnonascii1000.casecmp(unonascii1000) - casecmp-nonascii1000vs10: lnonascii1000.casecmp(unonascii10) \ No newline at end of file + casecmp-nonascii1000vs10: lnonascii1000.casecmp(unonascii10) From 62b5fe8984db6c36919ff6f0a69ccca37090ce0c Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 6 Aug 2025 17:59:15 -0500 Subject: [PATCH 105/157] [DOC] Tweaks for GC.config --- gc.rb | 85 +++++++++++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/gc.rb b/gc.rb index 1298e30056..54a88cf8f7 100644 --- a/gc.rb +++ b/gc.rb @@ -258,71 +258,58 @@ module GC # call-seq: # GC.config -> hash - # GC.config(hash) -> hash + # GC.config(hash_to_merge) -> merged_hash # - # Sets or gets information about the current \GC config. + # This method is expected to be defined, useful, and well-behaved + # only in the CRuby implementation. # - # Configuration parameters are \GC implementation-specific and may change - # without notice. + # Sets or gets information about the current \GC configuration. # - # This method can be called without parameters to retrieve the current config - # as a +Hash+ with +Symbol+ keys. + # With no argument given, returns a hash containing the configuration: # - # This method can also be called with a +Hash+ argument to assign values to - # valid config keys. Config keys missing from the passed +Hash+ will be left - # unmodified. + # GC.config + # # => {rgengc_allow_full_mark: true, implementation: "default"} # - # If a key/value pair is passed to this function that does not correspond to - # a valid config key for the \GC implementation being used, no config will be - # updated, the key will be present in the returned Hash, and its value will - # be +nil+. This is to facilitate easy migration between \GC implementations. + # With argument +hash_to_merge+ given, + # merges that hash into the stored configuration hash; + # ignores unknown hash keys; + # returns the implementation-specific configuration hash (see below): # - # In both call-seqs, the return value of GC.config will be a +Hash+ - # containing the most recent full configuration, i.e., all keys and values - # defined by the specific \GC implementation being used. In the case of a - # config update, the return value will include the new values being updated. + # GC.config(rgengc_allow_full_mark: false) + # # => {rgengc_allow_full_mark: false} + # GC.config + # # => {rgengc_allow_full_mark: false, implementation: "default"} + # GC.config(foo: 'bar') + # # => {rgengc_allow_full_mark: false} + # GC.config + # # => {rgengc_allow_full_mark: false, implementation: "default"} # - # This method is only expected to work on CRuby. + # All-Implementations Configuration # - # === \GC Implementation independent values + # The single read-only entry for all implementations is: # - # The GC.config hash can also contain keys that are global and - # read-only. These keys are not specific to any one \GC library implementation - # and attempting to write to them will raise +ArgumentError+. + # - +implementation+: + # the string name of the implementation; + # for the Ruby default implementation, 'default'. # - # There is currently only one global, read-only key: + # Implementation-Specific Configuration # - # [implementation] - # Returns a +String+ containing the name of the currently loaded \GC library, - # if one has been loaded using +RUBY_GC_LIBRARY+, and "default" in all other - # cases + # A \GC implementation maintains its own implementation-specific configuration. # - # === \GC Implementation specific values + # For Ruby's default implementation the single entry is: # - # \GC libraries are expected to document their own configuration. Valid keys - # for Ruby's default \GC implementation are: + # - +rgengc_allow_full_mark+: + # Controls whether the \GC is allowed to run a full mark (young & old objects): # - # [rgengc_allow_full_mark] - # Controls whether the \GC is allowed to run a full mark (young & old objects). + # - +true+ (default): \GC interleaves major and minor collections. + # - +false+: \GC does not initiate a full marking cycle unless + # explicitly directed by user code; + # see GC.start. # - # When +true+, \GC interleaves major and minor collections. This is the default. \GC - # will function as intended. - # - # When +false+, the \GC will never trigger a full marking cycle unless - # explicitly requested by user code. Instead, only a minor mark will run— - # only young objects will be marked. When the heap space is exhausted, new - # pages will be allocated immediately instead of running a full mark. - # - # A flag will be set to notify that a full mark has been - # requested. This flag is accessible using - # GC.latest_gc_info(:need_major_by) - # - # The user can trigger a major collection at any time using - # GC.start(full_mark: true) - # - # When +false+, Young to Old object promotion is disabled. For performance - # reasons, it is recommended to warm up an application using +Process.warmup+ + # Setting this parameter to +false+ disables young-to-old promotion . + # For performance reasons, we recommended warming up the application using Process.warmup # before setting this parameter to +false+. + # def self.config hash = nil return Primitive.gc_config_get unless hash From ad146320957cd84311d5ce857591e05f50f87ca6 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 8 Aug 2025 11:19:40 -0500 Subject: [PATCH 106/157] [DOC] Tweaks for GC.config --- gc.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gc.rb b/gc.rb index 54a88cf8f7..a7620fd9ac 100644 --- a/gc.rb +++ b/gc.rb @@ -260,11 +260,12 @@ module GC # GC.config -> hash # GC.config(hash_to_merge) -> merged_hash # - # This method is expected to be defined, useful, and well-behaved - # only in the CRuby implementation. + # This method is implementation-specific to CRuby. # # Sets or gets information about the current \GC configuration. # + # Configuration parameters are \GC implementation-specific and may change without notice. + # # With no argument given, returns a hash containing the configuration: # # GC.config @@ -302,13 +303,13 @@ module GC # Controls whether the \GC is allowed to run a full mark (young & old objects): # # - +true+ (default): \GC interleaves major and minor collections. - # - +false+: \GC does not initiate a full marking cycle unless - # explicitly directed by user code; + # A flag is set to notify GC that a full mark has been requested. + # This flag is accessible via GC.latest_gc_info(:need_major_by). + # - +false+: \GC does not initiate a full marking cycle unless explicitly directed by user code; # see GC.start. - # - # Setting this parameter to +false+ disables young-to-old promotion . - # For performance reasons, we recommended warming up the application using Process.warmup - # before setting this parameter to +false+. + # Setting this parameter to +false+ disables young-to-old promotion. + # For performance reasons, we recommended warming up the application using Process.warmup + # before setting this parameter to +false+. # def self.config hash = nil return Primitive.gc_config_get unless hash From e0b72ad2f1f4836e3303596ecae1f62aa60b2d80 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 11 Aug 2025 08:25:07 -0500 Subject: [PATCH 107/157] [DOC] Update JIT options --- doc/ruby/options.md | 46 +++++---------------------------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/doc/ruby/options.md b/doc/ruby/options.md index bfbd2530de..95f8cf453c 100644 --- a/doc/ruby/options.md +++ b/doc/ruby/options.md @@ -672,6 +672,11 @@ $ ruby --internal-encoding=cesu-8 -e 'puts Encoding::default_internal' CESU-8 ``` +### `--jit` + +Option `--jit` is an alias for option `--yjit`, which enables YJIT; +see additional YJIT options in the [YJIT documentation](rdoc-ref:yjit/yjit.md). + ### `--verbose`: Set `$VERBOSE` Option `--verbose` sets global variable `$VERBOSE` to `true` @@ -681,44 +686,3 @@ and disables input from `$stdin`. Option `--version` prints the version of the Ruby interpreter, then exits. -## Experimental Options - -These options are experimental in the current Ruby release, -and may be modified or withdrawn in later releases. - -### `--jit` - -Option `-jit` enables JIT compilation with the default option. - -#### `--jit-debug` - -Option `--jit-debug` enables JIT debugging (very slow); -adds compiler flags if given. - -#### `--jit-max-cache=num` - -Option `--jit-max-cache=num` sets the maximum number of methods -to be JIT-ed in a cache; default: 100). - -#### `--jit-min-calls=num` - -Option `jit-min-calls=num` sets the minimum number of calls to trigger JIT -(for testing); default: 10000). - -#### `--jit-save-temps` - -Option `--jit-save-temps` saves JIT temporary files in $TMP or /tmp (for testing). - -#### `--jit-verbose` - -Option `--jit-verbose` prints JIT logs of level `num` or less -to `$stderr`; default: 0. - -#### `--jit-wait` - -Option `--jit-wait` waits until JIT compilation finishes every time (for testing). - -#### `--jit-warnings` - -Option `--jit-warnings` enables printing of JIT warnings. - From 4775d1ffa8a34f0bca3f6124c98426d56eb8e1b6 Mon Sep 17 00:00:00 2001 From: S-H-GAMELINKS Date: Sun, 10 Aug 2025 21:30:41 +0900 Subject: [PATCH 108/157] Add NODE IN locations Add locations to struct `RNode_IN`. memo: ```bash > ruby -e 'case 1; in 2 then 3; end' --parser=prism --dump=parsetree @ ProgramNode (location: (1,0)-(1,24)) +-- locals: [] +-- statements: @ StatementsNode (location: (1,0)-(1,24)) +-- body: (length: 1) +-- @ CaseMatchNode (location: (1,0)-(1,24)) +-- predicate: | @ IntegerNode (location: (1,5)-(1,6)) | +-- IntegerBaseFlags: decimal | +-- value: 1 +-- conditions: (length: 1) | +-- @ InNode (location: (1,8)-(1,19)) | +-- pattern: | | @ IntegerNode (location: (1,11)-(1,12)) | | +-- IntegerBaseFlags: decimal | | +-- value: 2 | +-- statements: | | @ StatementsNode (location: (1,18)-(1,19)) | | +-- body: (length: 1) | | +-- @ IntegerNode (location: (1,18)-(1,19)) | | +-- IntegerBaseFlags: decimal | | +-- value: 3 | +-- in_loc: (1,8)-(1,10) = "in" | +-- then_loc: (1,13)-(1,17) = "then" +-- else_clause: nil +-- case_keyword_loc: (1,0)-(1,4) = "case" +-- end_keyword_loc: (1,21)-(1,24) = "end" ``` --- ast.c | 6 ++++++ node_dump.c | 5 ++++- parse.y | 15 +++++++++------ rubyparser.h | 3 +++ test/ruby/test_ast.rb | 14 ++++++++++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ast.c b/ast.c index bc2adeacd6..04b9543854 100644 --- a/ast.c +++ b/ast.c @@ -866,6 +866,12 @@ node_locations(VALUE ast_value, const NODE *node) location_new(&RNODE_IF(node)->if_keyword_loc), location_new(&RNODE_IF(node)->then_keyword_loc), location_new(&RNODE_IF(node)->end_keyword_loc)); + case NODE_IN: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_IN(node)->in_keyword_loc), + location_new(&RNODE_IN(node)->then_keyword_loc), + location_new(&RNODE_IN(node)->operator_loc)); case NODE_MODULE: return rb_ary_new_from_args(3, location_new(nd_code_loc(node)), diff --git a/node_dump.c b/node_dump.c index c318baeeed..18ac3d7b35 100644 --- a/node_dump.c +++ b/node_dump.c @@ -309,8 +309,11 @@ dump_node(VALUE buf, VALUE indent, int comment, const NODE * node) ANN("example: case x; in 1; foo; in 2; bar; else baz; end"); F_NODE(nd_head, RNODE_IN, "in pattern"); F_NODE(nd_body, RNODE_IN, "in body"); - LAST_NODE; F_NODE(nd_next, RNODE_IN, "next in clause"); + F_LOC(in_keyword_loc, RNODE_IN); + F_LOC(then_keyword_loc, RNODE_IN); + LAST_NODE; + F_LOC(operator_loc, RNODE_IN); return; case NODE_WHILE: diff --git a/parse.y b/parse.y index e77dc790bc..c0f46a395f 100644 --- a/parse.y +++ b/parse.y @@ -1070,7 +1070,7 @@ static rb_node_case_t *rb_node_case_new(struct parser_params *p, NODE *nd_head, static rb_node_case2_t *rb_node_case2_new(struct parser_params *p, NODE *nd_body, const YYLTYPE *loc, const YYLTYPE *case_keyword_loc, const YYLTYPE *end_keyword_loc); static rb_node_case3_t *rb_node_case3_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, const YYLTYPE *loc, const YYLTYPE *case_keyword_loc, const YYLTYPE *end_keyword_loc); static rb_node_when_t *rb_node_when_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd_next, const YYLTYPE *loc, const YYLTYPE *keyword_loc, const YYLTYPE *then_keyword_loc); -static rb_node_in_t *rb_node_in_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd_next, const YYLTYPE *loc); +static rb_node_in_t *rb_node_in_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd_next, const YYLTYPE *loc, const YYLTYPE *in_keyword_loc, const YYLTYPE *then_keyword_loc, const YYLTYPE *operator_loc); static rb_node_while_t *rb_node_while_new(struct parser_params *p, NODE *nd_cond, NODE *nd_body, long nd_state, const YYLTYPE *loc, const YYLTYPE *keyword_loc, const YYLTYPE *closing_loc); static rb_node_until_t *rb_node_until_new(struct parser_params *p, NODE *nd_cond, NODE *nd_body, long nd_state, const YYLTYPE *loc, const YYLTYPE *keyword_loc, const YYLTYPE *closing_loc); static rb_node_iter_t *rb_node_iter_new(struct parser_params *p, rb_node_args_t *nd_args, NODE *nd_body, const YYLTYPE *loc); @@ -1178,7 +1178,7 @@ static rb_node_error_t *rb_node_error_new(struct parser_params *p, const YYLTYPE #define NEW_CASE2(b,loc,ck_loc,ek_loc) (NODE *)rb_node_case2_new(p,b,loc,ck_loc,ek_loc) #define NEW_CASE3(h,b,loc,ck_loc,ek_loc) (NODE *)rb_node_case3_new(p,h,b,loc,ck_loc,ek_loc) #define NEW_WHEN(c,t,e,loc,k_loc,t_loc) (NODE *)rb_node_when_new(p,c,t,e,loc,k_loc,t_loc) -#define NEW_IN(c,t,e,loc) (NODE *)rb_node_in_new(p,c,t,e,loc) +#define NEW_IN(c,t,e,loc,ik_loc,tk_loc,o_loc) (NODE *)rb_node_in_new(p,c,t,e,loc,ik_loc,tk_loc,o_loc) #define NEW_WHILE(c,b,n,loc,k_loc,c_loc) (NODE *)rb_node_while_new(p,c,b,n,loc,k_loc,c_loc) #define NEW_UNTIL(c,b,n,loc,k_loc,c_loc) (NODE *)rb_node_until_new(p,c,b,n,loc,k_loc,c_loc) #define NEW_ITER(a,b,loc) (NODE *)rb_node_iter_new(p,a,b,loc) @@ -3472,7 +3472,7 @@ expr : command_call pop_pktbl(p, $p_pktbl); pop_pvtbl(p, $p_pvtbl); p->ctxt.in_kwarg = $ctxt.in_kwarg; - $$ = NEW_CASE3($arg, NEW_IN($body, 0, 0, &@body), &@$, &NULL_LOC, &NULL_LOC); + $$ = NEW_CASE3($arg, NEW_IN($body, 0, 0, &@body, &NULL_LOC, &NULL_LOC, &@2), &@$, &NULL_LOC, &NULL_LOC); /*% ripper: case!($:arg, in!($:body, Qnil, Qnil)) %*/ } | arg keyword_in @@ -3485,7 +3485,7 @@ expr : command_call pop_pktbl(p, $p_pktbl); pop_pvtbl(p, $p_pvtbl); p->ctxt.in_kwarg = $ctxt.in_kwarg; - $$ = NEW_CASE3($arg, NEW_IN($body, NEW_TRUE(&@body), NEW_FALSE(&@body), &@body), &@$, &NULL_LOC, &NULL_LOC); + $$ = NEW_CASE3($arg, NEW_IN($body, NEW_TRUE(&@body), NEW_FALSE(&@body), &@body, &@keyword_in, &NULL_LOC, &NULL_LOC), &@$, &NULL_LOC, &NULL_LOC); /*% ripper: case!($:arg, in!($:body, Qnil, Qnil)) %*/ } | arg %prec tLBRACE_ARG @@ -5399,7 +5399,7 @@ p_case_body : keyword_in compstmt(stmts) p_cases[cases] { - $$ = NEW_IN($expr, $compstmt, $cases, &@$); + $$ = NEW_IN($expr, $compstmt, $cases, &@$, &@keyword_in, &@then, &NULL_LOC); /*% ripper: in!($:expr, $:compstmt, $:cases) %*/ } ; @@ -11528,12 +11528,15 @@ rb_node_when_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd } static rb_node_in_t * -rb_node_in_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd_next, const YYLTYPE *loc) +rb_node_in_new(struct parser_params *p, NODE *nd_head, NODE *nd_body, NODE *nd_next, const YYLTYPE *loc, const YYLTYPE *in_keyword_loc, const YYLTYPE *then_keyword_loc, const YYLTYPE *operator_loc) { rb_node_in_t *n = NODE_NEWNODE(NODE_IN, rb_node_in_t, loc); n->nd_head = nd_head; n->nd_body = nd_body; n->nd_next = nd_next; + n->in_keyword_loc = *in_keyword_loc; + n->then_keyword_loc = *then_keyword_loc; + n->operator_loc = *operator_loc; return n; } diff --git a/rubyparser.h b/rubyparser.h index e436d1c404..cc63efd3f8 100644 --- a/rubyparser.h +++ b/rubyparser.h @@ -324,6 +324,9 @@ typedef struct RNode_IN { struct RNode *nd_head; struct RNode *nd_body; struct RNode *nd_next; + rb_code_location_t in_keyword_loc; + rb_code_location_t then_keyword_loc; + rb_code_location_t operator_loc; } rb_node_in_t; typedef struct RNode_LOOP { diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 6372b0d34e..9a7d75c270 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -1514,6 +1514,20 @@ dummy assert_locations(node.children[-1].children[1].children[0].locations, [[1, 11, 1, 17], [1, 13, 1, 15], nil, nil]) end + def test_in_locations + node = ast_parse("case 1; in 2 then 3; end") + assert_locations(node.children[-1].children[1].locations, [[1, 8, 1, 20], [1, 8, 1, 10], [1, 13, 1, 17], nil]) + + node = ast_parse("1 => a") + assert_locations(node.children[-1].children[1].locations, [[1, 5, 1, 6], nil, nil, [1, 2, 1, 4]]) + + node = ast_parse("1 in a") + assert_locations(node.children[-1].children[1].locations, [[1, 5, 1, 6], [1, 2, 1, 4], nil, nil]) + + node = ast_parse("case 1; in 2; 3; end") + assert_locations(node.children[-1].children[1].locations, [[1, 8, 1, 16], [1, 8, 1, 10], [1, 12, 1, 13], nil]) + end + def test_next_locations node = ast_parse("loop { next 1 }") assert_locations(node.children[-1].children[-1].children[-1].locations, [[1, 7, 1, 13], [1, 7, 1, 11]]) From 61fff8a92f5b7fbcdd0bea46150ce0845637483e Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 8 Aug 2025 11:15:15 -0400 Subject: [PATCH 109/157] Fix return value of setting in GC.config gc_config_set returned rb_gc_impl_config_get, but gc_config_get also added the implementation key to the return value. This caused the return value of GC.config to differ depending on whether the optional hash argument is provided or not. --- gc.c | 2 +- gc.rb | 8 ++++---- test/ruby/test_gc.rb | 9 +++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/gc.c b/gc.c index 4c8a042c1e..7663e82f41 100644 --- a/gc.c +++ b/gc.c @@ -4357,7 +4357,7 @@ gc_config_set(rb_execution_context_t *ec, VALUE self, VALUE hash) rb_gc_impl_config_set(objspace, hash); - return rb_gc_impl_config_get(objspace); + return Qnil; } static VALUE diff --git a/gc.rb b/gc.rb index a7620fd9ac..603520df53 100644 --- a/gc.rb +++ b/gc.rb @@ -312,17 +312,17 @@ module GC # before setting this parameter to +false+. # def self.config hash = nil - return Primitive.gc_config_get unless hash - - if(Primitive.cexpr!("RBOOL(RB_TYPE_P(hash, T_HASH))")) + if Primitive.cexpr!("RBOOL(RB_TYPE_P(hash, T_HASH))") if hash.include?(:implementation) raise ArgumentError, 'Attempting to set read-only key "Implementation"' end Primitive.gc_config_set hash - else + elsif hash != nil raise ArgumentError end + + Primitive.gc_config_get end # call-seq: diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 85022cbc4d..ccccd212b6 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -75,12 +75,9 @@ class TestGc < Test::Unit::TestCase GC.start end - def test_gc_config_setting_returns_nil_for_missing_keys - missing_value = GC.config(no_such_key: true)[:no_such_key] - assert_nil(missing_value) - ensure - GC.config(full_mark: true) - GC.start + def test_gc_config_setting_returns_config_hash + hash = GC.config(no_such_key: true) + assert_equal(GC.config, hash) end def test_gc_config_disable_major From 5b956fbf60ee36ba1e7b56e7747f31c0e3586c46 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 11 Aug 2025 14:31:16 -0400 Subject: [PATCH 110/157] ZJIT: Fix `mismatched_lifetime_syntaxes`, new in Rust 1.89.0 --- zjit/src/backend/lir.rs | 4 ++-- zjit/src/hir.rs | 12 ++++++------ zjit/src/hir_type/mod.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 3263392cf6..0902d347c7 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -549,13 +549,13 @@ pub enum Insn { impl Insn { /// Create an iterator that will yield a non-mutable reference to each /// operand in turn for this instruction. - pub(super) fn opnd_iter(&self) -> InsnOpndIterator { + pub(super) fn opnd_iter(&self) -> InsnOpndIterator<'_> { InsnOpndIterator::new(self) } /// Create an iterator that will yield a mutable reference to each operand /// in turn for this instruction. - pub(super) fn opnd_iter_mut(&mut self) -> InsnOpndMutIterator { + pub(super) fn opnd_iter_mut(&mut self) -> InsnOpndMutIterator<'_> { InsnOpndMutIterator::new(self) } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 87d2a613d0..bff0fcd757 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -67,7 +67,7 @@ impl std::fmt::Display for VALUE { } impl VALUE { - pub fn print(self, ptr_map: &PtrPrintMap) -> VALUEPrinter { + pub fn print(self, ptr_map: &PtrPrintMap) -> VALUEPrinter<'_> { VALUEPrinter { inner: self, ptr_map } } } @@ -136,7 +136,7 @@ pub enum Invariant { } impl Invariant { - pub fn print(self, ptr_map: &PtrPrintMap) -> InvariantPrinter { + pub fn print(self, ptr_map: &PtrPrintMap) -> InvariantPrinter<'_> { InvariantPrinter { inner: self, ptr_map } } } @@ -810,12 +810,12 @@ pub struct Block { impl Block { /// Return an iterator over params - pub fn params(&self) -> Iter { + pub fn params(&self) -> Iter<'_, InsnId> { self.params.iter() } /// Return an iterator over insns - pub fn insns(&self) -> Iter { + pub fn insns(&self) -> Iter<'_, InsnId> { self.insns.iter() } } @@ -2450,12 +2450,12 @@ impl FrameState { } /// Iterate over all stack slots - pub fn stack(&self) -> Iter { + pub fn stack(&self) -> Iter<'_, InsnId> { self.stack.iter() } /// Iterate over all local variables - pub fn locals(&self) -> Iter { + pub fn locals(&self) -> Iter<'_, InsnId> { self.locals.iter() } diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 607ccbde84..c18b2735be 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -501,7 +501,7 @@ impl Type { self.is_subtype(types::Immediate) } - pub fn print(self, ptr_map: &PtrPrintMap) -> TypePrinter { + pub fn print(self, ptr_map: &PtrPrintMap) -> TypePrinter<'_> { TypePrinter { inner: self, ptr_map } } } From 6e3790b17f1b58d67616ef1f9b899bc4af91d334 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 11 Aug 2025 14:31:52 -0400 Subject: [PATCH 111/157] YJIT: Fix `mismatched_lifetime_syntaxes`, new in Rust 1.89.0 --- yjit/src/backend/ir.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yjit/src/backend/ir.rs b/yjit/src/backend/ir.rs index 1df151433a..b704a24985 100644 --- a/yjit/src/backend/ir.rs +++ b/yjit/src/backend/ir.rs @@ -528,13 +528,13 @@ pub enum Insn { impl Insn { /// Create an iterator that will yield a non-mutable reference to each /// operand in turn for this instruction. - pub(super) fn opnd_iter(&self) -> InsnOpndIterator { + pub(super) fn opnd_iter(&self) -> InsnOpndIterator<'_> { InsnOpndIterator::new(self) } /// Create an iterator that will yield a mutable reference to each operand /// in turn for this instruction. - pub(super) fn opnd_iter_mut(&mut self) -> InsnOpndMutIterator { + pub(super) fn opnd_iter_mut(&mut self) -> InsnOpndMutIterator<'_> { InsnOpndMutIterator::new(self) } From 6968668570fd43065cf4b9b4a1063a6b3fe888aa Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 11 Aug 2025 13:18:52 -0700 Subject: [PATCH 112/157] ZJIT: Add RubyVM::ZJIT.enabled? (#14159) Co-authored-by: Max Bernstein --- test/ruby/test_zjit.rb | 34 ++++++++++++++++++++++++++-------- zjit.rb | 9 +++++++-- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index c86ac62a9f..bf43fd1324 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -9,6 +9,15 @@ require_relative '../lib/jit_support' return unless JITSupport.zjit_supported? class TestZJIT < Test::Unit::TestCase + def test_enabled + assert_runs 'false', <<~RUBY, zjit: false + RubyVM::ZJIT.enabled? + RUBY + assert_runs 'true', <<~RUBY, zjit: true + RubyVM::ZJIT.enabled? + RUBY + end + def test_call_itself assert_compiles '42', <<~RUBY, call_threshold: 2 def test = 42.itself @@ -1547,14 +1556,23 @@ class TestZJIT < Test::Unit::TestCase end # Run a Ruby process with ZJIT options and a pipe for writing test results - def eval_with_jit(script, call_threshold: 1, num_profiles: 1, stats: false, debug: true, timeout: 1000, pipe_fd:) - args = [ - "--disable-gems", - "--zjit-call-threshold=#{call_threshold}", - "--zjit-num-profiles=#{num_profiles}", - ] - args << "--zjit-stats" if stats - args << "--zjit-debug" if debug + def eval_with_jit( + script, + call_threshold: 1, + num_profiles: 1, + zjit: true, + stats: false, + debug: true, + timeout: 1000, + pipe_fd: + ) + args = ["--disable-gems"] + if zjit + args << "--zjit-call-threshold=#{call_threshold}" + args << "--zjit-num-profiles=#{num_profiles}" + args << "--zjit-stats" if stats + args << "--zjit-debug" if debug + end args << "-e" << script_shell_encode(script) pipe_r, pipe_w = IO.pipe # Separate thread so we don't deadlock when diff --git a/zjit.rb b/zjit.rb index 2bc779ef28..a307abdf88 100644 --- a/zjit.rb +++ b/zjit.rb @@ -14,7 +14,12 @@ module RubyVM::ZJIT end class << RubyVM::ZJIT - # Return ZJIT statistics as a Hash + # Check if \ZJIT is enabled + def enabled? + Primitive.cexpr! 'RBOOL(rb_zjit_enabled_p)' + end + + # Return \ZJIT statistics as a Hash def stats stats = Primitive.rb_zjit_stats @@ -26,7 +31,7 @@ class << RubyVM::ZJIT stats end - # Get the summary of ZJIT statistics as a String + # Get the summary of \ZJIT statistics as a String def stats_string buf = +'' stats = self.stats From 319550527ff8fbff6ee586fb75da2234de5d2feb Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 11 Aug 2025 13:21:45 -0700 Subject: [PATCH 113/157] ZJIT: Add compile/profile/GC/invalidation time stats (#14158) Co-authored-by: Stan Lo --- zjit.rb | 24 ++++++++++++- zjit/src/codegen.rs | 7 ++-- zjit/src/gc.rs | 46 ++++++++++++++----------- zjit/src/invariants.rs | 24 +++++++------ zjit/src/profile.rs | 8 +++-- zjit/src/stats.rs | 77 +++++++++++++++++++++++++++++++++++------- 6 files changed, 138 insertions(+), 48 deletions(-) diff --git a/zjit.rb b/zjit.rb index a307abdf88..fc2c80c52f 100644 --- a/zjit.rb +++ b/zjit.rb @@ -22,6 +22,7 @@ class << RubyVM::ZJIT # Return \ZJIT statistics as a Hash def stats stats = Primitive.rb_zjit_stats + return nil if stats.nil? if stats.key?(:vm_insns_count) && stats.key?(:zjit_insns_count) stats[:total_insns_count] = stats[:vm_insns_count] + stats[:zjit_insns_count] @@ -37,15 +38,29 @@ class << RubyVM::ZJIT stats = self.stats [ + :compile_time_ns, + :profile_time_ns, + :gc_time_ns, + :invalidation_time_ns, :total_insns_count, :vm_insns_count, :zjit_insns_count, :ratio_in_zjit, ].each do |key| + # Some stats like vm_insns_count and ratio_in_zjit are not supported on the release build + next unless stats.key?(key) value = stats[key] - if key == :ratio_in_zjit + + case key + when :ratio_in_zjit value = '%0.1f%%' % value + when /_time_ns\z/ + key = key.to_s.sub(/_time_ns\z/, '_time') + value = "#{number_with_delimiter(value / 10**6)}ms" + else + value = number_with_delimiter(value) end + buf << "#{'%-18s' % "#{key}:"} #{value}\n" end buf @@ -59,6 +74,13 @@ class << RubyVM::ZJIT # :stopdoc: private + def number_with_delimiter(number) + s = number.to_s + i = s.index('.') || s.size + s.insert(i -= 3, ',') while i > 3 + s + end + # Print ZJIT stats def print_stats $stderr.write stats_string diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1d6901bac4..f9532dfe03 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -7,7 +7,7 @@ use crate::backend::current::{Reg, ALLOC_REGS}; use crate::invariants::{track_bop_assumption, track_cme_assumption, track_single_ractor_assumption, track_stable_constant_names_assumption}; use crate::gc::{append_gc_offsets, get_or_create_iseq_payload, get_or_create_iseq_payload_ptr}; use crate::state::ZJITState; -use crate::stats::{counter_ptr, Counter}; +use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::compile_time_ns}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, asm_comment, asm_ccall, Assembler, Opnd, SideExitContext, Target, CFP, C_ARG_OPNDS, C_RET_OPND, EC, NATIVE_STACK_PTR, NATIVE_BASE_PTR, SP}; use crate::hir::{iseq_to_hir, Block, BlockId, BranchEdge, Invariant, RangeType, SideExitReason, SideExitReason::*, SpecialObjectType, SELF_PARAM_IDX}; @@ -86,7 +86,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *co // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. let code_ptr = with_vm_lock(src_loc!(), || { - gen_iseq_entry_point(iseq) + with_time_stat(compile_time_ns, || gen_iseq_entry_point(iseq)) }); // Assert that the ISEQ compiles if RubyVM::ZJIT.assert_compiles is enabled @@ -1359,7 +1359,8 @@ c_callable! { with_vm_lock(src_loc!(), || { // Get a pointer to compiled code or the side-exit trampoline let cb = ZJITState::get_code_block(); - let code_ptr = if let Some(code_ptr) = function_stub_hit_body(cb, iseq, branch_ptr) { + let code_ptr = with_time_stat(compile_time_ns, || function_stub_hit_body(cb, iseq, branch_ptr)); + let code_ptr = if let Some(code_ptr) = code_ptr { code_ptr } else { // gen_push_frame() doesn't set PC and SP, so we need to set them for side-exit diff --git a/zjit/src/gc.rs b/zjit/src/gc.rs index d94d86036b..ea1b0ed2ea 100644 --- a/zjit/src/gc.rs +++ b/zjit/src/gc.rs @@ -1,7 +1,8 @@ // This module is responsible for marking/moving objects on GC. use std::{ffi::c_void, ops::Range}; -use crate::{cruby::*, profile::IseqProfile, state::ZJITState, virtualmem::CodePtr}; +use crate::{cruby::*, profile::IseqProfile, state::ZJITState, stats::with_time_stat, virtualmem::CodePtr}; +use crate::stats::Counter::gc_time_ns; /// This is all the data ZJIT stores on an ISEQ. We mark objects in this struct on GC. #[derive(Debug)] @@ -65,6 +66,7 @@ fn payload_ptr_as_mut(payload_ptr: *mut IseqPayload) -> &'static mut IseqPayload unsafe { payload_ptr.as_mut() }.unwrap() } +/// GC callback for marking GC objects in the per-ISEQ payload. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_iseq_mark(payload: *mut c_void) { let payload = if payload.is_null() { @@ -80,7 +82,29 @@ pub extern "C" fn rb_zjit_iseq_mark(payload: *mut c_void) { &*(payload as *const IseqPayload) } }; + with_time_stat(gc_time_ns, || iseq_mark(payload)); +} +/// GC callback for updating GC objects in the per-ISEQ payload. +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_iseq_update_references(payload: *mut c_void) { + let payload = if payload.is_null() { + return; // nothing to update + } else { + // SAFETY: The GC takes the VM lock while marking, which + // we assert, so we should be synchronized and data race free. + // + // For aliasing, having the VM lock hopefully also implies that no one + // else has an overlapping &mut IseqPayload. + unsafe { + rb_assert_holding_vm_lock(); + &mut *(payload as *mut IseqPayload) + } + }; + with_time_stat(gc_time_ns, || iseq_update_references(payload)); +} + +fn iseq_mark(payload: &IseqPayload) { // Mark objects retained by profiling instructions payload.profile.each_object(|object| { unsafe { rb_gc_mark_movable(object); } @@ -100,24 +124,8 @@ pub extern "C" fn rb_zjit_iseq_mark(payload: *mut c_void) { } } -/// GC callback for updating GC objects in the per-ISEQ payload. -/// This is a mirror of [rb_zjit_iseq_mark]. -#[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_iseq_update_references(payload: *mut c_void) { - let payload = if payload.is_null() { - return; // nothing to update - } else { - // SAFETY: The GC takes the VM lock while marking, which - // we assert, so we should be synchronized and data race free. - // - // For aliasing, having the VM lock hopefully also implies that no one - // else has an overlapping &mut IseqPayload. - unsafe { - rb_assert_holding_vm_lock(); - &mut *(payload as *mut IseqPayload) - } - }; - +/// This is a mirror of [iseq_mark]. +fn iseq_update_references(payload: &mut IseqPayload) { // Move objects retained by profiling instructions payload.profile.each_object_mut(|object| { *object = unsafe { rb_gc_location(*object) }; diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 85bc04fc71..3f291415be 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -1,20 +1,24 @@ use std::{collections::{HashMap, HashSet}, mem}; use crate::{backend::lir::{asm_comment, Assembler}, cruby::{rb_callable_method_entry_t, ruby_basic_operators, src_loc, with_vm_lock, IseqPtr, RedefinitionFlag, ID}, gc::IseqPayload, hir::Invariant, options::debug, state::{zjit_enabled_p, ZJITState}, virtualmem::CodePtr}; +use crate::stats::with_time_stat; +use crate::stats::Counter::invalidation_time_ns; use crate::gc::remove_gc_offsets; macro_rules! compile_patch_points { ($cb:expr, $patch_points:expr, $($comment_args:tt)*) => { - for patch_point in $patch_points { - let written_range = $cb.with_write_ptr(patch_point.patch_point_ptr, |cb| { - let mut asm = Assembler::new(); - asm_comment!(asm, $($comment_args)*); - asm.jmp(patch_point.side_exit_ptr.into()); - asm.compile(cb).expect("can write existing code"); - }); - // Stop marking GC offsets corrupted by the jump instruction - remove_gc_offsets(patch_point.payload_ptr, &written_range); - } + with_time_stat(invalidation_time_ns, || { + for patch_point in $patch_points { + let written_range = $cb.with_write_ptr(patch_point.patch_point_ptr, |cb| { + let mut asm = Assembler::new(); + asm_comment!(asm, $($comment_args)*); + asm.jmp(patch_point.side_exit_ptr.into()); + asm.compile(cb).expect("can write existing code"); + }); + // Stop marking GC offsets corrupted by the jump instruction + remove_gc_offsets(patch_point.payload_ptr, &written_range); + } + }); }; } diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 12b10b98ee..7ffaea29dc 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -3,6 +3,8 @@ use crate::{cruby::*, gc::get_or_create_iseq_payload, options::get_option}; use crate::distribution::{Distribution, DistributionSummary}; +use crate::stats::Counter::profile_time_ns; +use crate::stats::with_time_stat; /// Ephemeral state for profiling runtime information struct Profiler { @@ -41,13 +43,13 @@ impl Profiler { #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_profile_insn(bare_opcode: u32, ec: EcPtr) { with_vm_lock(src_loc!(), || { - let mut profiler = Profiler::new(ec); - profile_insn(&mut profiler, bare_opcode as ruby_vminsn_type); + with_time_stat(profile_time_ns, || profile_insn(bare_opcode as ruby_vminsn_type, ec)); }); } /// Profile a YARV instruction -fn profile_insn(profiler: &mut Profiler, bare_opcode: ruby_vminsn_type) { +fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { + let profiler = &mut Profiler::new(ec); let profile = &mut get_or_create_iseq_payload(profiler.iseq).profile; match bare_opcode { YARVINSN_opt_nil_p => profile_operands(profiler, profile, 1), diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 4fbad5d247..5b39ecdf4b 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -1,38 +1,75 @@ -// Maxime would like to rebuild an improved stats system -// Individual stats should be tagged as always available, or only available in stats mode -// We could also tag which stats are fallback or exit counters, etc. Maybe even tag units? -// -// Comptime vs Runtime stats? +use std::time::Instant; use crate::{cruby::*, options::get_option, state::{zjit_enabled_p, ZJITState}}; macro_rules! make_counters { - ($($counter_name:ident,)+) => { + ( + default { + $($default_counter_name:ident,)+ + } + $($counter_name:ident,)+ + ) => { /// Struct containing the counter values #[derive(Default, Debug)] - pub struct Counters { $(pub $counter_name: u64),+ } + pub struct Counters { + $(pub $default_counter_name: u64,)+ + $(pub $counter_name: u64,)+ + } /// Enum to represent a counter #[allow(non_camel_case_types)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] - pub enum Counter { $($counter_name),+ } + pub enum Counter { + $($default_counter_name,)+ + $($counter_name,)+ + } + + impl Counter { + pub fn name(&self) -> String { + match self { + $( Counter::$default_counter_name => stringify!($default_counter_name).to_string(), )+ + $( Counter::$counter_name => stringify!($counter_name).to_string(), )+ + } + } + } /// Map a counter to a pointer pub fn counter_ptr(counter: Counter) -> *mut u64 { let counters = $crate::state::ZJITState::get_counters(); match counter { - $( Counter::$counter_name => std::ptr::addr_of_mut!(counters.$counter_name) ),+ + $( Counter::$default_counter_name => std::ptr::addr_of_mut!(counters.$default_counter_name), )+ + $( Counter::$counter_name => std::ptr::addr_of_mut!(counters.$counter_name), )+ } } + + /// The list of counters that are available without --zjit-stats. + /// They are incremented only by `incr_counter()` and don't use `gen_incr_counter()`. + pub const DEFAULT_COUNTERS: &'static [Counter] = &[ + $( Counter::$default_counter_name, )+ + ]; } } // Declare all the counters we track make_counters! { + // Default counters that are available without --zjit-stats + default { + compile_time_ns, + profile_time_ns, + gc_time_ns, + invalidation_time_ns, + } + // The number of times YARV instructions are executed on JIT code zjit_insns_count, } +/// Increase a counter by a specified amount +fn incr_counter(counter: Counter, amount: u64) { + let ptr = counter_ptr(counter); + unsafe { *ptr += amount; } +} + pub fn zjit_alloc_size() -> usize { 0 // TODO: report the actual memory usage } @@ -49,14 +86,30 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE) -> VALUE { } let hash = unsafe { rb_hash_new() }; - // TODO: Set counters that are always available here + let counters = ZJITState::get_counters(); + + for &counter in DEFAULT_COUNTERS { + let counter_val = unsafe { *counter_ptr(counter) }; + set_stat(hash, &counter.name(), counter_val); + } // Set counters that are enabled when --zjit-stats is enabled if get_option!(stats) { - let counters = ZJITState::get_counters(); set_stat(hash, "zjit_insns_count", counters.zjit_insns_count); - set_stat(hash, "vm_insns_count", unsafe { rb_vm_insns_count }); + + if unsafe { rb_vm_insns_count } > 0 { + set_stat(hash, "vm_insns_count", unsafe { rb_vm_insns_count }); + } } hash } + +/// Measure the time taken by func() and add that to zjit_compile_time. +pub fn with_time_stat(counter: Counter, func: F) -> R where F: FnOnce() -> R { + let start = Instant::now(); + let ret = func(); + let nanos = Instant::now().duration_since(start).as_nanos(); + incr_counter(counter, nanos as u64); + ret +} From 4f34eddbd3c701bdc1ccc93a192a127e0c33202c Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 11 Aug 2025 14:35:34 -0700 Subject: [PATCH 114/157] YJIT, ZJIT: Fix JITs compiling prelude (#14171) --- ruby.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ruby.c b/ruby.c index a01e3d8afa..6d2b5833b6 100644 --- a/ruby.c +++ b/ruby.c @@ -1819,8 +1819,10 @@ ruby_opt_init(ruby_cmdline_options_t *opt) if (rb_namespace_available()) rb_initialize_main_namespace(); + rb_namespace_init_done(); + ruby_init_prelude(); - // Initialize JITs after prelude because JITing prelude is typically not optimal. + // Initialize JITs after ruby_init_prelude() because JITing prelude is typically not optimal. #if USE_YJIT rb_yjit_init(opt->yjit); #endif @@ -1831,8 +1833,6 @@ ruby_opt_init(ruby_cmdline_options_t *opt) } #endif - rb_namespace_init_done(); - ruby_init_prelude(); ruby_set_script_name(opt->script_name); require_libraries(&opt->req_list); } From e29d33345402f554220c35617e897e6d52bbecff Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 11 Aug 2025 23:07:26 +0100 Subject: [PATCH 115/157] ZJIT: Implement `concatstrings` insn (#14154) Co-authored-by: Alexander Momchilov --- test/ruby/test_zjit.rb | 16 +++++ zjit/src/backend/lir.rs | 6 ++ zjit/src/codegen.rs | 51 ++++++++++++++++ zjit/src/hir.rs | 126 ++++++++++++++++++++++++++++------------ 4 files changed, 161 insertions(+), 38 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index bf43fd1324..4dc0919b6b 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1506,6 +1506,22 @@ class TestZJIT < Test::Unit::TestCase }, call_threshold: 2 end + def test_string_concat + assert_compiles '"123"', %q{ + def test = "#{1}#{2}#{3}" + + test + }, insns: [:concatstrings] + end + + def test_string_concat_empty + assert_compiles '""', %q{ + def test = "#{}" + + test + }, insns: [:concatstrings] + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 0902d347c7..86bea62fcd 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2233,6 +2233,12 @@ impl Assembler { out } + pub fn sub_into(&mut self, left: Opnd, right: Opnd) -> Opnd { + let out = self.sub(left, right); + self.mov(left, out); + out + } + #[must_use] pub fn mul(&mut self, left: Opnd, right: Opnd) -> Opnd { let out = self.new_vreg(Opnd::match_num_bits(&[left, right])); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index f9532dfe03..b1b43abbe6 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -330,6 +330,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::NewRange { low, high, flag, state } => gen_new_range(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), + Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, opnds!(strings), &function.frame_state(*state))?, Insn::Param { idx } => unreachable!("block.insns should not have Insn::Param({idx})"), Insn::Snapshot { .. } => return Some(()), // we don't need to do anything for this instruction at the moment Insn::Jump(branch) => return gen_jump(jit, asm, branch), @@ -1456,6 +1457,56 @@ pub fn gen_stub_exit(cb: &mut CodeBlock) -> Option { }) } +fn gen_string_concat(jit: &mut JITState, asm: &mut Assembler, strings: Vec, state: &FrameState) -> Option { + let n = strings.len(); + + // concatstrings shouldn't have 0 strings + // If it happens we abort the compilation for now + if n == 0 { + return None; + } + + gen_prepare_non_leaf_call(jit, asm, state)?; + + // Calculate the compile-time NATIVE_STACK_PTR offset from NATIVE_BASE_PTR + // At this point, frame_setup(&[], jit.c_stack_slots) has been called, + // which allocated aligned_stack_bytes(jit.c_stack_slots) on the stack + let frame_size = aligned_stack_bytes(jit.c_stack_slots); + let allocation_size = aligned_stack_bytes(n); + + asm_comment!(asm, "allocate {} bytes on C stack for {} strings", allocation_size, n); + asm.sub_into(NATIVE_STACK_PTR, allocation_size.into()); + + // Calculate the total offset from NATIVE_BASE_PTR to our buffer + let total_offset_from_base = (frame_size + allocation_size) as i32; + + for (idx, &string_opnd) in strings.iter().enumerate() { + let slot_offset = -total_offset_from_base + (idx as i32 * SIZEOF_VALUE_I32); + asm.mov( + Opnd::mem(VALUE_BITS, NATIVE_BASE_PTR, slot_offset), + string_opnd + ); + } + + let first_string_ptr = asm.lea(Opnd::mem(64, NATIVE_BASE_PTR, -total_offset_from_base)); + + let result = asm_ccall!(asm, rb_str_concat_literals, n.into(), first_string_ptr); + + asm_comment!(asm, "restore C stack pointer"); + asm.add_into(NATIVE_STACK_PTR, allocation_size.into()); + + Some(result) +} + +/// Given the number of spill slots needed for a function, return the number of bytes +/// the function needs to allocate on the stack for the stack frame. +fn aligned_stack_bytes(num_slots: usize) -> usize { + // Both x86_64 and arm64 require the stack to be aligned to 16 bytes. + // Since SIZEOF_VALUE is 8 bytes, we need to round up the size to the nearest even number. + let num_slots = num_slots + (num_slots % 2); + num_slots * SIZEOF_VALUE +} + impl Assembler { /// Make a C call while marking the start and end positions of it fn ccall_with_branch(&mut self, fptr: *const u8, opnds: Vec, branch: &Rc) -> Opnd { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bff0fcd757..5111ab30f9 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -445,6 +445,7 @@ pub enum Insn { StringCopy { val: InsnId, chilled: bool, state: InsnId }, StringIntern { val: InsnId }, + StringConcat { strings: Vec, state: InsnId }, /// Put special object (VMCORE, CBASE, etc.) based on value_type PutSpecialObject { value_type: SpecialObjectType }, @@ -675,6 +676,16 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::ArrayDup { val, .. } => { write!(f, "ArrayDup {val}") } Insn::HashDup { val, .. } => { write!(f, "HashDup {val}") } Insn::StringCopy { val, .. } => { write!(f, "StringCopy {val}") } + Insn::StringConcat { strings, .. } => { + write!(f, "StringConcat")?; + let mut prefix = " "; + for string in strings { + write!(f, "{prefix}{string}")?; + prefix = ", "; + } + + Ok(()) + } Insn::Test { val } => { write!(f, "Test {val}") } Insn::IsNil { val } => { write!(f, "IsNil {val}") } Insn::Jump(target) => { write!(f, "Jump {target}") } @@ -1135,6 +1146,7 @@ impl Function { &Throw { throw_state, val } => Throw { throw_state, val: find!(val) }, &StringCopy { val, chilled, state } => StringCopy { val: find!(val), chilled, state }, &StringIntern { val } => StringIntern { val: find!(val) }, + &StringConcat { ref strings, state } => StringConcat { strings: find_vec!(strings), state: find!(state) }, &Test { val } => Test { val: find!(val) }, &IsNil { val } => IsNil { val: find!(val) }, &Jump(ref target) => Jump(find_branch_edge!(target)), @@ -1258,6 +1270,7 @@ impl Function { Insn::IsNil { .. } => types::CBool, Insn::StringCopy { .. } => types::StringExact, Insn::StringIntern { .. } => types::StringExact, + Insn::StringConcat { .. } => types::StringExact, Insn::NewArray { .. } => types::ArrayExact, Insn::ArrayDup { .. } => types::ArrayExact, Insn::NewHash { .. } => types::HashExact, @@ -1887,6 +1900,10 @@ impl Function { worklist.push_back(high); worklist.push_back(state); } + &Insn::StringConcat { ref strings, state, .. } => { + worklist.extend(strings); + worklist.push_back(state); + } | &Insn::StringIntern { val } | &Insn::Return { val } | &Insn::Throw { val, .. } @@ -2469,6 +2486,16 @@ impl FrameState { self.stack.pop().ok_or_else(|| ParseError::StackUnderflow(self.clone())) } + fn stack_pop_n(&mut self, count: usize) -> Result, ParseError> { + // Check if we have enough values on the stack + let stack_len = self.stack.len(); + if stack_len < count { + return Err(ParseError::StackUnderflow(self.clone())); + } + + Ok(self.stack.split_off(stack_len - count)) + } + /// Get a stack-top operand fn stack_top(&self) -> Result { self.stack.last().ok_or_else(|| ParseError::StackUnderflow(self.clone())).copied() @@ -2789,24 +2816,23 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let insn_id = fun.push_insn(block, Insn::StringIntern { val }); state.stack_push(insn_id); } + YARVINSN_concatstrings => { + let count = get_arg(pc, 0).as_u32(); + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let strings = state.stack_pop_n(count as usize)?; + let insn_id = fun.push_insn(block, Insn::StringConcat { strings, state: exit_id }); + state.stack_push(insn_id); + } YARVINSN_newarray => { let count = get_arg(pc, 0).as_usize(); let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - let mut elements = vec![]; - for _ in 0..count { - elements.push(state.stack_pop()?); - } - elements.reverse(); + let elements = state.stack_pop_n(count)?; state.stack_push(fun.push_insn(block, Insn::NewArray { elements, state: exit_id })); } YARVINSN_opt_newarray_send => { let count = get_arg(pc, 0).as_usize(); let method = get_arg(pc, 1).as_u32(); - let mut elements = vec![]; - for _ in 0..count { - elements.push(state.stack_pop()?); - } - elements.reverse(); + let elements = state.stack_pop_n(count)?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let (bop, insn) = match method { VM_OPT_NEWARRAY_SEND_MAX => (BOP_MAX, Insn::ArrayMax { elements, state: exit_id }), @@ -2871,13 +2897,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } YARVINSN_pushtoarray => { let count = get_arg(pc, 0).as_usize(); - let mut vals = vec![]; - for _ in 0..count { - vals.push(state.stack_pop()?); - } + let vals = state.stack_pop_n(count)?; let array = state.stack_pop()?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - for val in vals.into_iter().rev() { + for val in vals.into_iter() { fun.push_insn(block, Insn::ArrayPush { array, val, state: exit_id }); } state.stack_push(array); @@ -3079,12 +3102,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let argc = unsafe { vm_ci_argc((*cd).ci) }; - let mut args = vec![]; - for _ in 0..argc { - args.push(state.stack_pop()?); - } - args.reverse(); - + let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: exit_id }); @@ -3160,12 +3178,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let argc = unsafe { vm_ci_argc((*cd).ci) }; - let mut args = vec![]; - for _ in 0..argc { - args.push(state.stack_pop()?); - } - args.reverse(); - + let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: exit_id }); @@ -3183,12 +3196,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let argc = unsafe { vm_ci_argc((*cd).ci) }; - let mut args = vec![]; - for _ in 0..argc { - args.push(state.stack_pop()?); - } - args.reverse(); - + let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let send = fun.push_insn(block, Insn::Send { self_val: recv, cd, blockiseq, args, state: exit_id }); @@ -5224,7 +5232,47 @@ mod tests { v3:Fixnum[1] = Const Value(1) v5:BasicObject = ObjToString v3 v7:String = AnyToString v3, str: v5 - SideExit UnknownOpcode(concatstrings) + v9:StringExact = StringConcat v2, v7 + Return v9 + "#]]); + } + + #[test] + fn test_string_concat() { + eval(r##" + def test = "#{1}#{2}#{3}" + "##); + assert_method_hir_with_opcode("test", YARVINSN_concatstrings, expect![[r#" + fn test@:2: + bb0(v0:BasicObject): + v2:Fixnum[1] = Const Value(1) + v4:BasicObject = ObjToString v2 + v6:String = AnyToString v2, str: v4 + v7:Fixnum[2] = Const Value(2) + v9:BasicObject = ObjToString v7 + v11:String = AnyToString v7, str: v9 + v12:Fixnum[3] = Const Value(3) + v14:BasicObject = ObjToString v12 + v16:String = AnyToString v12, str: v14 + v18:StringExact = StringConcat v6, v11, v16 + Return v18 + "#]]); + } + + #[test] + fn test_string_concat_empty() { + eval(r##" + def test = "#{}" + "##); + assert_method_hir_with_opcode("test", YARVINSN_concatstrings, expect![[r#" + fn test@:2: + bb0(v0:BasicObject): + v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v3:NilClass = Const Value(nil) + v5:BasicObject = ObjToString v3 + v7:String = AnyToString v3, str: v5 + v9:StringExact = StringConcat v2, v7 + Return v9 "#]]); } @@ -7172,7 +7220,8 @@ mod opt_tests { v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v5:StringExact = StringCopy v3 - SideExit UnknownOpcode(concatstrings) + v11:StringExact = StringConcat v2, v5 + Return v11 "#]]); } @@ -7186,9 +7235,10 @@ mod opt_tests { bb0(v0:BasicObject): v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v3:Fixnum[1] = Const Value(1) - v10:BasicObject = SendWithoutBlock v3, :to_s - v7:String = AnyToString v3, str: v10 - SideExit UnknownOpcode(concatstrings) + v11:BasicObject = SendWithoutBlock v3, :to_s + v7:String = AnyToString v3, str: v11 + v9:StringExact = StringConcat v2, v7 + Return v9 "#]]); } From 9fb34f4f169736d457e3ff4d6fb4a7a596e211c6 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 11 Aug 2025 15:36:37 -0700 Subject: [PATCH 116/157] ZJIT: Add --zjit-exec-mem-size (#14175) * ZJIT: Add --zjit-exec-mem-size * Add a comment about the limit --- zjit/src/options.rs | 35 ++++++++++++++++++++++++++++++----- zjit/src/state.rs | 6 +++--- zjit/src/stats.rs | 9 +++++---- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 92f56b8916..07584c9b99 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -22,6 +22,10 @@ pub static mut OPTIONS: Option = None; #[derive(Clone, Debug)] pub struct Options { + /// Hard limit of the executable memory block to allocate in bytes. + /// Note that the command line argument is expressed in MiB and not bytes. + pub exec_mem_bytes: usize, + /// Number of times YARV instructions should be profiled. pub num_profiles: u8, @@ -58,6 +62,7 @@ pub struct Options { impl Default for Options { fn default() -> Self { Options { + exec_mem_bytes: 64 * 1024 * 1024, num_profiles: 1, stats: false, debug: false, @@ -74,12 +79,18 @@ impl Default for Options { } /// `ruby --help` descriptions for user-facing options. Do not add options for ZJIT developers. -/// Note that --help allows only 80 chars per line, including indentation. 80-char limit --> | +/// Note that --help allows only 80 chars per line, including indentation, and it also puts the +/// description in a separate line if the option name is too long. 80-char limit --> | (any character beyond this `|` column fails the test) pub const ZJIT_OPTIONS: &'static [(&str, &str)] = &[ - ("--zjit-call-threshold=num", "Number of calls to trigger JIT (default: 2)."), - ("--zjit-num-profiles=num", "Number of profiled calls before JIT (default: 1, max: 255)."), - ("--zjit-stats", "Enable collecting ZJIT statistics."), - ("--zjit-perf", "Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf."), + // TODO: Hide --zjit-exec-mem-size from ZJIT_OPTIONS once we add --zjit-mem-size (Shopify/ruby#686) + ("--zjit-exec-mem-size=num", + "Size of executable memory block in MiB (default: 64)."), + ("--zjit-call-threshold=num", + "Number of calls to trigger JIT (default: 2)."), + ("--zjit-num-profiles=num", + "Number of profiled calls before JIT (default: 1, max: 255)."), + ("--zjit-stats", "Enable collecting ZJIT statistics."), + ("--zjit-perf", "Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf."), ("--zjit-log-compiled-iseqs=path", "Log compiled ISEQs to the file. The file will be truncated."), ]; @@ -163,6 +174,20 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { match (opt_name, opt_val) { ("", "") => {}, // Simply --zjit + ("mem-size", _) => match opt_val.parse::() { + Ok(n) => { + // Reject 0 or too large values that could overflow. + // The upper bound is 1 TiB but we could make it smaller. + if n == 0 || n > 1024 * 1024 { + return None + } + + // Convert from MiB to bytes internally for convenience + options.exec_mem_bytes = n * 1024 * 1024; + } + Err(_) => return None, + }, + ("call-threshold", _) => match opt_val.parse() { Ok(n) => { unsafe { rb_zjit_call_threshold = n; } diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 79be91fd85..dca04b7a72 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -48,7 +48,7 @@ impl ZJITState { use crate::cruby::*; use crate::options::*; - let exec_mem_size: usize = 64 * 1024 * 1024; // TODO: implement the option + let exec_mem_bytes: usize = get_option!(exec_mem_bytes); let virt_block: *mut u8 = unsafe { rb_zjit_reserve_addr_space(64 * 1024 * 1024) }; // Memory protection syscalls need page-aligned addresses, so check it here. Assuming @@ -73,8 +73,8 @@ impl ZJITState { crate::virtualmem::sys::SystemAllocator {}, page_size, NonNull::new(virt_block).unwrap(), - exec_mem_size, - 64 * 1024 * 1024, // TODO: support the option + exec_mem_bytes, + exec_mem_bytes, // TODO: change this to --zjit-mem-size (Shopify/ruby#686) ); let mem_block = Rc::new(RefCell::new(mem_block)); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 5b39ecdf4b..fa8b741eea 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -70,10 +70,6 @@ fn incr_counter(counter: Counter, amount: u64) { unsafe { *ptr += amount; } } -pub fn zjit_alloc_size() -> usize { - 0 // TODO: report the actual memory usage -} - /// Return a Hash object that contains ZJIT statistics #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE) -> VALUE { @@ -113,3 +109,8 @@ pub fn with_time_stat(counter: Counter, func: F) -> R where F: FnOnce() -> incr_counter(counter, nanos as u64); ret } + +/// The number of bytes ZJIT has allocated on the Rust heap. +pub fn zjit_alloc_size() -> usize { + 0 // TODO: report the actual memory usage to support --zjit-mem-size (Shopify/ruby#686) +} From 39effad4862c7cac31ad8b1dc0bdd984a3f894b6 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 11 Aug 2025 21:34:54 +0100 Subject: [PATCH 117/157] [DOC] ZJIT: Add ZJIT to autolink_excluded_words This tells RDoc to not automatically link to the `ZJIT` module so we don't need to keep escaping the word ZJIT in the documentation/comments. --- .rdoc_options | 1 + zjit.rb | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.rdoc_options b/.rdoc_options index 02cf1f00d8..76c1c8e0db 100644 --- a/.rdoc_options +++ b/.rdoc_options @@ -19,5 +19,6 @@ autolink_excluded_words: - RDoc - Ruby - Set +- ZJIT canonical_root: https://docs.ruby-lang.org/en/master diff --git a/zjit.rb b/zjit.rb index fc2c80c52f..7f98c5adc7 100644 --- a/zjit.rb +++ b/zjit.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -# This module allows for introspection of \ZJIT, CRuby's just-in-time compiler. +# This module allows for introspection of ZJIT, CRuby's just-in-time compiler. # Everything in the module is highly implementation specific and the API might # be less stable compared to the standard library. # -# This module may not exist if \ZJIT does not support the particular platform +# This module may not exist if ZJIT does not support the particular platform # for which CRuby is built. module RubyVM::ZJIT # Avoid calling a Ruby method here to avoid interfering with compilation tests @@ -14,12 +14,12 @@ module RubyVM::ZJIT end class << RubyVM::ZJIT - # Check if \ZJIT is enabled + # Check if ZJIT is enabled def enabled? Primitive.cexpr! 'RBOOL(rb_zjit_enabled_p)' end - # Return \ZJIT statistics as a Hash + # Return ZJIT statistics as a Hash def stats stats = Primitive.rb_zjit_stats return nil if stats.nil? @@ -32,7 +32,7 @@ class << RubyVM::ZJIT stats end - # Get the summary of \ZJIT statistics as a String + # Get the summary of ZJIT statistics as a String def stats_string buf = +'' stats = self.stats From 4da569b53ef355e7d11085ff448599f25599bad3 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 11 Aug 2025 22:27:27 +0100 Subject: [PATCH 118/157] [DOC] YJIT: Add YJIT to autolink_excluded_words --- .rdoc_options | 1 + yjit.rb | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.rdoc_options b/.rdoc_options index 76c1c8e0db..b8b511efe6 100644 --- a/.rdoc_options +++ b/.rdoc_options @@ -20,5 +20,6 @@ autolink_excluded_words: - Ruby - Set - ZJIT +- YJIT canonical_root: https://docs.ruby-lang.org/en/master diff --git a/yjit.rb b/yjit.rb index 1655529b5e..751400a43e 100644 --- a/yjit.rb +++ b/yjit.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true # :markup: markdown -# This module allows for introspection of \YJIT, CRuby's just-in-time compiler. +# This module allows for introspection of YJIT, CRuby's just-in-time compiler. # Everything in the module is highly implementation specific and the API might # be less stable compared to the standard library. # -# This module may not exist if \YJIT does not support the particular platform +# This module may not exist if YJIT does not support the particular platform # for which CRuby is built. module RubyVM::YJIT - # Check if \YJIT is enabled. + # Check if YJIT is enabled. def self.enabled? Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p)' end @@ -33,8 +33,8 @@ module RubyVM::YJIT Primitive.rb_yjit_reset_stats_bang end - # Enable \YJIT compilation. `stats` option decides whether to enable \YJIT stats or not. `log` decides - # whether to enable \YJIT compilation logging or not. Optional `mem_size` and `call_threshold` can be + # Enable YJIT compilation. `stats` option decides whether to enable YJIT stats or not. `log` decides + # whether to enable YJIT compilation logging or not. Optional `mem_size` and `call_threshold` can be # provided to override default configuration. # # * `stats`: From 0070c26aecdf0f6692ad6a03315ccf64f593c38e Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 11 Aug 2025 15:40:28 -0400 Subject: [PATCH 119/157] ZJIT: CI: Use Rust version built into GitHub Actions image Saves the work of installing Rust for most jobs. Keep a job on each platform that tests 1.85.0, the minimum supported version, though. --- .github/workflows/zjit-macos.yml | 10 +++++++--- .github/workflows/zjit-ubuntu.yml | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index ab922849f4..7b5d9f6b61 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -34,6 +34,7 @@ jobs: include: - test_task: 'zjit-check' configure: '--enable-yjit=dev --enable-zjit' + rust_version: "1.85.0" - test_task: 'ruby' # build test for combo build configure: '--enable-yjit --enable-zjit' @@ -81,14 +82,17 @@ jobs: # Set fetch-depth: 10 so that Launchable can receive commits information. fetch-depth: 10 + - name: Install Rust + if: ${{ matrix.rust_version }} + run: | + rustup install ${{ matrix.rust_version }} --profile minimal + rustup default ${{ matrix.rust_version }} + - uses: taiki-e/install-action@v2 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} - - name: Install Rust # TODO(alan): remove when GitHub images catch up past 1.85.0 - run: rustup default 1.85.0 - - name: Run configure run: ../src/configure -C --disable-install-doc ${{ matrix.configure }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 09fa137f45..da53a52b7d 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -39,6 +39,7 @@ jobs: - test_task: 'zjit-check' configure: '--enable-yjit --enable-zjit=dev' + rust_version: '1.85.0' - test_task: 'zjit-test-all' configure: '--enable-zjit=dev' @@ -98,7 +99,10 @@ jobs: fetch-depth: 10 - name: Install Rust - run: rustup default 1.85.0 + if: ${{ matrix.rust_version }} + run: | + rustup install ${{ matrix.rust_version }} --profile minimal + rustup default ${{ matrix.rust_version }} - name: Install rustfmt if: ${{ matrix.test_task == 'zjit-bindgen' }} From 8b1afbc6ed84364506f43c2107f6cb634d92f4be Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 11 Aug 2025 16:44:22 -0400 Subject: [PATCH 120/157] CI: Surface Rust warnings on PRs that touch any Rust code Rust PRs will have a failed CI step if they trigger any warnings. This helps us stay on top of warnings from new Rust releases and also ones we accidentally write. Fix a typo for demo, since this only runs when Rust files are changed. --- .github/workflows/rust-warnings.yml | 53 +++++++++++++++++++++++++++++ zjit/src/hir.rs | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/rust-warnings.yml diff --git a/.github/workflows/rust-warnings.yml b/.github/workflows/rust-warnings.yml new file mode 100644 index 0000000000..b05ebbfe64 --- /dev/null +++ b/.github/workflows/rust-warnings.yml @@ -0,0 +1,53 @@ +# Surface Rust warnings on PRs that touch any Rust code. +# Not a required check so we never block people over new warnings +# that might come from a new Rust version being released. +name: Rust warnings +on: + pull_request: + types: + - opened + - synchronize + - reopened + paths: + - '**.rs' + - '!**.inc.rs' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + + runs-on: ubuntu-24.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.head_commit.message, 'Document') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.title, 'Document') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust + run: rustup default beta + + - name: Rust warnings + run: | + set -euo pipefail + cargo check --quiet --all-features --message-format=json \ + | jq -r 'select(.reason == "compiler-message" and .message.level == "warning") | .message.rendered' \ + > warnings.txt + cat warnings.txt + ! grep --quiet '[^[:space:]]' warnings.txt diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 5111ab30f9..f3d39281ad 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -343,7 +343,7 @@ impl<'a> std::fmt::Display for ConstPrinter<'a> { /// /// Because this is extra state external to any pointer being printed, a /// printing adapter struct that wraps the pointer along with this map is -/// required to make use of this effectly. The [`std::fmt::Display`] +/// required to make use of this effectively. The [`std::fmt::Display`] /// implementation on the adapter struct can then be reused to implement /// `Display` on the inner type with a default [`PtrPrintMap`], which /// does not perform any mapping. From f2c7968a4295bdfd658501a2071bcc165eb3423d Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Tue, 12 Aug 2025 10:16:41 +0900 Subject: [PATCH 121/157] [ruby/stringio] Fix test name (https://github.com/ruby/stringio/pull/139) https://github.com/ruby/stringio/commit/0edc8e22da --- test/stringio/test_stringio.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index 5215a6d312..70bab8afe2 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -70,7 +70,7 @@ class TestStringIO < Test::Unit::TestCase assert_nil io.getc end - def test_pread_eof + def test_eof_null io = StringIO.new(nil) assert_predicate io, :eof? end From 306df1294985f6e4404046fc4f17f4c8082af152 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 11 Aug 2025 21:02:59 -0500 Subject: [PATCH 122/157] [DOC] New .md file to replace doc/globals.rdoc --- doc/globals.md | 572 +++++++++++++++++++++++++++++++++++++ doc/globals.rdoc | 416 --------------------------- doc/syntax/assignment.rdoc | 2 +- load.c | 2 +- 4 files changed, 574 insertions(+), 418 deletions(-) create mode 100644 doc/globals.md delete mode 100644 doc/globals.rdoc diff --git a/doc/globals.md b/doc/globals.md new file mode 100644 index 0000000000..db23831a10 --- /dev/null +++ b/doc/globals.md @@ -0,0 +1,572 @@ +# Pre-Defined Global Variables + +Some of the pre-defined global variables have synonyms +that are available via module English. +For each of those, the \English synonym is given. + +To use the module: + +``` +require 'English' +``` + +## Summary + +### Exceptions + +| Variable | English | Contains | +|-------------|--------------------------|----------------------------------------------------| +| $! | $ERROR_INFO | Exception object; set by Kernel#raise. | +| $@ | $ERROR_POSITION | Array of backtrace positions; set by Kernel#raise. | + +### Pattern Matching + +| Variable | English | Contains | +|-------------|----------------------------|--------------------------------------------------| +| $~ | $LAST_MATCH_INFO | MatchData object; set by matcher method. | +| $& | $MATCH | Matched substring; set by matcher method. | +| $` | $PRE_MATCH | Substring left of match; set by matcher method. | +| $' | $POST_MATCH | Substring right of match; set by matcher method. | +| $+ | $LAST_PAREN_MATCH | Last group matched; set by matcher method. | +| $1 | | First group matched; set by matcher method. | +| $2 | | Second group matched; set by matcher method. | +| $n | | nth group matched; set by matcher method. | + +### Separators + +| Variable | English | Contains | +|----------------------|-----------------------------------|--------------------------------------------------| +| $/ | $INPUT_RECORD_SEPARATOR | Input record separator; initially newline. | +| $\\\\\\\\ | $OUTPUT_RECORD_SEPARATOR | Output record separator; initially nil. | + +### Streams + +| Variable | English | Contains | +|------------------|-------------------------------------------|-----------------------------------------------------------| +| $stdin | | Standard input stream; initially STDIN. | +| $stdout | | Standard input stream; initially STDIOUT. | +| $stderr | | Standard input stream; initially STDERR. | +| $< | $DEFAULT_INPUT | Default standard input; ARGF or $stdin. | +| $> | $DEFAULT_OUTPUT | Default standard output; initially $stdout. | +| $. | $INPUT_LINE_NUMBER, $NR | Input position of most recently read stream. | +| $_ | $LAST_READ_LINE | String from most recently read stream. | + +### Processes + +| Variable | English | Contains | +|------------------------------------------------|-------------------------------------|--------------------------------------------------------| +| $0 | | Initially, the name of the executing program. | +| $* | $ARGV | Points to the ARGV array. | +| $$ | $PROCESS_ID, $PID | Process ID of the current process. | +| $? | $CHILD_STATUS | Process::Status of most recently exited child process. | +| $LOAD_PATH, $:, $-I | | Array of paths to be searched. | +| $LOADED_FEATURES, $" | | Array of paths to loaded files. | + +### Debugging + +| Variable | English | Contains | +|--------------------|---------|----------------------------------------------------------------------| +| $FILENAME | | The value returned by method ARGF.filename. | +| $DEBUG | | Initially, whether option -d or --debug was given. | +| $VERBOSE | | Initially, whether option -V or -W was given. | + +### Other Variables + +| Variable | English | Contains | +|--------------|---------|-------------------------------------------------------| +| $-a | | Whether option -a was given. | +| $-i | | Extension given with command-line option -i. | +| $-l | | Whether option -l was given. | +| $-p | | Whether option -p was given. | + +## Exceptions + +### `$!` (\Exception) + +Contains the Exception object set by Kernel#raise: + +``` +begin + raise RuntimeError.new('Boo!') +rescue RuntimeError + p $! +end +``` + +Output: + +``` +# +``` + +English - `$ERROR_INFO` + +### `$@` (Backtrace) + +Same as `$!.backtrace`; +returns an array of backtrace positions: + +``` +begin + raise RuntimeError.new('Boo!') +rescue RuntimeError + pp $@.take(4) +end +``` + +Output: + +``` +["(irb):338:in `'", + "/snap/ruby/317/lib/ruby/3.2.0/irb/workspace.rb:119:in `eval'", + "/snap/ruby/317/lib/ruby/3.2.0/irb/workspace.rb:119:in `evaluate'", + "/snap/ruby/317/lib/ruby/3.2.0/irb/context.rb:502:in `evaluate'"] +``` + +English - `$ERROR_POSITION`. + +## Pattern Matching + +These global variables store information about the most recent +successful match in the current scope. + +For details and examples, +see {Regexp Global Variables}[rdoc-ref:Regexp@Global+Variables]. + +### `$~` (\MatchData) + +MatchData object created from the match; +thread-local and frame-local. + +English - `$LAST_MATCH_INFO`. + +### `$&` (Matched Substring) + +The matched string. + +English - `$MATCH`. + +### `` $` `` (Pre-Match Substring) +The string to the left of the match. + +English - `$PREMATCH`. + +### `$'` (Post-Match Substring) + +The string to the right of the match. + +English - `$POSTMATCH`. + +### `$+` (Last Matched Group) + +The last group matched. + +English - `$LAST_PAREN_MATCH`. + +### `$1`, `$2`, \Etc. (Matched Group) + +For `$_n_` the _nth_ group of the match. + +No \English. + +## Separators + +### `$/` (Input Record Separator) + +An input record separator, initially newline. + +English - `$INPUT_RECORD_SEPARATOR`, `$RS`. + +Aliased as `$-0`. + +### `$\\` (Output Record Separator) + +An output record separator, initially +nil+. + +English - `$OUTPUT_RECORD_SEPARATOR`, `$ORS`. + +## Streams + +### `$stdin` (Standard Input) + +The current standard input stream; initially: + +``` +$stdin # => #> +``` + +### `$stdout` (Standard Output) + +The current standard output stream; initially: + +``` +$stdout # => #> +``` + +### `$stderr` (Standard Error) + +The current standard error stream; initially: + +``` +$stderr # => #> +``` + +### `$<` (\ARGF or $stdin) + +Points to stream ARGF if not empty, else to stream $stdin; read-only. + +English - `$DEFAULT_INPUT`. + +### `$>` (Default Standard Output) + +An output stream, initially `$stdout`. + +English - `$DEFAULT_OUTPUT` + +### `$.` (Input Position) + +The input position (line number) in the most recently read stream. + +English - `$INPUT_LINE_NUMBER`, `$NR` + +### `$_` (Last Read Line) + +The line (string) from the most recently read stream. + +English - `$LAST_READ_LINE`. + +## Processes + +### `$0` + +Initially, contains the name of the script being executed; +may be reassigned. + +### `$*` (\ARGV) + +Points to ARGV. + +English - `$ARGV`. + +### `$$` (Process ID) + +The process ID of the current process. Same as Process.pid. + +English - `$PROCESS_ID`, `$PID`. + +### `$?` (Child Status) + +Initially +nil+, otherwise the Process::Status object +created for the most-recently exited child process; +thread-local. + +English - `$CHILD_STATUS`. + +### `$LOAD_PATH` (Load Path) + +Contains the array of paths to be searched +by Kernel#load and Kernel#require. + +Singleton method `$LOAD_PATH.resolve_feature_path(feature)` +returns: + +- `[:rb, _path_]`, where +path+ is the path to the Ruby file + to be loaded for the given +feature+. +- `[:so+ _path_]`, where +path+ is the path to the shared object file + to be loaded for the given +feature+. +- +nil+ if there is no such +feature+ and +path+. + +Examples: + +``` +$LOAD_PATH.resolve_feature_path('timeout') +# => [:rb, "/snap/ruby/317/lib/ruby/3.2.0/timeout.rb"] +$LOAD_PATH.resolve_feature_path('date_core') +# => [:so, "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/date_core.so"] +$LOAD_PATH.resolve_feature_path('foo') +# => nil +``` + +Aliased as `$:` and `$-I`. + +### `$LOADED_FEATURES` + +Contains an array of the paths to the loaded files: + +``` +$LOADED_FEATURES.take(10) +# => +["enumerator.so", + "thread.rb", + "fiber.so", + "rational.so", + "complex.so", + "ruby2_keywords.rb", + "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/enc/encdb.so", + "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/enc/trans/transdb.so", + "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/rbconfig.rb", + "/snap/ruby/317/lib/ruby/3.2.0/rubygems/compatibility.rb"] +``` + +Aliased as `$"`. + +## Debugging + +### `$FILENAME` + +The value returned by method ARGF.filename. + +### `$DEBUG` + +Initially +true+ if command-line option `-d` or `--debug` is given, +otherwise initially +false+; +may be set to either value in the running program. + +When +true+, prints each raised exception to `$stderr`. + +Aliased as `$-d`. + +### `$VERBOSE` + +Initially +true+ if command-line option `-v` or `-w` is given, +otherwise initially +false+; +may be set to either value, or to +nil+, in the running program. + +When +true+, enables Ruby warnings. + +When +nil+, disables warnings, including those from Kernel#warn. + +Aliased as `$-v` and `$-w`. + +## Other Variables + +### `$-a` + +Whether command-line option `-a` was given; read-only. + +### `$-i` + +Contains the extension given with command-line option `-i`, +or +nil+ if none. + +An alias of ARGF.inplace_mode. + +### `$-l` + +Whether command-line option `-l` was set; read-only. + +### `$-p` + +Whether command-line option `-p` was given; read-only. + +## Deprecated + +### `$=` + +### `$,` + +### `$;` + +# Pre-Defined Global Constants + +## Summary + +### Streams + +| Constant | Contains | +|-----------------|-------------------------| +| STDIN | Standard input stream. | +| STDOUT | Standard output stream. | +| STDERR | Standard error stream. | + +### Environment + +| Constant | Contains | +|------------------------------|--------------------------------------------------------------------------------------| +| ENV | Hash of current environment variable names and values. | +| ARGF | String concatenation of files given on the command line, or $stdin if none. | +| ARGV | Array of the given command-line arguments. | +| TOPLEVEL_BINDING | Binding of the top level scope. | +| RUBY_VERSION | String Ruby version. | +| RUBY_RELEASE_DATE | String Ruby release date. | +| RUBY_PLATFORM | String Ruby platform. | +| RUBY_PATCH_LEVEL | String Ruby patch level. | +| RUBY_REVISION | String Ruby revision. | +| RUBY_COPYRIGHT | String Ruby copyright. | +| RUBY_ENGINE | String Ruby engine. | +| RUBY_ENGINE_VERSION | String Ruby engine version. | +| RUBY_DESCRIPTION | String Ruby description. | + +### Embedded Data + +| Constant | Contains | +|---------------|---------------------------------------------------------------------------| +| DATA | File containing embedded data (lines following __END__, if any). | + +## Streams + +### `STDIN` + +The standard input stream (the default value for `$stdin`): + +``` +STDIN # => #> +``` + +### `STDOUT` + +The standard output stream (the default value for `$stdout`): + +``` +STDOUT # => #> +``` + +### `STDERR` + +The standard error stream (the default value for `$stderr`): + +``` +STDERR # => #> +``` + +## Environment + +### ENV + +A hash of the contains current environment variables names and values: + +``` +ENV.take(5) +# => +[["COLORTERM", "truecolor"], + ["DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus"], + ["DESKTOP_SESSION", "ubuntu"], + ["DISPLAY", ":0"], + ["GDMSESSION", "ubuntu"]] +``` + +### ARGF + +The virtual concatenation of the files given on the command line, or from +`$stdin` if no files were given, `"-"` is given, or after +all files have been read. + +### `ARGV` + +An array of the given command-line arguments. + +### `TOPLEVEL_BINDING` + +The Binding of the top level scope: + +``` +TOPLEVEL_BINDING # => # +``` + +### `RUBY_VERSION` + +The Ruby version: + +``` +RUBY_VERSION # => "3.2.2" +``` + +### `RUBY_RELEASE_DATE` + +The release date string: + +``` +RUBY_RELEASE_DATE # => "2023-03-30" +``` + +### `RUBY_PLATFORM` + +The platform identifier: + +``` +RUBY_PLATFORM # => "x86_64-linux" +``` + +### `RUBY_PATCHLEVEL` + +The integer patch level for this Ruby: + +``` +RUBY_PATCHLEVEL # => 53 +``` + +For a development build the patch level will be -1. + +### `RUBY_REVISION` + +The git commit hash for this Ruby: + +``` +RUBY_REVISION # => "e51014f9c05aa65cbf203442d37fef7c12390015" +``` + +### `RUBY_COPYRIGHT` + +The copyright string: + +``` +RUBY_COPYRIGHT +# => "ruby - Copyright (C) 1993-2023 Yukihiro Matsumoto" +``` + +### `RUBY_ENGINE` + +The name of the Ruby implementation: + +``` +RUBY_ENGINE # => "ruby" +``` + +### `RUBY_ENGINE_VERSION` + +The version of the Ruby implementation: + +``` +RUBY_ENGINE_VERSION # => "3.2.2" +``` + +### `RUBY_DESCRIPTION` + +The description of the Ruby implementation: + +``` +RUBY_DESCRIPTION +# => "ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]" +``` + +## Embedded \Data + +### `DATA` + +Defined if and only if the program has this line: + +``` +__END__ +``` + +When defined, `DATA` is a File object +containing the lines following the `__END__`, +positioned at the first of those lines: + +``` +p DATA +DATA.each_line { |line| p line } +__END__ +Foo +Bar +Baz +``` + +Output: + +``` +# +"Foo\n" +"Bar\n" +"Baz\n" +``` diff --git a/doc/globals.rdoc b/doc/globals.rdoc deleted file mode 100644 index 9466005be7..0000000000 --- a/doc/globals.rdoc +++ /dev/null @@ -1,416 +0,0 @@ -= Pre-Defined Global Variables - -Some of the pre-defined global variables have synonyms -that are available via module English. -For each of those, the \English synonym is given. - -To use the module: - - require 'English' - -== Exceptions - -=== $! (\Exception) - -Contains the Exception object set by Kernel#raise: - - begin - raise RuntimeError.new('Boo!') - rescue RuntimeError - p $! - end - -Output: - - # - -English - $ERROR_INFO - -=== $@ (Backtrace) - -Same as $!.backtrace; -returns an array of backtrace positions: - - begin - raise RuntimeError.new('Boo!') - rescue RuntimeError - pp $@.take(4) - end - -Output: - - ["(irb):338:in `'", - "/snap/ruby/317/lib/ruby/3.2.0/irb/workspace.rb:119:in `eval'", - "/snap/ruby/317/lib/ruby/3.2.0/irb/workspace.rb:119:in `evaluate'", - "/snap/ruby/317/lib/ruby/3.2.0/irb/context.rb:502:in `evaluate'"] - -English - $ERROR_POSITION. - -== Pattern Matching - -These global variables store information about the most recent -successful match in the current scope. - -For details and examples, -see {Regexp Global Variables}[rdoc-ref:Regexp@Global+Variables]. - -=== $~ (\MatchData) - -MatchData object created from the match; -thread-local and frame-local. - -English - $LAST_MATCH_INFO. - -=== $& (Matched Substring) - -The matched string. - -English - $MATCH. - -=== $` (Pre-Match Substring) - -The string to the left of the match. - -English - $PREMATCH. - -=== $' (Post-Match Substring) - -The string to the right of the match. - -English - $POSTMATCH. - -=== $+ (Last Matched Group) - -The last group matched. - -English - $LAST_PAREN_MATCH. - -=== $1, $2, \Etc. (Matched Group) - -For $_n_ the _nth_ group of the match. - -No \English. - -== Separators - -=== $/ (Input Record Separator) - -An input record separator, initially newline. - -English - $INPUT_RECORD_SEPARATOR, $RS. - -Aliased as $-0. - -=== $\\ (Output Record Separator) - -An output record separator, initially +nil+. - -English - $OUTPUT_RECORD_SEPARATOR, $ORS. - -== Streams - -=== $stdin (Standard Input) - -The current standard input stream; initially: - - $stdin # => #> - -=== $stdout (Standard Output) - -The current standard output stream; initially: - - $stdout # => #> - -=== $stderr (Standard Error) - -The current standard error stream; initially: - - $stderr # => #> - -=== $< (\ARGF or $stdin) - -Points to stream ARGF if not empty, else to stream $stdin; read-only. - -English - $DEFAULT_INPUT. - -=== $> (Default Standard Output) - -An output stream, initially $stdout. - -English - $DEFAULT_OUTPUT - -=== $. (Input Position) - -The input position (line number) in the most recently read stream. - -English - $INPUT_LINE_NUMBER, $NR - -=== $_ (Last Read Line) - -The line (string) from the most recently read stream. - -English - $LAST_READ_LINE. - -== Processes - -=== $0 - -Initially, contains the name of the script being executed; -may be reassigned. - -=== $* (\ARGV) - -Points to ARGV. - -English - $ARGV. - -=== $$ (Process ID) - -The process ID of the current process. Same as Process.pid. - -English - $PROCESS_ID, $PID. - -=== $? (Child Status) - -Initially +nil+, otherwise the Process::Status object -created for the most-recently exited child process; -thread-local. - -English - $CHILD_STATUS. - -=== $LOAD_PATH (Load Path) - -Contains the array of paths to be searched -by Kernel#load and Kernel#require. - -Singleton method $LOAD_PATH.resolve_feature_path(feature) -returns: - -- [:rb, _path_], where +path+ is the path to the Ruby file - to be loaded for the given +feature+. -- [:so+ _path_], where +path+ is the path to the shared object file - to be loaded for the given +feature+. -- +nil+ if there is no such +feature+ and +path+. - -Examples: - - $LOAD_PATH.resolve_feature_path('timeout') - # => [:rb, "/snap/ruby/317/lib/ruby/3.2.0/timeout.rb"] - $LOAD_PATH.resolve_feature_path('date_core') - # => [:so, "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/date_core.so"] - $LOAD_PATH.resolve_feature_path('foo') - # => nil - -Aliased as $: and $-I. - -=== $LOADED_FEATURES - -Contains an array of the paths to the loaded files: - - $LOADED_FEATURES.take(10) - # => - ["enumerator.so", - "thread.rb", - "fiber.so", - "rational.so", - "complex.so", - "ruby2_keywords.rb", - "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/enc/encdb.so", - "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/enc/trans/transdb.so", - "/snap/ruby/317/lib/ruby/3.2.0/x86_64-linux/rbconfig.rb", - "/snap/ruby/317/lib/ruby/3.2.0/rubygems/compatibility.rb"] - -Aliased as $". - -== Debugging - -=== $FILENAME - -The value returned by method ARGF.filename. - -=== $DEBUG - -Initially +true+ if command-line option -d or --debug is given, -otherwise initially +false+; -may be set to either value in the running program. - -When +true+, prints each raised exception to $stderr. - -Aliased as $-d. - -=== $VERBOSE - -Initially +true+ if command-line option -v or -w is given, -otherwise initially +false+; -may be set to either value, or to +nil+, in the running program. - -When +true+, enables Ruby warnings. - -When +nil+, disables warnings, including those from Kernel#warn. - -Aliased as $-v and $-w. - -== Other Variables - -=== $-a - -Whether command-line option -a was given; read-only. - -=== $-i - -Contains the extension given with command-line option -i, -or +nil+ if none. - -An alias of ARGF.inplace_mode. - -=== $-l - -Whether command-line option -l was set; read-only. - -=== $-p - -Whether command-line option -p was given; read-only. - -== Deprecated - -=== $= - -=== $, - -=== $; - -= Pre-Defined Global Constants - -= Streams - -=== STDIN - -The standard input stream (the default value for $stdin): - - STDIN # => #> - -=== STDOUT - -The standard output stream (the default value for $stdout): - - STDOUT # => #> - -=== STDERR - -The standard error stream (the default value for $stderr): - - STDERR # => #> - -== Environment - -=== ENV - -A hash of the contains current environment variables names and values: - - ENV.take(5) - # => - [["COLORTERM", "truecolor"], - ["DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus"], - ["DESKTOP_SESSION", "ubuntu"], - ["DISPLAY", ":0"], - ["GDMSESSION", "ubuntu"]] - -=== ARGF - -The virtual concatenation of the files given on the command line, or from -$stdin if no files were given, "-" is given, or after -all files have been read. - -=== ARGV - -An array of the given command-line arguments. - -=== TOPLEVEL_BINDING - -The Binding of the top level scope: - - TOPLEVEL_BINDING # => # - -=== RUBY_VERSION - -The Ruby version: - - RUBY_VERSION # => "3.2.2" - -=== RUBY_RELEASE_DATE - -The release date string: - - RUBY_RELEASE_DATE # => "2023-03-30" - -=== RUBY_PLATFORM - -The platform identifier: - - RUBY_PLATFORM # => "x86_64-linux" - -=== RUBY_PATCHLEVEL - -The integer patch level for this Ruby: - - RUBY_PATCHLEVEL # => 53 - -For a development build the patch level will be -1. - -=== RUBY_REVISION - -The git commit hash for this Ruby: - - RUBY_REVISION # => "e51014f9c05aa65cbf203442d37fef7c12390015" - -=== RUBY_COPYRIGHT - -The copyright string: - - RUBY_COPYRIGHT - # => "ruby - Copyright (C) 1993-2023 Yukihiro Matsumoto" - -=== RUBY_ENGINE - -The name of the Ruby implementation: - - RUBY_ENGINE # => "ruby" - -=== RUBY_ENGINE_VERSION - -The version of the Ruby implementation: - - RUBY_ENGINE_VERSION # => "3.2.2" - -=== RUBY_DESCRIPTION - -The description of the Ruby implementation: - - RUBY_DESCRIPTION - # => "ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]" - -== Embedded \Data - -=== DATA - -Defined if and only if the program has this line: - - __END__ - -When defined, DATA is a File object -containing the lines following the __END__, -positioned at the first of those lines: - - p DATA - DATA.each_line { |line| p line } - __END__ - Foo - Bar - Baz - -Output: - - # - "Foo\n" - "Bar\n" - "Baz\n" diff --git a/doc/syntax/assignment.rdoc b/doc/syntax/assignment.rdoc index f45f5bc0ea..68d4ae97be 100644 --- a/doc/syntax/assignment.rdoc +++ b/doc/syntax/assignment.rdoc @@ -279,7 +279,7 @@ An uninitialized global variable has a value of +nil+. Ruby has some special globals that behave differently depending on context such as the regular expression match variables or that have a side-effect when -assigned to. See the {global variables documentation}[rdoc-ref:globals.rdoc] +assigned to. See the {global variables documentation}[rdoc-ref:globals.md] for details. == Assignment Methods diff --git a/load.c b/load.c index 017c236483..b85a247c18 100644 --- a/load.c +++ b/load.c @@ -1318,7 +1318,7 @@ no_feature_p(vm_ns_t *vm_ns, const char *feature, const char *ext, int rb, int e return 0; } -// Documented in doc/globals.rdoc +// Documented in doc/globals.md VALUE rb_resolve_feature_path(VALUE klass, VALUE fname) { From 428937a5365192e5cf2ef97d2191e25cc9bd963f Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 11 Aug 2025 10:40:38 -0400 Subject: [PATCH 123/157] [DOC] Fix docs for GC.config After commit 61fff8a, GC.config now returns the same hash for getting and setting. --- gc.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/gc.rb b/gc.rb index 603520df53..ac04cb2e25 100644 --- a/gc.rb +++ b/gc.rb @@ -258,7 +258,7 @@ module GC # call-seq: # GC.config -> hash - # GC.config(hash_to_merge) -> merged_hash + # GC.config(hash_to_merge) -> hash # # This method is implementation-specific to CRuby. # @@ -274,15 +274,11 @@ module GC # With argument +hash_to_merge+ given, # merges that hash into the stored configuration hash; # ignores unknown hash keys; - # returns the implementation-specific configuration hash (see below): + # returns the configuration hash: # # GC.config(rgengc_allow_full_mark: false) - # # => {rgengc_allow_full_mark: false} - # GC.config # # => {rgengc_allow_full_mark: false, implementation: "default"} # GC.config(foo: 'bar') - # # => {rgengc_allow_full_mark: false} - # GC.config # # => {rgengc_allow_full_mark: false, implementation: "default"} # # All-Implementations Configuration From 6b2d9ed2a5f60606731efe13a26be12d685a49eb Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 13:47:22 +0900 Subject: [PATCH 124/157] Handle preperly comments in middle of lines in gems/bundled_gems --- common.mk | 12 +++++++----- defs/gmake.mk | 2 +- tool/fetch-bundled_gems.rb | 3 +-- tool/outdate-bundled-gems.rb | 2 +- tool/test-bundled-gems.rb | 3 +-- tool/update-bundled_gems.rb | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common.mk b/common.mk index f362948593..e2c3d72cc8 100644 --- a/common.mk +++ b/common.mk @@ -1539,12 +1539,14 @@ prepare-gems: $(HAVE_BASERUBY:yes=update-gems) $(HAVE_BASERUBY:yes=extract-gems) extract-gems: $(HAVE_BASERUBY:yes=update-gems) $(HAVE_BASERUBY:yes=outdate-bundled-gems) update-gems: $(HAVE_BASERUBY:yes=outdate-bundled-gems) +split_option = -F"\s+|\#.*" + update-gems$(sequential): PHONY $(ECHO) Downloading bundled gem files... $(Q) $(BASERUBY) -C "$(srcdir)" \ - -I./tool -rdownloader -answ \ + -I./tool -rdownloader $(split_option) -answ \ -e 'gem, ver = *$$F' \ - -e 'next if !ver or /^#/=~gem' \ + -e 'next if !ver' \ -e 'old = Dir.glob("gems/#{gem}-*.gem")' \ -e 'gem = "#{gem}-#{ver}.gem"' \ -e 'Downloader::RubyGems.download(gem, "gems", nil) and' \ @@ -1556,10 +1558,10 @@ update-gems$(sequential): PHONY extract-gems$(sequential): PHONY $(ECHO) Extracting bundled gem files... $(Q) $(BASERUBY) -C "$(srcdir)" \ - -Itool/lib -rfileutils -rbundled_gem -answ \ + -Itool/lib -rfileutils -rbundled_gem $(split_option) -answ \ -e 'BEGIN {d = ".bundle/gems"}' \ -e 'gem, ver, _, rev = *$$F' \ - -e 'next if !ver or /^#/=~gem' \ + -e 'next if !ver' \ -e 'g = "#{gem}-#{ver}"' \ -e 'unless File.directory?("#{d}/#{g}")' \ -e 'if rev and File.exist?(gs = "gems/src/#{gem}/#{gem}.gemspec")' \ @@ -1623,7 +1625,7 @@ yes-install-for-test-bundled-gems: yes-update-default-gemspecs test-bundled-gems-fetch: yes-test-bundled-gems-fetch yes-test-bundled-gems-fetch: clone-bundled-gems-src clone-bundled-gems-src: PHONY - $(Q) $(BASERUBY) -C $(srcdir)/gems ../tool/fetch-bundled_gems.rb BUNDLED_GEMS="$(BUNDLED_GEMS)" src bundled_gems + $(Q) $(BASERUBY) -C $(srcdir) tool/fetch-bundled_gems.rb BUNDLED_GEMS="$(BUNDLED_GEMS)" gems/src gems/bundled_gems no-test-bundled-gems-fetch: test-bundled-gems-prepare: $(TEST_RUNNABLE)-test-bundled-gems-prepare diff --git a/defs/gmake.mk b/defs/gmake.mk index 068425cb1d..6382e3d003 100644 --- a/defs/gmake.mk +++ b/defs/gmake.mk @@ -309,7 +309,7 @@ HELP_EXTRA_TASKS = \ # 4. "gem x.y.z URL" -> "gem-x.y.z" bundled-gems := $(shell sed \ -e 's/[ ][ ]*/ /g' \ - -e 's/^ //;/\#/d;s/ *$$//;/^$$/d' \ + -e 's/^ //;s/\#.*//;s/ *$$//;/^$$/d' \ $(if $(filter yes,$(HAVE_GIT)), \ -e 's/^\(.*\) \(.*\) \(.*\) \(.*\)/\1|\2|\4|\3/' \ ) \ diff --git a/tool/fetch-bundled_gems.rb b/tool/fetch-bundled_gems.rb index b76feefd94..e46c5bdc1c 100755 --- a/tool/fetch-bundled_gems.rb +++ b/tool/fetch-bundled_gems.rb @@ -1,4 +1,4 @@ -#!ruby -an +#!ruby -alnF\s+|#.* BEGIN { require 'fileutils' require_relative 'lib/colorize' @@ -21,7 +21,6 @@ BEGIN { n, v, u, r = $F next unless n -next if n =~ /^#/ next if bundled_gems&.all? {|pat| !File.fnmatch?(pat, n)} unless File.exist?("#{n}/.git") diff --git a/tool/outdate-bundled-gems.rb b/tool/outdate-bundled-gems.rb index c82d31d743..47ee80bc89 100755 --- a/tool/outdate-bundled-gems.rb +++ b/tool/outdate-bundled-gems.rb @@ -115,7 +115,7 @@ srcdir = Removal.new(ARGV.shift) curdir = !srcdir.base || File.identical?(srcdir.base, ".") ? srcdir : Removal.new bundled = File.readlines("#{srcdir.base}gems/bundled_gems"). - grep(/^(\w\S+)\s+\S+(?:\s+\S+\s+(\S+))?/) {$~.captures}.to_h rescue nil + grep(/^(\w[^\#\s]+)\s+[^\#\s]+(?:\s+[^\#\s]+\s+([^\#\s]+))?/) {$~.captures}.to_h rescue nil srcdir.glob(".bundle/gems/*/") do |dir| base = File.basename(dir) diff --git a/tool/test-bundled-gems.rb b/tool/test-bundled-gems.rb index 2b7e8916e5..027ed647f1 100644 --- a/tool/test-bundled-gems.rb +++ b/tool/test-bundled-gems.rb @@ -25,8 +25,7 @@ exit_code = 0 ruby = ENV['RUBY'] || RbConfig.ruby failed = [] File.foreach("#{gem_dir}/bundled_gems") do |line| - next if /^\s*(?:#|$)/ =~ line - gem = line.split.first + next unless gem = line[/^[^\s\#]+/] next unless bundled_gems.empty? || bundled_gems.split(",").include?(gem) next unless File.directory?("#{gem_dir}/src/#{gem}/test") diff --git a/tool/update-bundled_gems.rb b/tool/update-bundled_gems.rb index e20566546b..dec6b49cee 100755 --- a/tool/update-bundled_gems.rb +++ b/tool/update-bundled_gems.rb @@ -1,4 +1,4 @@ -#!ruby -pla +#!ruby -alpF\s+|#.* BEGIN { require 'rubygems' date = nil @@ -9,7 +9,7 @@ output = STDERR if ARGF.file == STDIN END { output.print date.strftime("latest_date=%F") if date } -unless /^[^#]/ !~ (gem = $F[0]) +if gem = $F[0] ver = Gem::Version.new($F[1]) (gem, src), = Gem::SpecFetcher.fetcher.detect(:latest) {|s| s.platform == "ruby" && s.name == gem From 401932c18f72db086a1a9890d34efb2e48a42e35 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 18:19:09 +0900 Subject: [PATCH 125/157] NMake needs caret to escape a hash sign --- common.mk | 2 +- template/Makefile.in | 2 ++ win32/Makefile.sub | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/common.mk b/common.mk index e2c3d72cc8..de90083c80 100644 --- a/common.mk +++ b/common.mk @@ -1539,7 +1539,7 @@ prepare-gems: $(HAVE_BASERUBY:yes=update-gems) $(HAVE_BASERUBY:yes=extract-gems) extract-gems: $(HAVE_BASERUBY:yes=update-gems) $(HAVE_BASERUBY:yes=outdate-bundled-gems) update-gems: $(HAVE_BASERUBY:yes=outdate-bundled-gems) -split_option = -F"\s+|\#.*" +split_option = -F"\s+|$(HASH_SIGN).*" update-gems$(sequential): PHONY $(ECHO) Downloading bundled gem files... diff --git a/template/Makefile.in b/template/Makefile.in index ea6ab349b0..66ac10de1b 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -289,6 +289,8 @@ ABI_VERSION_HDR = $(hdrdir)/ruby/internal/abi.h CAT_DEPEND = sed -e 's/{\$$([^(){}]*)[^{}]*}//g' -e /AUTOGENERATED/q +HASH_SIGN = \# + .SUFFIXES: .inc .h .c .y .i .$(ASMEXT) .$(DTRACE_EXT) all: diff --git a/win32/Makefile.sub b/win32/Makefile.sub index 4dc63a99f2..664d54e5ff 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -464,6 +464,7 @@ ASMEXT = asm INSTALLED_LIST= .installed.list +HASH_SIGN = ^# SRC_FILE = $(<:\=/) OS_SRC_FILE = $(<:/=\) DEST_FILE = $(@:\=/) From e2aeb7d97758f51d5de21e13d8ddd8c04239cd3e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 18:54:23 +0900 Subject: [PATCH 126/157] Use `$(SRC_FILE)` and `$(OS_DEST_FILE)` NMake combines VPATH and stem with a backslash. The resulting source name is embedded verbatim, backslash included, into the generated file using the `#line` pragma (e.g., "src\gc.rb"). This causes the warning "C4129: Unrecognized character escape sequence". --- common.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common.mk b/common.mk index de90083c80..9e5098ef4b 100644 --- a/common.mk +++ b/common.mk @@ -1353,11 +1353,11 @@ preludes: {$(VPATH)}miniprelude.c {$(srcdir)}.rb.rbbin: $(ECHO) making $@ - $(Q) $(MINIRUBY) $(tooldir)/mk_rbbin.rb $< > $@ + $(Q) $(MINIRUBY) $(tooldir)/mk_rbbin.rb $(SRC_FILE) > $(OS_DEST_FILE) {$(srcdir)}.rb.rbinc: $(ECHO) making $@ - $(Q) $(BASERUBY) $(tooldir)/mk_builtin_loader.rb $< + $(Q) $(BASERUBY) $(tooldir)/mk_builtin_loader.rb $(SRC_FILE) $(BUILTIN_BINARY:yes=built)in_binary.rbbin: $(PREP) $(BUILTIN_RB_SRCS) $(srcdir)/template/builtin_binary.rbbin.tmpl $(Q) $(MINIRUBY) $(tooldir)/generic_erb.rb -o $@ \ From 099df0b40b215b2fc5db59569d45c59ee48111a7 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 22 Jul 2025 16:12:46 +0200 Subject: [PATCH 127/157] CI: ubuntu.yml: Add GitHub Actions s390x case Add the s390x case using GitHub Actions ppc64le/s390x service. https://github.com/IBM/actionspz We can run the ppc64le/s390x cases only in the registered upstream repositories. https://github.com/IBM/actionspz/blob/main/docs/FAQ.md#what-about-forked-repos The following matrix upstream logic is to skip the ppc64le/s390x in the downstream (fork) repositories. ``` + upstream: + - ${{ github.repository == 'ruby/ruby' }} ``` Use the "os" list to determine the excluded ppc64le/s390x cases by using the "exclude" syntax. Because the "exclude" syntax are executed before the "include" syntax. Add the ubuntu-24.04-ppc64le as a comment, because the GitHub Actions ppc64le case has the following test errors and failures. https://bugs.ruby-lang.org/issues/21534 --- .github/workflows/ubuntu.yml | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index fa271e5cb5..ed56cd6600 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -24,6 +24,22 @@ jobs: make: strategy: matrix: + test_task: [check] + configure: [''] + arch: [''] + os: + - ubuntu-24.04 + - ubuntu-24.04-arm + # FIXME Comment out ppc64le due to failing tests on GitHub Actions + # ppc64le + # https://bugs.ruby-lang.org/issues/21534 + # - ubuntu-24.04-ppc64le + - ubuntu-24.04-s390x + # The ppc64le/s390x runners work only in the registered repositories. + # They don't work in forked repositories. + # https://github.com/IBM/actionspz/blob/main/docs/FAQ.md#what-about-forked-repos + upstream: + - ${{ github.repository == 'ruby/ruby' }} include: - test_task: check configure: 'cppflags=-DVM_CHECK_MODE' @@ -36,10 +52,11 @@ jobs: - test_task: test-bundler-parallel timeout: 50 - test_task: test-bundled-gems - - test_task: check - os: ubuntu-24.04 - - test_task: check - os: ubuntu-24.04-arm + exclude: + - os: ubuntu-24.04-ppc64le + upstream: false + - os: ubuntu-24.04-s390x + upstream: false fail-fast: false env: @@ -72,7 +89,15 @@ jobs: with: ruby-version: '3.1' bundler: none - if: ${{ !endsWith(matrix.os, 'arm') }} + if: ${{ !endsWith(matrix.os, 'arm') && !endsWith(matrix.os, 'ppc64le') && !endsWith(matrix.os, 's390x') }} + + # Avoid possible test failures with the zlib applying the following patch + # on s390x CPU architecture. + # https://github.com/madler/zlib/pull/410 + - name: Disable DFLTCC + run: echo "DFLTCC=0" >> $GITHUB_ENV + working-directory: + if: ${{ endsWith(matrix.os, 's390x') }} - uses: ./.github/actions/setup/directories with: From 05b654b43f6d0e92fbc3e1e908d811f031d59e40 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 22 Jul 2025 16:12:46 +0200 Subject: [PATCH 128/157] CI: ubuntu.yml: Set HOME env on ppc64le and s390x This is a temporary workaround. --- .github/workflows/ubuntu.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index ed56cd6600..749a3f3531 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -99,6 +99,16 @@ jobs: working-directory: if: ${{ endsWith(matrix.os, 's390x') }} + # A temporary workaround: Set HOME env to pass the step + # ./.github/actions/setup/directories. + # https://github.com/IBM/actionspz/issues/30 + - name: Set HOME env + run: | + echo "HOME: #{HOME}" + echo "HOME=$(ls -d ~)" >> $GITHUB_ENV + working-directory: + if: ${{ endsWith(matrix.os, 'ppc64le') || endsWith(matrix.os, 's390x') }} + - uses: ./.github/actions/setup/directories with: srcdir: src From 9fa87a668836f83ab836d0cbcefb4056622a0ed6 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 12 Aug 2025 12:32:49 +0200 Subject: [PATCH 129/157] CI: ubuntu.yml: Skip user ground id test on ppc64le and s390x This is a temporary workaround. --- .github/workflows/ubuntu.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 749a3f3531..e17d6dc3ed 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -157,6 +157,17 @@ jobs: continue-on-error: true timeout-minutes: 3 + # A temporary workaround: Skip user ground id test + # There is a mismatch between the group IDs of "id -g" and C function + # getpwuid(uid_t uid) pw_gid. + # https://github.com/IBM/actionspz/issues/31 + - name: Skip user group id test + run: | + sed -i.orig '/^ it "returns user group id" do/a\ skip' \ + ../src/spec/ruby/library/etc/struct_passwd_spec.rb + diff -u ../src/spec/ruby/library/etc/struct_passwd_spec.rb{.orig,} || : + if: ${{ endsWith(matrix.os, 'ppc64le') || endsWith(matrix.os, 's390x') }} + - name: make ${{ matrix.test_task }} run: | test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") From 0019e7c7020e616fae7fb06745d995e3f682683a Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 11 Aug 2025 11:01:24 -0400 Subject: [PATCH 130/157] Use rb_gc_mark_and_move for autoload_const --- variable.c | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/variable.c b/variable.c index 76b16b04cb..b5ad8343d7 100644 --- a/variable.c +++ b/variable.c @@ -2777,27 +2777,15 @@ static const rb_data_type_t autoload_data_type = { }; static void -autoload_const_compact(void *ptr) +autoload_const_mark_and_move(void *ptr) { struct autoload_const *ac = ptr; - ac->module = rb_gc_location(ac->module); - ac->autoload_data_value = rb_gc_location(ac->autoload_data_value); - ac->value = rb_gc_location(ac->value); - ac->file = rb_gc_location(ac->file); - ac->namespace = rb_gc_location(ac->namespace); -} - -static void -autoload_const_mark(void *ptr) -{ - struct autoload_const *ac = ptr; - - rb_gc_mark_movable(ac->module); - rb_gc_mark_movable(ac->autoload_data_value); - rb_gc_mark_movable(ac->value); - rb_gc_mark_movable(ac->file); - rb_gc_mark_movable(ac->namespace); + rb_gc_mark_and_move(&ac->module); + rb_gc_mark_and_move(&ac->autoload_data_value); + rb_gc_mark_and_move(&ac->value); + rb_gc_mark_and_move(&ac->file); + rb_gc_mark_and_move(&ac->namespace); } static size_t @@ -2817,7 +2805,7 @@ autoload_const_free(void *ptr) static const rb_data_type_t autoload_const_type = { "autoload_const", - {autoload_const_mark, autoload_const_free, autoload_const_memsize, autoload_const_compact,}, + {autoload_const_mark_and_move, autoload_const_free, autoload_const_memsize, autoload_const_mark_and_move,}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; From 814eaf336adca17dbad79717519ac06ee22edb4f Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 11 Aug 2025 11:03:25 -0400 Subject: [PATCH 131/157] Use rb_gc_mark_and_move for autoload_data --- variable.c | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/variable.c b/variable.c index b5ad8343d7..e0a85b8f48 100644 --- a/variable.c +++ b/variable.c @@ -2734,21 +2734,12 @@ struct autoload_data { }; static void -autoload_data_compact(void *ptr) +autoload_data_mark_and_move(void *ptr) { struct autoload_data *p = ptr; - p->feature = rb_gc_location(p->feature); - p->mutex = rb_gc_location(p->mutex); -} - -static void -autoload_data_mark(void *ptr) -{ - struct autoload_data *p = ptr; - - rb_gc_mark_movable(p->feature); - rb_gc_mark_movable(p->mutex); + rb_gc_mark_and_move(&p->feature); + rb_gc_mark_and_move(&p->mutex); } static void @@ -2772,7 +2763,7 @@ autoload_data_memsize(const void *ptr) static const rb_data_type_t autoload_data_type = { "autoload_data", - {autoload_data_mark, autoload_data_free, autoload_data_memsize, autoload_data_compact}, + {autoload_data_mark_and_move, autoload_data_free, autoload_data_memsize, autoload_data_mark_and_move}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; From 37e991b02c8664875007779352aa45e9924f3528 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 23:33:46 +0900 Subject: [PATCH 132/157] [DOC] Use the specified revision RDoc --- .github/workflows/check_misc.yml | 41 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 4b0da6a0fe..5267bd35f1 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -61,14 +61,11 @@ jobs: exit $fail working-directory: include - - name: Generate docs - id: docs + - name: Check if to generate documents + id: rdoc run: | - $RDOC -C -x ^ext -x ^lib . - $RDOC --op html . - echo htmlout=ruby-html-${GITHUB_SHA:0:10} >> $GITHUB_OUTPUT - env: - RDOC: ruby -W0 --disable-gems tool/rdoc-srcdir -q + ref=$(sed 's/#.*//;/^rdoc /!d' gems/bundled_gems | awk '{print $4}') + echo ref=$ref >> $GITHUB_OUTPUT # Generate only when document commit/PR if: >- ${{false @@ -80,6 +77,36 @@ jobs: || contains(github.event.pull_request.labels.*.name, 'Documentation') }} + - name: Checkout rdoc + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: ruby/rdoc + ref: ${{ steps.rdoc.outputs.ref }} + path: .bundle/gems/rdoc-0 + if: ${{ steps.rdoc.outputs.ref != '' }} + + - name: Generate rdoc + run: | + set -x + gempath=$(ruby -e 'print Gem.user_dir, "/bin"') + PATH=$gempath:$PATH + gem install --user bundler + bundle config --local path vendor/bundle + bundle install --jobs 4 + bundle exec rake generate + working-directory: .bundle/gems/rdoc-0 + if: ${{ steps.rdoc.outputs.ref != '' }} + + - name: Generate docs + id: docs + run: | + $RDOC -C -x ^ext -x ^lib . + $RDOC --op html . + echo htmlout=ruby-html-${GITHUB_SHA:0:10} >> $GITHUB_OUTPUT + env: + RDOC: ruby -W0 --disable-gems tool/rdoc-srcdir -q + if: ${{ steps.rdoc.outcome == 'success' }} + - name: Upload docs uses: actions/upload-artifact@v4 with: From 8f6f9e88c70bbae08b2830d2bec63c89d6367b27 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:41:08 +0900 Subject: [PATCH 133/157] [DOC] Try the latest RDoc --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index 4fed6a994d..4c59614011 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -39,7 +39,7 @@ ostruct 0.6.3 https://github.com/ruby/ostruct pstore 0.2.0 https://github.com/ruby/pstore benchmark 0.4.1 https://github.com/ruby/benchmark logger 1.7.0 https://github.com/ruby/logger -rdoc 6.14.2 https://github.com/ruby/rdoc +rdoc 6.14.2 https://github.com/ruby/rdoc f4a90c6010b2346cb5426d4496f5a37a136a82fb # for markdown win32ole 1.9.2 https://github.com/ruby/win32ole irb 1.15.2 https://github.com/ruby/irb 331c4e851296b115db766c291e8cf54a2492fb36 reline 0.6.2 https://github.com/ruby/reline From 58dbfe5285f3b85c6181875220191289087d5603 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:02:14 +0900 Subject: [PATCH 134/157] [DOC] Fix a typo --- doc/globals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/globals.md b/doc/globals.md index db23831a10..b0bc02b8a6 100644 --- a/doc/globals.md +++ b/doc/globals.md @@ -272,7 +272,7 @@ returns: - `[:rb, _path_]`, where +path+ is the path to the Ruby file to be loaded for the given +feature+. -- `[:so+ _path_]`, where +path+ is the path to the shared object file +- `[:so, _path_]`, where +path+ is the path to the shared object file to be loaded for the given +feature+. - +nil+ if there is no such +feature+ and +path+. From e07510d1a3b07438bcb0aef2b6a913d58f06ff9f Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:04:23 +0900 Subject: [PATCH 135/157] [DOC] Markup constants as code --- doc/globals.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/globals.md b/doc/globals.md index b0bc02b8a6..b4ade2f8cb 100644 --- a/doc/globals.md +++ b/doc/globals.md @@ -431,7 +431,7 @@ STDERR # => #> ## Environment -### ENV +### `ENV` A hash of the contains current environment variables names and values: @@ -445,7 +445,7 @@ ENV.take(5) ["GDMSESSION", "ubuntu"]] ``` -### ARGF +### `ARGF` The virtual concatenation of the files given on the command line, or from `$stdin` if no files were given, `"-"` is given, or after From 74b45dc3eebe14582dd0a5aaf3641de07b89adc2 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:23:54 +0900 Subject: [PATCH 136/157] [DOC] Use backticks instead of `+` in markdown mode --- doc/globals.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/globals.md b/doc/globals.md index b4ade2f8cb..99429e293d 100644 --- a/doc/globals.md +++ b/doc/globals.md @@ -181,7 +181,7 @@ Aliased as `$-0`. ### `$\\` (Output Record Separator) -An output record separator, initially +nil+. +An output record separator, initially `nil`. English - `$OUTPUT_RECORD_SEPARATOR`, `$ORS`. @@ -256,7 +256,7 @@ English - `$PROCESS_ID`, `$PID`. ### `$?` (Child Status) -Initially +nil+, otherwise the Process::Status object +Initially `nil`, otherwise the Process::Status object created for the most-recently exited child process; thread-local. @@ -270,11 +270,11 @@ by Kernel#load and Kernel#require. Singleton method `$LOAD_PATH.resolve_feature_path(feature)` returns: -- `[:rb, _path_]`, where +path+ is the path to the Ruby file - to be loaded for the given +feature+. -- `[:so, _path_]`, where +path+ is the path to the shared object file - to be loaded for the given +feature+. -- +nil+ if there is no such +feature+ and +path+. +- `[:rb, _path_]`, where `path` is the path to the Ruby file + to be loaded for the given `feature`. +- `[:so, _path_]`, where `path` is the path to the shared object file + to be loaded for the given `feature`. +- `nil` if there is no such `feature` and `path`. Examples: @@ -318,23 +318,23 @@ The value returned by method ARGF.filename. ### `$DEBUG` -Initially +true+ if command-line option `-d` or `--debug` is given, -otherwise initially +false+; +Initially `true` if command-line option `-d` or `--debug` is given, +otherwise initially `false`; may be set to either value in the running program. -When +true+, prints each raised exception to `$stderr`. +When `true`, prints each raised exception to `$stderr`. Aliased as `$-d`. ### `$VERBOSE` -Initially +true+ if command-line option `-v` or `-w` is given, -otherwise initially +false+; -may be set to either value, or to +nil+, in the running program. +Initially `true` if command-line option `-v` or `-w` is given, +otherwise initially `false`; +may be set to either value, or to `nil`, in the running program. -When +true+, enables Ruby warnings. +When `true`, enables Ruby warnings. -When +nil+, disables warnings, including those from Kernel#warn. +When `nil`, disables warnings, including those from Kernel#warn. Aliased as `$-v` and `$-w`. @@ -347,7 +347,7 @@ Whether command-line option `-a` was given; read-only. ### `$-i` Contains the extension given with command-line option `-i`, -or +nil+ if none. +or `nil` if none. An alias of ARGF.inplace_mode. From cefd4a233f4c6504ff7b02ea9b3c11a4385b754b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:37:50 +0900 Subject: [PATCH 137/157] [DOC] Use backticks instead of `` except for nested markups --- doc/globals.md | 148 ++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/doc/globals.md b/doc/globals.md index 99429e293d..4f7dafea2c 100644 --- a/doc/globals.md +++ b/doc/globals.md @@ -14,70 +14,70 @@ require 'English' ### Exceptions -| Variable | English | Contains | -|-------------|--------------------------|----------------------------------------------------| -| $! | $ERROR_INFO | Exception object; set by Kernel#raise. | -| $@ | $ERROR_POSITION | Array of backtrace positions; set by Kernel#raise. | +| Variable | English | Contains | +|-------------|-------------------|----------------------------------------------------| +| `$!` | `$ERROR_INFO` | Exception object; set by Kernel#raise. | +| `$@` | `$ERROR_POSITION` | Array of backtrace positions; set by Kernel#raise. | ### Pattern Matching -| Variable | English | Contains | -|-------------|----------------------------|--------------------------------------------------| -| $~ | $LAST_MATCH_INFO | MatchData object; set by matcher method. | -| $& | $MATCH | Matched substring; set by matcher method. | -| $` | $PRE_MATCH | Substring left of match; set by matcher method. | -| $' | $POST_MATCH | Substring right of match; set by matcher method. | -| $+ | $LAST_PAREN_MATCH | Last group matched; set by matcher method. | -| $1 | | First group matched; set by matcher method. | -| $2 | | Second group matched; set by matcher method. | -| $n | | nth group matched; set by matcher method. | +| Variable | English | Contains | +|---------------|---------------------|--------------------------------------------------| +| `$~` | `$LAST_MATCH_INFO` | MatchData object; set by matcher method. | +| `$&` | `$MATCH` | Matched substring; set by matcher method. | +| `` $` `` | `$PRE_MATCH` | Substring left of match; set by matcher method. | +| `$'` | `$POST_MATCH` | Substring right of match; set by matcher method. | +| `$+` | `$LAST_PAREN_MATCH` | Last group matched; set by matcher method. | +| `$1` | | First group matched; set by matcher method. | +| `$2` | | Second group matched; set by matcher method. | +| $_n_ | | nth group matched; set by matcher method. | ### Separators -| Variable | English | Contains | -|----------------------|-----------------------------------|--------------------------------------------------| -| $/ | $INPUT_RECORD_SEPARATOR | Input record separator; initially newline. | -| $\\\\\\\\ | $OUTPUT_RECORD_SEPARATOR | Output record separator; initially nil. | +| Variable | English | Contains | +|----------|----------------------------|--------------------------------------------| +| `$/` | `$INPUT_RECORD_SEPARATOR` | Input record separator; initially newline. | +| `$\` | `$OUTPUT_RECORD_SEPARATOR` | Output record separator; initially `nil`. | ### Streams -| Variable | English | Contains | -|------------------|-------------------------------------------|-----------------------------------------------------------| -| $stdin | | Standard input stream; initially STDIN. | -| $stdout | | Standard input stream; initially STDIOUT. | -| $stderr | | Standard input stream; initially STDERR. | -| $< | $DEFAULT_INPUT | Default standard input; ARGF or $stdin. | -| $> | $DEFAULT_OUTPUT | Default standard output; initially $stdout. | -| $. | $INPUT_LINE_NUMBER, $NR | Input position of most recently read stream. | -| $_ | $LAST_READ_LINE | String from most recently read stream. | +| Variable | English | Contains | +|-----------|-----------------------------|-----------------------------------------------| +| `$stdin` | | Standard input stream; initially `STDIN`. | +| `$stdout` | | Standard input stream; initially `STDIOUT`. | +| `$stderr` | | Standard input stream; initially `STDERR`. | +| `$<` | `$DEFAULT_INPUT` | Default standard input; `ARGF` or `$stdin`. | +| `$>` | `$DEFAULT_OUTPUT` | Default standard output; initially `$stdout`. | +| `$.` | `$INPUT_LINE_NUMBER`, `$NR` | Input position of most recently read stream. | +| `$_` | `$LAST_READ_LINE` | String from most recently read stream. | ### Processes -| Variable | English | Contains | -|------------------------------------------------|-------------------------------------|--------------------------------------------------------| -| $0 | | Initially, the name of the executing program. | -| $* | $ARGV | Points to the ARGV array. | -| $$ | $PROCESS_ID, $PID | Process ID of the current process. | -| $? | $CHILD_STATUS | Process::Status of most recently exited child process. | -| $LOAD_PATH, $:, $-I | | Array of paths to be searched. | -| $LOADED_FEATURES, $" | | Array of paths to loaded files. | +| Variable | English | Contains | +|---------------------------|-----------------------|--------------------------------------------------------| +| `$0` | | Initially, the name of the executing program. | +| `$*` | `$ARGV` | Points to the `ARGV` array. | +| `$$` | `$PROCESS_ID`, `$PID` | Process ID of the current process. | +| `$?` | `$CHILD_STATUS` | Process::Status of most recently exited child process. | +| `$LOAD_PATH`, `$:`, `$-I` | | Array of paths to be searched. | +| `$LOADED_FEATURES`, `$"` | | Array of paths to loaded files. | ### Debugging -| Variable | English | Contains | -|--------------------|---------|----------------------------------------------------------------------| -| $FILENAME | | The value returned by method ARGF.filename. | -| $DEBUG | | Initially, whether option -d or --debug was given. | -| $VERBOSE | | Initially, whether option -V or -W was given. | +| Variable | English | Contains | +|-------------|---------|--------------------------------------------------------| +| `$FILENAME` | | The value returned by method ARGF.filename. | +| `$DEBUG` | | Initially, whether option `-d` or `--debug` was given. | +| `$VERBOSE` | | Initially, whether option `-V` or `-W` was given. | ### Other Variables -| Variable | English | Contains | -|--------------|---------|-------------------------------------------------------| -| $-a | | Whether option -a was given. | -| $-i | | Extension given with command-line option -i. | -| $-l | | Whether option -l was given. | -| $-p | | Whether option -p was given. | +| Variable | English | Contains | +|----------|---------|------------------------------------------------| +| `$-a` | | Whether option `-a` was given. | +| `$-i` | | Extension given with command-line option `-i`. | +| `$-l` | | Whether option `-l` was given. | +| `$-p` | | Whether option `-p` was given. | ## Exceptions @@ -165,7 +165,7 @@ English - `$LAST_PAREN_MATCH`. ### `$1`, `$2`, \Etc. (Matched Group) -For `$_n_` the _nth_ group of the match. +For $_n_ the nth group of the match. No \English. @@ -179,7 +179,7 @@ English - `$INPUT_RECORD_SEPARATOR`, `$RS`. Aliased as `$-0`. -### `$\\` (Output Record Separator) +### `$\` (Output Record Separator) An output record separator, initially `nil`. @@ -270,9 +270,9 @@ by Kernel#load and Kernel#require. Singleton method `$LOAD_PATH.resolve_feature_path(feature)` returns: -- `[:rb, _path_]`, where `path` is the path to the Ruby file - to be loaded for the given `feature`. -- `[:so, _path_]`, where `path` is the path to the shared object file +- [:rb, _path_], where `path` is the path to the Ruby file to be + loaded for the given `feature`. +- [:so, _path_], where `path` is the path to the shared object file to be loaded for the given `feature`. - `nil` if there is no such `feature` and `path`. @@ -373,35 +373,35 @@ Whether command-line option `-p` was given; read-only. ### Streams -| Constant | Contains | -|-----------------|-------------------------| -| STDIN | Standard input stream. | -| STDOUT | Standard output stream. | -| STDERR | Standard error stream. | +| Constant | Contains | +|----------|-------------------------| +| `STDIN` | Standard input stream. | +| `STDOUT` | Standard output stream. | +| `STDERR` | Standard error stream. | ### Environment -| Constant | Contains | -|------------------------------|--------------------------------------------------------------------------------------| -| ENV | Hash of current environment variable names and values. | -| ARGF | String concatenation of files given on the command line, or $stdin if none. | -| ARGV | Array of the given command-line arguments. | -| TOPLEVEL_BINDING | Binding of the top level scope. | -| RUBY_VERSION | String Ruby version. | -| RUBY_RELEASE_DATE | String Ruby release date. | -| RUBY_PLATFORM | String Ruby platform. | -| RUBY_PATCH_LEVEL | String Ruby patch level. | -| RUBY_REVISION | String Ruby revision. | -| RUBY_COPYRIGHT | String Ruby copyright. | -| RUBY_ENGINE | String Ruby engine. | -| RUBY_ENGINE_VERSION | String Ruby engine version. | -| RUBY_DESCRIPTION | String Ruby description. | +| Constant | Contains | +|-----------------------|-------------------------------------------------------------------------------| +| `ENV` | Hash of current environment variable names and values. | +| `ARGF` | String concatenation of files given on the command line, or `$stdin` if none. | +| `ARGV` | Array of the given command-line arguments. | +| `TOPLEVEL_BINDING` | Binding of the top level scope. | +| `RUBY_VERSION` | String Ruby version. | +| `RUBY_RELEASE_DATE` | String Ruby release date. | +| `RUBY_PLATFORM` | String Ruby platform. | +| `RUBY_PATCH_LEVEL` | String Ruby patch level. | +| `RUBY_REVISION` | String Ruby revision. | +| `RUBY_COPYRIGHT` | String Ruby copyright. | +| `RUBY_ENGINE` | String Ruby engine. | +| `RUBY_ENGINE_VERSION` | String Ruby engine version. | +| `RUBY_DESCRIPTION` | String Ruby description. | ### Embedded Data -| Constant | Contains | -|---------------|---------------------------------------------------------------------------| -| DATA | File containing embedded data (lines following __END__, if any). | +| Constant | Contains | +|----------|--------------------------------------------------------------------| +| `DATA` | File containing embedded data (lines following `__END__`, if any). | ## Streams From c5c894c6e41dff1e3b053518922b60866c1ab214 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 12 Aug 2025 20:39:08 +0900 Subject: [PATCH 138/157] [DOC] Markup example code as ruby --- doc/globals.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/globals.md b/doc/globals.md index 4f7dafea2c..b9315f5ff9 100644 --- a/doc/globals.md +++ b/doc/globals.md @@ -6,7 +6,7 @@ For each of those, the \English synonym is given. To use the module: -``` +```ruby require 'English' ``` @@ -85,7 +85,7 @@ require 'English' Contains the Exception object set by Kernel#raise: -``` +```ruby begin raise RuntimeError.new('Boo!') rescue RuntimeError @@ -106,7 +106,7 @@ English - `$ERROR_INFO` Same as `$!.backtrace`; returns an array of backtrace positions: -``` +```ruby begin raise RuntimeError.new('Boo!') rescue RuntimeError @@ -191,7 +191,7 @@ English - `$OUTPUT_RECORD_SEPARATOR`, `$ORS`. The current standard input stream; initially: -``` +```ruby $stdin # => #> ``` @@ -199,7 +199,7 @@ $stdin # => #> The current standard output stream; initially: -``` +```ruby $stdout # => #> ``` @@ -207,7 +207,7 @@ $stdout # => #> The current standard error stream; initially: -``` +```ruby $stderr # => #> ``` @@ -278,7 +278,7 @@ returns: Examples: -``` +```ruby $LOAD_PATH.resolve_feature_path('timeout') # => [:rb, "/snap/ruby/317/lib/ruby/3.2.0/timeout.rb"] $LOAD_PATH.resolve_feature_path('date_core') @@ -293,7 +293,7 @@ Aliased as `$:` and `$-I`. Contains an array of the paths to the loaded files: -``` +```ruby $LOADED_FEATURES.take(10) # => ["enumerator.so", @@ -409,7 +409,7 @@ Whether command-line option `-p` was given; read-only. The standard input stream (the default value for `$stdin`): -``` +```ruby STDIN # => #> ``` @@ -417,7 +417,7 @@ STDIN # => #> The standard output stream (the default value for `$stdout`): -``` +```ruby STDOUT # => #> ``` @@ -425,7 +425,7 @@ STDOUT # => #> The standard error stream (the default value for `$stderr`): -``` +```ruby STDERR # => #> ``` @@ -435,7 +435,7 @@ STDERR # => #> A hash of the contains current environment variables names and values: -``` +```ruby ENV.take(5) # => [["COLORTERM", "truecolor"], @@ -459,7 +459,7 @@ An array of the given command-line arguments. The Binding of the top level scope: -``` +```ruby TOPLEVEL_BINDING # => # ``` @@ -467,7 +467,7 @@ TOPLEVEL_BINDING # => # The Ruby version: -``` +```ruby RUBY_VERSION # => "3.2.2" ``` @@ -475,7 +475,7 @@ RUBY_VERSION # => "3.2.2" The release date string: -``` +```ruby RUBY_RELEASE_DATE # => "2023-03-30" ``` @@ -483,7 +483,7 @@ RUBY_RELEASE_DATE # => "2023-03-30" The platform identifier: -``` +```ruby RUBY_PLATFORM # => "x86_64-linux" ``` @@ -491,7 +491,7 @@ RUBY_PLATFORM # => "x86_64-linux" The integer patch level for this Ruby: -``` +```ruby RUBY_PATCHLEVEL # => 53 ``` @@ -501,7 +501,7 @@ For a development build the patch level will be -1. The git commit hash for this Ruby: -``` +```ruby RUBY_REVISION # => "e51014f9c05aa65cbf203442d37fef7c12390015" ``` @@ -509,7 +509,7 @@ RUBY_REVISION # => "e51014f9c05aa65cbf203442d37fef7c12390015" The copyright string: -``` +```ruby RUBY_COPYRIGHT # => "ruby - Copyright (C) 1993-2023 Yukihiro Matsumoto" ``` @@ -518,7 +518,7 @@ RUBY_COPYRIGHT The name of the Ruby implementation: -``` +```ruby RUBY_ENGINE # => "ruby" ``` @@ -526,7 +526,7 @@ RUBY_ENGINE # => "ruby" The version of the Ruby implementation: -``` +```ruby RUBY_ENGINE_VERSION # => "3.2.2" ``` @@ -534,7 +534,7 @@ RUBY_ENGINE_VERSION # => "3.2.2" The description of the Ruby implementation: -``` +```ruby RUBY_DESCRIPTION # => "ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]" ``` @@ -545,7 +545,7 @@ RUBY_DESCRIPTION Defined if and only if the program has this line: -``` +```ruby __END__ ``` @@ -553,7 +553,7 @@ When defined, `DATA` is a File object containing the lines following the `__END__`, positioned at the first of those lines: -``` +```ruby p DATA DATA.each_line { |line| p line } __END__ From e26ab5dbf24c1a37a18721e29671e7efd4bea803 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 12 Aug 2025 09:54:50 -0700 Subject: [PATCH 139/157] ZJIT: Avoid splitting add_into/sub_into for x86_64 (#14177) * ZJIT: Avoid splitting add_into/sub_into * Require add_into/sub_into to take a Reg --- zjit/src/backend/lir.rs | 11 +++++----- zjit/src/backend/x86_64/mod.rs | 40 +++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 86bea62fcd..7d3afab7a9 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1889,9 +1889,9 @@ impl Assembler { out } - pub fn add_into(&mut self, left: Opnd, right: Opnd) -> Opnd { + pub fn add_into(&mut self, left: Opnd, right: Opnd) { + assert!(matches!(left, Opnd::Reg(_)), "Destination of add_into must be Opnd::Reg, but got: {left:?}"); self.push_insn(Insn::Add { left, right, out: left }); - left } #[must_use] @@ -2233,10 +2233,9 @@ impl Assembler { out } - pub fn sub_into(&mut self, left: Opnd, right: Opnd) -> Opnd { - let out = self.sub(left, right); - self.mov(left, out); - out + pub fn sub_into(&mut self, left: Opnd, right: Opnd) { + assert!(matches!(left, Opnd::Reg(_)), "Destination of sub_into must be Opnd::Reg, but got: {left:?}"); + self.push_insn(Insn::Sub { left, right, out: left }); } #[must_use] diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 8027c74b18..718f76837b 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -197,9 +197,15 @@ impl Assembler } }, // We have to load memory operands to avoid corrupting them - (Opnd::Mem(_) | Opnd::Reg(_), _) => { + (Opnd::Mem(_), _) => { *left = asm.load(*left); }, + // We have to load register operands to avoid corrupting them + (Opnd::Reg(_), _) => { + if *left != *out { + *left = asm.load(*left); + } + }, // The first operand can't be an immediate value (Opnd::UImm(_), _) => { *left = asm.load(*left); @@ -1164,7 +1170,21 @@ mod tests { asm.mov(CFP, sp); // should be merged to add asm.compile_with_num_regs(&mut cb, 1); - assert_eq!(format!("{:x}", cb), "4983c540"); + assert_disasm!(cb, "4983c540", {" + 0x0: add r13, 0x40 + "}); + } + + #[test] + fn test_add_into() { + let (mut asm, mut cb) = setup_asm(); + + asm.add_into(CFP, Opnd::UImm(0x40)); + asm.compile_with_num_regs(&mut cb, 1); + + assert_disasm!(cb, "4983c540", {" + 0x0: add r13, 0x40 + "}); } #[test] @@ -1175,7 +1195,21 @@ mod tests { asm.mov(CFP, sp); // should be merged to add asm.compile_with_num_regs(&mut cb, 1); - assert_eq!(format!("{:x}", cb), "4983ed40"); + assert_disasm!(cb, "4983ed40", {" + 0x0: sub r13, 0x40 + "}); + } + + #[test] + fn test_sub_into() { + let (mut asm, mut cb) = setup_asm(); + + asm.sub_into(CFP, Opnd::UImm(0x40)); + asm.compile_with_num_regs(&mut cb, 1); + + assert_disasm!(cb, "4983ed40", {" + 0x0: sub r13, 0x40 + "}); } #[test] From 998be6b3a4b078cfd09aa5b4a4a628c31c395451 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 12 Aug 2025 10:00:22 -0700 Subject: [PATCH 140/157] ZJIT: Add flag to disable the HIR optimizer (#14181) Also add a check in the bisect script that can assign blame to the HIR optimizer. --- tool/zjit_bisect.rb | 5 +++++ zjit/src/codegen.rs | 4 +++- zjit/src/options.rs | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index a4280a4ec2..47d6071d3b 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -118,6 +118,11 @@ Tempfile.create "jit_list" do |temp_file| jit_list = File.readlines(temp_file.path).map(&:strip).reject(&:empty?) end LOGGER.info("Starting with JIT list of #{jit_list.length} items.") +# Try running without the optimizer +_, stderr, exitcode = run_with_jit_list(RUBY, ["--zjit-disable-hir-opt", *OPTIONS], jit_list) +if exitcode == 0 + LOGGER.warn "*** Command suceeded with HIR optimizer disabled. HIR optimizer is probably at fault. ***" +end # Now narrow it down command = lambda do |items| _, _, exitcode = run_with_jit_list(RUBY, OPTIONS, items) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b1b43abbe6..9fc3b643b7 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1275,7 +1275,9 @@ fn compile_iseq(iseq: IseqPtr) -> Option { return None; } }; - function.optimize(); + if !get_option!(disable_hir_opt) { + function.optimize(); + } if let Err(err) = function.validate() { debug!("ZJIT: compile_iseq: {err:?}"); return None; diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 07584c9b99..94a6988a4f 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -35,6 +35,9 @@ pub struct Options { /// Enable debug logging pub debug: bool, + /// Turn off the HIR optimizer + pub disable_hir_opt: bool, + /// Dump initial High-level IR before optimization pub dump_hir_init: Option, @@ -66,6 +69,7 @@ impl Default for Options { num_profiles: 1, stats: false, debug: false, + disable_hir_opt: false, dump_hir_init: None, dump_hir_opt: None, dump_hir_graphviz: false, @@ -210,6 +214,8 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { ("debug", "") => options.debug = true, + ("disable-hir-opt", "") => options.disable_hir_opt = true, + // --zjit-dump-hir dumps the actual input to the codegen, which is currently the same as --zjit-dump-hir-opt. ("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot), ("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All), From 3e1e2bda49724c619cbb82935b2a11254e810b6c Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 12 Aug 2025 10:14:57 -0400 Subject: [PATCH 141/157] Make Enumerator::Chain write-barrier protected --- enumerator.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enumerator.c b/enumerator.c index 8d02c836e9..3855f79084 100644 --- a/enumerator.c +++ b/enumerator.c @@ -3073,7 +3073,7 @@ static const rb_data_type_t enum_chain_data_type = { enum_chain_memsize, enum_chain_mark_and_move, }, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static struct enum_chain * @@ -3123,7 +3123,7 @@ enum_chain_initialize(VALUE obj, VALUE enums) if (!ptr) rb_raise(rb_eArgError, "unallocated chain"); - ptr->enums = rb_ary_freeze(enums); + RB_OBJ_WRITE(obj, &ptr->enums, rb_ary_freeze(enums)); ptr->pos = -1; return obj; @@ -3157,7 +3157,7 @@ enum_chain_init_copy(VALUE obj, VALUE orig) if (!ptr1) rb_raise(rb_eArgError, "unallocated chain"); - ptr1->enums = ptr0->enums; + RB_OBJ_WRITE(obj, &ptr1->enums, ptr0->enums); ptr1->pos = ptr0->pos; return obj; From a9230e76ee19716c7d2e035be7bd1be9bdca2b59 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 12 Aug 2025 10:16:55 -0400 Subject: [PATCH 142/157] Make Enumerator::Product write-barrier protected --- enumerator.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enumerator.c b/enumerator.c index 3855f79084..0d54058215 100644 --- a/enumerator.c +++ b/enumerator.c @@ -3388,7 +3388,7 @@ static const rb_data_type_t enum_product_data_type = { enum_product_memsize, enum_product_mark_and_move, }, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static struct enum_product * @@ -3444,7 +3444,7 @@ enum_product_initialize(int argc, VALUE *argv, VALUE obj) if (!ptr) rb_raise(rb_eArgError, "unallocated product"); - ptr->enums = rb_ary_freeze(enums); + RB_OBJ_WRITE(obj, &ptr->enums, rb_ary_freeze(enums)); return obj; } @@ -3462,7 +3462,7 @@ enum_product_init_copy(VALUE obj, VALUE orig) if (!ptr1) rb_raise(rb_eArgError, "unallocated product"); - ptr1->enums = ptr0->enums; + RB_OBJ_WRITE(obj, &ptr1->enums, ptr0->enums); return obj; } From 2f95eb4e803f3107c157d8eccf6ba62f0487e9ad Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 18 Jul 2025 23:03:46 -0700 Subject: [PATCH 143/157] Rename rbimpl_atomic.*_set to _store "store" is the terminology the C11 standard uses, which allows us to use this as a fallback. This only changes the private rbimpl_ version of the method, RUBY_ATOMIC_SET et al. keep the same name. --- include/ruby/atomic.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/include/ruby/atomic.h b/include/ruby/atomic.h index b778276f62..2f5da85b45 100644 --- a/include/ruby/atomic.h +++ b/include/ruby/atomic.h @@ -160,7 +160,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `val`. */ -#define RUBY_ATOMIC_SET(var, val) rbimpl_atomic_set(&(var), (val)) +#define RUBY_ATOMIC_SET(var, val) rbimpl_atomic_store(&(var), (val)) /** * Identical to #RUBY_ATOMIC_FETCH_ADD, except for the return type. @@ -327,7 +327,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_PTR_SET(var, val) \ - rbimpl_atomic_ptr_set((volatile void **)&(var), (val)) + rbimpl_atomic_ptr_store((volatile void **)&(var), (val)) /** * Identical to #RUBY_ATOMIC_CAS, except it expects its arguments are `void*`. @@ -354,7 +354,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_VALUE_SET(var, val) \ - rbimpl_atomic_value_set(&(var), (val)) + rbimpl_atomic_value_store(&(var), (val)) /** * Identical to #RUBY_ATOMIC_EXCHANGE, except it expects its arguments are @@ -859,7 +859,7 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_set(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_store(volatile size_t *ptr, size_t val) { #if 0 @@ -904,13 +904,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_ptr_set(volatile void **ptr, void *val) +rbimpl_atomic_ptr_store(volatile void **ptr, void *val) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - rbimpl_atomic_size_set(sptr, sval); + rbimpl_atomic_size_store(sptr, sval); } RBIMPL_ATTR_ARTIFICIAL() @@ -931,13 +931,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_value_set(volatile VALUE *ptr, VALUE val) +rbimpl_atomic_value_store(volatile VALUE *ptr, VALUE val) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - rbimpl_atomic_size_set(sptr, sval); + rbimpl_atomic_size_store(sptr, sval); } RBIMPL_ATTR_ARTIFICIAL() @@ -959,7 +959,7 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_set(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_store(volatile rb_atomic_t *ptr, rb_atomic_t val) { #if 0 From 1d9f76096e8072bf4fa4e2eb22d02079e1cbe429 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 28 Jul 2025 16:13:37 -0700 Subject: [PATCH 144/157] Update rbimpl_atomic_* to all take a memory order --- include/ruby/atomic.h | 249 +++++++++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 101 deletions(-) diff --git a/include/ruby/atomic.h b/include/ruby/atomic.h index 2f5da85b45..89e1111e4c 100644 --- a/include/ruby/atomic.h +++ b/include/ruby/atomic.h @@ -84,6 +84,28 @@ typedef unsigned int rb_atomic_t; # error No atomic operation found #endif +/* Memory ordering constants */ +#if defined(HAVE_GCC_ATOMIC_BUILTINS) +# define RBIMPL_ATOMIC_RELAXED __ATOMIC_RELAXED +# define RBIMPL_ATOMIC_ACQUIRE __ATOMIC_ACQUIRE +# define RBIMPL_ATOMIC_RELEASE __ATOMIC_RELEASE +# define RBIMPL_ATOMIC_ACQ_REL __ATOMIC_ACQ_REL +# define RBIMPL_ATOMIC_SEQ_CST __ATOMIC_SEQ_CST +#elif defined(HAVE_STDATOMIC_H) +# define RBIMPL_ATOMIC_RELAXED memory_order_relaxed +# define RBIMPL_ATOMIC_ACQUIRE memory_order_acquire +# define RBIMPL_ATOMIC_RELEASE memory_order_release +# define RBIMPL_ATOMIC_ACQ_REL memory_order_acq_rel +# define RBIMPL_ATOMIC_SEQ_CST memory_order_seq_cst +#else +/* Dummy values for unsupported platforms */ +# define RBIMPL_ATOMIC_RELAXED 0 +# define RBIMPL_ATOMIC_ACQUIRE 1 +# define RBIMPL_ATOMIC_RELEASE 2 +# define RBIMPL_ATOMIC_ACQ_REL 3 +# define RBIMPL_ATOMIC_SEQ_CST 4 +#endif + /** * Atomically replaces the value pointed by `var` with the result of addition * of `val` to the old value of `var`. @@ -93,7 +115,7 @@ typedef unsigned int rb_atomic_t; * @return What was stored in `var` before the addition. * @post `var` holds `var + val`. */ -#define RUBY_ATOMIC_FETCH_ADD(var, val) rbimpl_atomic_fetch_add(&(var), (val)) +#define RUBY_ATOMIC_FETCH_ADD(var, val) rbimpl_atomic_fetch_add(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Atomically replaces the value pointed by `var` with the result of @@ -104,7 +126,7 @@ typedef unsigned int rb_atomic_t; * @return What was stored in `var` before the subtraction. * @post `var` holds `var - val`. */ -#define RUBY_ATOMIC_FETCH_SUB(var, val) rbimpl_atomic_fetch_sub(&(var), (val)) +#define RUBY_ATOMIC_FETCH_SUB(var, val) rbimpl_atomic_fetch_sub(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Atomically replaces the value pointed by `var` with the result of @@ -116,7 +138,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `var | val`. * @note For portability, this macro can return void. */ -#define RUBY_ATOMIC_OR(var, val) rbimpl_atomic_or(&(var), (val)) +#define RUBY_ATOMIC_OR(var, val) rbimpl_atomic_or(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Atomically replaces the value pointed by `var` with `val`. This is just an @@ -127,7 +149,7 @@ typedef unsigned int rb_atomic_t; * @return What was stored in `var` before the assignment. * @post `var` holds `val`. */ -#define RUBY_ATOMIC_EXCHANGE(var, val) rbimpl_atomic_exchange(&(var), (val)) +#define RUBY_ATOMIC_EXCHANGE(var, val) rbimpl_atomic_exchange(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Atomic compare-and-swap. This stores `val` to `var` if and only if the @@ -141,7 +163,7 @@ typedef unsigned int rb_atomic_t; * @retval otherwise Something else is at `var`; not updated. */ #define RUBY_ATOMIC_CAS(var, oldval, newval) \ - rbimpl_atomic_cas(&(var), (oldval), (newval)) + rbimpl_atomic_cas(&(var), (oldval), (newval), RBIMPL_ATOMIC_SEQ_CST, RBIMPL_ATOMIC_SEQ_CST) /** * Atomic load. This loads `var` with an atomic intrinsic and returns @@ -150,7 +172,7 @@ typedef unsigned int rb_atomic_t; * @param var A variable of ::rb_atomic_t * @return What was stored in `var`j */ -#define RUBY_ATOMIC_LOAD(var) rbimpl_atomic_load(&(var)) +#define RUBY_ATOMIC_LOAD(var) rbimpl_atomic_load(&(var), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_EXCHANGE, except for the return type. @@ -160,7 +182,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `val`. */ -#define RUBY_ATOMIC_SET(var, val) rbimpl_atomic_store(&(var), (val)) +#define RUBY_ATOMIC_SET(var, val) rbimpl_atomic_store(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_FETCH_ADD, except for the return type. @@ -170,7 +192,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var + val`. */ -#define RUBY_ATOMIC_ADD(var, val) rbimpl_atomic_add(&(var), (val)) +#define RUBY_ATOMIC_ADD(var, val) rbimpl_atomic_add(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_FETCH_SUB, except for the return type. @@ -180,7 +202,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var - val`. */ -#define RUBY_ATOMIC_SUB(var, val) rbimpl_atomic_sub(&(var), (val)) +#define RUBY_ATOMIC_SUB(var, val) rbimpl_atomic_sub(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Atomically increments the value pointed by `var`. @@ -189,7 +211,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var + 1`. */ -#define RUBY_ATOMIC_INC(var) rbimpl_atomic_inc(&(var)) +#define RUBY_ATOMIC_INC(var) rbimpl_atomic_inc(&(var), RBIMPL_ATOMIC_SEQ_CST) /** * Atomically decrements the value pointed by `var`. @@ -198,7 +220,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var - 1`. */ -#define RUBY_ATOMIC_DEC(var) rbimpl_atomic_dec(&(var)) +#define RUBY_ATOMIC_DEC(var) rbimpl_atomic_dec(&(var), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_FETCH_ADD, except it expects its arguments to be `size_t`. @@ -210,7 +232,7 @@ typedef unsigned int rb_atomic_t; * @return What was stored in `var` before the addition. * @post `var` holds `var + val`. */ -#define RUBY_ATOMIC_SIZE_FETCH_ADD(var, val) rbimpl_atomic_size_fetch_add(&(var), (val)) +#define RUBY_ATOMIC_SIZE_FETCH_ADD(var, val) rbimpl_atomic_size_fetch_add(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_INC, except it expects its argument is `size_t`. @@ -221,7 +243,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var + 1`. */ -#define RUBY_ATOMIC_SIZE_INC(var) rbimpl_atomic_size_inc(&(var)) +#define RUBY_ATOMIC_SIZE_INC(var) rbimpl_atomic_size_inc(&(var), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_DEC, except it expects its argument is `size_t`. @@ -232,7 +254,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var - 1`. */ -#define RUBY_ATOMIC_SIZE_DEC(var) rbimpl_atomic_size_dec(&(var)) +#define RUBY_ATOMIC_SIZE_DEC(var) rbimpl_atomic_size_dec(&(var), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_EXCHANGE, except it expects its arguments are @@ -246,7 +268,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_SIZE_EXCHANGE(var, val) \ - rbimpl_atomic_size_exchange(&(var), (val)) + rbimpl_atomic_size_exchange(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_CAS, except it expects its arguments are `size_t`. @@ -260,7 +282,7 @@ typedef unsigned int rb_atomic_t; * @retval otherwise Something else is at `var`; not updated. */ #define RUBY_ATOMIC_SIZE_CAS(var, oldval, newval) \ - rbimpl_atomic_size_cas(&(var), (oldval), (newval)) + rbimpl_atomic_size_cas(&(var), (oldval), (newval), RBIMPL_ATOMIC_SEQ_CST, RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_ADD, except it expects its arguments are `size_t`. @@ -272,7 +294,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var + val`. */ -#define RUBY_ATOMIC_SIZE_ADD(var, val) rbimpl_atomic_size_add(&(var), (val)) +#define RUBY_ATOMIC_SIZE_ADD(var, val) rbimpl_atomic_size_add(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_SUB, except it expects its arguments are `size_t`. @@ -284,7 +306,7 @@ typedef unsigned int rb_atomic_t; * @return void * @post `var` holds `var - val`. */ -#define RUBY_ATOMIC_SIZE_SUB(var, val) rbimpl_atomic_size_sub(&(var), (val)) +#define RUBY_ATOMIC_SIZE_SUB(var, val) rbimpl_atomic_size_sub(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_EXCHANGE, except it expects its arguments are @@ -303,7 +325,7 @@ typedef unsigned int rb_atomic_t; * some pointers, most notably function pointers. */ #define RUBY_ATOMIC_PTR_EXCHANGE(var, val) \ - RBIMPL_CAST(rbimpl_atomic_ptr_exchange((void **)&(var), (void *)val)) + RBIMPL_CAST(rbimpl_atomic_ptr_exchange((void **)&(var), (void *)val, RBIMPL_ATOMIC_SEQ_CST)) /** * Identical to #RUBY_ATOMIC_LOAD, except it expects its arguments are `void*`. @@ -314,7 +336,7 @@ typedef unsigned int rb_atomic_t; * @return The value of `var` (without tearing) */ #define RUBY_ATOMIC_PTR_LOAD(var) \ - RBIMPL_CAST(rbimpl_atomic_ptr_load((void **)&var)) + RBIMPL_CAST(rbimpl_atomic_ptr_load((void **)&var, RBIMPL_ATOMIC_SEQ_CST)) /** * Identical to #RUBY_ATOMIC_SET, except it expects its arguments are @@ -327,7 +349,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_PTR_SET(var, val) \ - rbimpl_atomic_ptr_store((volatile void **)&(var), (val)) + rbimpl_atomic_ptr_store((volatile void **)&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_CAS, except it expects its arguments are `void*`. @@ -341,7 +363,7 @@ typedef unsigned int rb_atomic_t; * @retval otherwise Something else is at `var`; not updated. */ #define RUBY_ATOMIC_PTR_CAS(var, oldval, newval) \ - RBIMPL_CAST(rbimpl_atomic_ptr_cas((void **)&(var), (void *)(oldval), (void *)(newval))) + RBIMPL_CAST(rbimpl_atomic_ptr_cas((void **)&(var), (void *)(oldval), (void *)(newval), RBIMPL_ATOMIC_SEQ_CST, RBIMPL_ATOMIC_SEQ_CST)) /** * Identical to #RUBY_ATOMIC_SET, except it expects its arguments are @@ -354,7 +376,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_VALUE_SET(var, val) \ - rbimpl_atomic_value_store(&(var), (val)) + rbimpl_atomic_value_store(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_EXCHANGE, except it expects its arguments are @@ -368,7 +390,7 @@ typedef unsigned int rb_atomic_t; * @post `var` holds `val`. */ #define RUBY_ATOMIC_VALUE_EXCHANGE(var, val) \ - rbimpl_atomic_value_exchange(&(var), (val)) + rbimpl_atomic_value_exchange(&(var), (val), RBIMPL_ATOMIC_SEQ_CST) /** * Identical to #RUBY_ATOMIC_CAS, except it expects its arguments are ::VALUE. @@ -382,19 +404,20 @@ typedef unsigned int rb_atomic_t; * @retval otherwise Something else is at `var`; not updated. */ #define RUBY_ATOMIC_VALUE_CAS(var, oldval, newval) \ - rbimpl_atomic_value_cas(&(var), (oldval), (newval)) + rbimpl_atomic_value_cas(&(var), (oldval), (newval), RBIMPL_ATOMIC_SEQ_CST, RBIMPL_ATOMIC_SEQ_CST) /** @cond INTERNAL_MACRO */ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline rb_atomic_t -rbimpl_atomic_fetch_add(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_fetch_add(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_fetch_add(ptr, val, __ATOMIC_SEQ_CST); + return __atomic_fetch_add(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) return __sync_fetch_and_add(ptr, val); @@ -412,7 +435,7 @@ rbimpl_atomic_fetch_add(volatile rb_atomic_t *ptr, rb_atomic_t val) return atomic_add_int_nv(ptr, val) - val; #elif defined(HAVE_STDATOMIC_H) - return atomic_fetch_add((_Atomic volatile rb_atomic_t *)ptr, val); + return atomic_fetch_add_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -424,12 +447,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline size_t -rbimpl_atomic_size_fetch_add(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_fetch_add(volatile size_t *ptr, size_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_fetch_add(ptr, val, __ATOMIC_SEQ_CST); + return __atomic_fetch_add(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) return __sync_fetch_and_add(ptr, val); @@ -446,10 +470,10 @@ rbimpl_atomic_size_fetch_add(volatile size_t *ptr, size_t val) RBIMPL_STATIC_ASSERT(size_of_rb_atomic_t, sizeof *ptr == sizeof(rb_atomic_t)); volatile rb_atomic_t *const tmp = RBIMPL_CAST((volatile rb_atomic_t *)ptr); - rbimpl_atomic_fetch_add(tmp, val); + rbimpl_atomic_fetch_add(tmp, val, memory_order); #elif defined(HAVE_STDATOMIC_H) - return atomic_fetch_add((_Atomic volatile size_t *)ptr, val); + return atomic_fetch_add_explicit((_Atomic volatile size_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -460,8 +484,9 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_add(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_add(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) @@ -470,7 +495,7 @@ rbimpl_atomic_add(volatile rb_atomic_t *ptr, rb_atomic_t val) * return value is not used, then compiles it into single `LOCK ADD` * instruction. */ - __atomic_add_fetch(ptr, val, __ATOMIC_SEQ_CST); + __atomic_add_fetch(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) __sync_add_and_fetch(ptr, val); @@ -500,12 +525,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_add(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_add(volatile size_t *ptr, size_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_add_fetch(ptr, val, __ATOMIC_SEQ_CST); + __atomic_add_fetch(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) __sync_add_and_fetch(ptr, val); @@ -523,7 +549,7 @@ rbimpl_atomic_size_add(volatile size_t *ptr, size_t val) RBIMPL_STATIC_ASSERT(size_of_rb_atomic_t, sizeof *ptr == sizeof(rb_atomic_t)); volatile rb_atomic_t *const tmp = RBIMPL_CAST((volatile rb_atomic_t *)ptr); - rbimpl_atomic_add(tmp, val); + rbimpl_atomic_add(tmp, val, memory_order); #elif defined(HAVE_STDATOMIC_H) *(_Atomic volatile size_t *)ptr += val; @@ -537,12 +563,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_inc(volatile rb_atomic_t *ptr) +rbimpl_atomic_inc(volatile rb_atomic_t *ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) || defined(HAVE_GCC_SYNC_BUILTINS) - rbimpl_atomic_add(ptr, 1); + rbimpl_atomic_add(ptr, 1, memory_order); #elif defined(_WIN32) InterlockedIncrement(ptr); @@ -551,7 +578,7 @@ rbimpl_atomic_inc(volatile rb_atomic_t *ptr) atomic_inc_uint(ptr); #elif defined(HAVE_STDATOMIC_H) - rbimpl_atomic_add(ptr, 1); + rbimpl_atomic_add(ptr, 1, memory_order); #else # error Unsupported platform. @@ -562,12 +589,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_inc(volatile size_t *ptr) +rbimpl_atomic_size_inc(volatile size_t *ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) || defined(HAVE_GCC_SYNC_BUILTINS) - rbimpl_atomic_size_add(ptr, 1); + rbimpl_atomic_size_add(ptr, 1, memory_order); #elif defined(_WIN64) InterlockedIncrement64(ptr); @@ -578,10 +606,10 @@ rbimpl_atomic_size_inc(volatile size_t *ptr) #elif defined(_WIN32) || (defined(__sun) && defined(HAVE_ATOMIC_H)) RBIMPL_STATIC_ASSERT(size_of_size_t, sizeof *ptr == sizeof(rb_atomic_t)); - rbimpl_atomic_size_add(ptr, 1); + rbimpl_atomic_size_add(ptr, 1, memory_order); #elif defined(HAVE_STDATOMIC_H) - rbimpl_atomic_size_add(ptr, 1); + rbimpl_atomic_size_add(ptr, 1, memory_order); #else # error Unsupported platform. @@ -592,12 +620,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline rb_atomic_t -rbimpl_atomic_fetch_sub(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_fetch_sub(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_fetch_sub(ptr, val, __ATOMIC_SEQ_CST); + return __atomic_fetch_sub(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) return __sync_fetch_and_sub(ptr, val); @@ -613,7 +642,7 @@ rbimpl_atomic_fetch_sub(volatile rb_atomic_t *ptr, rb_atomic_t val) return atomic_add_int_nv(ptr, neg * val) + val; #elif defined(HAVE_STDATOMIC_H) - return atomic_fetch_sub((_Atomic volatile rb_atomic_t *)ptr, val); + return atomic_fetch_sub_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -624,12 +653,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_sub(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_sub(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_sub_fetch(ptr, val, __ATOMIC_SEQ_CST); + __atomic_sub_fetch(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) __sync_sub_and_fetch(ptr, val); @@ -654,12 +684,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_sub(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_sub(volatile size_t *ptr, size_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_sub_fetch(ptr, val, __ATOMIC_SEQ_CST); + __atomic_sub_fetch(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) __sync_sub_and_fetch(ptr, val); @@ -677,7 +708,7 @@ rbimpl_atomic_size_sub(volatile size_t *ptr, size_t val) RBIMPL_STATIC_ASSERT(size_of_rb_atomic_t, sizeof *ptr == sizeof(rb_atomic_t)); volatile rb_atomic_t *const tmp = RBIMPL_CAST((volatile rb_atomic_t *)ptr); - rbimpl_atomic_sub(tmp, val); + rbimpl_atomic_sub(tmp, val, memory_order); #elif defined(HAVE_STDATOMIC_H) *(_Atomic volatile size_t *)ptr -= val; @@ -691,12 +722,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_dec(volatile rb_atomic_t *ptr) +rbimpl_atomic_dec(volatile rb_atomic_t *ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) || defined(HAVE_GCC_SYNC_BUILTINS) - rbimpl_atomic_sub(ptr, 1); + rbimpl_atomic_sub(ptr, 1, memory_order); #elif defined(_WIN32) InterlockedDecrement(ptr); @@ -705,7 +737,7 @@ rbimpl_atomic_dec(volatile rb_atomic_t *ptr) atomic_dec_uint(ptr); #elif defined(HAVE_STDATOMIC_H) - rbimpl_atomic_sub(ptr, 1); + rbimpl_atomic_sub(ptr, 1, memory_order); #else # error Unsupported platform. @@ -716,12 +748,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_dec(volatile size_t *ptr) +rbimpl_atomic_size_dec(volatile size_t *ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) || defined(HAVE_GCC_SYNC_BUILTINS) - rbimpl_atomic_size_sub(ptr, 1); + rbimpl_atomic_size_sub(ptr, 1, memory_order); #elif defined(_WIN64) InterlockedDecrement64(ptr); @@ -732,10 +765,10 @@ rbimpl_atomic_size_dec(volatile size_t *ptr) #elif defined(_WIN32) || (defined(__sun) && defined(HAVE_ATOMIC_H)) RBIMPL_STATIC_ASSERT(size_of_size_t, sizeof *ptr == sizeof(rb_atomic_t)); - rbimpl_atomic_size_sub(ptr, 1); + rbimpl_atomic_size_sub(ptr, 1, memory_order); #elif defined(HAVE_STDATOMIC_H) - rbimpl_atomic_size_sub(ptr, 1); + rbimpl_atomic_size_sub(ptr, 1, memory_order); #else # error Unsupported platform. @@ -746,12 +779,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_or(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_or(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_or_fetch(ptr, val, __ATOMIC_SEQ_CST); + __atomic_or_fetch(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) __sync_or_and_fetch(ptr, val); @@ -796,12 +830,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline rb_atomic_t -rbimpl_atomic_exchange(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_exchange(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_exchange_n(ptr, val, __ATOMIC_SEQ_CST); + return __atomic_exchange_n(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) return __sync_lock_test_and_set(ptr, val); @@ -813,7 +848,7 @@ rbimpl_atomic_exchange(volatile rb_atomic_t *ptr, rb_atomic_t val) return atomic_swap_uint(ptr, val); #elif defined(HAVE_STDATOMIC_H) - return atomic_exchange((_Atomic volatile rb_atomic_t *)ptr, val); + return atomic_exchange_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -824,12 +859,13 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline size_t -rbimpl_atomic_size_exchange(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_exchange(volatile size_t *ptr, size_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_exchange_n(ptr, val, __ATOMIC_SEQ_CST); + return __atomic_exchange_n(ptr, val, memory_order); #elif defined(HAVE_GCC_SYNC_BUILTINS) return __sync_lock_test_and_set(ptr, val); @@ -844,11 +880,11 @@ rbimpl_atomic_size_exchange(volatile size_t *ptr, size_t val) RBIMPL_STATIC_ASSERT(size_of_size_t, sizeof *ptr == sizeof(rb_atomic_t)); volatile rb_atomic_t *const tmp = RBIMPL_CAST((volatile rb_atomic_t *)ptr); - const rb_atomic_t ret = rbimpl_atomic_exchange(tmp, val); + const rb_atomic_t ret = rbimpl_atomic_exchange(tmp, val, memory_order); return RBIMPL_CAST((size_t)ret); #elif defined(HAVE_STDATOMIC_H) - return atomic_exchange((_Atomic volatile size_t *)ptr, val); + return atomic_exchange_explicit((_Atomic volatile size_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -859,15 +895,16 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_size_store(volatile size_t *ptr, size_t val) +rbimpl_atomic_size_store(volatile size_t *ptr, size_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_store_n(ptr, val, __ATOMIC_SEQ_CST); + __atomic_store_n(ptr, val, memory_order); #else - rbimpl_atomic_size_exchange(ptr, val); + rbimpl_atomic_size_exchange(ptr, val, memory_order); #endif } @@ -876,8 +913,9 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void * -rbimpl_atomic_ptr_exchange(void *volatile *ptr, const void *val) +rbimpl_atomic_ptr_exchange(void *volatile *ptr, const void *val, int memory_order) { + (void)memory_order; #if 0 #elif defined(InterlockedExchangePointer) @@ -894,7 +932,7 @@ rbimpl_atomic_ptr_exchange(void *volatile *ptr, const void *val) const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - const size_t sret = rbimpl_atomic_size_exchange(sptr, sval); + const size_t sret = rbimpl_atomic_size_exchange(sptr, sval, memory_order); return RBIMPL_CAST((void *)sret); #endif @@ -904,26 +942,26 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_ptr_store(volatile void **ptr, void *val) +rbimpl_atomic_ptr_store(volatile void **ptr, void *val, int memory_order) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - rbimpl_atomic_size_store(sptr, sval); + rbimpl_atomic_size_store(sptr, sval, memory_order); } RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline VALUE -rbimpl_atomic_value_exchange(volatile VALUE *ptr, VALUE val) +rbimpl_atomic_value_exchange(volatile VALUE *ptr, VALUE val, int memory_order) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - const size_t sret = rbimpl_atomic_size_exchange(sptr, sval); + const size_t sret = rbimpl_atomic_size_exchange(sptr, sval, memory_order); return RBIMPL_CAST((VALUE)sret); } @@ -931,27 +969,28 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_value_store(volatile VALUE *ptr, VALUE val) +rbimpl_atomic_value_store(volatile VALUE *ptr, VALUE val, int memory_order) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t sval = RBIMPL_CAST((size_t)val); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - rbimpl_atomic_size_store(sptr, sval); + rbimpl_atomic_size_store(sptr, sval, memory_order); } RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline rb_atomic_t -rbimpl_atomic_load(volatile rb_atomic_t *ptr) +rbimpl_atomic_load(volatile rb_atomic_t *ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_load_n(ptr, __ATOMIC_SEQ_CST); + return __atomic_load_n(ptr, memory_order); #else - return rbimpl_atomic_fetch_add(ptr, 0); + return rbimpl_atomic_fetch_add(ptr, 0, memory_order); #endif } @@ -959,16 +998,17 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void -rbimpl_atomic_store(volatile rb_atomic_t *ptr, rb_atomic_t val) +rbimpl_atomic_store(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - __atomic_store_n(ptr, val, __ATOMIC_SEQ_CST); + __atomic_store_n(ptr, val, memory_order); #else /* Maybe std::atomic::store can be faster? */ - rbimpl_atomic_exchange(ptr, val); + rbimpl_atomic_exchange(ptr, val, memory_order); #endif } @@ -977,13 +1017,15 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline rb_atomic_t -rbimpl_atomic_cas(volatile rb_atomic_t *ptr, rb_atomic_t oldval, rb_atomic_t newval) +rbimpl_atomic_cas(volatile rb_atomic_t *ptr, rb_atomic_t oldval, rb_atomic_t newval, int success_memorder, int failure_memorder) { + (void)success_memorder; + (void)failure_memorder; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) __atomic_compare_exchange_n( - ptr, &oldval, newval, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); + ptr, &oldval, newval, 0, success_memorder, failure_memorder); return oldval; #elif defined(HAVE_GCC_SYNC_BUILTINS) @@ -1003,8 +1045,8 @@ rbimpl_atomic_cas(volatile rb_atomic_t *ptr, rb_atomic_t oldval, rb_atomic_t new return atomic_cas_uint(ptr, oldval, newval); #elif defined(HAVE_STDATOMIC_H) - atomic_compare_exchange_strong( - (_Atomic volatile rb_atomic_t *)ptr, &oldval, newval); + atomic_compare_exchange_strong_explicit( + (_Atomic volatile rb_atomic_t *)ptr, &oldval, newval, success_memorder, failure_memorder); return oldval; #else @@ -1025,13 +1067,15 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline size_t -rbimpl_atomic_size_cas(volatile size_t *ptr, size_t oldval, size_t newval) +rbimpl_atomic_size_cas(volatile size_t *ptr, size_t oldval, size_t newval, int success_memorder, int failure_memorder) { + (void)success_memorder; + (void)failure_memorder; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) __atomic_compare_exchange_n( - ptr, &oldval, newval, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); + ptr, &oldval, newval, 0, success_memorder, failure_memorder); return oldval; #elif defined(HAVE_GCC_SYNC_BUILTINS) @@ -1047,11 +1091,11 @@ rbimpl_atomic_size_cas(volatile size_t *ptr, size_t oldval, size_t newval) RBIMPL_STATIC_ASSERT(size_of_size_t, sizeof *ptr == sizeof(rb_atomic_t)); volatile rb_atomic_t *tmp = RBIMPL_CAST((volatile rb_atomic_t *)ptr); - return rbimpl_atomic_cas(tmp, oldval, newval); + return rbimpl_atomic_cas(tmp, oldval, newval, success_memorder, failure_memorder); #elif defined(HAVE_STDATOMIC_H) - atomic_compare_exchange_strong( - (_Atomic volatile size_t *)ptr, &oldval, newval); + atomic_compare_exchange_strong_explicit( + (_Atomic volatile size_t *)ptr, &oldval, newval, success_memorder, failure_memorder); return oldval; #else @@ -1063,8 +1107,10 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void * -rbimpl_atomic_ptr_cas(void **ptr, const void *oldval, const void *newval) +rbimpl_atomic_ptr_cas(void **ptr, const void *oldval, const void *newval, int success_memorder, int failure_memorder) { + (void)success_memorder; + (void)failure_memorder; #if 0 #elif defined(InterlockedExchangePointer) @@ -1087,7 +1133,7 @@ rbimpl_atomic_ptr_cas(void **ptr, const void *oldval, const void *newval) const size_t snew = RBIMPL_CAST((size_t)newval); const size_t sold = RBIMPL_CAST((size_t)oldval); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - const size_t sret = rbimpl_atomic_size_cas(sptr, sold, snew); + const size_t sret = rbimpl_atomic_size_cas(sptr, sold, snew, success_memorder, failure_memorder); return RBIMPL_CAST((void *)sret); #endif @@ -1097,15 +1143,16 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline void * -rbimpl_atomic_ptr_load(void **ptr) +rbimpl_atomic_ptr_load(void **ptr, int memory_order) { + (void)memory_order; #if 0 #elif defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_load_n(ptr, __ATOMIC_SEQ_CST); + return __atomic_load_n(ptr, memory_order); #else void *val = *ptr; - return rbimpl_atomic_ptr_cas(ptr, val, val); + return rbimpl_atomic_ptr_cas(ptr, val, val, memory_order, memory_order); #endif } @@ -1113,14 +1160,14 @@ RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) static inline VALUE -rbimpl_atomic_value_cas(volatile VALUE *ptr, VALUE oldval, VALUE newval) +rbimpl_atomic_value_cas(volatile VALUE *ptr, VALUE oldval, VALUE newval, int success_memorder, int failure_memorder) { RBIMPL_STATIC_ASSERT(sizeof_value, sizeof *ptr == sizeof(size_t)); const size_t snew = RBIMPL_CAST((size_t)newval); const size_t sold = RBIMPL_CAST((size_t)oldval); volatile size_t *const sptr = RBIMPL_CAST((volatile size_t *)ptr); - const size_t sret = rbimpl_atomic_size_cas(sptr, sold, snew); + const size_t sret = rbimpl_atomic_size_cas(sptr, sold, snew, success_memorder, failure_memorder); return RBIMPL_CAST((VALUE)sret); } /** @endcond */ From 77d29ef73cba81e7c18ab9bb95e0756b9f173f38 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 28 Jul 2025 23:22:34 -0700 Subject: [PATCH 145/157] Convert ATOMIC_LOAD_RELAXED to use new rbimpl_* --- ruby_atomic.h | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/ruby_atomic.h b/ruby_atomic.h index 1ccabcbdf6..2923275636 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -27,16 +27,7 @@ #define ATOMIC_VALUE_CAS(var, oldval, val) RUBY_ATOMIC_VALUE_CAS(var, oldval, val) #define ATOMIC_VALUE_EXCHANGE(var, val) RUBY_ATOMIC_VALUE_EXCHANGE(var, val) -static inline rb_atomic_t -rbimpl_atomic_load_relaxed(volatile rb_atomic_t *ptr) -{ -#if defined(HAVE_GCC_ATOMIC_BUILTINS) - return __atomic_load_n(ptr, __ATOMIC_RELAXED); -#else - return *ptr; -#endif -} -#define ATOMIC_LOAD_RELAXED(var) rbimpl_atomic_load_relaxed(&(var)) +#define ATOMIC_LOAD_RELAXED(var) rbimpl_atomic_load(&(var), RBIMPL_ATOMIC_RELAXED) typedef RBIMPL_ALIGNAS(8) uint64_t rbimpl_atomic_uint64_t; From cb360b0b4b2e53f2335f77f477df54337fc4d87e Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 28 Jul 2025 23:24:11 -0700 Subject: [PATCH 146/157] Implement rbimpl_atomic_value_load This only adds the rbimpl_ version to include/ruby/atomic.h so that it is not a new public interface. We were already using RUBY_ATOMIC_VALUE_LOAD in a few locations. This will allow us to use other memory orders internally when desired. --- include/ruby/atomic.h | 9 +++++++++ ruby_atomic.h | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/ruby/atomic.h b/include/ruby/atomic.h index 89e1111e4c..650891ab9c 100644 --- a/include/ruby/atomic.h +++ b/include/ruby/atomic.h @@ -1156,6 +1156,15 @@ rbimpl_atomic_ptr_load(void **ptr, int memory_order) #endif } +RBIMPL_ATTR_ARTIFICIAL() +RBIMPL_ATTR_NOALIAS() +RBIMPL_ATTR_NONNULL((1)) +static inline VALUE +rbimpl_atomic_value_load(volatile VALUE *ptr, int memory_order) +{ + return RBIMPL_CAST((VALUE)rbimpl_atomic_ptr_load((void **)ptr, memory_order)); +} + RBIMPL_ATTR_ARTIFICIAL() RBIMPL_ATTR_NOALIAS() RBIMPL_ATTR_NONNULL((1)) diff --git a/ruby_atomic.h b/ruby_atomic.h index 2923275636..ad53356f06 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -3,7 +3,7 @@ #include "ruby/atomic.h" -#define RUBY_ATOMIC_VALUE_LOAD(x) (VALUE)(RUBY_ATOMIC_PTR_LOAD(x)) +#define RUBY_ATOMIC_VALUE_LOAD(x) rbimpl_atomic_value_load(&(x), RBIMPL_ATOMIC_SEQ_CST) /* shim macros only */ #define ATOMIC_ADD(var, val) RUBY_ATOMIC_ADD(var, val) From 4cf05ea77a82368f77fc50c193934b3b1a027b03 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 29 Jul 2025 00:14:54 -0700 Subject: [PATCH 147/157] Replace stdatomic ops with explicit mem order My previous pass missed these atomic operations using operators. --- include/ruby/atomic.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/ruby/atomic.h b/include/ruby/atomic.h index 650891ab9c..c7043b0476 100644 --- a/include/ruby/atomic.h +++ b/include/ruby/atomic.h @@ -514,7 +514,7 @@ rbimpl_atomic_add(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) atomic_add_int(ptr, val); #elif defined(HAVE_STDATOMIC_H) - *(_Atomic volatile rb_atomic_t *)ptr += val; + atomic_fetch_add_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -552,7 +552,7 @@ rbimpl_atomic_size_add(volatile size_t *ptr, size_t val, int memory_order) rbimpl_atomic_add(tmp, val, memory_order); #elif defined(HAVE_STDATOMIC_H) - *(_Atomic volatile size_t *)ptr += val; + atomic_fetch_add_explicit((_Atomic volatile size_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -673,7 +673,7 @@ rbimpl_atomic_sub(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) atomic_add_int(ptr, neg * val); #elif defined(HAVE_STDATOMIC_H) - *(_Atomic volatile rb_atomic_t *)ptr -= val; + atomic_fetch_sub_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -711,7 +711,7 @@ rbimpl_atomic_size_sub(volatile size_t *ptr, size_t val, int memory_order) rbimpl_atomic_sub(tmp, val, memory_order); #elif defined(HAVE_STDATOMIC_H) - *(_Atomic volatile size_t *)ptr -= val; + atomic_fetch_sub_explicit((_Atomic volatile size_t *)ptr, val, memory_order); #else # error Unsupported platform. @@ -810,7 +810,7 @@ rbimpl_atomic_or(volatile rb_atomic_t *ptr, rb_atomic_t val, int memory_order) atomic_or_uint(ptr, val); #elif !defined(_WIN32) && defined(HAVE_STDATOMIC_H) - *(_Atomic volatile rb_atomic_t *)ptr |= val; + atomic_fetch_or_explicit((_Atomic volatile rb_atomic_t *)ptr, val, memory_order); #else # error Unsupported platform. From 507b1e4bde074bdda3083df6b4c2190a385f84bf Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 13 Aug 2025 02:36:02 +0900 Subject: [PATCH 148/157] [ruby/openssl] pkey: skip tests using invalid keys in the FIPS mode In OpenSSL's master branch, importing/loading a key in the FIPS mode automatically performs a pair-wise consistency check. This breaks tests for OpenSSL::PKey::EC#check_key and DH#params_ok? as they use deliberately invalid keys. These methods would not be useful in the FIPS mode anyway. Fixes https://github.com/ruby/openssl/issues/926 https://github.com/ruby/openssl/commit/25ad8f4bdb --- test/openssl/test_pkey_dh.rb | 2 ++ test/openssl/test_pkey_ec.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/test/openssl/test_pkey_dh.rb b/test/openssl/test_pkey_dh.rb index c82f642c01..f0c42866ea 100644 --- a/test/openssl/test_pkey_dh.rb +++ b/test/openssl/test_pkey_dh.rb @@ -103,6 +103,8 @@ class OpenSSL::TestPKeyDH < OpenSSL::PKeyTestCase end if !openssl?(3, 0, 0) def test_params_ok? + omit_on_fips + # Skip the tests in old OpenSSL version 1.1.1c or early versions before # applying the following commits in OpenSSL 1.1.1d to make `DH_check` # function pass the RFC 7919 FFDHE group texts. diff --git a/test/openssl/test_pkey_ec.rb b/test/openssl/test_pkey_ec.rb index 1953b4c2da..e569397c0a 100644 --- a/test/openssl/test_pkey_ec.rb +++ b/test/openssl/test_pkey_ec.rb @@ -72,6 +72,8 @@ class OpenSSL::TestEC < OpenSSL::PKeyTestCase end def test_check_key + omit_on_fips + key0 = Fixtures.pkey("p256") assert_equal(true, key0.check_key) assert_equal(true, key0.private?) From 85c52079aa35a1d2e063a5b40eebe91701c8cb9e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 7 Aug 2025 13:30:53 +0200 Subject: [PATCH 149/157] set.c: Store `set_table->bins` at the end of `set_table->entries` This saves one pointer in `struct set_table`, which would allow `Set` objects to still fit in 80B TypedData slots even if RTypedData goes from 32B to 40B large. The existing set benchmark seem to show this doesn't have a very significant impact. Smaller sets are a bit faster, larger sets a bit slower. It seem consistent over multiple runs, but it's unclear how much of that is just error margin. ``` compare-ruby: ruby 3.5.0dev (2025-08-12T02:14:57Z master 428937a536) +YJIT +PRISM [arm64-darwin24] built-ruby: ruby 3.5.0dev (2025-08-12T07:22:26Z set-entries-bounds da30024fdc) +YJIT +PRISM [arm64-darwin24] warming up........ | |compare-ruby|built-ruby| |:------------------------|-----------:|---------:| |new_0 | 15.459M| 15.823M| | | -| 1.02x| |new_10 | 3.484M| 3.574M| | | -| 1.03x| |new_100 | 546.992k| 564.679k| | | -| 1.03x| |new_1000 | 49.391k| 48.169k| | | 1.03x| -| |aref_0 | 18.643M| 19.350M| | | -| 1.04x| |aref_10 | 5.941M| 6.006M| | | -| 1.01x| |aref_100 | 822.197k| 814.219k| | | 1.01x| -| |aref_1000 | 83.230k| 79.411k| | | 1.05x| -| ``` --- internal/set_table.h | 9 ++-- set.c | 1 - st.c | 96 +++++++++++++++++++++++++------------------ test/ruby/test_set.rb | 12 ++++++ 4 files changed, 73 insertions(+), 45 deletions(-) diff --git a/internal/set_table.h b/internal/set_table.h index 3cb9c64349..6242e979c6 100644 --- a/internal/set_table.h +++ b/internal/set_table.h @@ -15,13 +15,16 @@ struct set_table { const struct st_hash_type *type; /* Number of entries currently in the table. */ st_index_t num_entries; - /* Array of bins used for access by keys. */ - st_index_t *bins; + /* Start and bound index of entries in array entries. entries_starts and entries_bound are in interval [0,allocated_entries]. */ st_index_t entries_start, entries_bound; - /* Array of size 2^entry_power. */ + + /** + * Array of size 2^entry_power. + * Followed by st_index_t *bins, Array of bins used for access by keys. + */ set_table_entry *entries; }; diff --git a/set.c b/set.c index c589fb4523..60c099bf49 100644 --- a/set.c +++ b/set.c @@ -139,7 +139,6 @@ set_mark(void *ptr) static void set_free_embedded(struct set_object *sobj) { - free((&sobj->table)->bins); free((&sobj->table)->entries); } diff --git a/st.c b/st.c index 195a16b8ad..ef9ffbec5e 100644 --- a/st.c +++ b/st.c @@ -2395,18 +2395,44 @@ set_get_allocated_entries(const set_table *tab) return ((st_index_t) 1)<entry_power; } +static inline size_t +set_allocated_entries_size(const set_table *tab) +{ + return set_get_allocated_entries(tab) * sizeof(set_table_entry); +} + +static inline bool +set_has_bins(const set_table *tab) +{ + return tab->entry_power > MAX_POWER2_FOR_TABLES_WITHOUT_BINS; +} + /* Return size of the allocated bins of table TAB. */ static inline st_index_t set_bins_size(const set_table *tab) { - return features[tab->entry_power].bins_words * sizeof (st_index_t); + if (set_has_bins(tab)) { + return features[tab->entry_power].bins_words * sizeof (st_index_t); + } + + return 0; +} + +static inline st_index_t * +set_bins_ptr(const set_table *tab) +{ + if (set_has_bins(tab)) { + return (st_index_t *)(((char *)tab->entries) + set_allocated_entries_size(tab)); + } + + return NULL; } /* Mark all bins of table TAB as empty. */ static void set_initialize_bins(set_table *tab) { - memset(tab->bins, 0, set_bins_size(tab)); + memset(set_bins_ptr(tab), 0, set_bins_size(tab)); } /* Make table TAB empty. */ @@ -2415,7 +2441,7 @@ set_make_tab_empty(set_table *tab) { tab->num_entries = 0; tab->entries_start = tab->entries_bound = 0; - if (tab->bins != NULL) + if (set_bins_ptr(tab) != NULL) set_initialize_bins(tab); } @@ -2443,13 +2469,13 @@ set_init_existing_table_with_size(set_table *tab, const struct st_hash_type *typ tab->entry_power = n; tab->bin_power = features[n].bin_power; tab->size_ind = features[n].size_ind; - if (n <= MAX_POWER2_FOR_TABLES_WITHOUT_BINS) - tab->bins = NULL; - else { - tab->bins = (st_index_t *) malloc(set_bins_size(tab)); + + size_t memsize = 0; + if (set_has_bins(tab)) { + memsize += set_bins_size(tab); } - tab->entries = (set_table_entry *) malloc(set_get_allocated_entries(tab) - * sizeof(set_table_entry)); + memsize += set_get_allocated_entries(tab) * sizeof(set_table_entry); + tab->entries = (set_table_entry *)malloc(memsize); set_make_tab_empty(tab); tab->rebuilds_num = 0; return tab; @@ -2499,7 +2525,6 @@ set_table_clear(set_table *tab) void set_free_table(set_table *tab) { - free(tab->bins); free(tab->entries); free(tab); } @@ -2509,7 +2534,7 @@ size_t set_memsize(const set_table *tab) { return(sizeof(set_table) - + (tab->bins == NULL ? 0 : set_bins_size(tab)) + + (tab->entry_power <= MAX_POWER2_FOR_TABLES_WITHOUT_BINS ? 0 : set_bins_size(tab)) + set_get_allocated_entries(tab) * sizeof(set_table_entry)); } @@ -2542,7 +2567,7 @@ set_rebuild_table(set_table *tab) || tab->num_entries < (1 << MINIMAL_POWER2)) { /* Compaction: */ tab->num_entries = 0; - if (tab->bins != NULL) + if (set_has_bins(tab)) set_initialize_bins(tab); set_rebuild_table_with(tab, tab); } @@ -2572,7 +2597,7 @@ set_rebuild_table_with(set_table *const new_tab, set_table *const tab) new_entries = new_tab->entries; ni = 0; - bins = new_tab->bins; + bins = set_bins_ptr(new_tab); size_ind = set_get_size_ind(new_tab); st_index_t bound = tab->entries_bound; set_table_entry *entries = tab->entries; @@ -2602,8 +2627,6 @@ set_rebuild_move_table(set_table *const new_tab, set_table *const tab) tab->entry_power = new_tab->entry_power; tab->bin_power = new_tab->bin_power; tab->size_ind = new_tab->size_ind; - free(tab->bins); - tab->bins = new_tab->bins; free(tab->entries); tab->entries = new_tab->entries; free(new_tab); @@ -2688,7 +2711,7 @@ set_find_table_entry_ind(set_table *tab, st_hash_t hash_value, st_data_t key) perturb = hash_value; #endif for (;;) { - bin = get_bin(tab->bins, set_get_size_ind(tab), ind); + bin = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), ind); if (! EMPTY_OR_DELETED_BIN_P(bin)) { DO_PTR_EQUAL_CHECK(tab, &entries[bin - ENTRY_BASE], hash_value, key, eq_p, rebuilt_p); if (EXPECT(rebuilt_p, 0)) @@ -2732,7 +2755,7 @@ set_find_table_bin_ind(set_table *tab, st_hash_t hash_value, st_data_t key) perturb = hash_value; #endif for (;;) { - bin = get_bin(tab->bins, set_get_size_ind(tab), ind); + bin = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), ind); if (! EMPTY_OR_DELETED_BIN_P(bin)) { DO_PTR_EQUAL_CHECK(tab, &entries[bin - ENTRY_BASE], hash_value, key, eq_p, rebuilt_p); if (EXPECT(rebuilt_p, 0)) @@ -2773,7 +2796,7 @@ set_find_table_bin_ind_direct(set_table *tab, st_hash_t hash_value, st_data_t ke perturb = hash_value; #endif for (;;) { - bin = get_bin(tab->bins, set_get_size_ind(tab), ind); + bin = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), ind); if (EMPTY_OR_DELETED_BIN_P(bin)) return ind; #ifdef QUADRATIC_PROBE @@ -2787,7 +2810,7 @@ set_find_table_bin_ind_direct(set_table *tab, st_hash_t hash_value, st_data_t ke /* Mark I-th bin of table TAB as empty, in other words not corresponding to any entry. */ -#define MARK_SET_BIN_EMPTY(tab, i) (set_bin((tab)->bins, set_get_size_ind(tab), i, EMPTY_BIN)) +#define MARK_SET_BIN_EMPTY(tab, i) (set_bin(set_bins_ptr(tab), set_get_size_ind(tab), i, EMPTY_BIN)) /* Return index of table TAB bin for HASH_VALUE and KEY through BIN_IND and the pointed value as the function result. Reserve the @@ -2823,7 +2846,7 @@ set_find_table_bin_ptr_and_reserve(set_table *tab, st_hash_t *hash_value, firset_deleted_bin_ind = UNDEFINED_BIN_IND; entries = tab->entries; for (;;) { - entry_index = get_bin(tab->bins, set_get_size_ind(tab), ind); + entry_index = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), ind); if (EMPTY_BIN_P(entry_index)) { tab->num_entries++; entry_index = UNDEFINED_ENTRY_IND; @@ -2863,7 +2886,7 @@ set_table_lookup(set_table *tab, st_data_t key) st_hash_t hash = set_do_hash(key, tab); retry: - if (tab->bins == NULL) { + if (!set_has_bins(tab)) { bin = set_find_entry(tab, hash, key); if (EXPECT(bin == REBUILT_TABLE_ENTRY_IND, 0)) goto retry; @@ -2907,7 +2930,7 @@ set_insert(set_table *tab, st_data_t key) hash_value = set_do_hash(key, tab); retry: set_rebuild_table_if_necessary(tab); - if (tab->bins == NULL) { + if (!set_has_bins(tab)) { bin = set_find_entry(tab, hash_value, key); if (EXPECT(bin == REBUILT_TABLE_ENTRY_IND, 0)) goto retry; @@ -2930,7 +2953,7 @@ set_insert(set_table *tab, st_data_t key) entry->hash = hash_value; entry->key = key; if (bin_ind != UNDEFINED_BIN_IND) - set_bin(tab->bins, set_get_size_ind(tab), bin_ind, ind + ENTRY_BASE); + set_bin(set_bins_ptr(tab), set_get_size_ind(tab), bin_ind, ind + ENTRY_BASE); return 0; } return 1; @@ -2941,18 +2964,9 @@ static set_table * set_replace(set_table *new_tab, set_table *old_tab) { *new_tab = *old_tab; - if (old_tab->bins == NULL) - new_tab->bins = NULL; - else { - new_tab->bins = (st_index_t *) malloc(set_bins_size(old_tab)); - } - new_tab->entries = (set_table_entry *) malloc(set_get_allocated_entries(old_tab) - * sizeof(set_table_entry)); - MEMCPY(new_tab->entries, old_tab->entries, set_table_entry, - set_get_allocated_entries(old_tab)); - if (old_tab->bins != NULL) - MEMCPY(new_tab->bins, old_tab->bins, char, set_bins_size(old_tab)); - + size_t memsize = set_allocated_entries_size(old_tab) + set_bins_size(old_tab); + new_tab->entries = (set_table_entry *)malloc(memsize); + MEMCPY(new_tab->entries, old_tab->entries, char, memsize); return new_tab; } @@ -2991,7 +3005,7 @@ set_update_range_for_deleted(set_table *tab, st_index_t n) corresponding to deleted entries. */ #define MARK_SET_BIN_DELETED(tab, i) \ do { \ - set_bin((tab)->bins, set_get_size_ind(tab), i, DELETED_BIN); \ + set_bin(set_bins_ptr(tab), set_get_size_ind(tab), i, DELETED_BIN); \ } while (0) /* Delete entry with KEY from table TAB, and return non-zero. If @@ -3006,7 +3020,7 @@ set_table_delete(set_table *tab, st_data_t *key) hash = set_do_hash(*key, tab); retry: - if (tab->bins == NULL) { + if (!set_has_bins(tab)) { bin = set_find_entry(tab, hash, *key); if (EXPECT(bin == REBUILT_TABLE_ENTRY_IND, 0)) goto retry; @@ -3021,7 +3035,7 @@ set_table_delete(set_table *tab, st_data_t *key) if (bin_ind == UNDEFINED_BIN_IND) { return 0; } - bin = get_bin(tab->bins, set_get_size_ind(tab), bin_ind) - ENTRY_BASE; + bin = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), bin_ind) - ENTRY_BASE; MARK_SET_BIN_DELETED(tab, bin_ind); } entry = &tab->entries[bin]; @@ -3052,7 +3066,7 @@ set_general_foreach(set_table *tab, set_foreach_check_callback_func *func, st_index_t i, rebuilds_num; st_hash_t hash; st_data_t key; - int error_p, packed_p = tab->bins == NULL; + int error_p, packed_p = !set_has_bins(tab); entries = tab->entries; /* The bound can change inside the loop even without rebuilding @@ -3074,7 +3088,7 @@ set_general_foreach(set_table *tab, set_foreach_check_callback_func *func, if (rebuilds_num != tab->rebuilds_num) { retry: entries = tab->entries; - packed_p = tab->bins == NULL; + packed_p = !set_has_bins(tab); if (packed_p) { i = set_find_entry(tab, hash, key); if (EXPECT(i == REBUILT_TABLE_ENTRY_IND, 0)) @@ -3122,7 +3136,7 @@ set_general_foreach(set_table *tab, set_foreach_check_callback_func *func, goto again; if (bin_ind == UNDEFINED_BIN_IND) break; - bin = get_bin(tab->bins, set_get_size_ind(tab), bin_ind) - ENTRY_BASE; + bin = get_bin(set_bins_ptr(tab), set_get_size_ind(tab), bin_ind) - ENTRY_BASE; MARK_SET_BIN_DELETED(tab, bin_ind); } curr_entry_ptr = &entries[bin]; diff --git a/test/ruby/test_set.rb b/test/ruby/test_set.rb index 934a470c1e..af5f65bea0 100644 --- a/test/ruby/test_set.rb +++ b/test/ruby/test_set.rb @@ -924,6 +924,18 @@ class TC_Set < Test::Unit::TestCase end end; end + + def test_larger_sets + set = Set.new + 10_000.times do |i| + set << i + end + set = set.dup + + 10_000.times do |i| + assert_includes set, i + end + end end class TC_Enumerable < Test::Unit::TestCase From 360be94d0492f766b08cc39e33f5e248f49a89b7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 6 Aug 2025 19:47:08 +0200 Subject: [PATCH 150/157] RTypedData: keep direct reference to IMEMO/fields Similar to f3206cc79bec2fd852e81ec56de59f0a67ab32b7 but for TypedData. It's quite common for TypedData objects to have a mix of reference in their struct and some ivars. Since we do happen to have 8B free in the RtypedData struct, we could use it to keep a direct reference to the IMEMO/fields saving having to synchronize the VM and lookup the `gen_fields_tbl` on every ivar access. For old school Data classes however, we don't have free space, but this API is soft-deprecated and no longer very common. --- gc.c | 27 +++++++++++++++++-------- include/ruby/internal/abi.h | 2 +- include/ruby/internal/core/rdata.h | 12 +++++------ include/ruby/internal/core/rtypeddata.h | 3 +++ variable.c | 27 ++++++++++++++++++++++--- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/gc.c b/gc.c index 7663e82f41..5cfef6ff2c 100644 --- a/gc.c +++ b/gc.c @@ -1047,7 +1047,7 @@ rb_data_object_wrap(VALUE klass, void *datap, RUBY_DATA_FUNC dmark, RUBY_DATA_FU { RUBY_ASSERT_ALWAYS(dfree != (RUBY_DATA_FUNC)1); if (klass) rb_data_object_check(klass); - return newobj_of(GET_RACTOR(), klass, T_DATA, (VALUE)dmark, (VALUE)datap, (VALUE)dfree, !dmark, sizeof(struct RTypedData)); + return newobj_of(GET_RACTOR(), klass, T_DATA, (VALUE)dmark, (VALUE)dfree, (VALUE)datap, !dmark, sizeof(struct RTypedData)); } VALUE @@ -1064,7 +1064,7 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - return newobj_of(GET_RACTOR(), klass, T_DATA, ((VALUE)type) | IS_TYPED_DATA | typed_flag, (VALUE)datap, 0, wb_protected, size); + return newobj_of(GET_RACTOR(), klass, T_DATA, 0, ((VALUE)type) | IS_TYPED_DATA | typed_flag, (VALUE)datap, wb_protected, size); } VALUE @@ -3173,10 +3173,15 @@ rb_gc_mark_children(void *objspace, VALUE obj) break; case T_DATA: { - void *const ptr = RTYPEDDATA_P(obj) ? RTYPEDDATA_GET_DATA(obj) : DATA_PTR(obj); + bool typed_data = RTYPEDDATA_P(obj); + void *const ptr = typed_data ? RTYPEDDATA_GET_DATA(obj) : DATA_PTR(obj); + + if (typed_data) { + gc_mark_internal(RTYPEDDATA(obj)->fields_obj); + } if (ptr) { - if (RTYPEDDATA_P(obj) && gc_declarative_marking_p(RTYPEDDATA_TYPE(obj))) { + if (typed_data && gc_declarative_marking_p(RTYPEDDATA_TYPE(obj))) { size_t *offset_list = TYPED_DATA_REFS_OFFSET_LIST(obj); for (size_t offset = *offset_list; offset != RUBY_REF_END; offset = *offset_list++) { @@ -3184,7 +3189,7 @@ rb_gc_mark_children(void *objspace, VALUE obj) } } else { - RUBY_DATA_FUNC mark_func = RTYPEDDATA_P(obj) ? + RUBY_DATA_FUNC mark_func = typed_data ? RTYPEDDATA_TYPE(obj)->function.dmark : RDATA(obj)->dmark; if (mark_func) (*mark_func)(ptr); @@ -4121,9 +4126,15 @@ rb_gc_update_object_references(void *objspace, VALUE obj) case T_DATA: /* Call the compaction callback, if it exists */ { - void *const ptr = RTYPEDDATA_P(obj) ? RTYPEDDATA_GET_DATA(obj) : DATA_PTR(obj); + bool typed_data = RTYPEDDATA_P(obj); + void *const ptr = typed_data ? RTYPEDDATA_GET_DATA(obj) : DATA_PTR(obj); + + if (typed_data) { + UPDATE_IF_MOVED(objspace, RTYPEDDATA(obj)->fields_obj); + } + if (ptr) { - if (RTYPEDDATA_P(obj) && gc_declarative_marking_p(RTYPEDDATA_TYPE(obj))) { + if (typed_data && gc_declarative_marking_p(RTYPEDDATA_TYPE(obj))) { size_t *offset_list = TYPED_DATA_REFS_OFFSET_LIST(obj); for (size_t offset = *offset_list; offset != RUBY_REF_END; offset = *offset_list++) { @@ -4131,7 +4142,7 @@ rb_gc_update_object_references(void *objspace, VALUE obj) *ref = gc_location_internal(objspace, *ref); } } - else if (RTYPEDDATA_P(obj)) { + else if (typed_data) { RUBY_DATA_FUNC compact_func = RTYPEDDATA_TYPE(obj)->function.dcompact; if (compact_func) (*compact_func)(ptr); } diff --git a/include/ruby/internal/abi.h b/include/ruby/internal/abi.h index 0c99d93bf9..7ceb8c40b7 100644 --- a/include/ruby/internal/abi.h +++ b/include/ruby/internal/abi.h @@ -24,7 +24,7 @@ * In released versions of Ruby, this number is not defined since teeny * versions of Ruby should guarantee ABI compatibility. */ -#define RUBY_ABI_VERSION 2 +#define RUBY_ABI_VERSION 3 /* Windows does not support weak symbols so ruby_abi_version will not exist * in the shared library. */ diff --git a/include/ruby/internal/core/rdata.h b/include/ruby/internal/core/rdata.h index cab412af72..bebb2a8822 100644 --- a/include/ruby/internal/core/rdata.h +++ b/include/ruby/internal/core/rdata.h @@ -133,12 +133,6 @@ struct RData { */ RUBY_DATA_FUNC dmark; - /** Pointer to the actual C level struct that you want to wrap. - * This is in between dmark and dfree to allow DATA_PTR to continue - * to work for both RData and non-embedded RTypedData. - */ - void *data; - /** * This function is called when the object is no longer used. You need to * do whatever necessary to avoid memory leaks. @@ -147,6 +141,12 @@ struct RData { * impossible at that moment (that is why GC runs). */ RUBY_DATA_FUNC dfree; + + /** Pointer to the actual C level struct that you want to wrap. + * This is in between dmark and dfree to allow DATA_PTR to continue + * to work for both RData and non-embedded RTypedData. + */ + void *data; }; RBIMPL_SYMBOL_EXPORT_BEGIN() diff --git a/include/ruby/internal/core/rtypeddata.h b/include/ruby/internal/core/rtypeddata.h index edf482267a..539a2f86d2 100644 --- a/include/ruby/internal/core/rtypeddata.h +++ b/include/ruby/internal/core/rtypeddata.h @@ -355,6 +355,9 @@ struct RTypedData { /** The part that all ruby objects have in common. */ struct RBasic basic; + /** Direct reference to the slots that holds instance variables, if any **/ + VALUE fields_obj; + /** * This is a `const rb_data_type_t *const` value, with the low bits set: * diff --git a/variable.c b/variable.c index e0a85b8f48..7e5875519d 100644 --- a/variable.c +++ b/variable.c @@ -1231,13 +1231,20 @@ rb_obj_fields(VALUE obj, ID field_name) VALUE fields_obj = 0; if (rb_shape_obj_has_fields(obj)) { switch (BUILTIN_TYPE(obj)) { + case T_DATA: + if (LIKELY(RTYPEDDATA_P(obj))) { + fields_obj = RTYPEDDATA(obj)->fields_obj; + break; + } + goto generic_fields; case T_STRUCT: if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { fields_obj = RSTRUCT_FIELDS_OBJ(obj); break; } - // fall through + goto generic_fields; default: + generic_fields: RB_VM_LOCKING() { if (!st_lookup(generic_fields_tbl_, (st_data_t)obj, (st_data_t *)&fields_obj)) { rb_bug("Object is missing entry in generic_fields_tbl"); @@ -1254,13 +1261,20 @@ rb_free_generic_ivar(VALUE obj) if (rb_obj_exivar_p(obj)) { st_data_t key = (st_data_t)obj, value; switch (BUILTIN_TYPE(obj)) { + case T_DATA: + if (LIKELY(RTYPEDDATA_P(obj))) { + RB_OBJ_WRITE(obj, &RTYPEDDATA(obj)->fields_obj, 0); + break; + } + goto generic_fields; case T_STRUCT: if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { RSTRUCT_SET_FIELDS_OBJ(obj, 0); break; } - // fall through + goto generic_fields; default: + generic_fields: RB_VM_LOCKING() { st_delete(generic_fields_tbl_no_ractor_check(), &key, &value); } @@ -1279,13 +1293,20 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie if (fields_obj != original_fields_obj) { switch (BUILTIN_TYPE(obj)) { + case T_DATA: + if (LIKELY(RTYPEDDATA_P(obj))) { + RB_OBJ_WRITE(obj, &RTYPEDDATA(obj)->fields_obj, fields_obj); + break; + } + goto generic_fields; case T_STRUCT: if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { RSTRUCT_SET_FIELDS_OBJ(obj, fields_obj); break; } - // fall through + goto generic_fields; default: + generic_fields: RB_VM_LOCKING() { st_insert(generic_fields_tbl_, (st_data_t)obj, (st_data_t)fields_obj); } From 231407c251d82573f578caf569a934c0ebb344e5 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 12 Aug 2025 13:40:42 -0700 Subject: [PATCH 151/157] ZJIT: Avoid compiling failed ISEQs repeatedly (#14195) --- zjit/src/codegen.rs | 53 ++++++++++++++++++++++++++++++--------------- zjit/src/gc.rs | 16 ++++++++++---- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 9fc3b643b7..01ed0f0590 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -5,7 +5,7 @@ use std::ffi::{c_int, c_void}; use crate::asm::Label; use crate::backend::current::{Reg, ALLOC_REGS}; use crate::invariants::{track_bop_assumption, track_cme_assumption, track_single_ractor_assumption, track_stable_constant_names_assumption}; -use crate::gc::{append_gc_offsets, get_or_create_iseq_payload, get_or_create_iseq_payload_ptr}; +use crate::gc::{append_gc_offsets, get_or_create_iseq_payload, get_or_create_iseq_payload_ptr, IseqStatus}; use crate::state::ZJITState; use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::compile_time_ns}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; @@ -136,7 +136,7 @@ fn gen_iseq_entry_point_body(cb: &mut CodeBlock, iseq: IseqPtr) -> Option Option<(CodePtr, Vec<(Rc, IseqPtr)>)> { // Return an existing pointer if it's already compiled let payload = get_or_create_iseq_payload(iseq); - if let Some(start_ptr) = payload.start_ptr { - return Some((start_ptr, vec![])); + match payload.status { + IseqStatus::Compiled(start_ptr) => return Some((start_ptr, vec![])), + IseqStatus::CantCompile => return None, + IseqStatus::NotCompiled => {}, } // Convert ISEQ into High-level IR and optimize HIR let function = match compile_iseq(iseq) { Some(function) => function, - None => return None, + None => { + payload.status = IseqStatus::CantCompile; + return None; + } }; // Compile the High-level IR let result = gen_function(cb, iseq, &function); if let Some((start_ptr, gc_offsets, jit)) = result { - payload.start_ptr = Some(start_ptr); + payload.status = IseqStatus::Compiled(start_ptr); append_gc_offsets(iseq, &gc_offsets); Some((start_ptr, jit.branch_iseqs)) } else { + payload.status = IseqStatus::CantCompile; None } } @@ -1356,27 +1362,40 @@ macro_rules! c_callable { pub(crate) use c_callable; c_callable! { - /// Generated code calls this function with the SysV calling convention. - /// See [gen_function_stub]. + /// Generated code calls this function with the SysV calling convention. See [gen_function_stub]. + /// This function is expected to be called repeatedly when ZJIT fails to compile the stub. + /// We should be able to compile most (if not all) function stubs by side-exiting at unsupported + /// instructions, so this should be used primarily for cb.has_dropped_bytes() situations. fn function_stub_hit(iseq: IseqPtr, branch_ptr: *const c_void, ec: EcPtr, sp: *mut VALUE) -> *const u8 { with_vm_lock(src_loc!(), || { - // Get a pointer to compiled code or the side-exit trampoline - let cb = ZJITState::get_code_block(); - let code_ptr = with_time_stat(compile_time_ns, || function_stub_hit_body(cb, iseq, branch_ptr)); - let code_ptr = if let Some(code_ptr) = code_ptr { - code_ptr - } else { - // gen_push_frame() doesn't set PC and SP, so we need to set them for side-exit - // TODO: We could generate code that sets PC/SP. Note that we'd still need to handle OOM. + /// gen_push_frame() doesn't set PC and SP, so we need to set them before exit + fn set_pc_and_sp(iseq: IseqPtr, ec: EcPtr, sp: *mut VALUE) { let cfp = unsafe { get_ec_cfp(ec) }; let pc = unsafe { rb_iseq_pc_at_idx(iseq, 0) }; // TODO: handle opt_pc once supported unsafe { rb_set_cfp_pc(cfp, pc) }; unsafe { rb_set_cfp_sp(cfp, sp) }; + } + // If we already know we can't compile the ISEQ, fail early without cb.mark_all_executable(). + // TODO: Alan thinks the payload status part of this check can happen without the VM lock, since the whole + // code path can be made read-only. But you still need the check as is while holding the VM lock in any case. + let cb = ZJITState::get_code_block(); + let payload = get_or_create_iseq_payload(iseq); + if cb.has_dropped_bytes() || payload.status == IseqStatus::CantCompile { // Exit to the interpreter + set_pc_and_sp(iseq, ec, sp); + return ZJITState::get_stub_exit().raw_ptr(cb); + } + + // Otherwise, attempt to compile the ISEQ. We have to mark_all_executable() beyond this point. + let code_ptr = with_time_stat(compile_time_ns, || function_stub_hit_body(cb, iseq, branch_ptr)); + let code_ptr = if let Some(code_ptr) = code_ptr { + code_ptr + } else { + // Exit to the interpreter + set_pc_and_sp(iseq, ec, sp); ZJITState::get_stub_exit() }; - cb.mark_all_executable(); code_ptr.raw_ptr(cb) }) diff --git a/zjit/src/gc.rs b/zjit/src/gc.rs index ea1b0ed2ea..52a036d49e 100644 --- a/zjit/src/gc.rs +++ b/zjit/src/gc.rs @@ -7,12 +7,12 @@ use crate::stats::Counter::gc_time_ns; /// This is all the data ZJIT stores on an ISEQ. We mark objects in this struct on GC. #[derive(Debug)] pub struct IseqPayload { + /// Compilation status of the ISEQ. It has the JIT code address of the first block if Compiled. + pub status: IseqStatus, + /// Type information of YARV instruction operands pub profile: IseqProfile, - /// JIT code address of the first block - pub start_ptr: Option, - /// GC offsets of the JIT code. These are the addresses of objects that need to be marked. pub gc_offsets: Vec, } @@ -20,13 +20,21 @@ pub struct IseqPayload { impl IseqPayload { fn new(iseq_size: u32) -> Self { Self { + status: IseqStatus::NotCompiled, profile: IseqProfile::new(iseq_size), - start_ptr: None, gc_offsets: vec![], } } } +#[derive(Debug, PartialEq)] +pub enum IseqStatus { + /// CodePtr has the JIT code address of the first block + Compiled(CodePtr), + CantCompile, + NotCompiled, +} + /// Get a pointer to the payload object associated with an ISEQ. Create one if none exists. pub fn get_or_create_iseq_payload_ptr(iseq: IseqPtr) -> *mut IseqPayload { type VoidPtr = *mut c_void; From b934f989b9e22c8c46ee6a4accdbb78072d38442 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 12 Aug 2025 12:32:09 -0500 Subject: [PATCH 152/157] [DOC] Tweaks for GC.disable --- gc.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gc.rb b/gc.rb index ac04cb2e25..135c97666d 100644 --- a/gc.rb +++ b/gc.rb @@ -66,11 +66,13 @@ module GC # call-seq: # GC.disable -> true or false # - # Disables garbage collection, returning +true+ if garbage - # collection was already disabled. + # Disables garbage collection (but GC.start remains potent): + # returns whether garbage collection was already disabled. + # + # GC.enable + # GC.disable # => false + # GC.disable # => true # - # GC.disable #=> false - # GC.disable #=> true def self.disable Primitive.gc_disable end From 8b530e12826a4add7145c5da589929e5d752d94c Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 12 Aug 2025 12:37:00 -0500 Subject: [PATCH 153/157] [DOC] Tweaks for GC.enable --- gc.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gc.rb b/gc.rb index 135c97666d..2972343e2a 100644 --- a/gc.rb +++ b/gc.rb @@ -50,14 +50,14 @@ module GC end # call-seq: - # GC.enable -> true or false + # GC.enable -> true or false # - # Enables garbage collection, returning +true+ if garbage - # collection was previously disabled. + # Enables garbage collection; + # returns whether garbage collection was disabled: # - # GC.disable #=> false - # GC.enable #=> true - # GC.enable #=> false + # GC.disable + # GC.enable # => true + # GC.enable # => false # def self.enable Primitive.gc_enable From 31e8a9fced895845a7aac123080d325768177b19 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 12 Aug 2025 13:40:39 -0500 Subject: [PATCH 154/157] [DOC] Tweaks for GC.latest_gc_info --- gc.rb | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/gc.rb b/gc.rb index 2972343e2a..1d45b48b34 100644 --- a/gc.rb +++ b/gc.rb @@ -324,19 +324,47 @@ module GC end # call-seq: - # GC.latest_gc_info -> hash - # GC.latest_gc_info(hash) -> hash - # GC.latest_gc_info(key) -> value + # GC.latest_gc_info -> new_hash + # GC.latest_gc_info(key) -> value + # GC.latest_gc_info(hash) -> hash # - # Returns information about the most recent garbage collection. + # With no argument given, + # returns information about the most recent garbage collection: # - # If the argument +hash+ is given and is a Hash object, - # it is overwritten and returned. - # This is intended to avoid the probe effect. + # GC.latest_gc_info + # # => + # {major_by: :force, + # need_major_by: nil, + # gc_by: :method, + # have_finalizer: false, + # immediate_sweep: true, + # state: :none, + # weak_references_count: 0, + # retained_weak_references_count: 0} + # + # With symbol argument +key+ given, + # returns the value for that key: + # + # GC.latest_gc_info(:gc_by) # => :newobj + # + # With hash argument +hash+ given, + # returns that hash with GC information merged into its content; + # this form may be useful in minimizing {probe effects}[https://en.wikipedia.org/wiki/Probe_effect]: + # + # h = {foo: 0, bar: 1} + # GC.latest_gc_info(h) + # # => + # {foo: 0, + # bar: 1, + # major_by: nil, + # need_major_by: nil, + # gc_by: :newobj, + # have_finalizer: false, + # immediate_sweep: false, + # state: :sweeping, + # weak_references_count: 0, + # retained_weak_references_count: 0} # - # If the argument +key+ is given and is a Symbol object, - # it returns the value associated with the key. - # This is equivalent to GC.latest_gc_info[key]. def self.latest_gc_info hash_or_key = nil if hash_or_key == nil hash_or_key = {} From 1afc07e815051e2f73493f055f2130cb642ba12a Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Wed, 6 Aug 2025 14:30:03 -0400 Subject: [PATCH 155/157] Allow encodings to be autoloaded through transcoding functions Make sure VM lock is not held when calling `load_transcoder_entry`, as that causes deadlock inside ractors. `String#encode` now works inside ractors, among others. Atomic load the rb_encoding_list Without this, wbcheck would sometimes hit a missing write barrier. Co-authored-by: John Hawthorn Hold VM lock when iterating over global_enc_table.names This st_table can be inserted into at runtime when autoloading encodings. minor optimization when calling Encoding.list --- encoding.c | 101 +++++++++++++++++++++++------------- hash.c | 37 +++++++------ test/ruby/test_transcode.rb | 40 ++++++++++++++ transcode.c | 73 +++++++++++--------------- 4 files changed, 157 insertions(+), 94 deletions(-) diff --git a/encoding.c b/encoding.c index 7bca8d1b2b..78964476c0 100644 --- a/encoding.c +++ b/encoding.c @@ -29,6 +29,7 @@ #include "ruby/util.h" #include "ruby_assert.h" #include "vm_sync.h" +#include "ruby_atomic.h" #ifndef ENC_DEBUG #define ENC_DEBUG 0 @@ -144,10 +145,14 @@ enc_list_update(int index, rb_raw_encoding *encoding) { RUBY_ASSERT(index < ENCODING_LIST_CAPA); - VALUE list = rb_encoding_list; + VALUE list = RUBY_ATOMIC_VALUE_LOAD(rb_encoding_list); + if (list && NIL_P(rb_ary_entry(list, index))) { + VALUE new_list = rb_ary_dup(list); + RBASIC_CLEAR_CLASS(new_list); /* initialize encoding data */ - rb_ary_store(list, index, enc_new(encoding)); + rb_ary_store(new_list, index, enc_new(encoding)); + RUBY_ATOMIC_VALUE_SET(rb_encoding_list, new_list); } } @@ -157,7 +162,7 @@ enc_list_lookup(int idx) VALUE list, enc = Qnil; if (idx < ENCODING_LIST_CAPA) { - list = rb_encoding_list; + list = RUBY_ATOMIC_VALUE_LOAD(rb_encoding_list); RUBY_ASSERT(list); enc = rb_ary_entry(list, idx); } @@ -258,6 +263,7 @@ must_encindex(int index) int rb_to_encoding_index(VALUE enc) { + ASSERT_vm_unlocking(); // can load encoding, so must not hold VM lock int idx; const char *name; @@ -667,15 +673,15 @@ int rb_enc_alias(const char *alias, const char *orig) { int idx, r; + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + enc_check_addable(enc_table, alias); // can raise + } + + idx = rb_enc_find_index(orig); + if (idx < 0) return -1; GLOBAL_ENC_TABLE_LOCKING(enc_table) { - enc_check_addable(enc_table, alias); - if ((idx = rb_enc_find_index(orig)) < 0) { - r = -1; - } - else { - r = enc_alias(enc_table, alias, idx); - } + r = enc_alias(enc_table, alias, idx); } return r; @@ -742,6 +748,7 @@ int rb_require_internal_silent(VALUE fname); static int load_encoding(const char *name) { + ASSERT_vm_unlocking(); VALUE enclib = rb_sprintf("enc/%s.so", name); VALUE debug = ruby_debug; VALUE errinfo; @@ -757,7 +764,7 @@ load_encoding(const char *name) enclib = rb_fstring(enclib); ruby_debug = Qfalse; errinfo = rb_errinfo(); - loaded = rb_require_internal_silent(enclib); + loaded = rb_require_internal_silent(enclib); // must run without VM_LOCK ruby_debug = debug; rb_set_errinfo(errinfo); @@ -781,6 +788,7 @@ enc_autoload_body(rb_encoding *enc) { rb_encoding *base; int i = 0; + ASSERT_vm_unlocking(); GLOBAL_ENC_TABLE_LOCKING(enc_table) { base = enc_table->list[ENC_TO_ENCINDEX(enc)].base; @@ -792,30 +800,32 @@ enc_autoload_body(rb_encoding *enc) } } while (enc_table->list[i].enc != base && (++i, 1)); } + } - if (i != -1) { - if (base) { - bool do_register = true; - if (rb_enc_autoload_p(base)) { - if (rb_enc_autoload(base) < 0) { - do_register = false; - i = -1; - } + + if (i != -1) { + if (base) { + bool do_register = true; + if (rb_enc_autoload_p(base)) { + if (rb_enc_autoload(base) < 0) { + do_register = false; + i = -1; } + } - i = enc->ruby_encoding_index; - if (do_register) { + if (do_register) { + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + i = enc->ruby_encoding_index; enc_register_at(enc_table, i & ENC_INDEX_MASK, rb_enc_name(enc), base); ((rb_raw_encoding *)enc)->ruby_encoding_index = i; } + } - i &= ENC_INDEX_MASK; - } - else { - i = -2; - } + i &= ENC_INDEX_MASK; + } + else { + i = -2; } - } return i; @@ -824,6 +834,7 @@ enc_autoload_body(rb_encoding *enc) int rb_enc_autoload(rb_encoding *enc) { + ASSERT_vm_unlocking(); int i = enc_autoload_body(enc); if (i == -2) { i = load_encoding(rb_enc_name(enc)); @@ -844,6 +855,7 @@ int rb_enc_find_index(const char *name) { int i; + ASSERT_vm_unlocking(); // it needs to be unlocked so it can call `load_encoding` if necessary GLOBAL_ENC_TABLE_LOCKING(enc_table) { i = enc_registered(enc_table, name); } @@ -1019,7 +1031,6 @@ rb_enc_associate_index(VALUE obj, int idx) rb_encoding *enc; int oldidx, oldtermlen, termlen; -/* enc_check_capable(obj);*/ rb_check_frozen(obj); oldidx = rb_enc_get_index(obj); if (oldidx == idx) @@ -1355,7 +1366,10 @@ enc_names(VALUE self) args[0] = (VALUE)rb_to_encoding_index(self); args[1] = rb_ary_new2(0); - st_foreach(global_enc_table.names, enc_names_i, (st_data_t)args); + + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + st_foreach(enc_table->names, enc_names_i, (st_data_t)args); + } return args[1]; } @@ -1380,8 +1394,9 @@ enc_names(VALUE self) static VALUE enc_list(VALUE klass) { - VALUE ary = rb_ary_new2(0); - rb_ary_replace(ary, rb_encoding_list); + VALUE ary = rb_ary_new2(ENCODING_LIST_CAPA); + VALUE list = RUBY_ATOMIC_VALUE_LOAD(rb_encoding_list); + rb_ary_replace(ary, list); return ary; } @@ -1526,6 +1541,9 @@ int rb_locale_charmap_index(void); int rb_locale_encindex(void) { + // `rb_locale_charmap_index` can call `enc_find_index`, which can + // load an encoding. This needs to be done without VM lock held. + ASSERT_vm_unlocking(); int idx = rb_locale_charmap_index(); if (idx < 0) idx = ENCINDEX_UTF_8; @@ -1584,6 +1602,10 @@ enc_set_default_encoding(struct default_encoding *def, VALUE encoding, const cha /* Already set */ overridden = TRUE; + if (!NIL_P(encoding)) { + enc_check_encoding(encoding); // loads it if necessary. Needs to be done outside of VM lock. + } + GLOBAL_ENC_TABLE_LOCKING(enc_table) { if (NIL_P(encoding)) { def->index = -1; @@ -1854,8 +1876,11 @@ rb_enc_name_list_i(st_data_t name, st_data_t idx, st_data_t arg) static VALUE rb_enc_name_list(VALUE klass) { - VALUE ary = rb_ary_new2(global_enc_table.names->num_entries); - st_foreach(global_enc_table.names, rb_enc_name_list_i, (st_data_t)ary); + VALUE ary; + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + ary = rb_ary_new2(enc_table->names->num_entries); + st_foreach(enc_table->names, rb_enc_name_list_i, (st_data_t)ary); + } return ary; } @@ -1901,7 +1926,9 @@ rb_enc_aliases(VALUE klass) aliases[0] = rb_hash_new(); aliases[1] = rb_ary_new(); - st_foreach(global_enc_table.names, rb_enc_aliases_enc_i, (st_data_t)aliases); + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + st_foreach(enc_table->names, rb_enc_aliases_enc_i, (st_data_t)aliases); + } return aliases[0]; } @@ -1969,9 +1996,9 @@ Init_Encoding(void) struct enc_table *enc_table = &global_enc_table; + rb_gc_register_address(&rb_encoding_list); list = rb_encoding_list = rb_ary_new2(ENCODING_LIST_CAPA); RBASIC_CLEAR_CLASS(list); - rb_vm_register_global_object(list); for (i = 0; i < enc_table->count; ++i) { rb_ary_push(list, enc_new(enc_table->list[i].enc)); @@ -2003,5 +2030,7 @@ Init_encodings(void) void rb_enc_foreach_name(int (*func)(st_data_t name, st_data_t idx, st_data_t arg), st_data_t arg) { - st_foreach(global_enc_table.names, func, arg); + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + st_foreach(enc_table->names, func, arg); + } } diff --git a/hash.c b/hash.c index 7ce1b768e0..de9bc97ea6 100644 --- a/hash.c +++ b/hash.c @@ -5192,25 +5192,26 @@ env_enc_str_new(const char *ptr, long len, rb_encoding *enc) } static VALUE -env_str_new(const char *ptr, long len) +env_str_new(const char *ptr, long len, rb_encoding *enc) { - return env_enc_str_new(ptr, len, env_encoding()); + return env_enc_str_new(ptr, len, enc); } static VALUE -env_str_new2(const char *ptr) +env_str_new2(const char *ptr, rb_encoding *enc) { if (!ptr) return Qnil; - return env_str_new(ptr, strlen(ptr)); + return env_str_new(ptr, strlen(ptr), enc); } static VALUE getenv_with_lock(const char *name) { VALUE ret; + rb_encoding *enc = env_encoding(); ENV_LOCKING() { const char *val = getenv(name); - ret = env_str_new2(val); + ret = env_str_new2(val, enc); } return ret; } @@ -5773,13 +5774,14 @@ env_values(void) { VALUE ary = rb_ary_new(); + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); while (*env) { char *s = strchr(*env, '='); if (s) { - rb_ary_push(ary, env_str_new2(s+1)); + rb_ary_push(ary, env_str_new2(s+1, enc)); } env++; } @@ -5865,14 +5867,15 @@ env_each_pair(VALUE ehash) VALUE ary = rb_ary_new(); + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); while (*env) { char *s = strchr(*env, '='); if (s) { - rb_ary_push(ary, env_str_new(*env, s-*env)); - rb_ary_push(ary, env_str_new2(s+1)); + rb_ary_push(ary, env_str_new(*env, s-*env, enc)); + rb_ary_push(ary, env_str_new2(s+1, enc)); } env++; } @@ -6255,13 +6258,14 @@ env_to_a(VALUE _) { VALUE ary = rb_ary_new(); + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); while (*env) { char *s = strchr(*env, '='); if (s) { - rb_ary_push(ary, rb_assoc_new(env_str_new(*env, s-*env), - env_str_new2(s+1))); + rb_ary_push(ary, rb_assoc_new(env_str_new(*env, s-*env, enc), + env_str_new2(s+1, enc))); } env++; } @@ -6509,6 +6513,7 @@ env_key(VALUE dmy, VALUE value) StringValue(value); VALUE str = Qnil; + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); while (*env) { @@ -6516,7 +6521,7 @@ env_key(VALUE dmy, VALUE value) if (s++) { long len = strlen(s); if (RSTRING_LEN(value) == len && strncmp(s, RSTRING_PTR(value), len) == 0) { - str = env_str_new(*env, s-*env-1); + str = env_str_new(*env, s-*env-1, enc); break; } } @@ -6533,13 +6538,14 @@ env_to_hash(void) { VALUE hash = rb_hash_new(); + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); while (*env) { char *s = strchr(*env, '='); if (s) { - rb_hash_aset(hash, env_str_new(*env, s-*env), - env_str_new2(s+1)); + rb_hash_aset(hash, env_str_new(*env, s-*env, enc), + env_str_new2(s+1, enc)); } env++; } @@ -6684,14 +6690,15 @@ env_shift(VALUE _) VALUE result = Qnil; VALUE key = Qnil; + rb_encoding *enc = env_encoding(); ENV_LOCKING() { char **env = GET_ENVIRON(environ); if (*env) { const char *p = *env; char *s = strchr(p, '='); if (s) { - key = env_str_new(p, s-p); - VALUE val = env_str_new2(getenv(RSTRING_PTR(key))); + key = env_str_new(p, s-p, enc); + VALUE val = env_str_new2(getenv(RSTRING_PTR(key)), enc); result = rb_assoc_new(key, val); } } diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 63d37f4ba4..2b6f8234ce 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2320,6 +2320,46 @@ class TestTranscode < Test::Unit::TestCase assert_equal("A\nB\nC", s.encode(usascii, newline: :lf)) end + def test_ractor_lazy_load_encoding + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + rs = [] + autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze + 7.times do + rs << Ractor.new(autoload_encodings) do |encodings| + str = "\u0300" + encodings.each do |enc| + str.encode(enc) rescue Encoding::UndefinedConversionError + end + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert rs.empty? + end; + end + + def test_ractor_lazy_load_encoding_random + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + begin; + rs = [] + 100.times do + rs << Ractor.new do + "\u0300".encode(Encoding.list.sample) rescue Encoding::UndefinedConversionError + end + end + + while rs.any? + r, _obj = Ractor.select(*rs) + rs.delete(r) + end + assert rs.empty? + end; + end + private def assert_conversion_both_ways_utf8(utf8, raw, encoding) diff --git a/transcode.c b/transcode.c index d8cd90e56d..507bce78e1 100644 --- a/transcode.c +++ b/transcode.c @@ -340,7 +340,7 @@ transcode_search_path(const char *sname, const char *dname, bfs.queue_last_ptr = &q->next; bfs.queue = q; - bfs.visited = st_init_strcasetable(); + bfs.visited = st_init_strcasetable(); // due to base encodings, we need to do search in a loop st_add_direct(bfs.visited, (st_data_t)sname, (st_data_t)NULL); RB_VM_LOCKING() { @@ -351,14 +351,14 @@ transcode_search_path(const char *sname, const char *dname, bfs.queue_last_ptr = &bfs.queue; } - lookup_res = st_lookup(transcoder_table, (st_data_t)q->enc, &val); + lookup_res = st_lookup(transcoder_table, (st_data_t)q->enc, &val); // src => table2 if (!lookup_res) { xfree(q); continue; } table2 = (st_table *)val; - if (st_lookup(table2, (st_data_t)dname, &val)) { + if (st_lookup(table2, (st_data_t)dname, &val)) { // dest => econv st_add_direct(bfs.visited, (st_data_t)dname, (st_data_t)q->enc); xfree(q); found = true; @@ -411,8 +411,7 @@ int rb_require_internal_silent(VALUE fname); static const rb_transcoder * load_transcoder_entry(transcoder_entry_t *entry) { - // changes result of entry->transcoder depending on if it's required or not, so needs lock - ASSERT_vm_locking(); + ASSERT_vm_unlocking(); if (entry->transcoder) return entry->transcoder; @@ -427,7 +426,7 @@ load_transcoder_entry(transcoder_entry_t *entry) memcpy(path + sizeof(transcoder_lib_prefix) - 1, lib, len); rb_str_set_len(fn, total_len); OBJ_FREEZE(fn); - rb_require_internal_silent(fn); + rb_require_internal_silent(fn); // Sets entry->transcoder } if (entry->transcoder) @@ -981,7 +980,6 @@ rb_econv_open_by_transcoder_entries(int n, transcoder_entry_t **entries) { rb_econv_t *ec; int i, ret; - ASSERT_vm_locking(); for (i = 0; i < n; i++) { const rb_transcoder *tr; @@ -1026,10 +1024,8 @@ rb_econv_open0(const char *sname, const char *dname, int ecflags) transcoder_entry_t **entries = NULL; int num_trans; rb_econv_t *ec; - ASSERT_vm_locking(); - /* Just check if sname and dname are defined */ - /* (This check is needed?) */ + // loads encodings if not loaded already if (*sname) rb_enc_find_index(sname); if (*dname) rb_enc_find_index(dname); @@ -1117,15 +1113,13 @@ rb_econv_open(const char *sname, const char *dname, int ecflags) if (num_decorators == -1) return NULL; - RB_VM_LOCKING() { - ec = rb_econv_open0(sname, dname, ecflags & ECONV_ERROR_HANDLER_MASK); - if (ec) { - for (i = 0; i < num_decorators; i++) { - if (rb_econv_decorate_at_last(ec, decorators[i]) == -1) { - rb_econv_close(ec); - ec = NULL; - break; - } + ec = rb_econv_open0(sname, dname, ecflags & ECONV_ERROR_HANDLER_MASK); + if (ec) { + for (i = 0; i < num_decorators; i++) { + if (rb_econv_decorate_at_last(ec, decorators[i]) == -1) { + rb_econv_close(ec); + ec = NULL; + break; } } } @@ -1960,12 +1954,9 @@ rb_econv_add_converter(rb_econv_t *ec, const char *sname, const char *dname, int if (ec->started != 0) return -1; - RB_VM_LOCKING() { - entry = get_transcoder_entry(sname, dname); - if (entry) { - tr = load_transcoder_entry(entry); - } - + entry = get_transcoder_entry(sname, dname); + if (entry) { + tr = load_transcoder_entry(entry); } return tr ? rb_econv_add_transcoder_at(ec, tr, n) : -1; @@ -2681,21 +2672,19 @@ rb_econv_open_opts(const char *source_encoding, const char *destination_encoding replacement = rb_hash_aref(opthash, sym_replace); } - RB_VM_LOCKING() { - ec = rb_econv_open(source_encoding, destination_encoding, ecflags); - if (ec) { - if (!NIL_P(replacement)) { - int ret; - rb_encoding *enc = rb_enc_get(replacement); + ec = rb_econv_open(source_encoding, destination_encoding, ecflags); + if (ec) { + if (!NIL_P(replacement)) { + int ret; + rb_encoding *enc = rb_enc_get(replacement); - ret = rb_econv_set_replacement(ec, - (const unsigned char *)RSTRING_PTR(replacement), - RSTRING_LEN(replacement), - rb_enc_name(enc)); - if (ret == -1) { - rb_econv_close(ec); - ec = NULL; - } + ret = rb_econv_set_replacement(ec, + (const unsigned char *)RSTRING_PTR(replacement), + RSTRING_LEN(replacement), + rb_enc_name(enc)); + if (ret == -1) { + rb_econv_close(ec); + ec = NULL; } } } @@ -3132,10 +3121,8 @@ decorate_convpath(VALUE convpath, int ecflags) const char *dname = rb_enc_name(rb_to_encoding(RARRAY_AREF(pair, 1))); transcoder_entry_t *entry; const rb_transcoder *tr; - RB_VM_LOCKING() { - entry = get_transcoder_entry(sname, dname); - tr = load_transcoder_entry(entry); - } + entry = get_transcoder_entry(sname, dname); + tr = load_transcoder_entry(entry); if (!tr) return -1; if (!DECORATOR_P(tr->src_encoding, tr->dst_encoding) && From df7d9812cc504f3361792f3dd4843d1ffa3c5ead Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 13 Aug 2025 01:39:46 +0100 Subject: [PATCH 156/157] ZJIT: Prepare non-leaf calls for SetGlobal (#14197) When trace_var is used, setting a global variable can cause exceptions to be raised. We need to prepare for that. --- test/ruby/test_zjit.rb | 24 ++++++++++++++++++++++++ zjit/src/codegen.rs | 8 ++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 4dc0919b6b..98146b80db 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -61,6 +61,30 @@ class TestZJIT < Test::Unit::TestCase } end + def test_setglobal + assert_compiles '1', %q{ + def test + $a = 1 + $a + end + + test + }, insns: [:setglobal] + end + + def test_setglobal_with_trace_var_exception + assert_compiles '"rescued"', %q{ + def test + $a = 1 + rescue + "rescued" + end + + trace_var(:$a) { raise } + test + }, insns: [:setglobal] + end + def test_setlocal assert_compiles '3', %q{ def test(n) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 01ed0f0590..e7bd3285dd 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -367,7 +367,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::PatchPoint { invariant, state } => return gen_patch_point(jit, asm, invariant, &function.frame_state(*state)), Insn::CCall { cfun, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfun, opnds!(args))?, Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id), - Insn::SetGlobal { id, val, state: _ } => return Some(gen_setglobal(asm, *id, opnd!(val))), + Insn::SetGlobal { id, val, state } => return gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state)), Insn::GetGlobal { id, state: _ } => gen_getglobal(asm, *id), &Insn::GetLocal { ep_offset, level } => gen_getlocal_with_ep(asm, ep_offset, level)?, Insn::SetLocal { val, ep_offset, level } => return gen_setlocal_with_ep(asm, opnd!(val), *ep_offset, *level), @@ -592,8 +592,12 @@ fn gen_getglobal(asm: &mut Assembler, id: ID) -> Opnd { } /// Set global variables -fn gen_setglobal(asm: &mut Assembler, id: ID, val: Opnd) { +fn gen_setglobal(jit: &mut JITState, asm: &mut Assembler, id: ID, val: Opnd, state: &FrameState) -> Option<()> { + // When trace_var is used, setting a global variable can cause exceptions + gen_prepare_non_leaf_call(jit, asm, state)?; + asm_ccall!(asm, rb_gvar_set, id.0.into(), val); + Some(()) } /// Side-exit into the interpreter From 40d07f268e63aa2cdbaf3b31b227cecc5ba7e9e0 Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA Date: Thu, 17 Jul 2025 14:35:01 +0900 Subject: [PATCH 157/157] [DOC] Move Therad#join under Thread in NEWS-3.0.0.md --- doc/NEWS/NEWS-3.0.0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/NEWS/NEWS-3.0.0.md b/doc/NEWS/NEWS-3.0.0.md index 004fa4bf67..9fbaf504b4 100644 --- a/doc/NEWS/NEWS-3.0.0.md +++ b/doc/NEWS/NEWS-3.0.0.md @@ -367,11 +367,11 @@ Outstanding ones only. * Fiber.blocking? tells whether the current execution context is blocking. [[Feature #16786]] +* Thread + * Thread#join invokes the scheduler hooks `block`/`unblock` in a non-blocking execution context. [[Feature #16786]] -* Thread - * Thread.ignore_deadlock accessor has been added for disabling the default deadlock detection, allowing the use of signal handlers to break deadlock. [[Bug #13768]]