From d15f18d87ae5895fb77314f9baa52d22f07ebbfd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 18:03:52 +0900 Subject: [PATCH 01/14] [ruby/rubygems] Add Gem::Cooldown and the :cooldown: gemrc setting A cooldown period excludes gem versions published within the last N days from installation and update, as a mitigation against supply chain attacks through freshly published releases. This adds the configuration mechanism: the Gem::Cooldown judgment class, the :cooldown: gemrc setting, and the --cooldown DAYS option mixin. The --cooldown option takes precedence over gemrc, and 0 disables the cooldown. Versions with an unknown publish time are never excluded, so the cooldown fails open. https://github.com/ruby/rubygems/commit/ed07390c7e Co-Authored-By: Claude Fable 5 --- lib/rubygems/config_file.rb | 13 ++++- lib/rubygems/cooldown.rb | 68 +++++++++++++++++++++++++++ lib/rubygems/cooldown_option.rb | 21 +++++++++ test/rubygems/helper.rb | 4 ++ test/rubygems/test_gem_config_file.rb | 3 ++ test/rubygems/test_gem_cooldown.rb | 56 ++++++++++++++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 lib/rubygems/cooldown.rb create mode 100644 lib/rubygems/cooldown_option.rb create mode 100644 test/rubygems/test_gem_cooldown.rb diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index d5e9eb4e33aec6..3d63af60487b78 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -27,6 +27,7 @@ # # +:backtrace+:: See #backtrace # +:bulk_threshold+:: See #bulk_threshold +# +:cooldown+:: See #cooldown # +:verbose+:: See #verbose # +:update_sources+:: See #update_sources # +:concurrent_downloads+:: See #concurrent_downloads @@ -55,6 +56,7 @@ class Gem::ConfigFile DEFAULT_BACKTRACE = true DEFAULT_BULK_THRESHOLD = 1000 + DEFAULT_COOLDOWN = 0 DEFAULT_VERBOSITY = true DEFAULT_UPDATE_SOURCES = true DEFAULT_CONCURRENT_DOWNLOADS = 8 @@ -116,6 +118,13 @@ class Gem::ConfigFile attr_accessor :bulk_threshold + ## + # Number of days a newly published gem version must wait before it is + # considered for installation or update (the cooldown period). 0 + # disables the cooldown. + + attr_accessor :cooldown + ## # Verbose level of output: # * false -- No output @@ -211,6 +220,7 @@ def initialize(args) @backtrace = DEFAULT_BACKTRACE @bulk_threshold = DEFAULT_BULK_THRESHOLD + @cooldown = DEFAULT_COOLDOWN @verbose = DEFAULT_VERBOSITY @update_sources = DEFAULT_UPDATE_SOURCES @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS @@ -239,7 +249,7 @@ def initialize(args) @hash.transform_keys! do |k| # gemhome and gempath are not working with symbol keys - if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days + if %w[backtrace bulk_threshold cooldown verbose update_sources cert_expiration_length_days concurrent_downloads install_extension_in_lib ipv4_fallback_enabled global_gem_cache use_psych sources disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k) @@ -252,6 +262,7 @@ def initialize(args) # HACK: these override command-line args, which is bad @backtrace = @hash[:backtrace] if @hash.key? :backtrace @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold + @cooldown = @hash[:cooldown] if @hash.key? :cooldown @verbose = @hash[:verbose] if @hash.key? :verbose @update_sources = @hash[:update_sources] if @hash.key? :update_sources @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads diff --git a/lib/rubygems/cooldown.rb b/lib/rubygems/cooldown.rb new file mode 100644 index 00000000000000..b6474b52b82f3f --- /dev/null +++ b/lib/rubygems/cooldown.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative "user_interaction" + +## +# Applies a cooldown period to remote gem versions as a supply chain attack +# mitigation. When a cooldown of N days is configured, gem versions +# published within the last N days are not considered for installation or +# update. Versions whose publish time is unknown are never excluded, so +# sources that do not provide publish times keep working. +# +# The cooldown period comes from the --cooldown DAYS option when +# given, falling back to the :cooldown: setting in the gemrc file. +# A value of 0 disables the cooldown. + +class Gem::Cooldown + ## + # The cooldown period in days. + + attr_reader :days + + ## + # Creates a Cooldown from the command line +options+, preferring the + # --cooldown option over the :cooldown: gemrc setting. + + def self.from_options(options) + new(options[:cooldown] || Gem.configuration.cooldown) + end + + def initialize(days, now: Time.now) + @days = days.to_i + @now = now + end + + ## + # True when a cooldown period is configured. + + def active? + @days > 0 + end + + ## + # True when a gem version published at +created_at+ must not be + # considered. Versions with an unknown publish time (+nil+) are kept. + + def skip?(created_at) + return false unless active? + return false unless created_at + + (@now - created_at) < @days * 86_400 + end + + ## + # Warns once per process that +source+ did not provide publish times, so + # the cooldown cannot be applied to gems from it. + + def self.warn_missing_created_at(source) + return if @warned + @warned = true + + Gem::DefaultUserInteraction.ui.alert_warning \ + "#{source.uri} does not provide gem publish times, the cooldown period does not apply to gems from this source" + end + + def self.reset_warned_missing_created_at # :nodoc: + @warned = nil + end +end diff --git a/lib/rubygems/cooldown_option.rb b/lib/rubygems/cooldown_option.rb new file mode 100644 index 00000000000000..84195d39f1abaa --- /dev/null +++ b/lib/rubygems/cooldown_option.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "cooldown" + +## +# Mixin methods for the cooldown option for Gem::Commands. + +module Gem::CooldownOption + ## + # Add the --cooldown option to the option parser. + + def add_cooldown_option(group = nil) + args = [group, "--cooldown DAYS", Integer, + "Do not use gem versions published within", + "the last DAYS days (0 disables the cooldown)"].compact + + add_option(*args) do |value, options| + options[:cooldown] = value + end + end +end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 174bd258168b0d..dbb53600ba650a 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -543,6 +543,10 @@ def teardown Gem::RemoteFetcher.fetcher = nil end + if defined? Gem::Cooldown + Gem::Cooldown.reset_warned_missing_created_at + end + Dir.chdir @current_dir ENV.replace(@orig_env) diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 3c79cb0762d783..0ca05e7203ae00 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -44,6 +44,7 @@ def test_initialize assert_equal 365, @cfg.cert_expiration_length_days assert_equal false, @cfg.ipv4_fallback_enabled assert_equal true, @cfg.install_extension_in_lib + assert_equal Gem::ConfigFile::DEFAULT_COOLDOWN, @cfg.cooldown File.open @temp_conf, "w" do |fp| fp.puts ":backtrace: true" @@ -63,6 +64,7 @@ def test_initialize fp.puts ":install_extension_in_lib: false" fp.puts ":ipv4_fallback_enabled: true" fp.puts ":use_psych: true" + fp.puts ":cooldown: 7" end util_config_file @@ -81,6 +83,7 @@ def test_initialize assert_equal false, @cfg.install_extension_in_lib assert_equal true, @cfg.ipv4_fallback_enabled assert_equal true, @cfg.use_psych + assert_equal 7, @cfg.cooldown end def test_initialize_ipv4_fallback_enabled_env diff --git a/test/rubygems/test_gem_cooldown.rb b/test/rubygems/test_gem_cooldown.rb new file mode 100644 index 00000000000000..704b6c169f26f0 --- /dev/null +++ b/test/rubygems/test_gem_cooldown.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/cooldown" + +class TestGemCooldown < Gem::TestCase + def test_skip_eh + now = Time.now + cooldown = Gem::Cooldown.new 7, now: now + + assert cooldown.skip?(now - 6 * 86_400) + refute cooldown.skip?(now - 8 * 86_400) + end + + def test_skip_eh_boundary + now = Time.now + cooldown = Gem::Cooldown.new 7, now: now + + assert cooldown.skip?(now - 7 * 86_400 + 1) + refute cooldown.skip?(now - 7 * 86_400) + end + + def test_skip_eh_unknown_publish_time + refute Gem::Cooldown.new(7).skip?(nil) + end + + def test_skip_eh_inactive + cooldown = Gem::Cooldown.new 0 + + refute cooldown.active? + refute cooldown.skip?(Time.now) + end + + def test_from_options + orig_cooldown = Gem.configuration.cooldown + Gem.configuration.cooldown = 5 + + assert_equal 5, Gem::Cooldown.from_options({}).days + assert_equal 7, Gem::Cooldown.from_options(cooldown: 7).days + refute Gem::Cooldown.from_options(cooldown: 0).active? + ensure + Gem.configuration.cooldown = orig_cooldown + end + + def test_warn_missing_created_at_warns_once + source = Gem::Source.new @gem_repo + + use_ui @ui do + Gem::Cooldown.warn_missing_created_at source + Gem::Cooldown.warn_missing_created_at source + end + + assert_equal 1, @ui.error.scan("publish times").size + assert_match @gem_repo, @ui.error + end +end From 0b6793bd22db5cb775fc8640d7f640f336bc33dc Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 18:07:03 +0900 Subject: [PATCH 02/14] [ruby/rubygems] Apply the cooldown period to gem install resolution The resolver drops release candidates published within the cooldown period, alongside the platform and required_ruby_version filters, so gem install falls back to the newest version outside the window. Installed and lockfile specifications carry no publish time and are never dropped. When an explicitly requested version is within the window, resolution fails with a hint naming the cooldown period and the --cooldown 0 bypass. Sources that provide no publish times at all warn once and are not filtered. https://github.com/ruby/rubygems/commit/61ef01ef38 Co-Authored-By: Claude Fable 5 --- lib/rubygems/dependency_installer.rb | 3 + lib/rubygems/install_update_options.rb | 4 + lib/rubygems/request_set.rb | 9 ++ lib/rubygems/resolver.rb | 35 +++++ .../test_gem_commands_install_command.rb | 124 ++++++++++++++++++ 5 files changed, 175 insertions(+) diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index c842714d9580f4..804b20b359ef76 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../rubygems" +require_relative "cooldown" require_relative "dependency_list" require_relative "package" require_relative "installer" @@ -90,6 +91,7 @@ def initialize(options = {}) @prog_mode = options[:prog_mode] @build_extension = options[:build_extension] @install_plugin = options[:install_plugin] + @cooldown = Gem::Cooldown.from_options options # Indicates that we should not try to update any deps unless # we absolutely must. @@ -210,6 +212,7 @@ def resolve_dependencies(dep_or_name, version) # :nodoc: request_set.development_shallow = @dev_shallow request_set.soft_missing = @force request_set.prerelease = @prerelease + request_set.cooldown = @cooldown installer_set = Gem::Resolver::InstallerSet.new @domain installer_set.ignore_installed = (@minimal_deps == false) || @only_install_dir diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index e8859cadaf159d..942b1af214c031 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -7,12 +7,14 @@ #++ require_relative "../rubygems" +require_relative "cooldown_option" require_relative "security_option" ## # Mixin methods for install and update options for Gem::Commands module Gem::InstallUpdateOptions + include Gem::CooldownOption include Gem::SecurityOption ## @@ -204,6 +206,8 @@ def add_install_update_options "Defaults to true") do |v, _o| options[:install_plugin] = v end + + add_cooldown_option :"Install/Update" end ## diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 9737cba7bfb7e4..d97c313da3020b 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "cooldown" require_relative "vendored_tsort" ## @@ -23,6 +24,11 @@ class Gem::RequestSet attr_accessor :always_install + ## + # The Gem::Cooldown applied to release candidates, if any. + + attr_accessor :cooldown + attr_reader :dependencies attr_accessor :development @@ -96,6 +102,7 @@ def initialize(*deps) @always_install = [] @conservative = false + @cooldown = nil @dependency_names = {} @development = false @development_shallow = false @@ -223,6 +230,7 @@ def install_from_gemdeps(options, &block) @prerelease = options[:prerelease] @remote = options[:domain] != :local @conservative = true if options[:conservative] + @cooldown = Gem::Cooldown.from_options options gem_deps_api = load_gemdeps gemdeps, options[:without_groups], true @@ -442,6 +450,7 @@ def resolve(set = Gem::Resolver::BestSet.new) set.prerelease = @prerelease resolver = Gem::Resolver.new @dependencies, set + resolver.cooldown = @cooldown resolver.development = @development resolver.development_shallow = @development_shallow resolver.ignore_dependencies = @ignore_dependencies diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 788206c0566fd5..492617b52ba24c 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -34,6 +34,11 @@ class Gem::Resolver attr_accessor :ignore_dependencies + ## + # The Gem::Cooldown applied to release candidates, if any. + + attr_accessor :cooldown + ## # Hash of gems to skip resolution. Keyed by gem name, with arrays of # gem specifications as values. @@ -94,6 +99,7 @@ def initialize(needed, set = nil) @set = set || Gem::Resolver::IndexSet.new @needed = needed + @cooldown = nil @development = false @development_shallow = false @ignore_dependencies = false @@ -373,9 +379,27 @@ def filter_specs(specs) end end + filtered = filter_cooldown_specs(filtered) if @cooldown&.active? + filtered end + ## + # Rejects specs published within the cooldown period. Specs with an + # unknown publish time (installed gems, lockfiles, sources without + # timestamps) are kept, so the cooldown fails open. + + def filter_cooldown_specs(specs) + remote = specs.select do |s| + Gem::Resolver::APISpecification === s || Gem::Resolver::IndexSpecification === s + end + if remote.any? && remote.none?(&:created_at) + Gem::Cooldown.warn_missing_created_at remote.first.source + end + + specs.reject {|s| @cooldown.skip?(s.created_at) } + end + def spec_for(name, version) @spec_for_cache[name][version] end @@ -486,6 +510,17 @@ def build_extended_explanation(name, constraint) hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" end + # Check for specs filtered by the cooldown period + if @cooldown&.active? + cooldown_specs = installable.select do |s| + constraint.range.include?(s.version) && @cooldown.skip?(s.created_at) + end + if cooldown_specs.any? + versions = cooldown_specs.map(&:version).uniq.sort.reverse.first(3) + hints << "#{name} #{versions.join(", ")} published within the cooldown period (#{@cooldown.days} days). Use --cooldown 0 to bypass it." + end + end + # Check for specs filtered by prerelease status if prerelease_gated prerelease_versions = @all_versions[pkg].select(&:prerelease?) diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index d75ba349f96ada..27c200856e1bdb 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -729,6 +729,130 @@ def test_execute_remote assert_match "1 gem installed", @ui.output end + def util_setup_cooldown_repo(created_at: {}) + spec_fetcher + + a1, a1_gem = util_gem "a", 1 + a2, a2_gem = util_gem "a", 2 + + util_setup_compact_index a1, a2, created_at: created_at + + add_to_fetcher a1, a1_gem + add_to_fetcher a2, a2_gem + end + + def util_cooldown_time(days_ago) + (Time.now - days_ago * 86_400).utc.strftime("%Y-%m-%dT%H:%M:%SZ") + end + + def test_execute_remote_cooldown_falls_back_to_older_version + util_setup_cooldown_repo created_at: { + "a-1" => util_cooldown_time(30), + "a-2" => util_cooldown_time(1), + } + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = %w[a] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-1], @cmd.installed_specs.map(&:full_name) + end + + def test_execute_remote_cooldown_explicit_version_error + util_setup_cooldown_repo created_at: { + "a-1" => util_cooldown_time(30), + "a-2" => util_cooldown_time(1), + } + + @cmd.options[:cooldown] = 7 + @cmd.options[:version] = Gem::Requirement.new("= 2") + @cmd.options[:args] = %w[a] + + use_ui @ui do + e = assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end + + assert_equal 2, e.exit_code + end + + assert_empty @cmd.installed_specs + assert_match "cooldown period (7 days)", @ui.error + assert_match "--cooldown 0", @ui.error + end + + def test_execute_remote_cooldown_missing_created_at_fails_open + util_setup_cooldown_repo + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = %w[a] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-2], @cmd.installed_specs.map(&:full_name) + assert_equal 1, @ui.error.scan("publish times").size + end + + def test_execute_remote_cooldown_from_gemrc + util_setup_cooldown_repo created_at: { + "a-1" => util_cooldown_time(30), + "a-2" => util_cooldown_time(1), + } + + orig_cooldown = Gem.configuration.cooldown + Gem.configuration.cooldown = 7 + + @cmd.options[:args] = %w[a] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-1], @cmd.installed_specs.map(&:full_name) + ensure + Gem.configuration.cooldown = orig_cooldown + end + + def test_execute_remote_cooldown_zero_overrides_gemrc + util_setup_cooldown_repo created_at: { + "a-1" => util_cooldown_time(30), + "a-2" => util_cooldown_time(1), + } + + orig_cooldown = Gem.configuration.cooldown + Gem.configuration.cooldown = 7 + + @cmd.options[:cooldown] = 0 + @cmd.options[:args] = %w[a] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-2], @cmd.installed_specs.map(&:full_name) + ensure + Gem.configuration.cooldown = orig_cooldown + end + + def test_cooldown_option + @cmd.handle_options %w[--cooldown 7 a] + + assert_equal 7, @cmd.options[:cooldown] + end + def test_execute_with_invalid_gem_file FileUtils.touch("a.gem") From 69212fa6003d43c91303135c46751cdb5fc38bd1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 18:09:19 +0900 Subject: [PATCH 03/14] [ruby/rubygems] Apply the cooldown period to gem update and gem outdated gem update installs the resolved version with an exact pin, so the resolver filter cannot make it fall back. Filter instead when picking the target name tuple, using the new Gem::Source#created_at, which looks up a version's publish time through the compact index info file for the gem. While a cooldown is active the update and outdated lookups search the full index instead of the latest-only index, which carries nothing to fall back to. gem outdated picks the newest version outside the cooldown period as the update candidate and annotates a newer version still within the period with "(cooldown Nd)". https://github.com/ruby/rubygems/commit/c32396ac8a Co-Authored-By: Claude Fable 5 --- lib/rubygems/commands/outdated_command.rb | 79 +++++++++++++++++- lib/rubygems/commands/update_command.rb | 32 +++++++- lib/rubygems/source.rb | 36 +++++++++ lib/rubygems/spec_fetcher.rb | 7 +- .../test_gem_commands_outdated_command.rb | 63 +++++++++++++++ .../test_gem_commands_update_command.rb | 81 +++++++++++++++++++ test/rubygems/test_gem_source.rb | 33 ++++++++ 7 files changed, 325 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index 08a9221a261a9d..7721be88e71f4a 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require_relative "../command" +require_relative "../cooldown" +require_relative "../cooldown_option" require_relative "../local_remote_options" require_relative "../spec_fetcher" require_relative "../version_option" @@ -8,12 +10,14 @@ class Gem::Commands::OutdatedCommand < Gem::Command include Gem::LocalRemoteOptions include Gem::VersionOption + include Gem::CooldownOption def initialize super "outdated", "Display all gems that need updates" add_local_remote_options add_platform_option + add_cooldown_option end def description # :nodoc: @@ -26,8 +30,79 @@ def description # :nodoc: end def execute - Gem::Specification.outdated_and_latest_version.each do |spec, remote_version| - say "#{spec.name} (#{spec.version} < #{remote_version})" + @cooldown = Gem::Cooldown.from_options options + + unless @cooldown.active? + Gem::Specification.outdated_and_latest_version.each do |spec, remote_version| + say "#{spec.name} (#{spec.version} < #{remote_version})" + end + + return end + + execute_with_cooldown + end + + private + + ## + # Like Gem::Specification.outdated_and_latest_version, but the newest + # version outside the cooldown period becomes the update candidate, and + # newer versions still within the period are annotated. + + def execute_with_cooldown + fetcher = Gem::SpecFetcher.fetcher + + Gem::Specification.latest_specs(true).each do |local_spec| + dependency = Gem::Dependency.new local_spec.name, ">= #{local_spec.version}" + + # The :latest index carries only the newest version of each gem, + # which leaves nothing to fall back to when the cooldown excludes + # it, so search the full index instead. + remotes, = fetcher.search_for_dependency dependency, + type: dependency.prerelease? ? :complete : :released + + selectable, embargoed = partition_by_cooldown remotes + + candidate = selectable.max + candidate = nil unless candidate && local_spec.version < candidate + + pending = embargoed.max + pending = nil unless pending && local_spec.version < pending && + (candidate.nil? || candidate < pending) + + next unless candidate || pending + + pending = "#{pending} (cooldown #{@cooldown.days}d)" if pending + say "#{local_spec.name} (#{local_spec.version} < #{[candidate, pending].compact.join(", ")})" + end + end + + ## + # Splits [NameTuple, Gem::Source] pairs into versions outside and within + # the cooldown period. Tuples with an unknown publish time count as + # outside the period, so the cooldown fails open. + + def partition_by_cooldown(spec_tuples) + selectable = [] + embargoed = [] + + with_times = spec_tuples.map do |tup, source| + [tup, source, source.created_at(tup.name, tup.version, tup.platform)] + end + + if !with_times.empty? && with_times.none? {|_, _, created_at| created_at } + Gem::Cooldown.warn_missing_created_at with_times.first[1] + end + + with_times.each do |tup, _, created_at| + if @cooldown.skip?(created_at) + embargoed << tup.version + else + selectable << tup.version + end + end + + [selectable, embargoed] end end diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index cb0a2660dde47d..daa9f53a673d71 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -2,6 +2,7 @@ require_relative "../command" require_relative "../command_manager" +require_relative "../cooldown" require_relative "../dependency_installer" require_relative "../install_update_options" require_relative "../local_remote_options" @@ -95,6 +96,8 @@ def check_update_arguments # :nodoc: end def execute + @cooldown = Gem::Cooldown.from_options options + if options[:system] update_rubygems return @@ -140,7 +143,12 @@ def fetch_remote_gems(spec) # :nodoc: fetcher = Gem::SpecFetcher.fetcher - spec_tuples, errors = fetcher.search_for_dependency dependency + # The default indexes carry only the newest version of each gem, which + # leaves nothing to fall back to when the cooldown excludes it, so + # search the full index instead. + type = dependency.prerelease? ? :complete : :released if @cooldown&.active? + + spec_tuples, errors = fetcher.search_for_dependency dependency, type: type error = errors.find {|e| e.respond_to? :exception } @@ -167,12 +175,32 @@ def highest_installed_gems # :nodoc: def highest_remote_name_tuple(spec) # :nodoc: spec_tuples = fetch_remote_gems spec - highest_remote_gem = spec_tuples.max + highest_remote_gem = filter_cooldown_tuples(spec_tuples).max return unless highest_remote_gem highest_remote_gem.first end + ## + # Rejects [NameTuple, Gem::Source] pairs published within the cooldown + # period. Tuples with an unknown publish time are kept, so the cooldown + # fails open. + + def filter_cooldown_tuples(spec_tuples) # :nodoc: + return spec_tuples unless @cooldown&.active? + + with_times = spec_tuples.map do |tup, source| + [tup, source, source.created_at(tup.name, tup.version, tup.platform)] + end + + if !with_times.empty? && with_times.none? {|_, _, created_at| created_at } + Gem::Cooldown.warn_missing_created_at with_times.first[1] + end + + with_times.reject {|_, _, created_at| @cooldown.skip?(created_at) }. + map {|tup, source, _| [tup, source] } + end + def install_rubygems(spec) # :nodoc: args = update_rubygems_arguments version = spec.version diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index b01619371efc4e..3060bfaa3129f1 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -184,6 +184,42 @@ def compact_index_client # :nodoc: end end + ## + # The publish time of gem +name+ at +version+ for +platform+, when this + # source provides it through the compact index created_at metadata. + # Returns nil when the source, the gem or the version has no known + # publish time. + + def created_at(name, version, platform = Gem::Platform::RUBY) + return unless %w[http https].include?(uri.scheme) + + @created_at_info ||= {} + info = @created_at_info[name] ||= begin + compact_index_client.fetch_info(name) + rescue Gem::RemoteFetcher::FetchError, Gem::CompactIndexClient::Error + [] + end + + platform = (platform || Gem::Platform::RUBY).to_s + version = version.to_s + + row = info.find do |row_info| + row_info[Gem::CompactIndexClient::INFO_VERSION] == version && + (row_info[Gem::CompactIndexClient::INFO_PLATFORM] || Gem::Platform::RUBY) == platform + end + return unless row + + value = row[Gem::CompactIndexClient::INFO_REQS].assoc("created_at")&.last&.first + return unless value.is_a?(String) + + require "time" + begin + Time.iso8601(value) + rescue ArgumentError + nil + end + end + ## # Downloads +spec+ and writes it to +dir+. See also # Gem::RemoteFetcher#download. diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 835dedf9489a2c..6f06b554d2c143 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -82,13 +82,16 @@ def initialize(sources = nil) # Find and fetch gem name tuples that match +dependency+. # # If +matching_platform+ is false, gems for all platforms are returned. + # + # +type+ overrides the index type derived from +dependency+. See + # #available_specs for the list of types. - def search_for_dependency(dependency, matching_platform = true) + def search_for_dependency(dependency, matching_platform = true, type: nil) found = {} rejected_specs = {} - list, errors = available_specs(dependency.identity) + list, errors = available_specs(type || dependency.identity) list.each do |source, specs| if dependency.name.is_a?(String) && specs.respond_to?(:bsearch) diff --git a/test/rubygems/test_gem_commands_outdated_command.rb b/test/rubygems/test_gem_commands_outdated_command.rb index 07289d821fba4a..4f88aef0f03bef 100644 --- a/test/rubygems/test_gem_commands_outdated_command.rb +++ b/test/rubygems/test_gem_commands_outdated_command.rb @@ -50,6 +50,69 @@ def test_execute_compact_index assert_equal "", @ui.error end + def util_cooldown_time(days_ago) + (Time.now - days_ago * 86_400).utc.strftime("%Y-%m-%dT%H:%M:%SZ") + end + + def util_setup_cooldown_repo(created_at) + spec_fetcher do |fetcher| + fetcher.gem "foo", "0.1" + end + + specs = created_at.keys.map {|full_name| util_spec "foo", full_name.delete_prefix("foo-") } + util_setup_compact_index(*specs, created_at: created_at.compact) + + # drop the in-memory tuples spec_fetcher pre-populated so the lookup + # goes through Gem::Source#load_specs + Gem::SpecFetcher.fetcher = nil + end + + def test_execute_cooldown_annotates_newer_version_within_period + util_setup_cooldown_repo "foo-0.2" => util_cooldown_time(30), + "foo-0.3" => util_cooldown_time(1) + + @cmd.options[:cooldown] = 7 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.2, 0.3 (cooldown 7d))\n", @ui.output + assert_equal "", @ui.error + end + + def test_execute_cooldown_only_version_within_period + util_setup_cooldown_repo "foo-0.3" => util_cooldown_time(1) + + @cmd.options[:cooldown] = 7 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.3 (cooldown 7d))\n", @ui.output + assert_equal "", @ui.error + end + + def test_execute_cooldown_missing_created_at_fails_open + util_setup_cooldown_repo "foo-0.2" => nil, "foo-0.3" => nil + + @cmd.options[:cooldown] = 7 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.3)\n", @ui.output + assert_equal 1, @ui.error.scan("publish times").size + end + + def test_cooldown_option + @cmd.handle_options %w[--cooldown 7] + + assert_equal 7, @cmd.options[:cooldown] + end + def test_execute_with_up_to_date_platform_specific_gem spec_fetcher do |fetcher| fetcher.download "foo", "2.0" diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 89b56e0f7cfd30..d63ec7117480c6 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -70,6 +70,87 @@ def test_execute_compact_index assert_path_exist File.join(@gemhome, "specifications", "b-2.gemspec") end + def util_cooldown_time(days_ago) + (Time.now - days_ago * 86_400).utc.strftime("%Y-%m-%dT%H:%M:%SZ") + end + + def util_setup_cooldown_repo(b2_created_at:, b3_created_at:) + spec_fetcher do |fetcher| + fetcher.gem "b", 1 + end + + b2, b2_gem = util_gem "b", 2 + b3, b3_gem = util_gem "b", 3 + util_setup_compact_index b2, b3, created_at: { + "b-2" => b2_created_at, + "b-3" => b3_created_at, + }.compact + add_to_fetcher b2, b2_gem + add_to_fetcher b3, b3_gem + + # drop the in-memory tuples spec_fetcher pre-populated so the lookup + # goes through Gem::Source#load_specs + Gem::SpecFetcher.fetcher = nil + end + + def test_execute_cooldown_falls_back_to_older_version + util_setup_cooldown_repo b2_created_at: util_cooldown_time(30), + b3_created_at: util_cooldown_time(1) + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = [] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + assert_equal "Updating installed gems", out.shift + assert_equal "Updating b", out.shift + assert_equal "Gems updated: b", out.shift + assert_empty out + + assert_path_exist File.join(@gemhome, "specifications", "b-2.gemspec") + assert_path_not_exist File.join(@gemhome, "specifications", "b-3.gemspec") + end + + def test_execute_cooldown_all_new_versions_within_period + util_setup_cooldown_repo b2_created_at: util_cooldown_time(1), + b3_created_at: util_cooldown_time(1) + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = [] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + assert_equal "Updating installed gems", out.shift + assert_equal "Nothing to update", out.shift + assert_empty out + end + + def test_execute_cooldown_missing_created_at_fails_open + util_setup_cooldown_repo b2_created_at: nil, b3_created_at: nil + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = [] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + assert_equal "Updating installed gems", out.shift + assert_equal "Updating b", out.shift + assert_equal "Gems updated: b", out.shift + assert_empty out + + assert_path_exist File.join(@gemhome, "specifications", "b-3.gemspec") + assert_equal 1, @ui.error.scan("publish times").size + end + def test_execute_multiple spec_fetcher do |fetcher| fetcher.download "a", 2 diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index a030b58851c796..e63a5e61fa7c97 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -208,6 +208,39 @@ def test_load_specs_compact_index_skips_invalid_version assert_equal %w[a-1], released end + def test_created_at + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" + b2_java = util_spec "b", "2" do |s| + s.platform = "java" + end + + util_setup_compact_index a1, a2, b2_java, created_at: { + "a-2" => "2026-06-05T10:30:45Z", + "b-2-java" => "2026-06-06T00:00:00Z", + } + + assert_equal Time.utc(2026, 6, 5, 10, 30, 45), @source.created_at("a", v(2)) + assert_equal Time.utc(2026, 6, 6), @source.created_at("b", v(2), "java") + + # no created_at metadata for this version + assert_nil @source.created_at("a", v(1)) + + # unknown version and unknown gem + assert_nil @source.created_at("a", v(9)) + assert_nil @source.created_at("c", v(1)) + end + + def test_created_at_file_uri + source = Gem::Source.new "file:///tmp/gems" + + assert_nil source.created_at("a", v(1)) + end + + def test_created_at_fetch_error + assert_nil @source.created_at("a", v(1)) + end + def test_compact_index_cache_dir_removes_tmpdir_at_exit source = Gem::Source.new @gem_repo def source.update_cache? From da2f10648a66f9ff33f812a88040544975673e7a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 18:10:54 +0900 Subject: [PATCH 04/14] [ruby/rubygems] Cover gem update --system with a cooldown The tuple selection and resolver filters already apply to rubygems-update, so gem update --system needs no dedicated code path. Add coverage that the newest release within the cooldown period is passed over in favor of the newest one outside it. https://github.com/ruby/rubygems/commit/e29aac0147 Co-Authored-By: Claude Fable 5 --- .../test_gem_commands_update_command.rb | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index d63ec7117480c6..87f500fc1bdc31 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -151,6 +151,40 @@ def test_execute_cooldown_missing_created_at_fails_open assert_equal 1, @ui.error.scan("publish times").size end + def test_execute_system_cooldown + spec_fetcher + + ru8, ru8_gem = util_gem "rubygems-update", 8 do |s| + s.files = %w[setup.rb] + end + ru9, ru9_gem = util_gem "rubygems-update", 9 do |s| + s.files = %w[setup.rb] + end + + util_setup_compact_index ru8, ru9, created_at: { + "rubygems-update-8" => util_cooldown_time(30), + "rubygems-update-9" => util_cooldown_time(1), + } + add_to_fetcher ru8, ru8_gem + add_to_fetcher ru9, ru9_gem + + Gem::SpecFetcher.fetcher = nil + + @cmd.options[:args] = [] + @cmd.options[:system] = true + @cmd.options[:cooldown] = 7 + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + assert_equal "Installing RubyGems 8", out.shift + assert_equal "RubyGems system software updated", out.shift + + assert_empty out + end + def test_execute_multiple spec_fetcher do |fetcher| fetcher.download "a", 2 From 14b1316af82cb4c734f545602b48697a4661ea4a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 5 Aug 2026 18:50:31 +0900 Subject: [PATCH 05/14] [ruby/rubygems] Summarize cooldown-skipped versions after gem install and update Without a summary, a version the cooldown kept out is indistinguishable from a version that does not exist, which reads as environments resolving different versions for no visible reason. Match the Bundler summary added in #9762: after a completed install or update, report per gem the newest skipped version, when it will become available, and the version resolved instead. The resolver accumulates skipped candidates and reports only those newer than the resolved version and satisfying the final requirements. gem update also reports skipped name tuples newer than the version the update settled on. gem outdated already annotates versions within the window, so it gets no summary. https://github.com/ruby/rubygems/commit/8f6e62658c Co-Authored-By: Claude Fable 5 --- lib/rubygems/commands/install_command.rb | 7 ++ lib/rubygems/commands/update_command.rb | 58 +++++++++++++++- lib/rubygems/cooldown.rb | 33 ++++++++++ lib/rubygems/dependency_installer.rb | 8 +++ lib/rubygems/resolver.rb | 66 ++++++++++++++++++- .../test_gem_commands_install_command.rb | 2 + .../test_gem_commands_update_command.rb | 6 ++ 7 files changed, 176 insertions(+), 4 deletions(-) diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 6d3beec0b43261..2ebbc40a03080f 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -152,6 +152,7 @@ def execute end @installed_specs = [] + @cooldown_skipped = [] ENV.delete "GEM_PATH" if options[:install_dir].nil? @@ -163,6 +164,8 @@ def execute show_installed + Gem::Cooldown.output_skipped_summary @cooldown_skipped + say update_suggestion if eligible_for_update? terminate_interaction exit_code @@ -184,6 +187,8 @@ def install_from_gemdeps # :nodoc: @installed_specs = specs + Gem::Cooldown.output_skipped_summary rs.resolver&.cooldown_skipped + terminate_interaction end @@ -207,6 +212,8 @@ def install_gem(name, version) # :nodoc: @installed_specs.concat request_set.install options end + (@cooldown_skipped ||= []).concat dinst.cooldown_skipped + show_install_errors dinst.errors end diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index daa9f53a673d71..71942f920d377a 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -97,9 +97,11 @@ def check_update_arguments # :nodoc: def execute @cooldown = Gem::Cooldown.from_options options + @cooldown_skipped = [] if options[:system] update_rubygems + output_cooldown_skipped_summary return end @@ -135,6 +137,8 @@ def execute end say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty? say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty? + + output_cooldown_skipped_summary end def fetch_remote_gems(spec) # :nodoc: @@ -197,8 +201,54 @@ def filter_cooldown_tuples(spec_tuples) # :nodoc: Gem::Cooldown.warn_missing_created_at with_times.first[1] end - with_times.reject {|_, _, created_at| @cooldown.skip?(created_at) }. - map {|tup, source, _| [tup, source] } + with_times.reject do |tup, _, created_at| + next false unless @cooldown.skip?(created_at) + + (@cooldown_skipped_tuples ||= {})[[tup.name, tup.version]] ||= created_at + true + end.map {|tup, source, _| [tup, source] } + end + + ## + # Summary entries for tuples the cooldown kept out of the update, kept + # only when newer than the version the update actually settled on (the + # updated version, or the one already installed when nothing moved). + + def cooldown_skipped_tuple_entries # :nodoc: + skipped = @cooldown_skipped_tuples + return [] unless skipped + + resolved = {} + @updated.each do |spec| + version = resolved[spec.name] + resolved[spec.name] = spec.version if version.nil? || spec.version > version + end + + skipped.filter_map do |(name, version), created_at| + resolved_version = resolved[name] || resolved_fallback_version(name) + next unless resolved_version && version > resolved_version + + { + name: name, + version: version, + resolved: resolved_version, + available_in_days: @cooldown.remaining_days(created_at), + } + end + end + + def resolved_fallback_version(name) # :nodoc: + if name == "rubygems-update" + Gem::Version.new Gem::VERSION + else + Gem::Specification.find_all_by_name(name).map(&:version).max + end + end + + def output_cooldown_skipped_summary # :nodoc: + entries = (@cooldown_skipped || []) + cooldown_skipped_tuple_entries + + Gem::Cooldown.output_skipped_summary entries end def install_rubygems(spec) # :nodoc: @@ -284,6 +334,10 @@ def update_gem(name, version = Gem::Requirement.default) @installer.installed_gems.each do |spec| @updated << spec end + + (@cooldown_skipped ||= []).concat @installer.cooldown_skipped + + @installer.installed_gems end def update_gems(gems_to_update) diff --git a/lib/rubygems/cooldown.rb b/lib/rubygems/cooldown.rb index b6474b52b82f3f..499e08b3be1985 100644 --- a/lib/rubygems/cooldown.rb +++ b/lib/rubygems/cooldown.rb @@ -50,6 +50,39 @@ def skip?(created_at) (@now - created_at) < @days * 86_400 end + ## + # Number of days until a gem version published at +created_at+ leaves + # the cooldown period, rounded up and at least 1. + + def remaining_days(created_at) + remaining = @days * 86_400 - (@now - created_at) + + [(remaining / 86_400.0).ceil, 1].max + end + + ## + # Reports, per gem, the newest version the cooldown kept out of a + # completed installation or update. +entries+ are hashes with :name, + # :version, :resolved and :available_in_days keys; when several entries + # name the same gem only the newest version is shown. + + def self.output_skipped_summary(entries) + return if entries.nil? || entries.empty? + + newest = {} + entries.each do |entry| + current = newest[entry[:name]] + newest[entry[:name]] = entry if current.nil? || entry[:version] > current[:version] + end + + ui = Gem::DefaultUserInteraction.ui + ui.say "The following gem versions were skipped by the cooldown setting:" + newest.values.sort_by {|entry| entry[:name] }.each do |entry| + days = entry[:available_in_days] + ui.say " * #{entry[:name]} #{entry[:version]} (available in #{days} #{days == 1 ? "day" : "days"}), resolved #{entry[:resolved]} instead" + end + end + ## # Warns once per process that +source+ did not provide publish times, so # the cooldown cannot be applied to gems from it. diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 804b20b359ef76..f3076472850467 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -44,6 +44,12 @@ class Gem::DependencyInstaller attr_reader :installed_gems + ## + # Per-gem summary entries for the newest versions the cooldown kept out + # of the last resolution. See Gem::Cooldown.output_skipped_summary. + + attr_reader :cooldown_skipped + ## # Creates a new installer instance. # @@ -92,6 +98,7 @@ def initialize(options = {}) @build_extension = options[:build_extension] @install_plugin = options[:install_plugin] @cooldown = Gem::Cooldown.from_options options + @cooldown_skipped = [] # Indicates that we should not try to update any deps unless # we absolutely must. @@ -261,6 +268,7 @@ def resolve_dependencies(dep_or_name, version) # :nodoc: request_set.resolve installer_set @errors.concat request_set.errors + @cooldown_skipped = request_set.resolver&.cooldown_skipped || [] request_set end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 492617b52ba24c..4964f5be217ec9 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -39,6 +39,14 @@ class Gem::Resolver attr_accessor :cooldown + ## + # Per-gem summary entries for the newest versions the cooldown kept out + # of a successful resolution. See Gem::Cooldown.output_skipped_summary. + + def cooldown_skipped + @cooldown_skipped || [] + end + ## # Hash of gems to skip resolution. Keyed by gem name, with arrays of # gem specifications as values. @@ -164,13 +172,17 @@ def resolve # Convert to Array needed_by_name = @needed.group_by(&:name) - result.filter_map do |package, version| + requests = result.filter_map do |package, version| next if Gem::PubGrub::Package.root?(package) spec = spec_for(package.to_s, version) dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s) dep_request = DependencyRequest.new(dep, nil) ActivationRequest.new(spec, dep_request) end + + @cooldown_skipped = cooldown_skipped_summary(requests) if @cooldown&.active? + + requests rescue Gem::PubGrub::SolveFailure => e extended = extract_extended_explanation(e.incompatibility) if extended @@ -397,7 +409,57 @@ def filter_cooldown_specs(specs) Gem::Cooldown.warn_missing_created_at remote.first.source end - specs.reject {|s| @cooldown.skip?(s.created_at) } + specs.reject do |s| + next false unless @cooldown.skip?(s.created_at) + + (@cooldown_skipped_specs ||= {})[[s.name, s.version]] ||= s + true + end + end + + ## + # Reports, per gem, the newest version that the cooldown kept out of a + # successful resolution. A skipped version is only worth reporting when + # it is newer than the resolved version and satisfies every requirement + # the final resolution places on that gem, so we don't claim a version + # the resolver could never have picked anyway. + + def cooldown_skipped_summary(requests) + skipped_specs = @cooldown_skipped_specs || {} + return [] if skipped_specs.empty? + + requirements = Hash.new {|h, name| h[name] = [] } + @needed.each {|dep| requirements[dep.name] << dep.requirement } + requests.each do |request| + request.spec.dependencies.each do |dep| + next if dep.type == :development && !@development + requirements[dep.name] << dep.requirement + end + end + + resolved = {} + requests.each do |request| + version = resolved[request.spec.name] + resolved[request.spec.name] = request.spec.version if version.nil? || request.spec.version > version + end + + newest_skipped = {} + skipped_specs.each do |(name, version), spec| + resolved_version = resolved[name] + next unless resolved_version && version > resolved_version + next unless requirements[name].all? {|req| req.satisfied_by?(version) } + newest = newest_skipped[name] + newest_skipped[name] = spec if newest.nil? || version > newest.version + end + + newest_skipped.values.sort_by(&:name).map do |spec| + { + name: spec.name, + version: spec.version, + resolved: resolved[spec.name], + available_in_days: @cooldown.remaining_days(spec.created_at), + } + end end def spec_for(name, version) diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 27c200856e1bdb..6ae389db2fba49 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -761,6 +761,8 @@ def test_execute_remote_cooldown_falls_back_to_older_version end assert_equal %w[a-1], @cmd.installed_specs.map(&:full_name) + assert_match "The following gem versions were skipped by the cooldown setting:", @ui.output + assert_match "* a 2 (available in 6 days), resolved 1 instead", @ui.output end def test_execute_remote_cooldown_explicit_version_error diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 87f500fc1bdc31..9d15406bd13fc0 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -108,6 +108,8 @@ def test_execute_cooldown_falls_back_to_older_version assert_equal "Updating installed gems", out.shift assert_equal "Updating b", out.shift assert_equal "Gems updated: b", out.shift + assert_equal "The following gem versions were skipped by the cooldown setting:", out.shift + assert_equal " * b 3 (available in 6 days), resolved 2 instead", out.shift assert_empty out assert_path_exist File.join(@gemhome, "specifications", "b-2.gemspec") @@ -128,6 +130,8 @@ def test_execute_cooldown_all_new_versions_within_period out = @ui.output.split "\n" assert_equal "Updating installed gems", out.shift assert_equal "Nothing to update", out.shift + assert_equal "The following gem versions were skipped by the cooldown setting:", out.shift + assert_equal " * b 3 (available in 6 days), resolved 1 instead", out.shift assert_empty out end @@ -181,6 +185,8 @@ def test_execute_system_cooldown out = @ui.output.split "\n" assert_equal "Installing RubyGems 8", out.shift assert_equal "RubyGems system software updated", out.shift + assert_equal "The following gem versions were skipped by the cooldown setting:", out.shift + assert_equal " * rubygems-update 9 (available in 6 days), resolved 8 instead", out.shift assert_empty out end From d3838173cff0487950c248a1165779e681207758 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 7 Aug 2026 11:59:48 -0700 Subject: [PATCH 06/14] ZJIT: Respect no_side_exits policy in monomorphic invokeblock --- zjit/src/hir.rs | 199 +++++++++++++++++++------------------- zjit/src/hir/opt_tests.rs | 88 ++++++++++++++--- 2 files changed, 172 insertions(+), 115 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1ca36fe0048b64..e630cdc549ac07 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2972,7 +2972,7 @@ fn iseq_get_return_value(iseq: IseqPtr, captured_opnd: Option, ci_flags: } /// True if the `invokeblock` call flags permit inlining the block dispatch. -fn block_call_inlinable(flags: u32) -> bool { +fn can_direct_invoke_block(flags: u32) -> bool { (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_KWARG | VM_CALL_ARGS_BLOCKARG)) == 0 } @@ -2981,7 +2981,7 @@ fn block_call_inlinable(flags: u32) -> bool { /// with an exact arity match, avoid arg0 auto-splat, and contain no `throw` (break / /// non-local return). Anything else falls back to the generic `invokeblock` dispatch, /// with the returned reason attached to the fallback instruction. -fn block_call_inlinable_iseq(iseq: IseqPtr, argc: usize) -> Result<(), SendFallbackReason> { +fn can_direct_invoke_block_iseq(iseq: IseqPtr, argc: usize) -> Result<(), SendFallbackReason> { if !unsafe { rb_simple_iseq_p(iseq) } { return Err(InvokeBlockNotSpecialized); } @@ -3091,34 +3091,100 @@ impl Function { self.load_field(block, captured, FieldName::code_iseq, offset, types::CPtr) } - /// Emit the fast-path `yield` dispatch to a known ISEQ block. - /// When `guarded`, the block handler is read from the runtime LEP and guarded (tag + iseq - /// identity) because the profiled block can differ per caller. When the enclosing method is + /// Dispatch `yield` to a known ISEQ block without guarding tag or ISEQ. When the enclosing method is /// inlined and the caller passed a literal block, [`Insn::PushInlineFrame`] wrote that exact /// block into this frame's EP from a compile-time constant, so both guards are unnecessary. - fn push_invoke_block_iseq_direct(&mut self, block: BlockId, block_iseq: IseqPtr, level: u32, args: Vec, state: InsnId, guarded: bool) -> InsnId { + fn push_invoke_block_iseq_direct(&mut self, block: BlockId, block_iseq: IseqPtr, level: u32, args: Vec, state: InsnId) -> InsnId { let ep = self.get_ep(block, level); let block_handler = self.load_ep_env_field(block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); - if guarded { - // Guard the handler is an ISEQ block: VM_BH_ISEQ_BLOCK_P is `& 0x3 == 0x1`. - let tag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(0x3) }); - let tag = self.push_insn(block, Insn::IntAnd { left: block_handler, right: tag_mask }); - self.push_insn(block, Insn::GuardBitEquals { val: tag, expected: Const::CInt64(0x1), reason: Box::new(SideExitReason::InvokeBlockHandlerNotIseq), state, recompile: Some(Recompile) }); - } - // captured = block_handler & ~0x3 (struct rb_captured_block *) let untag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(!0x3) }); let captured = self.push_insn(block, Insn::IntAnd { left: block_handler, right: untag_mask }); - if guarded { - // Guard captured->code.iseq is the comptime block iseq. Compare the raw imemo pointer: + self.push_insn(block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args, state }) + } + + /// Dispatch `yield` to the profiled ISEQ blocks. + fn dispatch_invoke_block_iseqs( + &mut self, + iseqs: &[IseqPtr], + block: BlockId, + insn_idx: u32, + level: u32, + cd: *const rb_call_data, + args: Vec, + state: InsnId, + ) -> (BlockId, InsnId) { + assert!(!iseqs.is_empty()); + let ep = self.get_ep(block, level); + let block_handler = self.load_ep_env_field(block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); + + // The handler must be an ISEQ block: VM_BH_ISEQ_BLOCK_P is `& 0x3 == 0x1`. + let tag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(0x3) }); + let tag = self.push_insn(block, Insn::IntAnd { left: block_handler, right: tag_mask }); + + // Monomorphic: guard the tag and the ISEQ, then invoke directly in-place. + // No need for new HIR blocks. + if iseqs.len() == 1 && !self.policy.no_side_exits { + let block_iseq = iseqs[0]; + self.push_insn(block, Insn::GuardBitEquals { val: tag, expected: Const::CInt64(0x1), reason: Box::new(SideExitReason::InvokeBlockHandlerNotIseq), state, recompile: Some(Recompile) }); + + // captured = block_handler & ~0x3 (struct rb_captured_block *) + let untag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(!0x3) }); + let captured = self.push_insn(block, Insn::IntAnd { left: block_handler, right: untag_mask }); + + // Guard captured->code.iseq is the profiled block iseq. Compare the raw imemo pointer: // type inference (from_value) can't type an iseq imemo, so guard it as a CPtr identity. let captured_iseq = self.load_captured_code_iseq(block, captured); self.push_insn(block, Insn::GuardBitEquals { val: captured_iseq, expected: Const::CPtr(block_iseq as *const u8), reason: Box::new(SideExitReason::InvokeBlockIseqChanged), state, recompile: Some(Recompile) }); + + let result = self.push_insn(block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args, state }); + return (block, result); } - self.push_insn(block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args, state }) + // Otherwise, compare the handler against each candidate and use the fallback if missed. + let join_block = self.new_block(insn_idx); + let join_param = self.push_insn(join_block, Insn::Param); + let dispatch_block = self.new_block(insn_idx); + let fallback_block = self.new_block(insn_idx); + + let iseq_tag = self.push_insn(block, Insn::Const { val: Const::CInt64(0x1) }); + let tag_matches = self.push_insn(block, Insn::IsBitEqual { left: tag, right: iseq_tag }); + self.push_insn(block, Insn::CondBranch { + val: tag_matches, + if_true: BranchEdge { target: dispatch_block, args: vec![] }, + if_false: BranchEdge { target: fallback_block, args: vec![] }, + }); + + // captured = block_handler & ~0x3 (struct rb_captured_block *) + let untag_mask = self.push_insn(dispatch_block, Insn::Const { val: Const::CInt64(!0x3) }); + let captured = self.push_insn(dispatch_block, Insn::IntAnd { left: block_handler, right: untag_mask }); + let captured_iseq = self.load_captured_code_iseq(dispatch_block, captured); + + let mut compare_block = dispatch_block; + for &block_iseq in iseqs { + let expected = self.push_insn(compare_block, Insn::Const { val: Const::CPtr(block_iseq as *const u8) }); + let iseq_matches = self.push_insn(compare_block, Insn::IsBitEqual { left: captured_iseq, right: expected }); + let direct_block = self.new_block(insn_idx); + let miss_block = self.new_block(insn_idx); + self.push_insn(compare_block, Insn::CondBranch { + val: iseq_matches, + if_true: BranchEdge { target: direct_block, args: vec![] }, + if_false: BranchEdge { target: miss_block, args: vec![] }, + }); + let direct_result = self.push_insn(direct_block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args: args.clone(), state }); + self.push_insn(direct_block, Insn::Jump(BranchEdge { target: join_block, args: vec![direct_result] })); + compare_block = miss_block; + } + self.push_insn(compare_block, Insn::Jump(BranchEdge { target: fallback_block, args: vec![] })); + + let fallback_result = self.push_insn(fallback_block, Insn::InvokeBlock { + cd, args, state, reason: InvokeBlockPolymorphicMiss, + }); + self.push_insn(fallback_block, Insn::Jump(BranchEdge { target: join_block, args: vec![fallback_result] })); + + (join_block, join_param) } // Add an instruction to an SSA block @@ -9988,30 +10054,13 @@ fn add_iseq_to_hir( let is_ifunc = (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_KWARG)) == 0 && block_handler_class.is_some_and(|obj| unsafe { rb_IMEMO_TYPE_P(obj, imemo_ifunc) == 1 }); - // If the block handler is a known simple ISEQ block with exact arity and no - // non-local exit, push its frame inline instead of calling rb_vm_invokeblock. + // Collect the profiled ISEQ blocks that can be invoked directly with a JIT-to-JIT call. + // The first entry of buckets is the most common type, so the hottest block is compared first. let mut fallback_reason = InvokeBlockNotSpecialized; - let inline_iseq = if block_call_inlinable(flags) { - block_handler_class.and_then(|obj| { - if unsafe { rb_IMEMO_TYPE_P(obj, imemo_iseq) == 1 } { - let iseq = obj.as_iseq(); - match block_call_inlinable_iseq(iseq, args.len()) { - Ok(()) => return Some(iseq), - Err(reason) => fallback_reason = reason, - } - } - None - }) - } else { None }; - - // For polymorphic yield sites, collect the profiled ISEQ blocks that can - // dispatch directly. Iterators like Integer#times are typically called with a - // different block per call site, so requiring a monomorphic profile would - // leave every such shared yield site on the generic fallback. Buckets are - // ordered by frequency, so the hottest block is compared first below. - let mut polymorphic_iseqs: Vec = vec![]; + let mut direct_iseqs: Vec = vec![]; if let Some(summary) = block_handler_summary.as_ref() { - if block_call_inlinable(flags) && (summary.is_polymorphic() || summary.is_skewed_polymorphic()) { + if can_direct_invoke_block(flags) + && (summary.is_monomorphic() || summary.is_polymorphic() || summary.is_skewed_polymorphic()) { for &profiled_type in summary.buckets() { if profiled_type.is_empty() { break; @@ -10019,8 +10068,10 @@ fn add_iseq_to_hir( let obj = profiled_type.class(); if unsafe { rb_IMEMO_TYPE_P(obj, imemo_iseq) == 1 } { let iseq = obj.as_iseq(); - if !polymorphic_iseqs.contains(&iseq) && block_call_inlinable_iseq(iseq, args.len()).is_ok() { - polymorphic_iseqs.push(iseq); + match can_direct_invoke_block_iseq(iseq, args.len()) { + Ok(()) if !direct_iseqs.contains(&iseq) => direct_iseqs.push(iseq), + Ok(()) => {} + Err(reason) => fallback_reason = reason, } } } @@ -10028,7 +10079,7 @@ fn add_iseq_to_hir( } let inlined_known_block = if let AddIseqMode::Inlined { blockiseq: Some(bi), .. } = mode { - if block_call_inlinable(flags) + if can_direct_invoke_block(flags) // Only methods are inlined today, so exit_state.iseq is always a method iseq and this is // always 0. That matters because the emit below is guard-free and bakes in both level 0 // and *this* frame's block (bi) — sound only when the yield resolves to this exact frame. @@ -10036,7 +10087,7 @@ fn add_iseq_to_hir( // (level > 0). To stay guard-free we'd bake in get_lvar_level(...) as the level and fetch // that ancestor's blockiseq from the inline caller chain instead of bi. && get_lvar_level(exit_state.iseq) == 0 { - match block_call_inlinable_iseq(bi, args.len()) { + match can_direct_invoke_block_iseq(bi, args.len()) { Ok(()) => Some(bi), Err(reason) => { fallback_reason = reason; @@ -10047,65 +10098,13 @@ fn add_iseq_to_hir( } else { None }; let result = if let Some(block_iseq) = inlined_known_block { - fun.push_invoke_block_iseq_direct(block, block_iseq, 0, args, exit_id, false) - } else if let Some(block_iseq) = inline_iseq { + fun.push_invoke_block_iseq_direct(block, block_iseq, 0, args, exit_id) + } else if !direct_iseqs.is_empty() { let level = get_lvar_level(exit_state.iseq); - fun.push_invoke_block_iseq_direct(block, block_iseq, level, args, exit_id, true) - } else if !polymorphic_iseqs.is_empty() { - // Dispatch on the runtime block ISEQ over the profiled candidates, joining - // on the generic fallback for anything else. Unlike the monomorphic path - // above, a miss must not side-exit: the site is known to see multiple - // blocks, so a guard would keep failing and recompiling. - let level = get_lvar_level(exit_state.iseq); - let ep = fun.get_ep(block, level); - let block_handler = fun.load_ep_env_field(block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); - - let join_block = fun.new_block(insn_idx); - let join_param = fun.push_insn(join_block, Insn::Param); - let dispatch_block = fun.new_block(insn_idx); - let fallback_block = fun.new_block(insn_idx); - - // The handler must be an ISEQ block: VM_BH_ISEQ_BLOCK_P is `& 0x3 == 0x1`. - let tag_mask = fun.push_insn(block, Insn::Const { val: Const::CInt64(0x3) }); - let tag = fun.push_insn(block, Insn::IntAnd { left: block_handler, right: tag_mask }); - let iseq_tag = fun.push_insn(block, Insn::Const { val: Const::CInt64(0x1) }); - let tag_matches = fun.push_insn(block, Insn::IsBitEqual { left: tag, right: iseq_tag }); - fun.push_insn(block, Insn::CondBranch { - val: tag_matches, - if_true: BranchEdge { target: dispatch_block, args: vec![] }, - if_false: BranchEdge { target: fallback_block, args: vec![] }, - }); - - // captured = block_handler & ~0x3 (struct rb_captured_block *) - let untag_mask = fun.push_insn(dispatch_block, Insn::Const { val: Const::CInt64(!0x3) }); - let captured = fun.push_insn(dispatch_block, Insn::IntAnd { left: block_handler, right: untag_mask }); - let captured_iseq = fun.load_captured_code_iseq(dispatch_block, captured); - - let mut compare_block = dispatch_block; - for &block_iseq in &polymorphic_iseqs { - let expected = fun.push_insn(compare_block, Insn::Const { val: Const::CPtr(block_iseq as *const u8) }); - let iseq_matches = fun.push_insn(compare_block, Insn::IsBitEqual { left: captured_iseq, right: expected }); - let direct_block = fun.new_block(insn_idx); - let miss_block = fun.new_block(insn_idx); - fun.push_insn(compare_block, Insn::CondBranch { - val: iseq_matches, - if_true: BranchEdge { target: direct_block, args: vec![] }, - if_false: BranchEdge { target: miss_block, args: vec![] }, - }); - let direct_result = fun.push_insn(direct_block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args: args.clone(), state: exit_id }); - fun.push_insn(direct_block, Insn::Jump(BranchEdge { target: join_block, args: vec![direct_result] })); - compare_block = miss_block; - } - fun.push_insn(compare_block, Insn::Jump(BranchEdge { target: fallback_block, args: vec![] })); - - let fallback_result = fun.push_insn(fallback_block, Insn::InvokeBlock { - cd, args, state: exit_id, reason: InvokeBlockPolymorphicMiss, - }); - fun.push_insn(fallback_block, Insn::Jump(BranchEdge { target: join_block, args: vec![fallback_result] })); - - // Continue compilation from the join block - block = join_block; - join_param + let (continue_block, result) = fun.dispatch_invoke_block_iseqs(&direct_iseqs, block, insn_idx, level, cd, args, exit_id); + // Continue compilation from the block the dispatch ended in + block = continue_block; + result } else if is_ifunc { // Load the block handler from the current frame's LEP. In inlined // code, the function ISEQ is the caller while `exit_state.iseq` is the diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 06733fb301484e..5282d08645e9b7 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4363,10 +4363,10 @@ mod hir_opt_tests { v10:Fixnum[10] = Const Value(10) v12:CPtr = GetEP 0 v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 - v15:CInt64[3] = Const CInt64(3) - v16:CInt64 = IntAnd v13, v15 + v14:CInt64[3] = Const CInt64(3) + v15:CInt64 = IntAnd v13, v14 v17:CInt64[1] = Const CInt64(1) - v18:CBool = IsBitEqual v16, v17 + v18:CBool = IsBitEqual v15, v17 CondBranch v18, bb5(), bb6() bb5(): v20:CInt64[-4] = Const CInt64(-4) @@ -4390,9 +4390,9 @@ mod hir_opt_tests { bb6(): v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss Jump bb4(v34) - bb4(v14:BasicObject): + bb4(v16:BasicObject): CheckInterrupts - Return v14 + Return v16 "); } @@ -4428,10 +4428,10 @@ mod hir_opt_tests { v10:Fixnum[10] = Const Value(10) v12:CPtr = GetEP 0 v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 - v15:CInt64[3] = Const CInt64(3) - v16:CInt64 = IntAnd v13, v15 + v14:CInt64[3] = Const CInt64(3) + v15:CInt64 = IntAnd v13, v14 v17:CInt64[1] = Const CInt64(1) - v18:CBool = IsBitEqual v16, v17 + v18:CBool = IsBitEqual v15, v17 CondBranch v18, bb5(), bb6() bb5(): v20:CInt64[-4] = Const CInt64(-4) @@ -4455,9 +4455,9 @@ mod hir_opt_tests { bb6(): v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss Jump bb4(v34) - bb4(v14:BasicObject): + bb4(v16:BasicObject): CheckInterrupts - Return v14 + Return v16 "); } @@ -4492,10 +4492,10 @@ mod hir_opt_tests { v10:Fixnum[10] = Const Value(10) v12:CPtr = GetEP 0 v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 - v15:CInt64[3] = Const CInt64(3) - v16:CInt64 = IntAnd v13, v15 + v14:CInt64[3] = Const CInt64(3) + v15:CInt64 = IntAnd v13, v14 v17:CInt64[1] = Const CInt64(1) - v18:CBool = IsBitEqual v16, v17 + v18:CBool = IsBitEqual v15, v17 CondBranch v18, bb5(), bb6() bb5(): v20:CInt64[-4] = Const CInt64(-4) @@ -4519,9 +4519,67 @@ mod hir_opt_tests { bb6(): v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss Jump bb4(v34) - bb4(v14:BasicObject): + bb4(v16:BasicObject): CheckInterrupts - Return v14 + Return v16 + "); + } + + #[test] + fn test_specialize_monomorphic_invokeblock_on_final_version() { + // A monomorphic yield site compiled under the no-side-exits policy (the final + // version after an invalidation) must not guard, so instead of the in-place + // guarded dispatch it gets the same compare-plus-fallback chain as a polymorphic + // site, with a single ISEQ arm. The constant reassignment invalidates the first + // version through its constant cache patchpoint without a second block ever + // reaching the yield site, keeping its profile monomorphic. + set_max_versions(2); + set_inline_threshold(0); + eval(" + X = 10 + def invoke = yield(X) + def add_one = invoke { |x| x + 1 } + add_one; add_one + Object.send(:remove_const, :X) + X = 11 + "); + assert_snapshot!(hir_string("invoke"), @" + fn invoke@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:BasicObject = GetConstantPath 0x1000 + v12:CPtr = GetEP 0 + v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1010 + v14:CInt64[3] = Const CInt64(3) + v15:CInt64 = IntAnd v13, v14 + v17:CInt64[1] = Const CInt64(1) + v18:CBool = IsBitEqual v15, v17 + CondBranch v18, bb5(), bb6() + bb5(): + v20:CInt64[-4] = Const CInt64(-4) + v21:CInt64 = IntAnd v13, v20 + v22:CPtr = LoadField v21, :code_iseq@0x1011 + v23:CPtr[CPtr(0x1012)] = Const CPtr(0x1012) + v24:CBool = IsBitEqual v22, v23 + CondBranch v24, bb7(), bb8() + bb7(): + v26:BasicObject = InvokeBlockIseqDirect (0x1012), v21, v10 + Jump bb4(v26) + bb8(): + Jump bb6() + bb6(): + v29:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss + Jump bb4(v29) + bb4(v16:BasicObject): + CheckInterrupts + Return v16 "); } From bd77fa16592fa935f516e41b70bfff066f7493d0 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 18 Aug 2026 11:27:25 -0700 Subject: [PATCH 07/14] ZJIT: Extract untag_block_handler helper --- zjit/src/hir.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e630cdc549ac07..fad0185c683c45 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3091,17 +3091,20 @@ impl Function { self.load_field(block, captured, FieldName::code_iseq, offset, types::CPtr) } + /// Untag an ISEQ block handler into its `struct rb_captured_block *`: + /// captured = block_handler & ~0x3 + fn untag_block_handler(&mut self, block: BlockId, block_handler: InsnId) -> InsnId { + let untag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(!0x3) }); + self.push_insn(block, Insn::IntAnd { left: block_handler, right: untag_mask }) + } + /// Dispatch `yield` to a known ISEQ block without guarding tag or ISEQ. When the enclosing method is /// inlined and the caller passed a literal block, [`Insn::PushInlineFrame`] wrote that exact /// block into this frame's EP from a compile-time constant, so both guards are unnecessary. fn push_invoke_block_iseq_direct(&mut self, block: BlockId, block_iseq: IseqPtr, level: u32, args: Vec, state: InsnId) -> InsnId { let ep = self.get_ep(block, level); let block_handler = self.load_ep_env_field(block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); - - // captured = block_handler & ~0x3 (struct rb_captured_block *) - let untag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(!0x3) }); - let captured = self.push_insn(block, Insn::IntAnd { left: block_handler, right: untag_mask }); - + let captured = self.untag_block_handler(block, block_handler); self.push_insn(block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args, state }) } @@ -3129,10 +3132,7 @@ impl Function { if iseqs.len() == 1 && !self.policy.no_side_exits { let block_iseq = iseqs[0]; self.push_insn(block, Insn::GuardBitEquals { val: tag, expected: Const::CInt64(0x1), reason: Box::new(SideExitReason::InvokeBlockHandlerNotIseq), state, recompile: Some(Recompile) }); - - // captured = block_handler & ~0x3 (struct rb_captured_block *) - let untag_mask = self.push_insn(block, Insn::Const { val: Const::CInt64(!0x3) }); - let captured = self.push_insn(block, Insn::IntAnd { left: block_handler, right: untag_mask }); + let captured = self.untag_block_handler(block, block_handler); // Guard captured->code.iseq is the profiled block iseq. Compare the raw imemo pointer: // type inference (from_value) can't type an iseq imemo, so guard it as a CPtr identity. @@ -3157,9 +3157,7 @@ impl Function { if_false: BranchEdge { target: fallback_block, args: vec![] }, }); - // captured = block_handler & ~0x3 (struct rb_captured_block *) - let untag_mask = self.push_insn(dispatch_block, Insn::Const { val: Const::CInt64(!0x3) }); - let captured = self.push_insn(dispatch_block, Insn::IntAnd { left: block_handler, right: untag_mask }); + let captured = self.untag_block_handler(dispatch_block, block_handler); let captured_iseq = self.load_captured_code_iseq(dispatch_block, captured); let mut compare_block = dispatch_block; From b50b8a8f980f810b11504408360447b205a59198 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 18 Aug 2026 11:27:48 -0700 Subject: [PATCH 08/14] ZJIT: Comment on direct_iseqs collection in invokeblock --- zjit/src/hir.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index fad0185c683c45..ad35619aeb16cb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -10053,8 +10053,8 @@ fn add_iseq_to_hir( && block_handler_class.is_some_and(|obj| unsafe { rb_IMEMO_TYPE_P(obj, imemo_ifunc) == 1 }); // Collect the profiled ISEQ blocks that can be invoked directly with a JIT-to-JIT call. - // The first entry of buckets is the most common type, so the hottest block is compared first. let mut fallback_reason = InvokeBlockNotSpecialized; + // The first entry of buckets is the most common type, so it will be inserted to direct_iseqs first too. let mut direct_iseqs: Vec = vec![]; if let Some(summary) = block_handler_summary.as_ref() { if can_direct_invoke_block(flags) @@ -10067,8 +10067,10 @@ fn add_iseq_to_hir( if unsafe { rb_IMEMO_TYPE_P(obj, imemo_iseq) == 1 } { let iseq = obj.as_iseq(); match can_direct_invoke_block_iseq(iseq, args.len()) { - Ok(()) if !direct_iseqs.contains(&iseq) => direct_iseqs.push(iseq), - Ok(()) => {} + // Push an ISEQ that can be dispatched directly, deduplicating direct_iseqs. + Ok(()) => if !direct_iseqs.contains(&iseq) { + direct_iseqs.push(iseq); + } Err(reason) => fallback_reason = reason, } } From eae86776f0f2443c2d1f8ca1cd19b83be9e16d0e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 3 Aug 2026 07:31:49 +0900 Subject: [PATCH 09/14] [ruby/rubygems] Fix gem uninstall --user-install crash when GEM_HOME does not exist Gem::Uninstaller#initialize called File.realpath on Gem.dir, which raises Errno::ENOENT when only user-installed gems exist. Skip the realpath resolution when the directory is missing, as already done for Gem.user_dir. https://github.com/ruby/rubygems/issues/9149 https://github.com/ruby/rubygems/commit/488da16da5 Co-Authored-By: Claude Fable 5 --- lib/rubygems/uninstaller.rb | 3 ++- test/rubygems/test_gem_uninstaller.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index fe4c3a80cf923c..28bf33ea710605 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -64,7 +64,8 @@ def initialize(gem, options = {}) @gem = gem @version = options[:version] || Gem::Requirement.default @install_dir = options[:install_dir] - @gem_home = File.realpath(@install_dir || Gem.dir) + gem_home = @install_dir || Gem.dir + @gem_home = File.exist?(gem_home) ? File.realpath(gem_home) : gem_home @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir @force_executables = options[:executables] @force_all = options[:all] diff --git a/test/rubygems/test_gem_uninstaller.rb b/test/rubygems/test_gem_uninstaller.rb index 92ea01a3bca2dd..d1aacb40536bd0 100644 --- a/test/rubygems/test_gem_uninstaller.rb +++ b/test/rubygems/test_gem_uninstaller.rb @@ -451,6 +451,24 @@ def test_uninstall_user_install assert_same uninstaller, @post_uninstall_hook_arg end + def test_uninstall_user_install_with_missing_gem_home + FileUtils.rm_rf Gem.dir + + Gem::Specification.dirs = [Gem.user_dir] + + uninstaller = Gem::Uninstaller.new(@user_spec.name, + executables: true, + user_install: true) + + gem_dir = @user_spec.gem_dir + + assert_path_exist gem_dir + + uninstaller.uninstall + + assert_path_not_exist gem_dir + end + def test_uninstall_user_install_with_symlinked_home pend "Symlinks not supported or not enabled" unless symlink_supported? From 527f0f3cd30ad2cd7ab11da47438f8b87747e421 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 18 Aug 2026 18:39:07 +0900 Subject: [PATCH 10/14] [ruby/rubygems] Fix wrong $LOAD_PATH for gems installed in the same process EndpointSpecification#load_paths goes through full_require_paths, which reads raw_require_paths, so it reported "/lib" regardless of what the installed gemspec declares. Also drop source caches after install, since a resolution happening after that point would otherwise materialize against a pre-install snapshot of installed gems. Fixes https://github.com/ruby/rubygems/pull/9781. https://github.com/ruby/rubygems/commit/f247c6429d Co-Authored-By: Claude Fable 5 --- lib/bundler/endpoint_specification.rb | 13 +++++++++++ lib/bundler/installer.rb | 6 +++++ spec/bundler/runtime/inline_spec.rb | 32 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 5ef2d53eae3517..02d3ac37028166 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -50,6 +50,19 @@ def require_paths end end + # `require_paths` is overridden above, but `full_require_paths` (and so + # `load_paths`) is computed from `raw_require_paths`, which would otherwise + # report the default `lib` for every gem + def raw_require_paths + if @remote_specification + @remote_specification.raw_require_paths + elsif _local_specification + _local_specification.raw_require_paths + else + super + end + end + # needed for inline def load_paths # remote specs aren't installed, and can't have load_paths diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 0f6d59a7161c61..47f7b5b1e9e473 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -83,6 +83,12 @@ def run(options) install(options) Gem::Specification.reset # invalidate gem specification cache so that installed gems are immediately available + # Drop the source caches too, since releasing resolution memory dropped + # the source indexes. A resolution happening after this point would + # otherwise rebuild them around a snapshot of installed gems taken + # before the install, and materialize against remote specs that don't + # know where the gems they stand for ended up. + @definition.sources.clear_cache lock Standalone.new(options[:standalone], @definition).generate if options[:standalone] diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb index c6f9bbdbd7326f..540a0fa47cd218 100644 --- a/spec/bundler/runtime/inline_spec.rb +++ b/spec/bundler/runtime/inline_spec.rb @@ -706,6 +706,38 @@ def confirm(msg, newline = nil) expect(err).to be_empty end + it "sets up custom require paths for gems installed in the same process" do + build_repo4 do + build_gem "securerandom", "999" + + build_gem "unusual_paths", "1.0.0", no_default: true do |s| + s.require_paths = ["lib/unusual_paths"] + s.write "lib/unusual_paths/unusual_paths.rb", "UNUSUAL_PATHS = '1.0.0'" + end + end + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + # Simulate a platform where installing in a subprocess is not possible, so + # that the conflicting default gem forces a re-resolution in this process, + # after the gems have been installed. + class << Process + undef_method :fork + end + + gemfile(true) do + source "https://gem.repo4" + gem "securerandom" + gem "unusual_paths" + end + + puts UNUSUAL_PATHS + RUBY + + expect(out).to include("Installing unusual_paths 1.0.0") + expect(out).to include("1.0.0") + expect(err).to be_empty + end + it "installs a conflicting default gem alongside git sources" do build_repo4 do build_gem "securerandom", "999" From 5a96f89fecc5e04e98d6f20fcf97c5cdf64b0133 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 19 Aug 2026 10:28:04 +0900 Subject: [PATCH 11/14] [ruby/rubygems] Make the loaded-constant assertion distinct from the install message The "1.0.0" assertion was subsumed by the "Installing unusual_paths 1.0.0" line, so it could not detect a load failure on its own. https://github.com/ruby/rubygems/commit/48717bd339 Co-Authored-By: Claude Fable 5 --- spec/bundler/runtime/inline_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb index 540a0fa47cd218..487ac38fe2aa04 100644 --- a/spec/bundler/runtime/inline_spec.rb +++ b/spec/bundler/runtime/inline_spec.rb @@ -712,7 +712,7 @@ def confirm(msg, newline = nil) build_gem "unusual_paths", "1.0.0", no_default: true do |s| s.require_paths = ["lib/unusual_paths"] - s.write "lib/unusual_paths/unusual_paths.rb", "UNUSUAL_PATHS = '1.0.0'" + s.write "lib/unusual_paths/unusual_paths.rb", "UNUSUAL_PATHS = 'loaded'" end end @@ -734,7 +734,7 @@ class << Process RUBY expect(out).to include("Installing unusual_paths 1.0.0") - expect(out).to include("1.0.0") + expect(out).to include("loaded") expect(err).to be_empty end From 7550e7f5e4956f665010944fc7fe53ef83711a2f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 17 Aug 2026 16:37:44 -0400 Subject: [PATCH 12/14] ZJIT: Add test for shareable opt_new in multi-ractor mode --- zjit/src/hir/opt_tests.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 5282d08645e9b7..73f1e669edba20 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6184,6 +6184,40 @@ mod hir_opt_tests { "); } + #[test] + fn test_opt_new_with_shareable_object_multi_ractor() { + eval(" + def test = Object.new + Ractor.new {}.value + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:BasicObject = GetConstantPath 0x1000 + v12:NilClass = Const Value(nil) + v15:CBool = IsMethodCFunc v10, :new + CondBranch v15, bb6(), bb4() + bb6(): + v17:HeapBasicObject = ObjectAlloc v10 + PatchPoint NoSingletonClass(Object@0x1010) + PatchPoint MethodRedefined(Object@0x1010, initialize@0x1018, cme:0x1020) + v43:ObjectExact = GuardType v17, ObjectExact recompile + CheckInterrupts + Return v43 + bb4(): + SideExit NoProfileSend recompile + "); + } + #[test] fn test_opt_new_basic_object() { eval(" From 9603604dabf87210c2880d2305289f228fe65b1d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 17 Aug 2026 16:44:31 -0400 Subject: [PATCH 13/14] ZJIT: De-duplicate constcache_shareable helper into jit.c --- jit.c | 6 ++++++ yjit.c | 6 ------ yjit/bindgen/src/main.rs | 2 +- yjit/src/codegen.rs | 2 +- yjit/src/cruby_bindings.inc.rs | 2 +- zjit.c | 6 ------ zjit/bindgen/src/main.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/jit.c b/jit.c index 114d69808a8f37..fa672f1feef23a 100644 --- a/jit.c +++ b/jit.c @@ -586,6 +586,12 @@ rb_jit_multi_ractor_p(void) return rb_multi_ractor_p(); } +bool +rb_jit_constcache_shareable(const struct iseq_inline_constant_cache_entry *ice) +{ + return (ice->flags & IMEMO_CONST_CACHE_SHAREABLE) != 0; +} + // Acquire the VM lock and then signal all other Ruby threads (ractors) to // contend for the VM lock, putting them to sleep. ZJIT and YJIT use this to // evict threads running inside generated code so among other things, it can diff --git a/yjit.c b/yjit.c index 01e14fb772628c..e10c10f26d992b 100644 --- a/yjit.c +++ b/yjit.c @@ -373,12 +373,6 @@ rb_ENCODING_GET(VALUE obj) return RB_ENCODING_GET(obj); } -bool -rb_yjit_constcache_shareable(const struct iseq_inline_constant_cache_entry *ice) -{ - return (ice->flags & IMEMO_CONST_CACHE_SHAREABLE) != 0; -} - // For running write barriers from Rust. Required when we add a new edge in the // object graph from `old` to `young`. void diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index c5b5fbf451e5eb..93dc9b4be6a5b4 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -258,7 +258,6 @@ fn main() { .allowlist_function("rb_full_cfunc_return") .allowlist_function("rb_assert_(iseq|cme)_handle") .allowlist_function("rb_IMEMO_TYPE_P") - .allowlist_function("rb_yjit_constcache_shareable") .allowlist_function("rb_iseq_reset_jit_func") .allowlist_function("rb_yjit_dump_iseq_loc") .allowlist_function("rb_yjit_obj_written") @@ -279,6 +278,7 @@ fn main() { .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_complex_p") .allowlist_function("rb_jit_multi_ractor_p") + .allowlist_function("rb_jit_constcache_shareable") .allowlist_function("rb_jit_vm_lock_then_barrier") .allowlist_function("rb_jit_vm_unlock") .allowlist_function("rb_jit_for_each_iseq") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 556181e3e36bf2..2fefb0319406bc 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -10481,7 +10481,7 @@ fn gen_opt_getconstant_path( } let cref_sensitive = !unsafe { (*ice).ic_cref }.is_null(); - let is_shareable = unsafe { rb_yjit_constcache_shareable(ice) }; + let is_shareable = unsafe { rb_jit_constcache_shareable(ice) }; let needs_checks = cref_sensitive || (!is_shareable && !assume_single_ractor_mode(jit, asm)); if needs_checks { diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 9a915a14ebc2ef..75ff2642244b79 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1208,7 +1208,6 @@ extern "C" { pub fn rb_yjit_iseq_inspect(iseq: *const rb_iseq_t) -> *mut ::std::os::raw::c_char; pub fn rb_RSTRUCT_SET(st: VALUE, k: ::std::os::raw::c_int, v: VALUE); pub fn rb_ENCODING_GET(obj: VALUE) -> ::std::os::raw::c_int; - pub fn rb_yjit_constcache_shareable(ice: *const iseq_inline_constant_cache_entry) -> bool; pub fn rb_yjit_obj_written( old: VALUE, young: VALUE, @@ -1328,6 +1327,7 @@ extern "C" { pub fn rb_set_cfp_sp(cfp: *mut rb_control_frame_struct, sp: *mut VALUE); pub fn rb_jit_shape_complex_p(shape_id: shape_id_t) -> bool; pub fn rb_jit_multi_ractor_p() -> bool; + pub fn rb_jit_constcache_shareable(ice: *const iseq_inline_constant_cache_entry) -> bool; pub fn rb_jit_vm_lock_then_barrier( recursive_lock_level: *mut ::std::os::raw::c_uint, file: *const ::std::os::raw::c_char, diff --git a/zjit.c b/zjit.c index 52b80f536ce289..f915b4a8b5a525 100644 --- a/zjit.c +++ b/zjit.c @@ -86,12 +86,6 @@ rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit extern VALUE *rb_vm_base_ptr(struct rb_control_frame_struct *cfp); -bool -rb_zjit_constcache_shareable(const struct iseq_inline_constant_cache_entry *ice) -{ - return (ice->flags & IMEMO_CONST_CACHE_SHAREABLE) != 0; -} - // Convert a given ISEQ's instructions to zjit_* instructions void rb_zjit_profile_enable(const rb_iseq_t *iseq) diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 30e0aa5578aa10..773c24aff45d04 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -352,6 +352,7 @@ fn main() { .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_complex_p") .allowlist_function("rb_jit_multi_ractor_p") + .allowlist_function("rb_jit_constcache_shareable") .allowlist_function("rb_jit_vm_lock_then_barrier") .allowlist_function("rb_jit_vm_unlock") .allowlist_function("rb_jit_for_each_iseq") @@ -407,7 +408,6 @@ fn main() { .allowlist_function("rb_get_cfp_ep") .allowlist_function("rb_get_cfp_ep_level") .allowlist_function("rb_get_cme_def_type") - .allowlist_function("rb_zjit_constcache_shareable") .allowlist_function("rb_zjit_vm_search_method") .allowlist_function("rb_zjit_cme_is_cfunc") .allowlist_function("rb_get_cme_def_body_attr_id") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index a7087f8cf5a6a6..32b76962b8262c 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2335,7 +2335,6 @@ unsafe extern "C" { pub fn rb_zjit_profile_disable(iseq: *const rb_iseq_t); pub fn rb_zjit_insn_to_bare_insn(insn: ::std::os::raw::c_int) -> ::std::os::raw::c_int; pub fn rb_vm_base_ptr(cfp: *mut rb_control_frame_struct) -> *mut VALUE; - pub fn rb_zjit_constcache_shareable(ice: *const iseq_inline_constant_cache_entry) -> bool; pub fn rb_zjit_iseq_insn_set( iseq: *const rb_iseq_t, insn_idx: ::std::os::raw::c_uint, @@ -2459,6 +2458,7 @@ unsafe extern "C" { pub fn rb_set_cfp_sp(cfp: *mut rb_control_frame_struct, sp: *mut VALUE); pub fn rb_jit_shape_complex_p(shape_id: shape_id_t) -> bool; pub fn rb_jit_multi_ractor_p() -> bool; + pub fn rb_jit_constcache_shareable(ice: *const iseq_inline_constant_cache_entry) -> bool; pub fn rb_jit_vm_lock_then_barrier( recursive_lock_level: *mut ::std::os::raw::c_uint, file: *const ::std::os::raw::c_char, From 7838416cec9649128a6c74282d4a6d3cd7e2f848 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 17 Aug 2026 16:47:50 -0400 Subject: [PATCH 14/14] ZJIT: Allow shareable consts in multi-ractor mode Port https://github.com/ruby/ruby/pull/11917 to ZJIT. --- zjit/src/hir.rs | 5 +- zjit/src/hir/opt_tests.rs | 654 +++++++++++++++++++------------------- zjit/src/hir/tests.rs | 31 +- 3 files changed, 343 insertions(+), 347 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ad35619aeb16cb..da312c1e980d2a 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9099,7 +9099,10 @@ fn add_iseq_to_hir( let ic: *const iseq_inline_constant_cache = get_arg(pc, 0).as_ptr(); let idlist: *const ID = unsafe { (*ic).segments }; let ice = unsafe { (*ic).entry }; - let result = if !ice.is_null() && (unsafe { (*ice).ic_cref }.is_null() && fun.assume_single_ractor_mode(block, exit_id)) { + let can_fold = !ice.is_null() + && unsafe { (*ice).ic_cref }.is_null() + && (unsafe { rb_jit_constcache_shareable(ice) } || fun.assume_single_ractor_mode(block, exit_id)); + let result = if can_fold { // Invalidate output code on any constant writes associated with constants // referenced after the PatchPoint. fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::StableConstantNames { idlist }, state: exit_id }); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 73f1e669edba20..82037629c170a9 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2056,13 +2056,12 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v28:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile PushInlineFrame :foo, v28 (0x1038), num_args=0 - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1060, DEBUG) - v59:NilClass = Const Value(nil) + v58:NilClass = Const Value(nil) CheckInterrupts PopInlineFrame - v123:NilClass = Const Value(nil) - Return v123 + v120:NilClass = Const Value(nil) + Return v120 "); } @@ -2097,13 +2096,12 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v28:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile PushInlineFrame :foo, v28 (0x1038), num_args=0 - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1060, CALL_BLOCK) - v62:NilClass = Const Value(nil) + v61:NilClass = Const Value(nil) CheckInterrupts PopInlineFrame - v132:NilClass = Const Value(nil) - Return v132 + v129:NilClass = Const Value(nil) + Return v129 "); } @@ -3680,16 +3678,15 @@ mod hir_opt_tests { v6:NilClass = Const Value(nil) Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, M) - v15:ModuleExact[M@0x1008] = Const Value(VALUE(0x1008)) + v14:ModuleExact[M@0x1008] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(Module@0x1010) PatchPoint MethodRedefined(Module@0x1010, name@0x1018, cme:0x1020) - v33:StringExact|NilClass = CCall v15, :Module#name@0x1048 + v32:StringExact|NilClass = CCall v14, :Module#name@0x1048 PatchPoint NoEPEscape(test) - v23:Fixnum[1] = Const Value(1) + v22:Fixnum[1] = Const Value(1) CheckInterrupts - Return v23 + Return v22 "); } @@ -3739,11 +3736,10 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v12 + Return v11 "); } @@ -3764,18 +3760,17 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v12:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) PatchPoint StableConstantNames(0x1010, Class) - v16:ClassSubclass[Class@0x1018] = Const Value(VALUE(0x1018)) + v14:ClassSubclass[Class@0x1018] = Const Value(VALUE(0x1018)) PatchPoint StableConstantNames(0x1020, Module) - v20:ClassSubclass[Module@0x1028] = Const Value(VALUE(0x1028)) + v17:ClassSubclass[Module@0x1028] = Const Value(VALUE(0x1028)) PatchPoint StableConstantNames(0x1030, BasicObject) - v24:ClassSubclass[BasicObject@0x1038] = Const Value(VALUE(0x1038)) - v26:ArrayExact = NewArray v12, v16, v20, v24 + v20:ClassSubclass[BasicObject@0x1038] = Const Value(VALUE(0x1038)) + v22:ArrayExact = NewArray v11, v14, v17, v20 CheckInterrupts - Return v26 + Return v22 "); } @@ -3796,14 +3791,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Enumerable) - v12:ModuleExact[Enumerable@0x1008] = Const Value(VALUE(0x1008)) + v11:ModuleExact[Enumerable@0x1008] = Const Value(VALUE(0x1008)) PatchPoint StableConstantNames(0x1010, Kernel) - v16:ModuleSubclass[Kernel@0x1018] = Const Value(VALUE(0x1018)) - v18:ArrayExact = NewArray v12, v16 + v14:ModuleSubclass[Kernel@0x1018] = Const Value(VALUE(0x1018)) + v16:ArrayExact = NewArray v11, v14 CheckInterrupts - Return v18 + Return v16 "); } @@ -3826,11 +3820,10 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, MY_MODULE) - v12:ModuleSubclass[MY_MODULE@0x1008] = Const Value(VALUE(0x1008)) + v11:ModuleSubclass[MY_MODULE@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v12 + Return v11 "); } @@ -4049,14 +4042,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, M) - v12:ModuleExact[M@0x1008] = Const Value(VALUE(0x1008)) + v11:ModuleExact[M@0x1008] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(Module@0x1010) PatchPoint MethodRedefined(Module@0x1010, class@0x1018, cme:0x1020) - v23:ClassSubclass[Module@0x1010] = Const Value(VALUE(0x1010)) + v22:ClassSubclass[Module@0x1010] = Const Value(VALUE(0x1010)) CheckInterrupts - Return v23 + Return v22 "); } @@ -6040,11 +6032,10 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Kernel) - v12:ModuleSubclass[Kernel@0x1008] = Const Value(VALUE(0x1008)) + v11:ModuleSubclass[Kernel@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v12 + Return v11 "); } @@ -6071,11 +6062,10 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Foo::Bar::C) - v12:ClassSubclass[Foo::Bar::C@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[Foo::Bar::C@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v12 + Return v11 "); } @@ -6097,16 +6087,15 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v44:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v43:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) CheckInterrupts - Return v44 + Return v43 "); } @@ -6132,25 +6121,25 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) - v17:Fixnum[1] = Const Value(1) + v11:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) + v16:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v47:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v46:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame :initialize, v47 (0x1068), num_args=1 - v65:CShape = LoadField v47, :shape_id@0x1090 - v66:CShape[0x1091] = GuardBitEquals v65, CShape(0x1091) recompile - StoreField v47, :@x@0x1092, v17 - WriteBarrier v47, v17 - v69:CShape[0x1093] = Const CShape(0x1093) - StoreField v47, :shape_id@0x1090, v69 + PushInlineFrame :initialize, v46 (0x1068), num_args=1 + PatchPoint SingleRactorMode + v64:CShape = LoadField v46, :shape_id@0x1090 + v65:CShape[0x1091] = GuardBitEquals v64, CShape(0x1091) recompile + StoreField v46, :@x@0x1092, v16 + WriteBarrier v46, v16 + v68:CShape[0x1093] = Const CShape(0x1093) + StoreField v46, :shape_id@0x1090, v68 CheckInterrupts PopInlineFrame - Return v47 + Return v46 "); } @@ -6171,16 +6160,15 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Object) - v12:ClassSubclass[Object@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[Object@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1008, new@0x1009, cme:0x1010) - v44:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) + v43:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) PatchPoint NoSingletonClass(Object@0x1008) PatchPoint MethodRedefined(Object@0x1008, initialize@0x1038, cme:0x1040) CheckInterrupts - Return v44 + Return v43 "); } @@ -6193,6 +6181,40 @@ mod hir_opt_tests { "); assert_snapshot!(hir_string("test"), @" fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, Object) + v11:ClassSubclass[Object@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) + PatchPoint MethodRedefined(Object@0x1008, new@0x1009, cme:0x1010) + v43:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) + PatchPoint NoSingletonClass(Object@0x1008) + PatchPoint MethodRedefined(Object@0x1008, initialize@0x1038, cme:0x1040) + CheckInterrupts + Return v43 + "); + } + + #[test] + fn test_opt_new_with_non_shareable_object_multi_ractor() { + eval(" + class Factory + def new = Object.new + end + FACTORY = Factory.new + def test = FACTORY.new + Ractor.new {}.value + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:6: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -6208,13 +6230,22 @@ mod hir_opt_tests { CondBranch v15, bb6(), bb4() bb6(): v17:HeapBasicObject = ObjectAlloc v10 - PatchPoint NoSingletonClass(Object@0x1010) - PatchPoint MethodRedefined(Object@0x1010, initialize@0x1018, cme:0x1020) - v43:ObjectExact = GuardType v17, ObjectExact recompile - CheckInterrupts - Return v43 - bb4(): SideExit NoProfileSend recompile + bb4(): + PatchPoint NoSingletonClass(Factory@0x1010) + PatchPoint MethodRedefined(Factory@0x1010, new@0x1018, cme:0x1020) + v42:ObjectSubclass[class_exact:Factory] = GuardType v10, ObjectSubclass[class_exact:Factory] recompile + PushInlineFrame :new, v42 (0x1048), num_args=0 + PatchPoint StableConstantNames(0x1070, Object) + v50:ClassSubclass[Object@0x1078] = Const Value(VALUE(0x1078)) + v52:NilClass = Const Value(nil) + PatchPoint MethodRedefined(Object@0x1078, new@0x1018, cme:0x1080) + v86:ObjectExact = ObjectAllocClass Object:VALUE(0x1078) + PatchPoint NoSingletonClass(Object@0x1078) + PatchPoint MethodRedefined(Object@0x1078, initialize@0x10a8, cme:0x10b0) + CheckInterrupts + PopInlineFrame + Return v86 "); } @@ -6235,16 +6266,15 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, BasicObject) - v12:ClassSubclass[BasicObject@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[BasicObject@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(BasicObject@0x1008, new@0x1009, cme:0x1010) - v44:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) + v43:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) PatchPoint NoSingletonClass(BasicObject@0x1008) PatchPoint MethodRedefined(BasicObject@0x1008, initialize@0x1038, cme:0x1040) CheckInterrupts - Return v44 + Return v43 "); } @@ -6265,34 +6295,33 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Hash) - v12:ClassSubclass[Hash@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[Hash@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010) - v44:HashExact = ObjectAllocClass Hash:VALUE(0x1008) + v43:HashExact = ObjectAllocClass Hash:VALUE(0x1008) PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040) - v48:Fixnum[0] = Const Value(0) - v97:Fixnum[0] = Const Value(0) - v98:NilClass = Const Value(nil) - PushInlineFrame :initialize, v44 (0x1068), num_args=1 - v64:TrueClass = Const Value(true) - v82:CPtr = GetEP 0 - v83:CUInt64 = LoadField v82, :VM_ENV_DATA_INDEX_FLAGS@0x1090 - v84:CBool = IsBlockParamModified v83 - CondBranch v84, bb11(), bb12() + v47:Fixnum[0] = Const Value(0) + v96:Fixnum[0] = Const Value(0) + v97:NilClass = Const Value(nil) + PushInlineFrame :initialize, v43 (0x1068), num_args=1 + v63:TrueClass = Const Value(true) + v81:CPtr = GetEP 0 + v82:CUInt64 = LoadField v81, :VM_ENV_DATA_INDEX_FLAGS@0x1090 + v83:CBool = IsBlockParamModified v82 + CondBranch v83, bb11(), bb12() bb11(): - v86:BasicObject = LoadField v82, :block@0x1091 - Jump bb13(v86) + v85:BasicObject = LoadField v81, :block@0x1091 + Jump bb13(v85) bb12(): - v88:BasicObject = GetBlockParam :block, l0, EP@4 - Jump bb13(v88) - bb13(v81:BasicObject): - v91:BasicObject = InvokeBuiltin rb_hash_init, v44, v48, v64, v64, v81 + v87:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb13(v87) + bb13(v80:BasicObject): + v90:BasicObject = InvokeBuiltin rb_hash_init, v43, v47, v63, v63, v80 CheckInterrupts PopInlineFrame - Return v44 + Return v43 "); assert_snapshot!(inspect("test"), @"{}"); } @@ -6314,16 +6343,15 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Array) - v12:ClassSubclass[Array@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) - v17:Fixnum[1] = Const Value(1) + v11:ClassSubclass[Array@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) + v16:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Array@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v55:BasicObject = CCallVariadic v12, :Array.new@0x1040, v17 + v54:BasicObject = CCallVariadic v11, :Array.new@0x1040, v16 CheckInterrupts - Return v55 + Return v54 "); } @@ -6344,18 +6372,17 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Set) - v12:ClassSubclass[Set@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[Set@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(Set@0x1008, new@0x1009, cme:0x1010) - v19:HeapBasicObject = ObjectAlloc v12 + v18:HeapBasicObject = ObjectAlloc v11 PatchPoint NoSingletonClass(Set@0x1008) PatchPoint MethodRedefined(Set@0x1008, initialize@0x1038, cme:0x1040) - v47:SetExact = GuardType v19, SetExact recompile - v48:BasicObject = CCallVariadic v47, :Set#initialize@0x1068 + v46:SetExact = GuardType v18, SetExact recompile + v47:BasicObject = CCallVariadic v46, :Set#initialize@0x1068 CheckInterrupts - Return v47 + Return v46 "); } @@ -6376,15 +6403,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v12:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) + v11:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(String@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v52:BasicObject = CCallVariadic v12, :String.new@0x1040 + v51:BasicObject = CCallVariadic v11, :String.new@0x1040 CheckInterrupts - Return v52 + Return v51 "); } @@ -6405,19 +6431,18 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Regexp) - v12:ClassSubclass[Regexp@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) - v17:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) - v18:StringExact = StringCopy v17 + v11:ClassSubclass[Regexp@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) + v16:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) + v17:StringExact = StringCopy v16 PatchPoint MethodRedefined(Regexp@0x1008, new@0x1018, cme:0x1020) - v48:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) + v47:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) PatchPoint NoSingletonClass(Regexp@0x1008) PatchPoint MethodRedefined(Regexp@0x1008, initialize@0x1048, cme:0x1050) - v53:BasicObject = CCallVariadic v48, :Regexp#initialize@0x1078, v18 + v52:BasicObject = CCallVariadic v47, :Regexp#initialize@0x1078, v17 CheckInterrupts - Return v48 + Return v47 "); } @@ -6439,13 +6464,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, allocate@0x1018, cme:0x1020) - v23:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v22:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) CheckInterrupts - Return v23 + Return v22 "); } @@ -6469,13 +6493,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) - v14:Fixnum[1] = Const Value(1) - v16:BasicObject = Send v12, :allocate, v14 # SendFallbackReason: Argument count does not match parameter count + v11:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + v13:Fixnum[1] = Const Value(1) + v15:BasicObject = Send v11, :allocate, v13 # SendFallbackReason: Argument count does not match parameter count CheckInterrupts - Return v16 + Return v15 "); } @@ -6499,13 +6522,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, SC) - v12:ClassSubclass[Class@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[Class@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, allocate@0x1018, cme:0x1020) - v23:BasicObject = CCallWithFrame v12, :Class.allocate@0x1048 + v22:BasicObject = CCallWithFrame v11, :Class.allocate@0x1048 CheckInterrupts - Return v23 + Return v22 "); } @@ -9180,13 +9202,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Foo) - v12:ClassSubclass[Foo@0x1008] = Const Value(VALUE(0x1008)) - v14:Fixnum[100] = Const Value(100) + v11:ClassSubclass[Foo@0x1008] = Const Value(VALUE(0x1008)) + v13:Fixnum[100] = Const Value(100) PatchPoint MethodRedefined(Class@0x1010, identity@0x1018, cme:0x1020) CheckInterrupts - Return v14 + Return v13 "); } @@ -12319,15 +12340,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, H) - v12:HashExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:StaticSymbol[:a] = Const Value(VALUE(0x1010)) + v11:HashExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:StaticSymbol[:a] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(Hash@0x1018) PatchPoint MethodRedefined(Hash@0x1018, []@0x1020, cme:0x1028) - v27:BasicObject = HashAref v12, v14 + v26:BasicObject = HashAref v11, v13 CheckInterrupts - Return v27 + Return v26 "); } @@ -12452,15 +12472,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Thread) - v12:ClassSubclass[Thread@0x1008] = Const Value(VALUE(0x1008)) + v11:ClassSubclass[Thread@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) - v23:CPtr = LoadEC - v24:CPtr = LoadField v23, :thread_ptr@0x1048 - v25:BasicObject = LoadField v24, :self@0x1049 + v22:CPtr = LoadEC + v23:CPtr = LoadField v22, :thread_ptr@0x1048 + v24:BasicObject = LoadField v23, :self@0x1049 CheckInterrupts - Return v25 + Return v24 "); } @@ -15784,14 +15803,13 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, String) - v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) + v15:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) PatchPoint MethodRedefined(Class@0x1018, ===@0x1020, cme:0x1028) - v30:BoolExact = IsA v10, v16 + v29:BoolExact = IsA v10, v15 CheckInterrupts - Return v30 + Return v29 "); } @@ -15815,14 +15833,13 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Kernel) - v16:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) + v15:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) PatchPoint MethodRedefined(Module@0x1018, ===@0x1020, cme:0x1028) - v30:BoolExact = CCall v16, :Module#===@0x1050, v10 + v29:BoolExact = CCall v15, :Module#===@0x1050, v10 CheckInterrupts - Return v30 + Return v29 "); } @@ -15846,15 +15863,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, String) - v17:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) + v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1011, cme:0x1018) - v28:StringExact = GuardType v10, StringExact recompile - v29:BoolExact = IsA v28, v17 + v27:StringExact = GuardType v10, StringExact recompile + v28:BoolExact = IsA v27, v16 CheckInterrupts - Return v29 + Return v28 "); } @@ -15878,15 +15894,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Kernel) - v17:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) + v16:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, is_a?@0x1020, cme:0x1028) - v28:StringExact = GuardType v10, StringExact recompile - v29:BasicObject = CCallWithFrame v28, :Kernel#is_a?@0x1050, v17 + v27:StringExact = GuardType v10, StringExact recompile + v28:BasicObject = CCallWithFrame v27, :Kernel#is_a?@0x1050, v16 CheckInterrupts - Return v29 + Return v28 "); } @@ -15913,15 +15928,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Integer) - v17:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) + v16:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, is_a?@0x1020, cme:0x1028) - v32:StringExact = GuardType v10, StringExact recompile - v23:Fixnum[5] = Const Value(5) + v31:StringExact = GuardType v10, StringExact recompile + v22:Fixnum[5] = Const Value(5) CheckInterrupts - Return v23 + Return v22 "); } @@ -15948,14 +15962,13 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Integer) - v16:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) + v15:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) PatchPoint MethodRedefined(Class@0x1018, ===@0x1020, cme:0x1028) - v25:Fixnum[5] = Const Value(5) + v24:Fixnum[5] = Const Value(5) CheckInterrupts - Return v25 + Return v24 "); } @@ -15979,15 +15992,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, String) - v17:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) + v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, kind_of?@0x1011, cme:0x1018) - v28:StringExact = GuardType v10, StringExact recompile - v29:BoolExact = IsA v28, v17 + v27:StringExact = GuardType v10, StringExact recompile + v28:BoolExact = IsA v27, v16 CheckInterrupts - Return v29 + Return v28 "); } @@ -16011,15 +16023,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Kernel) - v17:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) + v16:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, kind_of?@0x1020, cme:0x1028) - v28:StringExact = GuardType v10, StringExact recompile - v29:BasicObject = CCallWithFrame v28, :Kernel#kind_of?@0x1050, v17 + v27:StringExact = GuardType v10, StringExact recompile + v28:BasicObject = CCallWithFrame v27, :Kernel#kind_of?@0x1050, v16 CheckInterrupts - Return v29 + Return v28 "); } @@ -16046,15 +16057,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Integer) - v17:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) + v16:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, kind_of?@0x1020, cme:0x1028) - v32:StringExact = GuardType v10, StringExact recompile - v23:Fixnum[5] = Const Value(5) + v31:StringExact = GuardType v10, StringExact recompile + v22:Fixnum[5] = Const Value(5) CheckInterrupts - Return v23 + Return v22 "); } @@ -16076,13 +16086,12 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[5] = Const Value(5) - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Integer) - v14:ClassSubclass[Integer@0x1008] = Const Value(VALUE(0x1008)) + v13:ClassSubclass[Integer@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Integer@0x1008, is_a?@0x1009, cme:0x1010) - v26:TrueClass = Const Value(true) + v25:TrueClass = Const Value(true) CheckInterrupts - Return v26 + Return v25 "); } @@ -16104,13 +16113,12 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[5] = Const Value(5) - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v14:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) + v13:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Integer@0x1010, is_a?@0x1018, cme:0x1020) - v26:FalseClass = Const Value(false) + v25:FalseClass = Const Value(false) CheckInterrupts - Return v26 + Return v25 "); } @@ -16137,12 +16145,12 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1000, O) v12:ArraySubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint StableConstantNames(0x1010, Array) - v16:ClassSubclass[Array@0x1018] = Const Value(VALUE(0x1018)) + v15:ClassSubclass[Array@0x1018] = Const Value(VALUE(0x1018)) PatchPoint NoSingletonClass(C@0x1020) PatchPoint MethodRedefined(C@0x1020, is_a?@0x1028, cme:0x1030) - v29:TrueClass = Const Value(true) + v28:TrueClass = Const Value(true) CheckInterrupts - Return v29 + Return v28 "); } @@ -16169,12 +16177,12 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1000, O) v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint StableConstantNames(0x1010, C) - v16:ClassSubclass[C@0x1018] = Const Value(VALUE(0x1018)) + v15:ClassSubclass[C@0x1018] = Const Value(VALUE(0x1018)) PatchPoint NoSingletonClass(C@0x1018) PatchPoint MethodRedefined(C@0x1018, is_a?@0x1019, cme:0x1020) - v29:TrueClass = Const Value(true) + v28:TrueClass = Const Value(true) CheckInterrupts - Return v29 + Return v28 "); } @@ -16196,15 +16204,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, O) - v12:StaticSymbol[:my_static_symbol] = Const Value(VALUE(0x1008)) + v11:StaticSymbol[:my_static_symbol] = Const Value(VALUE(0x1008)) PatchPoint StableConstantNames(0x1010, Symbol) - v16:ClassSubclass[Symbol@0x1018] = Const Value(VALUE(0x1018)) + v14:ClassSubclass[Symbol@0x1018] = Const Value(VALUE(0x1018)) PatchPoint MethodRedefined(Symbol@0x1018, is_a?@0x1019, cme:0x1020) - v28:TrueClass = Const Value(true) + v26:TrueClass = Const Value(true) CheckInterrupts - Return v28 + Return v26 "); } @@ -16464,15 +16471,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, X) - v12:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:Fixnum[0] = Const Value(0) + v11:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, []@0x1018, cme:0x1020) - v36:ModuleExact[VALUE(0x1048)] = Const Value(VALUE(0x1048)) + v35:ModuleExact[VALUE(0x1048)] = Const Value(VALUE(0x1048)) CheckInterrupts - Return v36 + Return v35 "); } @@ -16580,14 +16586,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, FROZEN_OBJ) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestFrozen@0x1010) PatchPoint MethodRedefined(TestFrozen@0x1010, a@0x1018, cme:0x1020) - v28:Fixnum[1] = Const Value(1) + v27:Fixnum[1] = Const Value(1) CheckInterrupts - Return v28 + Return v27 "); } @@ -16621,14 +16626,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, MULTI_FROZEN) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestMultiIvars@0x1010) PatchPoint MethodRedefined(TestMultiIvars@0x1010, b@0x1018, cme:0x1020) - v28:Fixnum[20] = Const Value(20) + v27:Fixnum[20] = Const Value(20) CheckInterrupts - Return v28 + Return v27 "); } @@ -16699,14 +16703,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, FROZEN_NIL) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestFrozenNil@0x1010) PatchPoint MethodRedefined(TestFrozenNil@0x1010, value@0x1018, cme:0x1020) - v28:NilClass = Const Value(nil) + v27:NilClass = Const Value(nil) CheckInterrupts - Return v28 + Return v27 "); } @@ -16779,14 +16782,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, FROZEN_READER) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestAttrReader@0x1010) PatchPoint MethodRedefined(TestAttrReader@0x1010, value@0x1018, cme:0x1020) - v28:Fixnum[42] = Const Value(42) + v27:Fixnum[42] = Const Value(42) CheckInterrupts - Return v28 + Return v27 "); } @@ -16818,14 +16820,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, FROZEN_SYM) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestFrozenSym@0x1010) PatchPoint MethodRedefined(TestFrozenSym@0x1010, sym@0x1018, cme:0x1020) - v28:StaticSymbol[:hello] = Const Value(VALUE(0x1048)) + v27:StaticSymbol[:hello] = Const Value(VALUE(0x1048)) CheckInterrupts - Return v28 + Return v27 "); } @@ -16857,14 +16858,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, FROZEN_TRUE) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestFrozenBool@0x1010) PatchPoint MethodRedefined(TestFrozenBool@0x1010, flag@0x1018, cme:0x1020) - v28:TrueClass = Const Value(true) + v27:TrueClass = Const Value(true) CheckInterrupts - Return v28 + Return v27 "); } @@ -16938,20 +16938,19 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, NESTED_FROZEN) - v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(TestNestedAccess@0x1010) PatchPoint MethodRedefined(TestNestedAccess@0x1010, x@0x1018, cme:0x1020) - v49:Fixnum[100] = Const Value(100) + v47:Fixnum[100] = Const Value(100) PatchPoint StableConstantNames(0x1048, NESTED_FROZEN) - v18:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v16:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(TestNestedAccess@0x1010, y@0x1050, cme:0x1058) - v51:Fixnum[200] = Const Value(200) + v49:Fixnum[200] = Const Value(200) PatchPoint MethodRedefined(Integer@0x1080, +@0x1088, cme:0x1090) - v52:Fixnum[300] = Const Value(300) + v50:Fixnum[300] = Const Value(300) CheckInterrupts - Return v52 + Return v50 "); } @@ -16973,15 +16972,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, S) - v12:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, bytesize@0x1018, cme:0x1020) - v24:CInt64 = LoadField v12, :len@0x1048 - v25:Fixnum = BoxFixnum v24 + v23:CInt64 = LoadField v11, :len@0x1048 + v24:Fixnum = BoxFixnum v23 CheckInterrupts - Return v25 + Return v24 "); } @@ -18535,32 +18533,31 @@ mod hir_opt_tests { bb11(): v85:Falsy = RefineType v75, Falsy PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020) - v131:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile - v132:BasicObject = CCallWithFrame v131, :Kernel#lambda@0x1048, block=0x1050 + v130:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile + v131:BasicObject = CCallWithFrame v130, :Kernel#lambda@0x1048, block=0x1050 v89:CPtr = GetEP 0 v90:BasicObject = LoadField v89, :list@0x1001 v91:BasicObject = LoadField v89, :sep@0x1002 v92:BasicObject = LoadField v89, :iter_method@0x1005 v93:BasicObject = LoadField v89, :kwsplat@0x1006 - SetLocal :sep, l0, EP@5, v132 - Jump bb8(v90, v132, v92, v93) + SetLocal :sep, l0, EP@5, v131 + Jump bb8(v90, v131, v92, v93) bb8(v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1078, CONST) - v107:HashExact[VALUE(0x1080)] = Const Value(VALUE(0x1080)) - SetLocal :kwsplat, l0, EP@3, v107 - v112:CPtr = GetEP 0 - v113:BasicObject = LoadField v112, :list@0x1001 - v115:CPtr = GetEP 0 - v116:BasicObject = LoadField v115, :iter_method@0x1005 - v118:BasicObject = Send v113, 0x1088, :__send__, v116 # SendFallbackReason: Send: unsupported method type Optimized - v119:CPtr = GetEP 0 - v120:BasicObject = LoadField v119, :list@0x1001 - v121:BasicObject = LoadField v119, :sep@0x1002 - v122:BasicObject = LoadField v119, :iter_method@0x1005 - v123:BasicObject = LoadField v119, :kwsplat@0x1006 + v106:HashExact[VALUE(0x1080)] = Const Value(VALUE(0x1080)) + SetLocal :kwsplat, l0, EP@3, v106 + v111:CPtr = GetEP 0 + v112:BasicObject = LoadField v111, :list@0x1001 + v114:CPtr = GetEP 0 + v115:BasicObject = LoadField v114, :iter_method@0x1005 + v117:BasicObject = Send v112, 0x1088, :__send__, v115 # SendFallbackReason: Send: unsupported method type Optimized + v118:CPtr = GetEP 0 + v119:BasicObject = LoadField v118, :list@0x1001 + v120:BasicObject = LoadField v118, :sep@0x1002 + v121:BasicObject = LoadField v118, :iter_method@0x1005 + v122:BasicObject = LoadField v118, :kwsplat@0x1006 CheckInterrupts - Return v118 + Return v117 "); } @@ -20027,15 +20024,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, A) - v12:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v11:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, length@0x1018, cme:0x1020) - v27:CInt64[4] = Const CInt64(4) - v26:Fixnum = BoxFixnum v27 + v26:CInt64[4] = Const CInt64(4) + v25:Fixnum = BoxFixnum v26 CheckInterrupts - Return v26 + Return v25 "); } @@ -21663,91 +21659,91 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Point) - v12:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) - v14:NilClass = Const Value(nil) - v17:Fixnum[1] = Const Value(1) - v19:Fixnum[2] = Const Value(2) + v11:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) + v13:NilClass = Const Value(nil) + v16:Fixnum[1] = Const Value(1) + v18:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v89:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v87:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame :initialize, v89 (0x1068), num_args=2 - v121:CShape = LoadField v89, :shape_id@0x1090 - v122:CShape[0x1091] = GuardBitEquals v121, CShape(0x1091) recompile - StoreField v89, :@x@0x1092, v17 - WriteBarrier v89, v17 - v125:CShape[0x1093] = Const CShape(0x1093) - StoreField v89, :shape_id@0x1090, v125 + PushInlineFrame :initialize, v87 (0x1068), num_args=2 + PatchPoint SingleRactorMode + v119:CShape = LoadField v87, :shape_id@0x1090 + v120:CShape[0x1091] = GuardBitEquals v119, CShape(0x1091) recompile + StoreField v87, :@x@0x1092, v16 + WriteBarrier v87, v16 + v123:CShape[0x1093] = Const CShape(0x1093) + StoreField v87, :shape_id@0x1090, v123 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v89, :@y@0x1094, v19 - WriteBarrier v89, v19 - v140:CShape[0x1095] = Const CShape(0x1095) - StoreField v89, :shape_id@0x1090, v140 + StoreField v87, :@y@0x1094, v18 + WriteBarrier v87, v18 + v138:CShape[0x1095] = Const CShape(0x1095) + StoreField v87, :shape_id@0x1090, v138 CheckInterrupts PopInlineFrame - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1098, Point) - v46:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) - v48:NilClass = Const Value(nil) - v51:Fixnum[1] = Const Value(1) - v53:Fixnum[2] = Const Value(2) + v44:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) + v46:NilClass = Const Value(nil) + v49:Fixnum[1] = Const Value(1) + v51:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v99:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v97:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame :initialize, v99 (0x1068), num_args=2 - v161:CShape = LoadField v99, :shape_id@0x1090 - v162:CShape[0x1091] = GuardBitEquals v161, CShape(0x1091) recompile - StoreField v99, :@x@0x1092, v51 - WriteBarrier v99, v51 - v165:CShape[0x1093] = Const CShape(0x1093) - StoreField v99, :shape_id@0x1090, v165 + PushInlineFrame :initialize, v97 (0x1068), num_args=2 + PatchPoint SingleRactorMode + v159:CShape = LoadField v97, :shape_id@0x1090 + v160:CShape[0x1091] = GuardBitEquals v159, CShape(0x1091) recompile + StoreField v97, :@x@0x1092, v49 + WriteBarrier v97, v49 + v163:CShape[0x1093] = Const CShape(0x1093) + StoreField v97, :shape_id@0x1090, v163 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v99, :@y@0x1094, v53 - WriteBarrier v99, v53 - v180:CShape[0x1095] = Const CShape(0x1095) - StoreField v99, :shape_id@0x1090, v180 + StoreField v97, :@y@0x1094, v51 + WriteBarrier v97, v51 + v178:CShape[0x1095] = Const CShape(0x1095) + StoreField v97, :shape_id@0x1090, v178 CheckInterrupts PopInlineFrame PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, ==@0x10a0, cme:0x10a8) - PushInlineFrame :==, v89 (0x10d0), num_args=1 + PushInlineFrame :==, v87 (0x10d0), num_args=1 PatchPoint SingleRactorMode - v199:CShape = LoadField v89, :shape_id@0x1090 - v200:CShape[0x1095] = GuardBitEquals v199, CShape(0x1095) recompile - v201:BasicObject = LoadField v89, :@x@0x1092 + v197:CShape = LoadField v87, :shape_id@0x1090 + v198:CShape[0x1095] = GuardBitEquals v197, CShape(0x1095) recompile + v199:BasicObject = LoadField v87, :@x@0x1092 PatchPoint NoEPEscape(==) PatchPoint MethodRedefined(Point@0x1008, x@0x10f8, cme:0x1100) PatchPoint MethodRedefined(Integer@0x1128, ==@0x10a0, cme:0x1130) - v255:Fixnum = GuardType v201, Fixnum recompile - v257:BoolExact = FixnumEq v255, v51 - v212:CBool = Test v257 - v213:FalseClass = RefineType v257, Falsy - CondBranch v212, bb19(), bb18(v213) + v253:Fixnum = GuardType v199, Fixnum recompile + v255:BoolExact = FixnumEq v253, v49 + v210:CBool = Test v255 + v211:FalseClass = RefineType v255, Falsy + CondBranch v210, bb19(), bb18(v211) bb19(): PatchPoint SingleRactorMode - v220:CShape = LoadField v89, :shape_id@0x1090 - v221:CShape[0x1095] = GuardBitEquals v220, CShape(0x1095) recompile - v222:BasicObject = LoadField v89, :@y@0x1094 + v218:CShape = LoadField v87, :shape_id@0x1090 + v219:CShape[0x1095] = GuardBitEquals v218, CShape(0x1095) recompile + v220:BasicObject = LoadField v87, :@y@0x1094 PatchPoint NoEPEscape(==) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, y@0x1158, cme:0x1160) - v262:CShape = LoadField v99, :shape_id@0x1090 - v263:CShape[0x1095] = GuardBitEquals v262, CShape(0x1095) recompile - v264:BasicObject = LoadField v99, :@y@0x1094 + v260:CShape = LoadField v97, :shape_id@0x1090 + v261:CShape[0x1095] = GuardBitEquals v260, CShape(0x1095) recompile + v262:BasicObject = LoadField v97, :@y@0x1094 PatchPoint MethodRedefined(Integer@0x1128, ==@0x10a0, cme:0x1130) - v267:Fixnum = GuardType v222, Fixnum recompile - v268:Fixnum = GuardType v264, Fixnum - v269:BoolExact = FixnumEq v267, v268 - Jump bb18(v269) - bb18(v234:BoolExact): + v265:Fixnum = GuardType v220, Fixnum recompile + v266:Fixnum = GuardType v262, Fixnum + v267:BoolExact = FixnumEq v265, v266 + Jump bb18(v267) + bb18(v232:BoolExact): CheckInterrupts PopInlineFrame - Return v234 + Return v232 "); } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index edf7389b714929..444e81f0751fa7 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -476,22 +476,21 @@ pub(crate) mod hir_build_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:NilClass = Const Value(nil) - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1008, Integer) - v20:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) - v22:BasicObject = CheckMatch v10, v20, CASE - v24:CBool = Test v22 - v25:Truthy = RefineType v22, Truthy - CondBranch v24, bb4(v9, v10, v14, v10), bb5() - bb4(v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject): - v45:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v45 + v19:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) + v21:BasicObject = CheckMatch v10, v19, CASE + v23:CBool = Test v21 + v24:Truthy = RefineType v21, Truthy + CondBranch v23, bb4(v9, v10, v14, v10), bb5() + bb4(v36:BasicObject, v37:BasicObject, v38:NilClass, v39:BasicObject): + v44:Fixnum[1] = Const Value(1) + CheckInterrupts + Return v44 bb5(): - v27:Falsy = RefineType v22, Falsy - v32:Fixnum[2] = Const Value(2) + v26:Falsy = RefineType v21, Falsy + v31:Fixnum[2] = Const Value(2) CheckInterrupts - Return v32 + Return v31 "); } @@ -2211,9 +2210,8 @@ pub(crate) mod hir_build_tests { PatchPoint NoEPEscape(test) v18:CPtr = LoadSP v19:BasicObject = LoadField v18, :block@0x1000 - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1030, ::RubyVM::ZJIT) - v25:ModuleSubclass[RubyVM::ZJIT@0x1038] = Const Value(VALUE(0x1038)) + v24:ModuleSubclass[RubyVM::ZJIT@0x1038] = Const Value(VALUE(0x1038)) SideExit DirectiveInduced "); } @@ -2252,9 +2250,8 @@ pub(crate) mod hir_build_tests { v17:Fixnum[1] = Const Value(1) v22:BasicObject = Send v11, 0x1008, :consume # SendFallbackReason: Uncategorized(send) PatchPoint NoEPEscape(test) - PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1030, ::RubyVM::ZJIT) - v30:ModuleSubclass[RubyVM::ZJIT@0x1038] = Const Value(VALUE(0x1038)) + v29:ModuleSubclass[RubyVM::ZJIT@0x1038] = Const Value(VALUE(0x1038)) SideExit DirectiveInduced "); }