From a8f269a2c665c0a471ecb28b3265cb4eb8a7ca2e Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Thu, 20 Nov 2025 14:47:01 -0500 Subject: [PATCH 01/29] ZJIT: Deduplicate successor and predecessor sets (#15263) Fixes https://github.com/Shopify/ruby/issues/877 I didn't consider the ability to have the successor or predecessor sets having duplicates when originally crafting the Iongraph support PR, but have added this to prevent that happening in the future. I don't think it interferes with the underlying Iongraph implementation, but it doesn't really make sense. I think this kind of behaviour happens when there are multiple jump instructions that go to the same basic block within a given block. --- zjit/src/hir.rs | 13 ++++++++++--- zjit/src/hir/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bbe5dd3435b75b..face61f1f67a7b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9,7 +9,7 @@ use crate::{ cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json }; use std::{ - cell::RefCell, collections::{HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter + cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter }; use crate::hir_type::{Type, types}; use crate::bitset::BitSet; @@ -5849,7 +5849,13 @@ impl<'a> ControlFlowInfo<'a> { // Since ZJIT uses extended basic blocks, one must check all instructions // for their ability to jump to another basic block, rather than just // the instructions at the end of a given basic block. - let successors: Vec = block + // + // Use BTreeSet to avoid duplicates and maintain an ordering. Also + // `BTreeSet` provides conversion trivially back to an `Vec`. + // Ordering is important so that the expect tests that serialize the predecessors + // and successors don't fail intermittently. + // todo(aidenfoxivey): Use `BlockSet` in lieu of `BTreeSet` + let successors: BTreeSet = block .insns .iter() .map(|&insn_id| uf.find_const(insn_id)) @@ -5867,7 +5873,8 @@ impl<'a> ControlFlowInfo<'a> { } // Store successors for this block. - successor_map.insert(block_id, successors); + // Convert successors from a `BTreeSet` to a `Vec`. + successor_map.insert(block_id, successors.iter().copied().collect()); } Self { diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 5e6ec118922b02..b487352748601a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -3484,6 +3484,27 @@ pub mod hir_build_tests { assert!(cfi.is_succeeded_by(bb1, bb0)); assert!(cfi.is_succeeded_by(bb3, bb1)); } + + #[test] + fn test_cfi_deduplicated_successors_and_predecessors() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + + // Construct two separate jump instructions. + let v1 = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: v1, target: edge(bb1)}); + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + let retval = function.push_insn(bb1, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb1, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + + assert_eq!(cfi.predecessors(bb1).collect::>().len(), 1); + assert_eq!(cfi.successors(bb0).collect::>().len(), 1); + } } /// Test dominator set computations. From 0b4420bfd5f32fd715704f9adcf2320971f05ab3 Mon Sep 17 00:00:00 2001 From: Steven Johnstone Date: Thu, 20 Nov 2025 11:02:33 +0000 Subject: [PATCH 02/29] [ruby/prism] Use memmove for overlapping memory ranges Fixes https://github.com/ruby/prism/pull/3736. https://github.com/ruby/prism/commit/1f5f192ab7 --- prism/prism.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prism/prism.c b/prism/prism.c index a87932f1b77a00..45817cdd8c05b2 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -13467,7 +13467,7 @@ parse_target_implicit_parameter(pm_parser_t *parser, pm_node_t *node) { // remaining nodes down to fill the gap. This is extremely unlikely // to happen. if (index != implicit_parameters->size - 1) { - memcpy(&implicit_parameters->nodes[index], &implicit_parameters->nodes[index + 1], (implicit_parameters->size - index - 1) * sizeof(pm_node_t *)); + memmove(&implicit_parameters->nodes[index], &implicit_parameters->nodes[index + 1], (implicit_parameters->size - index - 1) * sizeof(pm_node_t *)); } implicit_parameters->size--; From ba47c2f03305be10b7c28f7ff6783a533b57ef15 Mon Sep 17 00:00:00 2001 From: Thiago Araujo Date: Wed, 19 Nov 2025 21:05:29 -0700 Subject: [PATCH 03/29] [ruby/prism] Add tests to `regexp_encoding_option_mismatch` related to #2667 https://github.com/ruby/prism/commit/44f075bae4 --- test/prism/errors_test.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb index 9abed9265212d0..bd7a8a638166c5 100644 --- a/test/prism/errors_test.rb +++ b/test/prism/errors_test.rb @@ -64,6 +64,24 @@ def test_invalid_message_name assert_equal :"", Prism.parse_statement("+.@foo,+=foo").write_name end + def test_regexp_encoding_option_mismatch_error + # UTF-8 char with ASCII-8BIT modifier + result = Prism.parse('/Ȃ/n') + assert_includes result.errors.map(&:type), :regexp_encoding_option_mismatch + + # UTF-8 char with EUC-JP modifier + result = Prism.parse('/Ȃ/e') + assert_includes result.errors.map(&:type), :regexp_encoding_option_mismatch + + # UTF-8 char with Windows-31J modifier + result = Prism.parse('/Ȃ/s') + assert_includes result.errors.map(&:type), :regexp_encoding_option_mismatch + + # UTF-8 char with UTF-8 modifier + result = Prism.parse('/Ȃ/u') + assert_empty result.errors + end + private def assert_errors(filepath, version) From cb9c7a6a0a451fc158055145d0bce4891b62e32f Mon Sep 17 00:00:00 2001 From: eileencodes Date: Wed, 19 Nov 2025 12:08:43 -0500 Subject: [PATCH 04/29] [ruby/rubygems] Improve error messages and handling in tests This is a first pass to improve the way errors are handled and raised in bundler's tests. The goal is to clean up tests and modernize them - these were some obvious areas that could be cleaned up. - Instead of raising "ZOMG" in the load error tests, it now tests for the actual error and gem raising. - Improve error messages where applicable. - All errors raise a specific error class, rather than falling back to a default and just setting a message. - Removed arguments and `bundle_dir` option from `TheBundle` class as it wasn't actually used so therefore we don't need to raise an error for extra arguments. - Removed error from `BundlerBuilder`, as it won't work if it's not `bundler`, also it never uses `name`. The only reaon `name` is passed in is because of metaprogramming on loading the right builder. I think that should eventually be refactored. - Replaced and removed `update_repo3` and `update_repo4` in favor of just `build_repo3` and `build_repo4`. Rather than tell someone writing tests to use a different method, automatically use the right method. https://github.com/ruby/rubygems/commit/68c39c8451 --- spec/bundler/bundler/retry_spec.rb | 2 +- spec/bundler/commands/exec_spec.rb | 4 +-- spec/bundler/commands/install_spec.rb | 7 ++-- spec/bundler/commands/lock_spec.rb | 4 +-- spec/bundler/commands/update_spec.rb | 6 ++-- spec/bundler/install/gemfile/gemspec_spec.rb | 7 ++-- spec/bundler/install/gemfile/groups_spec.rb | 6 ++-- spec/bundler/install/gemfile/sources_spec.rb | 2 +- .../install/gems/compact_index_spec.rb | 6 ++-- .../install/gems/native_extensions_spec.rb | 8 ++--- spec/bundler/install/gems/standalone_spec.rb | 8 ++--- spec/bundler/plugins/install_spec.rb | 2 +- spec/bundler/realworld/edgecases_spec.rb | 2 +- spec/bundler/runtime/setup_spec.rb | 32 +++++++++++-------- .../artifice/compact_index_etag_match.rb | 2 +- spec/bundler/support/builders.rb | 26 ++++++++------- spec/bundler/support/helpers.rb | 12 +++---- spec/bundler/support/matchers.rb | 2 +- spec/bundler/support/the_bundle.rb | 8 ++--- 19 files changed, 76 insertions(+), 70 deletions(-) diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb index ffbc07807429e6..7481622a967d9d 100644 --- a/spec/bundler/bundler/retry_spec.rb +++ b/spec/bundler/bundler/retry_spec.rb @@ -12,7 +12,7 @@ end it "returns the first valid result" do - jobs = [proc { raise "foo" }, proc { :bar }, proc { raise "foo" }] + jobs = [proc { raise "job 1 failed" }, proc { :bar }, proc { raise "job 2 failed" }] attempts = 0 result = Bundler::Retry.new(nil, nil, 3).attempt do attempts += 1 diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb index 03f1d839c76c9a..1ac308bdda4bfc 100644 --- a/spec/bundler/commands/exec_spec.rb +++ b/spec/bundler/commands/exec_spec.rb @@ -1034,7 +1034,7 @@ def bin_path(a,b,c) puts 'Started' # For process sync STDOUT.flush sleep 1 # ignore quality_spec - raise "Didn't receive INT at all" + raise RuntimeError, "Didn't receive expected INT" end.join rescue Interrupt puts "foo" @@ -1218,7 +1218,7 @@ def require(path) build_repo4 do build_gem "openssl", openssl_version do |s| s.write("lib/openssl.rb", <<-RUBY) - raise "custom openssl should not be loaded, it's not in the gemfile!" + raise ArgumentError, "custom openssl should not be loaded" RUBY end end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index 3ab7d4ab880059..0dbe950f87b6d7 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -688,13 +688,12 @@ it "fails gracefully when downloading an invalid specification from the full index" do build_repo2(build_compact_index: false) do build_gem "ajp-rails", "0.0.0", gemspec: false, skip_validation: true do |s| - bad_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]] + invalid_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]] s. instance_variable_get(:@spec). - instance_variable_set(:@dependencies, bad_deps) - - raise "failed to set bad deps" unless s.dependencies == bad_deps + instance_variable_set(:@dependencies, invalid_deps) end + build_gem "ruby-ajp", "1.0.0" end diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb index 6ba69d466a9b8c..ab1926734c1e78 100644 --- a/spec/bundler/commands/lock_spec.rb +++ b/spec/bundler/commands/lock_spec.rb @@ -834,7 +834,7 @@ bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } expect(lockfile).to end_with("BUNDLED WITH\n 55\n") - update_repo4 do + build_repo4 do build_gem "bundler", "99" end @@ -1456,7 +1456,7 @@ before do gemfile_with_rails_weakling_and_foo_from_repo4 - update_repo4 do + build_repo4 do build_gem "foo", "2.0" end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index 61d8ece2798a75..cdaeb75c4a3399 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -247,7 +247,7 @@ expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") - update_repo4 do + build_repo4 do build_gem "slim", "4.0.0" do |s| s.add_dependency "tilt", [">= 2.0.6", "< 2.1"] end @@ -572,7 +572,7 @@ expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") - update_repo4 do + build_repo4 do build_gem "b", "2.0" do |s| s.add_dependency "c", "< 2" end @@ -976,7 +976,7 @@ bundle "update", all: true expect(out).to match(/Resolving dependencies\.\.\.\.*\nBundle updated!/) - update_repo4 do + build_repo4 do build_gem "foo", "2.0" end diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb index daa977fc9b7f9f..3d9766d21ff66f 100644 --- a/spec/bundler/install/gemfile/gemspec_spec.rb +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -420,12 +420,13 @@ end build_lib "foo", path: bundled_app do |s| - if platform_specific_type == :runtime + case platform_specific_type + when :runtime s.add_runtime_dependency dependency - elsif platform_specific_type == :development + when :development s.add_development_dependency dependency else - raise "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + raise ArgumentError, "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" end end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb index 32de3f2be2dcc3..4727d5ef9b02cf 100644 --- a/spec/bundler/install/gemfile/groups_spec.rb +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -25,7 +25,7 @@ puts ACTIVESUPPORT R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- activesupport/) end it "installs gems with inline :groups into those groups" do @@ -36,7 +36,7 @@ puts THIN R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up everything if Bundler.setup is used with no groups" do @@ -57,7 +57,7 @@ puts THIN RUBY - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up old groups when they have previously been removed" do diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb index bdc61c2b2694e1..c0b4d98f1c5cc6 100644 --- a/spec/bundler/install/gemfile/sources_spec.rb +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -570,7 +570,7 @@ bundle :install, artifice: "compact_index" # And then we add some new versions... - update_repo4 do + build_repo4 do build_gem "foo", "0.2" build_gem "bar", "0.3" end diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb index cd64f85f9295a7..bb4d4011f5b94f 100644 --- a/spec/bundler/install/gems/compact_index_spec.rb +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -770,7 +770,7 @@ def start gem 'myrack', '0.9.1' G - update_repo4 do + build_repo4 do build_gem "myrack", "1.0.0" end @@ -811,7 +811,7 @@ def start gem 'myrack', '0.9.1' G - update_repo4 do + build_repo4 do build_gem "myrack", "1.0.0" end @@ -833,7 +833,7 @@ def start gem 'myrack', '0.9.1' G - update_repo4 do + build_repo4 do build_gem "myrack", "1.0.0" end diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb index 874818fa874f92..7f230d132b9eab 100644 --- a/spec/bundler/install/gems/native_extensions_spec.rb +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -9,7 +9,7 @@ require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -53,7 +53,7 @@ require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -97,7 +97,7 @@ require "mkmf" name = "c_extension_bundle_#{n}" dir_config(name) - raise "OMG" unless with_config("c_extension_#{n}") == "#{n}" + raise ArgumentError unless with_config("c_extension_#{n}") == "#{n}" create_makefile(name) E @@ -149,7 +149,7 @@ require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola" + raise ArgumentError unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola" create_makefile(name) E diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb index d5f6c896cd6642..37997ffe482409 100644 --- a/spec/bundler/install/gems/standalone_spec.rb +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -385,7 +385,7 @@ RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end it "allows `without` configuration to limit the groups used in a standalone" do @@ -403,7 +403,7 @@ RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end it "allows `path` configuration to change the location of the standalone bundle" do @@ -437,7 +437,7 @@ RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end end @@ -519,6 +519,6 @@ RUBY expect(out).to eq("1.0.0") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end end diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb index 0cddeb09185bc7..6cace961f5228b 100644 --- a/spec/bundler/plugins/install_spec.rb +++ b/spec/bundler/plugins/install_spec.rb @@ -168,7 +168,7 @@ def exec(command, args) build_repo2 do build_plugin "chaplin" do |s| s.write "plugins.rb", <<-RUBY - raise "I got you man" + raise RuntimeError, "threw exception on load" RUBY end end diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb index 86b4c91a073685..391aa0cef6575a 100644 --- a/spec/bundler/realworld/edgecases_spec.rb +++ b/spec/bundler/realworld/edgecases_spec.rb @@ -16,7 +16,7 @@ def rubygems_version(name, requirement) index.search(#{name.dump}).select {|spec| requirement.satisfied_by?(spec.version) }.last end if rubygem.nil? - raise "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ + raise ArgumentError, "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ "Found specs:\n\#{index.send(:specs).inspect}" end puts "#{name} (\#{rubygem.version})" diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb index 9268d9d1cc4545..1ffaffef0ed20e 100644 --- a/spec/bundler/runtime/setup_spec.rb +++ b/spec/bundler/runtime/setup_spec.rb @@ -728,46 +728,52 @@ def clean_load_path(lp) G run <<-R - File.open(File.join(Gem.dir, "specifications", "broken.gemspec"), "w") do |f| + File.open(File.join(Gem.dir, "specifications", "invalid.gemspec"), "w") do |f| f.write <<-RUBY # -*- encoding: utf-8 -*- -# stub: broken 1.0.0 ruby lib +# stub: invalid 1.0.0 ruby lib Gem::Specification.new do |s| - s.name = "broken" + s.name = "invalid" s.version = "1.0.0" - raise "BROKEN GEMSPEC" + s.authors = ["Invalid Author"] + s.files = ["lib/invalid.rb"] + s.add_dependency "nonexistent-gem", "~> 999.999.999" + s.validate! end RUBY end R run <<-R - File.open(File.join(Gem.dir, "specifications", "broken-ext.gemspec"), "w") do |f| + File.open(File.join(Gem.dir, "specifications", "invalid-ext.gemspec"), "w") do |f| f.write <<-RUBY # -*- encoding: utf-8 -*- -# stub: broken-ext 1.0.0 ruby lib +# stub: invalid-ext 1.0.0 ruby lib # stub: a.ext\\0b.ext Gem::Specification.new do |s| - s.name = "broken-ext" + s.name = "invalid-ext" s.version = "1.0.0" - raise "BROKEN GEMSPEC EXT" + s.authors = ["Invalid Author"] + s.files = ["lib/invalid.rb"] + s.required_ruby_version = "~> 0.8.0" + s.validate! end RUBY end # Need to write the gem.build_complete file, # otherwise the full spec is loaded to check the installed_by_version extensions_dir = Gem.default_ext_dir_for(Gem.dir) || File.join(Gem.dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version) - Bundler::FileUtils.mkdir_p(File.join(extensions_dir, "broken-ext-1.0.0")) - File.open(File.join(extensions_dir, "broken-ext-1.0.0", "gem.build_complete"), "w") {} + Bundler::FileUtils.mkdir_p(File.join(extensions_dir, "invalid-ext-1.0.0")) + File.open(File.join(extensions_dir, "invalid-ext-1.0.0", "gem.build_complete"), "w") {} R run <<-R - puts "WIN" + puts "Success" R - expect(out).to eq("WIN") + expect(out).to eq("Success") end it "ignores empty gem paths" do @@ -1151,7 +1157,7 @@ def clean_load_path(lp) bundler_module = class << Bundler; self; end bundler_module.send(:remove_method, :require) def Bundler.require(path) - raise "LOSE" + raise StandardError, "didn't use binding from top level" end Bundler.load RUBY diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb index 08d7b5ec539129..6c621660513b1f 100644 --- a/spec/bundler/support/artifice/compact_index_etag_match.rb +++ b/spec/bundler/support/artifice/compact_index_etag_match.rb @@ -4,7 +4,7 @@ class CompactIndexEtagMatch < CompactIndexAPI get "/versions" do - raise "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] + raise ArgumentError, "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] headers "ETag" => env["HTTP_IF_NONE_MATCH"] status 304 body "" diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb index 31d4f30a3b96c0..6087ea8cc8c652 100644 --- a/spec/bundler/support/builders.rb +++ b/spec/bundler/support/builders.rb @@ -187,17 +187,25 @@ def build_repo2(**kwargs, &blk) # A repo that has no pre-installed gems included. (The caller completely # determines the contents with the block.) + # + # If the repo already exists, `#update_repo` will be called. def build_repo3(**kwargs, &blk) - raise "gem_repo3 already exists -- use update_repo3 instead" if File.exist?(gem_repo3) - build_repo gem_repo3, **kwargs, &blk + if File.exist?(gem_repo3) + update_repo(gem_repo3, &blk) + else + build_repo gem_repo3, **kwargs, &blk + end end # Like build_repo3, this is a repo that has no pre-installed gems included. - # We have two different methods for situations where two different empty - # sources are needed. + # + # If the repo already exists, `#udpate_repo` will be called def build_repo4(**kwargs, &blk) - raise "gem_repo4 already exists -- use update_repo4 instead" if File.exist?(gem_repo4) - build_repo gem_repo4, **kwargs, &blk + if File.exist?(gem_repo4) + update_repo gem_repo4, &blk + else + build_repo gem_repo4, **kwargs, &blk + end end def update_repo2(**kwargs, &blk) @@ -208,10 +216,6 @@ def update_repo3(&blk) update_repo(gem_repo3, &blk) end - def update_repo4(&blk) - update_repo(gem_repo4, &blk) - end - def build_security_repo build_repo security_repo do build_gem "myrack" @@ -420,8 +424,6 @@ def required_ruby_version=(*reqs) class BundlerBuilder def initialize(context, name, version) - raise "can only build bundler" unless name == "bundler" - @context = context @spec = Spec::Path.loaded_gemspec.dup @spec.version = version || Bundler::VERSION diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb index 12ff09b714a0fb..52e6ff5d9a3140 100644 --- a/spec/bundler/support/helpers.rb +++ b/spec/bundler/support/helpers.rb @@ -28,8 +28,8 @@ def reset! Gem.clear_paths end - def the_bundle(*args) - TheBundle.new(*args) + def the_bundle + TheBundle.new end MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/ @@ -54,7 +54,7 @@ def load_error_run(ruby, name, *args) begin #{ruby} rescue LoadError => e - warn "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + warn e.message if e.message.include?("-- #{name}") end RUBY opts = args.last.is_a?(Hash) ? args.pop : {} @@ -132,7 +132,7 @@ def load_error_ruby(ruby, name, opts = {}) begin #{ruby} rescue LoadError => e - warn "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + warn e.message if e.message.include?("-- #{name}") end R end @@ -324,7 +324,7 @@ def self.install_dev_bundler end def install_gem(path, install_dir, default = false) - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path) args = "--no-document --ignore-dependencies --verbose --local --install-dir #{install_dir}" @@ -415,7 +415,7 @@ def cache_gems(*gems, gem_repo: gem_repo1) gems.each do |g| path = "#{gem_repo}/gems/#{g}.gem" - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path) FileUtils.cp(path, "#{bundled_app}/vendor/cache") end end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb index 9f311fc0d77c39..5a3c38a4db361e 100644 --- a/spec/bundler/support/matchers.rb +++ b/spec/bundler/support/matchers.rb @@ -52,7 +52,7 @@ def failing_matcher end def self.define_compound_matcher(matcher, preconditions, &declarations) - raise "Must have preconditions to define a compound matcher" if preconditions.empty? + raise ArgumentError, "Must have preconditions to define a compound matcher" if preconditions.empty? define_method(matcher) do |*expected, &block_arg| Precondition.new( RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg), diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb index bda717f3b00bf3..452abd7d410171 100644 --- a/spec/bundler/support/the_bundle.rb +++ b/spec/bundler/support/the_bundle.rb @@ -8,10 +8,8 @@ class TheBundle attr_accessor :bundle_dir - def initialize(opts = {}) - opts = opts.dup - @bundle_dir = Pathname.new(opts.delete(:bundle_dir) { bundled_app }) - raise "Too many options! #{opts}" unless opts.empty? + def initialize + @bundle_dir = Pathname.new(bundled_app) end def to_s @@ -28,7 +26,7 @@ def lockfile end def locked_gems - raise "Cannot read lockfile if it doesn't exist" unless locked? + raise ArgumentError, "Cannot read lockfile if it doesn't exist" unless locked? Bundler::LockfileParser.new(lockfile.read) end From 59e0489248036f5923e04fdf16ce3d8244ed038d Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 20 Nov 2025 16:03:58 -0600 Subject: [PATCH 05/29] [DOC] Tweaks for String#upcase (#15244) --- doc/string/upcase.rdoc | 20 ++++++++++++++++++++ string.c | 11 +---------- 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 doc/string/upcase.rdoc diff --git a/doc/string/upcase.rdoc b/doc/string/upcase.rdoc new file mode 100644 index 00000000000000..5a9cce217f7972 --- /dev/null +++ b/doc/string/upcase.rdoc @@ -0,0 +1,20 @@ +Returns a new string containing the upcased characters in +self+: + + 'Hello, World!'.upcase # => "HELLO, WORLD!" + +The sizes of +self+ and the upcased result may differ: + + s = 'Straße' + s.size # => 6 + s.upcase # => "STRASSE" + s.upcase.size # => 7 + +Some characters (and some character sets) do not have upcased and downcased versions: + + s = 'よろしくお願いします' + s.upcase == s # => true + +The casing may be affected by the given +mapping+; +see {Case Mapping}[rdoc-ref:case_mapping.rdoc]. + +Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. diff --git a/string.c b/string.c index d78d7320be2b2b..c4ac00c3423e74 100644 --- a/string.c +++ b/string.c @@ -8001,16 +8001,7 @@ rb_str_upcase_bang(int argc, VALUE *argv, VALUE str) * call-seq: * upcase(mapping) -> string * - * Returns a string containing the upcased characters in +self+: - * - * s = 'Hello World!' # => "Hello World!" - * s.upcase # => "HELLO WORLD!" - * - * The casing may be affected by the given +mapping+; - * see {Case Mapping}[rdoc-ref:case_mapping.rdoc]. - * - * Related: String#upcase!, String#downcase, String#downcase!. - * + * :include: doc/string/upcase.rdoc */ static VALUE From a4a99a24e8299f9595cb41591f3252e6082d744f Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 19 Nov 2025 18:29:01 +0000 Subject: [PATCH 06/29] [DOC] TWeaks for String#upcase! --- string.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/string.c b/string.c index c4ac00c3423e74..9f5cd83c0da72e 100644 --- a/string.c +++ b/string.c @@ -7959,19 +7959,12 @@ upcase_single(VALUE str) * call-seq: * upcase!(mapping) -> self or nil * - * Upcases the characters in +self+; - * returns +self+ if any changes were made, +nil+ otherwise: + * Like String#upcase, except that: * - * s = 'Hello World!' # => "Hello World!" - * s.upcase! # => "HELLO WORLD!" - * s # => "HELLO WORLD!" - * s.upcase! # => nil - * - * The casing may be affected by the given +mapping+; - * see {Case Mapping}[rdoc-ref:case_mapping.rdoc]. - * - * Related: String#upcase, String#downcase, String#downcase!. + * - Changes character casings in +self+ (not in a copy of +self+). + * - Returns +self+ if any changes are made, +nil+ otherwise. * + * Related: See {Modifying}[rdoc-ref:String@Modifying]. */ static VALUE From 9b87a0b9b667ce12e9e6245d4eda40b5dfdeb5f9 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 4 Nov 2025 17:15:59 -0800 Subject: [PATCH 07/29] Fix missing write barrier on namespace classext Found by wbcheck It seems like here the classext was associated with the class, but it already had Ruby objects attached. rb_gc_writebarrier_remember works around that issue, but I suspect if we enabled autocompaction the values copied into the classext before it was attached could be broken. --- class.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/class.c b/class.c index 34bc7d100538e7..2f94b801471104 100644 --- a/class.c +++ b/class.c @@ -183,6 +183,12 @@ rb_class_set_box_classext(VALUE obj, const rb_box_t *box, rb_classext_t *ext) }; st_update(RCLASS_CLASSEXT_TBL(obj), (st_data_t)box->box_object, rb_class_set_box_classext_update, (st_data_t)&args); + + // FIXME: This is done here because this is the first time the objects in + // the classext are exposed via this class. It's likely that if GC + // compaction occurred between the VALUEs being copied in and this + // writebarrier trigger the values will be stale. + rb_gc_writebarrier_remember(obj); } RUBY_EXTERN rb_serial_t ruby_vm_global_cvar_state; From bd60600d00f13234cad83c9c5af5b6607a4b0fba Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Wed, 19 Nov 2025 15:59:08 +0100 Subject: [PATCH 08/29] [ruby/rubygems] Run git operations in parallel to speed things up: - ### Problem When you have a Gemfile that contains git gems, each repository will be fetched one by one. This is extremelly slow. A simple Gemfile with 5 git gems (small repositories) can take up to 10 seconds just to fetch the repos. We can speed this up by running multiple git process and fetching repositories silmutaneously. ### Solution The repositories are fetched in Bundler when `Source::Git#specs` is called. The problem is that `source.specs` is called in various places depending on Gemfile. I think the issue is that calling `source.specs` feels like that as a "side effect" we are going to clone repositories. I believe that fetching repositories should be an explicit call. For instance: ```ruby source "https://rubygems.org" gem "foo", github: "foo/foo" # The repository foo will be fetched as a side effect to the call to `source.spec_names` # https://github.com/ruby/rubygems/blob/6cc7d71dac3d0275c9727cf200c7acfbf6c78d37/bundler/lib/bundler/source_map.rb#L21 ``` ```ruby source "https://rubygems.org" gem "bar", source: "https://example.org" gem "foo", github: "foo/foo" # The repository foo will be fetched on a different codepath # https://github.com/ruby/rubygems/blob/6cc7d71dac3d0275c9727cf200c7acfbf6c78d37/bundler/lib/bundler/source/rubygems_aggregate.rb#L35 # That is because the gem "bar" has a source that doesn't have the `/dependencies` API # endpoint and therefore Bundler enters a different branch condition. ``` I opted to add a self explanatory call to fetch the git source repositories just before we start the resolution, and *just* before any other calls to `source.specs` is performed. https://github.com/ruby/rubygems/commit/f0ef526f23 --- lib/bundler/definition.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 3c8c13b1303171..21f3760e6d9d3f 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "lockfile_parser" +require_relative "worker" module Bundler class Definition @@ -1100,7 +1101,23 @@ def source_requirements @source_requirements ||= find_source_requirements end + def preload_git_source_worker + @preload_git_source_worker ||= Bundler::Worker.new(5, "Git source preloading", ->(source, _) { source.specs }) + end + + def preload_git_sources + sources.git_sources.each {|source| preload_git_source_worker.enq(source) } + ensure + preload_git_source_worker.stop + end + def find_source_requirements + if Gem.ruby_version >= "3.3" + # Ruby 3.2 has a bug that incorrectly triggers a circular dependency warning. This version will continue to + # fetch git repositories one by one. + preload_git_sources + end + # Record the specs available in each gem's source, so that those # specs will be available later when the resolver knows where to # look for that gemspec (or its dependencies) From 409c004affa30efbbfd384a9cd645f7969ccc11a Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Wed, 19 Nov 2025 23:15:41 +0100 Subject: [PATCH 09/29] [ruby/rubygems] Make the Bundler logger thread safe: - The Logger is not thread safe when calling `with_level`. This now becomes problematic because we are using multiple threads during the resolution phase in order to fetch git gems. https://github.com/ruby/rubygems/commit/380653ae74 --- lib/bundler/ui/shell.rb | 16 +++++++++------ spec/bundler/bundler/ui/shell_spec.rb | 28 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb index 6f080b64598f02..b836208da8e30c 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -17,6 +17,7 @@ def initialize(options = {}) @level = ENV["DEBUG"] ? "debug" : "info" @warning_history = [] @output_stream = :stdout + @thread_safe_logger_key = "logger_level_#{object_id}" end def add_color(string, *color) @@ -97,11 +98,13 @@ def level=(level) end def level(name = nil) - return @level unless name + current_level = Thread.current.thread_variable_get(@thread_safe_logger_key) || @level + return current_level unless name + unless index = LEVELS.index(name) raise "#{name.inspect} is not a valid level" end - index <= LEVELS.index(@level) + index <= LEVELS.index(current_level) end def output_stream=(symbol) @@ -167,12 +170,13 @@ def word_wrap(text, line_width = Thor::Terminal.terminal_width) end * "\n" end - def with_level(level) - original = @level - @level = level + def with_level(desired_level) + old_level = level + Thread.current.thread_variable_set(@thread_safe_logger_key, desired_level) + yield ensure - @level = original + Thread.current.thread_variable_set(@thread_safe_logger_key, old_level) end def with_output_stream(symbol) diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb index 422c850a6536e6..83f147191ef139 100644 --- a/spec/bundler/bundler/ui/shell_spec.rb +++ b/spec/bundler/bundler/ui/shell_spec.rb @@ -81,4 +81,32 @@ end end end + + describe "threads" do + it "is thread safe when using with_level" do + stop_thr1 = false + stop_thr2 = false + + expect(subject.level).to eq("debug") + + thr1 = Thread.new do + subject.silence do + sleep(0.1) until stop_thr1 + end + + stop_thr2 = true + end + + thr2 = Thread.new do + subject.silence do + stop_thr1 = true + sleep(0.1) until stop_thr2 + end + end + + [thr1, thr2].each(&:join) + + expect(subject.level).to eq("debug") + end + end end From 8b71234a4877b4bd2058cca2766ea9794fbcee41 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Thu, 20 Nov 2025 01:57:21 +0100 Subject: [PATCH 10/29] [ruby/rubygems] Change the logger instance for this spec: - With the logger change that is now threadsafe, such code no longer behaves the same: ```ruby Bundler.ui.silence do Bundler.ui.level = 'info' Bundler.ui.info("foo") # This used to output something. Now it doesn't. end ``` IMHO this is the right behaviour since we are in a silence block, changing the level should have no effect. And fortunately it seems that we only need to change this spec. The call to `Bundler.ui.silence` is done in a `around` block https://github.com/ruby/rubygems/blob/4a13684f07ebb1dea5501e3f826fab414f96bf47/bundler/spec/spec_helper.rb#L119 https://github.com/ruby/rubygems/commit/e716adb6c9 --- spec/bundler/commands/ssl_spec.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb index b4aca55194e23f..4220731b697073 100644 --- a/spec/bundler/commands/ssl_spec.rb +++ b/spec/bundler/commands/ssl_spec.rb @@ -16,16 +16,17 @@ end end - @previous_level = Bundler.ui.level - Bundler.ui.instance_variable_get(:@warning_history).clear - @previous_client = Gem::Request::ConnectionPools.client + @previous_ui = Bundler.ui + Bundler.ui = Bundler::UI::Shell.new Bundler.ui.level = "info" + + @previous_client = Gem::Request::ConnectionPools.client Artifice.activate_with(@dummy_endpoint) Gem::Request::ConnectionPools.client = Gem::Net::HTTP end after(:each) do - Bundler.ui.level = @previous_level + Bundler.ui = @previous_ui Artifice.deactivate Gem::Request::ConnectionPools.client = @previous_client end From d1b11592af75a5eee9199951a0c330eb8caa2825 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 19 Nov 2025 19:02:58 +0000 Subject: [PATCH 11/29] [DOC] Tweaks for String#upto --- doc/string/upto.rdoc | 38 ++++++++++++++++++++++++++++++++++++++ string.c | 28 +--------------------------- 2 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 doc/string/upto.rdoc diff --git a/doc/string/upto.rdoc b/doc/string/upto.rdoc new file mode 100644 index 00000000000000..f860fe84fe6485 --- /dev/null +++ b/doc/string/upto.rdoc @@ -0,0 +1,38 @@ +With a block given, calls the block with each +String+ value +returned by successive calls to String#succ; +the first value is +self+, the next is self.succ, and so on; +the sequence terminates when value +other_string+ is reached; +returns +self+: + + a = [] + 'a'.upto('f') {|c| a.push(c) } + a # => ["a", "b", "c", "d", "e", "f"] + + a = [] + 'Ж'.upto('П') {|c| a.push(c) } + a # => ["Ж", "З", "И", "Й", "К", "Л", "М", "Н", "О", "П"] + + a = [] + 'よ'.upto('ろ') {|c| a.push(c) } + a # => ["よ", "ら", "り", "る", "れ", "ろ"] + + a = [] + 'a8'.upto('b6') {|c| a.push(c) } + a # => ["a8", "a9", "b0", "b1", "b2", "b3", "b4", "b5", "b6"] + +If argument +exclusive+ is given as a truthy object, the last value is omitted: + + a = [] + 'a'.upto('f', true) {|c| a.push(c) } + a # => ["a", "b", "c", "d", "e"] + +If +other_string+ would not be reached, does not call the block: + + '25'.upto('5') {|s| fail s } + 'aa'.upto('a') {|s| fail s } + +With no block given, returns a new Enumerator: + + 'a8'.upto('b6') # => # + +Related: see {Iterating}[rdoc-ref:String@Iterating]. diff --git a/string.c b/string.c index 9f5cd83c0da72e..ebf36a7e08ebde 100644 --- a/string.c +++ b/string.c @@ -5463,33 +5463,7 @@ str_upto_i(VALUE str, VALUE arg) * upto(other_string, exclusive = false) {|string| ... } -> self * upto(other_string, exclusive = false) -> new_enumerator * - * With a block given, calls the block with each +String+ value - * returned by successive calls to String#succ; - * the first value is +self+, the next is self.succ, and so on; - * the sequence terminates when value +other_string+ is reached; - * returns +self+: - * - * 'a8'.upto('b6') {|s| print s, ' ' } # => "a8" - * Output: - * - * a8 a9 b0 b1 b2 b3 b4 b5 b6 - * - * If argument +exclusive+ is given as a truthy object, the last value is omitted: - * - * 'a8'.upto('b6', true) {|s| print s, ' ' } # => "a8" - * - * Output: - * - * a8 a9 b0 b1 b2 b3 b4 b5 - * - * If +other_string+ would not be reached, does not call the block: - * - * '25'.upto('5') {|s| fail s } - * 'aa'.upto('a') {|s| fail s } - * - * With no block given, returns a new Enumerator: - * - * 'a8'.upto('b6') # => # + * :include: doc/string/upto.rdoc * */ From ff1d23eccba3ab37e77bf2d2222cad9d6f99a0ab Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 5 Nov 2025 12:27:26 -0800 Subject: [PATCH 12/29] Use a serial to keep track of Mutex-owning Fiber Previously this held a pointer to the Fiber itself, which requires marking it (which was only implemented recently, prior to that it was buggy). Using a monotonically increasing integer instead allows us to avoid having a free function and keeps everything simpler. My main motivations in making this change are that the root fiber lazily allocates self, which makes the writebarrier implementation challenging to do correctly, and wanting to avoid sending Mutexes to the remembered set when locked by a short-lived Fiber. --- cont.c | 18 +++++++++++++ internal/cont.h | 1 + ruby_atomic.h | 23 +++++++++++++++++ thread.c | 14 +++++----- thread_sync.c | 69 ++++++++++++++++--------------------------------- 5 files changed, 71 insertions(+), 54 deletions(-) diff --git a/cont.c b/cont.c index f885cdb1095032..d167685f13bec3 100644 --- a/cont.c +++ b/cont.c @@ -268,6 +268,8 @@ struct rb_fiber_struct { unsigned int killed : 1; + rb_serial_t serial; + struct coroutine_context context; struct fiber_pool_stack stack; }; @@ -1010,6 +1012,13 @@ rb_fiber_threadptr(const rb_fiber_t *fiber) return fiber->cont.saved_ec.thread_ptr; } +rb_serial_t +rb_fiber_serial(const rb_fiber_t *fiber) +{ + VM_ASSERT(fiber->serial >= 1); + return fiber->serial; +} + static VALUE cont_thread_value(const rb_context_t *cont) { @@ -1995,6 +2004,13 @@ fiber_alloc(VALUE klass) return TypedData_Wrap_Struct(klass, &fiber_data_type, 0); } +static rb_serial_t +next_fiber_serial(void) +{ + static rbimpl_atomic_uint64_t fiber_serial = 1; + return (rb_serial_t)ATOMIC_U64_FETCH_ADD(fiber_serial, 1); +} + static rb_fiber_t* fiber_t_alloc(VALUE fiber_value, unsigned int blocking) { @@ -2011,6 +2027,7 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking) fiber->cont.type = FIBER_CONTEXT; fiber->blocking = blocking; fiber->killed = 0; + fiber->serial = next_fiber_serial(); cont_init(&fiber->cont, th); fiber->cont.saved_ec.fiber_ptr = fiber; @@ -2563,6 +2580,7 @@ rb_threadptr_root_fiber_setup(rb_thread_t *th) fiber->cont.saved_ec.thread_ptr = th; fiber->blocking = 1; fiber->killed = 0; + fiber->serial = next_fiber_serial(); fiber_status_set(fiber, FIBER_RESUMED); /* skip CREATED */ th->ec = &fiber->cont.saved_ec; cont_init_jit_cont(&fiber->cont); diff --git a/internal/cont.h b/internal/cont.h index 3c2528a02a6e77..21a054f37c1294 100644 --- a/internal/cont.h +++ b/internal/cont.h @@ -31,5 +31,6 @@ VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb VALUE rb_fiberptr_self(struct rb_fiber_struct *fiber); unsigned int rb_fiberptr_blocking(struct rb_fiber_struct *fiber); struct rb_execution_context_struct * rb_fiberptr_get_ec(struct rb_fiber_struct *fiber); +rb_serial_t rb_fiber_serial(const struct rb_fiber_struct *fiber); #endif /* INTERNAL_CONT_H */ diff --git a/ruby_atomic.h b/ruby_atomic.h index ad53356f069ce2..9eaa5a9651f96a 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -63,4 +63,27 @@ rbimpl_atomic_u64_set_relaxed(volatile rbimpl_atomic_uint64_t *address, uint64_t } #define ATOMIC_U64_SET_RELAXED(var, val) rbimpl_atomic_u64_set_relaxed(&(var), val) +static inline uint64_t +rbimpl_atomic_u64_fetch_add(volatile rbimpl_atomic_uint64_t *ptr, uint64_t val) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + return __atomic_fetch_add(ptr, val, __ATOMIC_SEQ_CST); +#elif defined(_WIN32) + return InterlockedExchangeAdd64((volatile LONG64 *)ptr, val); +#elif defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx)) + return atomic_add_64_nv(ptr, val) - val; +#elif defined(HAVE_STDATOMIC_H) + return atomic_fetch_add_explicit((_Atomic uint64_t *)ptr, val, memory_order_seq_cst); +#else + // Fallback using mutex for platforms without 64-bit atomics + static rb_native_mutex_t lock = RB_NATIVE_MUTEX_INITIALIZER; + rb_native_mutex_lock(&lock); + uint64_t old = *ptr; + *ptr = old + val; + rb_native_mutex_unlock(&lock); + return old; +#endif +} +#define ATOMIC_U64_FETCH_ADD(var, val) rbimpl_atomic_u64_fetch_add(&(var), val) + #endif diff --git a/thread.c b/thread.c index 5d75bf41228d3c..3e9bf3192d0c62 100644 --- a/thread.c +++ b/thread.c @@ -442,8 +442,8 @@ rb_threadptr_unlock_all_locking_mutexes(rb_thread_t *th) th->keeping_mutexes = mutex->next_mutex; // rb_warn("mutex #<%p> was not unlocked by thread #<%p>", (void *)mutex, (void*)th); - VM_ASSERT(mutex->fiber); - const char *error_message = rb_mutex_unlock_th(mutex, th, mutex->fiber); + VM_ASSERT(mutex->fiber_serial); + const char *error_message = rb_mutex_unlock_th(mutex, th, NULL); if (error_message) rb_bug("invalid keeping_mutexes: %s", error_message); } } @@ -5263,7 +5263,7 @@ rb_thread_shield_owned(VALUE self) rb_mutex_t *m = mutex_ptr(mutex); - return m->fiber == GET_EC()->fiber_ptr; + return m->fiber_serial == rb_fiber_serial(GET_EC()->fiber_ptr); } /* @@ -5282,7 +5282,7 @@ rb_thread_shield_wait(VALUE self) if (!mutex) return Qfalse; m = mutex_ptr(mutex); - if (m->fiber == GET_EC()->fiber_ptr) return Qnil; + if (m->fiber_serial == rb_fiber_serial(GET_EC()->fiber_ptr)) return Qnil; rb_thread_shield_waiting_inc(self); rb_mutex_lock(mutex); rb_thread_shield_waiting_dec(self); @@ -5799,8 +5799,8 @@ debug_deadlock_check(rb_ractor_t *r, VALUE msg) if (th->locking_mutex) { rb_mutex_t *mutex = mutex_ptr(th->locking_mutex); - rb_str_catf(msg, " mutex:%p cond:%"PRIuSIZE, - (void *)mutex->fiber, rb_mutex_num_waiting(mutex)); + rb_str_catf(msg, " mutex:%llu cond:%"PRIuSIZE, + (unsigned long long)mutex->fiber_serial, rb_mutex_num_waiting(mutex)); } { @@ -5840,7 +5840,7 @@ rb_check_deadlock(rb_ractor_t *r) } else if (th->locking_mutex) { rb_mutex_t *mutex = mutex_ptr(th->locking_mutex); - if (mutex->fiber == th->ec->fiber_ptr || (!mutex->fiber && !ccan_list_empty(&mutex->waitq))) { + if (mutex->fiber_serial == rb_fiber_serial(th->ec->fiber_ptr) || (!mutex->fiber_serial && !ccan_list_empty(&mutex->waitq))) { found = 1; } } diff --git a/thread_sync.c b/thread_sync.c index 0fc70224ff90ed..6cc23f7d87b32d 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -7,7 +7,7 @@ static VALUE rb_eClosedQueueError; /* Mutex */ typedef struct rb_mutex_struct { - rb_fiber_t *fiber; + rb_serial_t fiber_serial; VALUE thread; // even if the fiber is collected, we might need access to the thread in mutex_free struct rb_mutex_struct *next_mutex; struct ccan_list_head waitq; /* protected by GVL */ @@ -125,28 +125,7 @@ rb_thread_t* rb_fiber_threadptr(const rb_fiber_t *fiber); static bool locked_p(rb_mutex_t *mutex) { - return mutex->fiber != 0; -} - -static void -mutex_mark(void *ptr) -{ - rb_mutex_t *mutex = ptr; - VALUE fiber; - if (locked_p(mutex)) { - fiber = rb_fiberptr_self(mutex->fiber); // rb_fiber_t* doesn't move along with fiber object - if (fiber) rb_gc_mark_movable(fiber); - rb_gc_mark_movable(mutex->thread); - } -} - -static void -mutex_compact(void *ptr) -{ - rb_mutex_t *mutex = ptr; - if (locked_p(mutex)) { - mutex->thread = rb_gc_location(mutex->thread); - } + return mutex->fiber_serial != 0; } static void @@ -154,7 +133,7 @@ mutex_free(void *ptr) { rb_mutex_t *mutex = ptr; if (locked_p(mutex)) { - const char *err = rb_mutex_unlock_th(mutex, rb_thread_ptr(mutex->thread), mutex->fiber); + const char *err = rb_mutex_unlock_th(mutex, rb_thread_ptr(mutex->thread), NULL); if (err) rb_bug("%s", err); } ruby_xfree(ptr); @@ -168,8 +147,8 @@ mutex_memsize(const void *ptr) static const rb_data_type_t mutex_data_type = { "mutex", - {mutex_mark, mutex_free, mutex_memsize, mutex_compact,}, - 0, 0, RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY + {NULL, mutex_free, mutex_memsize,}, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; static rb_mutex_t * @@ -265,11 +244,7 @@ mutex_set_owner(VALUE self, rb_thread_t *th, rb_fiber_t *fiber) rb_mutex_t *mutex = mutex_ptr(self); mutex->thread = th->self; - mutex->fiber = fiber; - RB_OBJ_WRITTEN(self, Qundef, th->self); - if (fiber) { - RB_OBJ_WRITTEN(self, Qundef, rb_fiberptr_self(fiber)); - } + mutex->fiber_serial = rb_fiber_serial(fiber); } static void @@ -293,7 +268,7 @@ rb_mutex_trylock(VALUE self) { rb_mutex_t *mutex = mutex_ptr(self); - if (mutex->fiber == 0) { + if (mutex->fiber_serial == 0) { RUBY_DEBUG_LOG("%p ok", mutex); rb_fiber_t *fiber = GET_EC()->fiber_ptr; @@ -311,7 +286,7 @@ rb_mutex_trylock(VALUE self) static VALUE mutex_owned_p(rb_fiber_t *fiber, rb_mutex_t *mutex) { - return RBOOL(mutex->fiber == fiber); + return RBOOL(mutex->fiber_serial == rb_fiber_serial(fiber)); } static VALUE @@ -347,12 +322,12 @@ do_mutex_lock(VALUE self, int interruptible_p) } if (rb_mutex_trylock(self) == Qfalse) { - if (mutex->fiber == fiber) { + if (mutex->fiber_serial == rb_fiber_serial(fiber)) { rb_raise(rb_eThreadError, "deadlock; recursive locking"); } - while (mutex->fiber != fiber) { - VM_ASSERT(mutex->fiber != NULL); + while (mutex->fiber_serial != rb_fiber_serial(fiber)) { + VM_ASSERT(mutex->fiber_serial != 0); VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { @@ -366,12 +341,12 @@ do_mutex_lock(VALUE self, int interruptible_p) rb_ensure(call_rb_fiber_scheduler_block, self, delete_from_waitq, (VALUE)&sync_waiter); - if (!mutex->fiber) { + if (!mutex->fiber_serial) { mutex_set_owner(self, th, fiber); } } else { - if (!th->vm->thread_ignore_deadlock && rb_fiber_threadptr(mutex->fiber) == th) { + if (!th->vm->thread_ignore_deadlock && rb_thread_ptr(mutex->thread) == th) { rb_raise(rb_eThreadError, "deadlock; lock already owned by another fiber belonging to the same thread"); } @@ -407,7 +382,7 @@ do_mutex_lock(VALUE self, int interruptible_p) ccan_list_del(&sync_waiter.node); // unlocked by another thread while sleeping - if (!mutex->fiber) { + if (!mutex->fiber_serial) { mutex_set_owner(self, th, fiber); } @@ -421,12 +396,12 @@ do_mutex_lock(VALUE self, int interruptible_p) if (interruptible_p) { /* release mutex before checking for interrupts...as interrupt checking * code might call rb_raise() */ - if (mutex->fiber == fiber) { + if (mutex->fiber_serial == rb_fiber_serial(fiber)) { mutex->thread = Qfalse; - mutex->fiber = NULL; + mutex->fiber_serial = 0; } RUBY_VM_CHECK_INTS_BLOCKING(th->ec); /* may release mutex */ - if (!mutex->fiber) { + if (!mutex->fiber_serial) { mutex_set_owner(self, th, fiber); } } @@ -446,7 +421,7 @@ do_mutex_lock(VALUE self, int interruptible_p) } if (saved_ints) th->ec->interrupt_flag = saved_ints; - if (mutex->fiber == fiber) mutex_locked(th, fiber, self); + if (mutex->fiber_serial == rb_fiber_serial(fiber)) mutex_locked(th, fiber, self); } RUBY_DEBUG_LOG("%p locked", mutex); @@ -496,16 +471,16 @@ rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) { RUBY_DEBUG_LOG("%p", mutex); - if (mutex->fiber == 0) { + if (mutex->fiber_serial == 0) { return "Attempt to unlock a mutex which is not locked"; } - else if (mutex->fiber != fiber) { + else if (fiber && mutex->fiber_serial != rb_fiber_serial(fiber)) { return "Attempt to unlock a mutex which is locked by another thread/fiber"; } struct sync_waiter *cur = 0, *next; - mutex->fiber = 0; + mutex->fiber_serial = 0; thread_mutex_remove(th, mutex); ccan_list_for_each_safe(&mutex->waitq, cur, next, node) { @@ -583,7 +558,7 @@ rb_mutex_abandon_all(rb_mutex_t *mutexes) while (mutexes) { mutex = mutexes; mutexes = mutex->next_mutex; - mutex->fiber = 0; + mutex->fiber_serial = 0; mutex->next_mutex = 0; ccan_list_head_init(&mutex->waitq); } From 826e91a7e2c427f604f47f775d156d1d398dadc6 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Thu, 20 Nov 2025 20:57:34 +0000 Subject: [PATCH 13/29] [DOC] Harmonize mod methods --- numeric.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/numeric.c b/numeric.c index 3d6648f7d6fbd7..0d5f3f26f617e3 100644 --- a/numeric.c +++ b/numeric.c @@ -633,7 +633,7 @@ num_div(VALUE x, VALUE y) * call-seq: * self % other -> real_numeric * - * Returns +self+ modulo +other+ as a real number. + * Returns +self+ modulo +other+ as a real numeric (\Integer, \Float, or \Rational). * * Of the Core and Standard Library classes, * only Rational uses this implementation. @@ -1358,7 +1358,7 @@ ruby_float_mod(double x, double y) * call-seq: * self % other -> float * - * Returns +self+ modulo +other+ as a float. + * Returns +self+ modulo +other+ as a \Float. * * For float +f+ and real number +r+, these expressions are equivalent: * @@ -4316,9 +4316,9 @@ fix_mod(VALUE x, VALUE y) /* * call-seq: - * self % other -> real_number + * self % other -> real_numeric * - * Returns +self+ modulo +other+ as a real number. + * Returns +self+ modulo +other+ as a real numeric (\Integer, \Float, or \Rational). * * For integer +n+ and real number +r+, these expressions are equivalent: * From d5368fc515788e03a11fc42b0d9a42c7c1837d2d Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 20 Nov 2025 16:07:44 -0600 Subject: [PATCH 14/29] [DOC] Tweaks for String#valid_encoding? --- doc/string/valid_encoding_p.rdoc | 8 ++++++++ string.c | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 doc/string/valid_encoding_p.rdoc diff --git a/doc/string/valid_encoding_p.rdoc b/doc/string/valid_encoding_p.rdoc new file mode 100644 index 00000000000000..e1db55174a5428 --- /dev/null +++ b/doc/string/valid_encoding_p.rdoc @@ -0,0 +1,8 @@ +Returns whether +self+ is encoded correctly: + + s = 'Straße' + s.valid_encoding? # => true + s.encoding # => # + s.force_encoding(Encoding::ASCII).valid_encoding? # => false + +Related: see {Querying}[rdoc-ref:String@Querying]. diff --git a/string.c b/string.c index ebf36a7e08ebde..b1db9cb528fdfe 100644 --- a/string.c +++ b/string.c @@ -11469,11 +11469,8 @@ rb_str_b(VALUE str) * call-seq: * valid_encoding? -> true or false * - * Returns +true+ if +self+ is encoded correctly, +false+ otherwise: + * :include: doc/string/valid_encoding_p.rdoc * - * "\xc2\xa1".force_encoding(Encoding::UTF_8).valid_encoding? # => true - * "\xc2".force_encoding(Encoding::UTF_8).valid_encoding? # => false - * "\x80".force_encoding(Encoding::UTF_8).valid_encoding? # => false */ static VALUE From 55938a45e8c6872df258b3ef52bba94a2cda846d Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Thu, 20 Nov 2025 21:50:00 +0000 Subject: [PATCH 15/29] [DOC] Sort some methods in What's Here --- doc/string.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/string.rb b/doc/string.rb index b37cb5d324ec95..304ab60c298967 100644 --- a/doc/string.rb +++ b/doc/string.rb @@ -194,10 +194,10 @@ # # _Counts_ # -# - #length (aliased as #size): Returns the count of characters (not bytes). -# - #empty?: Returns whether the length of +self+ is zero. # - #bytesize: Returns the count of bytes. # - #count: Returns the count of substrings matching given strings. +# - #empty?: Returns whether the length of +self+ is zero. +# - #length (aliased as #size): Returns the count of characters (not bytes). # # _Substrings_ # From 2447380894e8ab4968d745f6844d2dc5278ebd6b Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 18 Nov 2025 17:07:21 -0500 Subject: [PATCH 16/29] Run rb_gc_before_fork after before_exec before_exec stops the timer thread, which requires locking the Ractor scheduler lock. This may deadlock if rb_gc_before_fork locks the VM. --- process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/process.c b/process.c index 2871596ceae14f..46c55abcce42c6 100644 --- a/process.c +++ b/process.c @@ -1575,8 +1575,8 @@ after_exec(void) static void before_fork_ruby(void) { - rb_gc_before_fork(); before_exec(); + rb_gc_before_fork(); } static void From 604fc059618b8f1f94b19efa51d468d827a766d1 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 13 Nov 2025 11:56:23 -0500 Subject: [PATCH 17/29] ZJIT: Rename array length reference to make the code easier to follow --- vm_insnhelper.c | 58 ++++++++++++++++++++++----------------------- zjit/src/codegen.rs | 4 ++-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index ff686d047a163d..97e63387bb7b55 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6350,15 +6350,15 @@ rb_vm_opt_duparray_include_p(rb_execution_context_t *ec, const VALUE ary, VALUE } static VALUE -vm_opt_newarray_max(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +vm_opt_newarray_max(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { if (BASIC_OP_UNREDEFINED_P(BOP_MAX, ARRAY_REDEFINED_OP_FLAG)) { - if (num == 0) { + if (array_len == 0) { return Qnil; } else { VALUE result = *ptr; - rb_snum_t i = num - 1; + rb_snum_t i = array_len - 1; while (i-- > 0) { const VALUE v = *++ptr; if (OPTIMIZED_CMP(v, result) > 0) { @@ -6369,26 +6369,26 @@ vm_opt_newarray_max(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) } } else { - return rb_vm_call_with_refinements(ec, rb_ary_new4(num, ptr), idMax, 0, NULL, RB_NO_KEYWORDS); + return rb_vm_call_with_refinements(ec, rb_ary_new4(array_len, ptr), idMax, 0, NULL, RB_NO_KEYWORDS); } } VALUE -rb_vm_opt_newarray_max(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +rb_vm_opt_newarray_max(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { - return vm_opt_newarray_max(ec, num, ptr); + return vm_opt_newarray_max(ec, array_len, ptr); } static VALUE -vm_opt_newarray_min(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +vm_opt_newarray_min(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { if (BASIC_OP_UNREDEFINED_P(BOP_MIN, ARRAY_REDEFINED_OP_FLAG)) { - if (num == 0) { + if (array_len == 0) { return Qnil; } else { VALUE result = *ptr; - rb_snum_t i = num - 1; + rb_snum_t i = array_len - 1; while (i-- > 0) { const VALUE v = *++ptr; if (OPTIMIZED_CMP(v, result) < 0) { @@ -6399,63 +6399,63 @@ vm_opt_newarray_min(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) } } else { - return rb_vm_call_with_refinements(ec, rb_ary_new4(num, ptr), idMin, 0, NULL, RB_NO_KEYWORDS); + return rb_vm_call_with_refinements(ec, rb_ary_new4(array_len, ptr), idMin, 0, NULL, RB_NO_KEYWORDS); } } VALUE -rb_vm_opt_newarray_min(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +rb_vm_opt_newarray_min(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { - return vm_opt_newarray_min(ec, num, ptr); + return vm_opt_newarray_min(ec, array_len, ptr); } static VALUE -vm_opt_newarray_hash(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +vm_opt_newarray_hash(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { // If Array#hash is _not_ monkeypatched, use the optimized call if (BASIC_OP_UNREDEFINED_P(BOP_HASH, ARRAY_REDEFINED_OP_FLAG)) { - return rb_ary_hash_values(num, ptr); + return rb_ary_hash_values(array_len, ptr); } else { - return rb_vm_call_with_refinements(ec, rb_ary_new4(num, ptr), idHash, 0, NULL, RB_NO_KEYWORDS); + return rb_vm_call_with_refinements(ec, rb_ary_new4(array_len, ptr), idHash, 0, NULL, RB_NO_KEYWORDS); } } VALUE -rb_vm_opt_newarray_hash(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr) +rb_vm_opt_newarray_hash(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr) { - return vm_opt_newarray_hash(ec, num, ptr); + return vm_opt_newarray_hash(ec, array_len, ptr); } VALUE rb_setup_fake_ary(struct RArray *fake_ary, const VALUE *list, long len); VALUE rb_ec_pack_ary(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer); static VALUE -vm_opt_newarray_include_p(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr, VALUE target) +vm_opt_newarray_include_p(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE target) { if (BASIC_OP_UNREDEFINED_P(BOP_INCLUDE_P, ARRAY_REDEFINED_OP_FLAG)) { struct RArray fake_ary = {RBASIC_INIT}; - VALUE ary = rb_setup_fake_ary(&fake_ary, ptr, num); + VALUE ary = rb_setup_fake_ary(&fake_ary, ptr, array_len); return rb_ary_includes(ary, target); } else { VALUE args[1] = {target}; - return rb_vm_call_with_refinements(ec, rb_ary_new4(num, ptr), idIncludeP, 1, args, RB_NO_KEYWORDS); + return rb_vm_call_with_refinements(ec, rb_ary_new4(array_len, ptr), idIncludeP, 1, args, RB_NO_KEYWORDS); } } VALUE -rb_vm_opt_newarray_include_p(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr, VALUE target) +rb_vm_opt_newarray_include_p(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE target) { - return vm_opt_newarray_include_p(ec, num, ptr, target); + return vm_opt_newarray_include_p(ec, array_len, ptr, target); } static VALUE -vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr, VALUE fmt, VALUE buffer) +vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE fmt, VALUE buffer) { if (BASIC_OP_UNREDEFINED_P(BOP_PACK, ARRAY_REDEFINED_OP_FLAG)) { struct RArray fake_ary = {RBASIC_INIT}; - VALUE ary = rb_setup_fake_ary(&fake_ary, ptr, num); + VALUE ary = rb_setup_fake_ary(&fake_ary, ptr, array_len); return rb_ec_pack_ary(ec, ary, fmt, (UNDEF_P(buffer) ? Qnil : buffer)); } else { @@ -6473,20 +6473,20 @@ vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t num, const VALU argc++; } - return rb_vm_call_with_refinements(ec, rb_ary_new4(num, ptr), idPack, argc, args, kw_splat); + return rb_vm_call_with_refinements(ec, rb_ary_new4(array_len, ptr), idPack, argc, args, kw_splat); } } VALUE -rb_vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr, VALUE fmt, VALUE buffer) +rb_vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE fmt, VALUE buffer) { - return vm_opt_newarray_pack_buffer(ec, num, ptr, fmt, buffer); + return vm_opt_newarray_pack_buffer(ec, array_len, ptr, fmt, buffer); } VALUE -rb_vm_opt_newarray_pack(rb_execution_context_t *ec, rb_num_t num, const VALUE *ptr, VALUE fmt) +rb_vm_opt_newarray_pack(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE fmt) { - return vm_opt_newarray_pack_buffer(ec, num, ptr, fmt, Qundef); + return vm_opt_newarray_pack_buffer(ec, array_len, ptr, fmt, Qundef); } #undef id_cmp diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8838a72fc887bd..71e40c640f42a9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1436,7 +1436,7 @@ fn gen_array_include( ) -> lir::Opnd { gen_prepare_non_leaf_call(jit, asm, state); - let num: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); + let array_len: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); // After gen_prepare_non_leaf_call, the elements are spilled to the Ruby stack. // The elements are at the bottom of the virtual stack, followed by the target. @@ -1450,7 +1450,7 @@ fn gen_array_include( asm_ccall!( asm, rb_vm_opt_newarray_include_p, - EC, num.into(), elements_ptr, target + EC, array_len.into(), elements_ptr, target ) } From b06dd644da0ae87f33649ade6fc827a301b55b0c Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Fri, 14 Nov 2025 14:43:57 -0500 Subject: [PATCH 18/29] ZJIT: Compile the VM_OPT_NEWARRAY_SEND_HASH variant of opt_newarray_send --- test/ruby/test_zjit.rb | 20 +++++++++++++++++++ zjit/src/codegen.rs | 28 +++++++++++++++++++++++++++ zjit/src/hir.rs | 16 +++++++++++++++ zjit/src/hir/tests.rs | 44 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 64372c231cf26f..ee046ad9bf1637 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1049,6 +1049,26 @@ def test(x) }, insns: [:opt_duparray_send], call_threshold: 1 end + def test_opt_newarray_send_hash + assert_compiles 'Integer', %q{ + def test(x) + [1, 2, x].hash + end + test(20).class + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_hash_redefinition + assert_compiles '42', %q{ + Array.class_eval { def hash = 42 } + + def test(x) + [1, 2, x].hash + end + test(20) + }, insns: [:opt_newarray_send], call_threshold: 1 + end + def test_new_hash_empty assert_compiles '{}', %q{ def test = {} diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 71e40c640f42a9..082db3fae44108 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -464,6 +464,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IsBlockGiven => gen_is_block_given(jit, asm), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, opnds!(elements), opnd!(target), &function.frame_state(*state)), &Insn::DupArrayInclude { ary, target, state } => gen_dup_array_include(jit, asm, ary, opnd!(target), &function.frame_state(state)), + Insn::ArrayHash { elements, state } => gen_opt_newarray_hash(jit, asm, opnds!(elements), &function.frame_state(*state)), &Insn::ArrayMax { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::Throw { state, .. } @@ -1427,6 +1428,33 @@ fn gen_array_length(asm: &mut Assembler, array: Opnd) -> lir::Opnd { asm_ccall!(asm, rb_jit_array_len, array) } +/// Compile opt_newarray_hash - create a hash from array elements +fn gen_opt_newarray_hash( + jit: &JITState, + asm: &mut Assembler, + elements: Vec, + state: &FrameState, +) -> lir::Opnd { + // `Array#hash` will hash the elements of the array. + gen_prepare_non_leaf_call(jit, asm, state); + + let array_len: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); + + // After gen_prepare_non_leaf_call, the elements are spilled to the Ruby stack. + // Get a pointer to the first element on the Ruby stack. + let stack_bottom = state.stack().len() - elements.len(); + let elements_ptr = asm.lea(Opnd::mem(64, SP, stack_bottom as i32 * SIZEOF_VALUE_I32)); + + unsafe extern "C" { + fn rb_vm_opt_newarray_hash(ec: EcPtr, array_len: u32, elts: *const VALUE) -> VALUE; + } + + asm.ccall( + rb_vm_opt_newarray_hash as *const u8, + vec![EC, (array_len as u32).into(), elements_ptr], + ) +} + fn gen_array_include( jit: &JITState, asm: &mut Assembler, diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index face61f1f67a7b..4232410f23d521 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -231,6 +231,7 @@ impl<'a> std::fmt::Display for InvariantPrinter<'a> { BOP_FREEZE => write!(f, "BOP_FREEZE")?, BOP_UMINUS => write!(f, "BOP_UMINUS")?, BOP_MAX => write!(f, "BOP_MAX")?, + BOP_HASH => write!(f, "BOP_HASH")?, BOP_AREF => write!(f, "BOP_AREF")?, _ => write!(f, "{bop}")?, } @@ -650,6 +651,7 @@ pub enum Insn { NewRange { low: InsnId, high: InsnId, flag: RangeType, state: InsnId }, NewRangeFixnum { low: InsnId, high: InsnId, flag: RangeType, state: InsnId }, ArrayDup { val: InsnId, state: InsnId }, + ArrayHash { elements: Vec, state: InsnId }, ArrayMax { elements: Vec, state: InsnId }, ArrayInclude { elements: Vec, target: InsnId, state: InsnId }, DupArrayInclude { ary: VALUE, target: InsnId, state: InsnId }, @@ -1040,6 +1042,15 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) } + Insn::ArrayHash { elements, .. } => { + write!(f, "ArrayHash")?; + let mut prefix = " "; + for element in elements { + write!(f, "{prefix}{element}")?; + prefix = ", "; + } + Ok(()) + } Insn::ArrayInclude { elements, target, .. } => { write!(f, "ArrayInclude")?; let mut prefix = " "; @@ -1887,6 +1898,7 @@ impl Function { &ArrayMax { ref elements, state } => ArrayMax { elements: find_vec!(elements), state: find!(state) }, &ArrayInclude { ref elements, target, state } => ArrayInclude { elements: find_vec!(elements), target: find!(target), state: find!(state) }, &DupArrayInclude { ary, target, state } => DupArrayInclude { ary, target: find!(target), state: find!(state) }, + &ArrayHash { ref elements, state } => ArrayHash { elements: find_vec!(elements), state }, &SetGlobal { id, val, state } => SetGlobal { id, val: find!(val), state }, &GetIvar { self_val, id, ic, state } => GetIvar { self_val: find!(self_val), id, ic, state }, &LoadField { recv, id, offset, return_type } => LoadField { recv: find!(recv), id, offset, return_type }, @@ -2032,6 +2044,7 @@ impl Function { Insn::ArrayMax { .. } => types::BasicObject, Insn::ArrayInclude { .. } => types::BoolExact, Insn::DupArrayInclude { .. } => types::BoolExact, + Insn::ArrayHash { .. } => types::Fixnum, Insn::GetGlobal { .. } => types::BasicObject, Insn::GetIvar { .. } => types::BasicObject, Insn::LoadPC => types::CPtr, @@ -3346,6 +3359,7 @@ impl Function { worklist.push_back(val) } &Insn::ArrayMax { ref elements, state } + | &Insn::ArrayHash { ref elements, state } | &Insn::NewHash { ref elements, state } | &Insn::NewArray { ref elements, state } => { worklist.extend(elements); @@ -4102,6 +4116,7 @@ impl Function { | Insn::InvokeBuiltin { ref args, .. } | Insn::InvokeBlock { ref args, .. } | Insn::NewArray { elements: ref args, .. } + | Insn::ArrayHash { elements: ref args, .. } | Insn::ArrayMax { elements: ref args, .. } => { for &arg in args { self.assert_subtype(insn_id, arg, types::BasicObject)?; @@ -4908,6 +4923,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let elements = state.stack_pop_n(count)?; let (bop, insn) = match method { VM_OPT_NEWARRAY_SEND_MAX => (BOP_MAX, Insn::ArrayMax { elements, state: exit_id }), + VM_OPT_NEWARRAY_SEND_HASH => (BOP_HASH, Insn::ArrayHash { elements, state: exit_id }), VM_OPT_NEWARRAY_SEND_INCLUDE_P => { let target = elements[elements.len() - 1]; let array_elements = elements[..elements.len() - 1].to_vec(); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index b487352748601a..1e058ce11adecf 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2013,7 +2013,49 @@ pub mod hir_build_tests { Jump bb2(v8, v9, v10, v11, v12) bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:NilClass, v18:NilClass): v25:BasicObject = SendWithoutBlock v15, :+, v16 - SideExit UnhandledNewarraySend(HASH) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_HASH) + v32:Fixnum = ArrayHash v15, v16 + PatchPoint NoEPEscape(test) + v39:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v40:ArrayExact = ArrayDup v39 + v42:BasicObject = SendWithoutBlock v14, :puts, v40 + PatchPoint NoEPEscape(test) + CheckInterrupts + Return v32 + "); + } + + #[test] + fn test_opt_newarray_send_hash_redefined() { + eval(" + Array.class_eval { def hash = 42 } + + def test(a,b) + sum = a+b + result = [a,b].hash + puts [1,2,3] + result + end + "); + assert_contains_opcode("test", YARVINSN_opt_newarray_send); + assert_snapshot!(hir_string("test"), @r" + fn test@:5: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@7 + v3:BasicObject = GetLocal l0, SP@6 + v4:NilClass = Const Value(nil) + v5:NilClass = Const Value(nil) + Jump bb2(v1, v2, v3, v4, v5) + bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + v11:NilClass = Const Value(nil) + v12:NilClass = Const Value(nil) + Jump bb2(v8, v9, v10, v11, v12) + bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:NilClass, v18:NilClass): + v25:BasicObject = SendWithoutBlock v15, :+, v16 + SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_HASH)) "); } From 7d2f9ab9e1992dded620fe221421c9c247dcd408 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Mon, 17 Nov 2025 11:41:30 -0500 Subject: [PATCH 19/29] ZJIT: Handle display formatting for all defined bops --- zjit/src/hir.rs | 50 +++++++++++++++++++++++++++++-------------- zjit/src/hir/tests.rs | 6 +++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 4232410f23d521..172b177c456f3b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -217,22 +217,40 @@ impl<'a> std::fmt::Display for InvariantPrinter<'a> { } write!(f, ", ")?; match bop { - BOP_PLUS => write!(f, "BOP_PLUS")?, - BOP_MINUS => write!(f, "BOP_MINUS")?, - BOP_MULT => write!(f, "BOP_MULT")?, - BOP_DIV => write!(f, "BOP_DIV")?, - BOP_MOD => write!(f, "BOP_MOD")?, - BOP_EQ => write!(f, "BOP_EQ")?, - BOP_NEQ => write!(f, "BOP_NEQ")?, - BOP_LT => write!(f, "BOP_LT")?, - BOP_LE => write!(f, "BOP_LE")?, - BOP_GT => write!(f, "BOP_GT")?, - BOP_GE => write!(f, "BOP_GE")?, - BOP_FREEZE => write!(f, "BOP_FREEZE")?, - BOP_UMINUS => write!(f, "BOP_UMINUS")?, - BOP_MAX => write!(f, "BOP_MAX")?, - BOP_HASH => write!(f, "BOP_HASH")?, - BOP_AREF => write!(f, "BOP_AREF")?, + BOP_PLUS => write!(f, "BOP_PLUS")?, + BOP_MINUS => write!(f, "BOP_MINUS")?, + BOP_MULT => write!(f, "BOP_MULT")?, + BOP_DIV => write!(f, "BOP_DIV")?, + BOP_MOD => write!(f, "BOP_MOD")?, + BOP_EQ => write!(f, "BOP_EQ")?, + BOP_EQQ => write!(f, "BOP_EQQ")?, + BOP_LT => write!(f, "BOP_LT")?, + BOP_LE => write!(f, "BOP_LE")?, + BOP_LTLT => write!(f, "BOP_LTLT")?, + BOP_AREF => write!(f, "BOP_AREF")?, + BOP_ASET => write!(f, "BOP_ASET")?, + BOP_LENGTH => write!(f, "BOP_LENGTH")?, + BOP_SIZE => write!(f, "BOP_SIZE")?, + BOP_EMPTY_P => write!(f, "BOP_EMPTY_P")?, + BOP_NIL_P => write!(f, "BOP_NIL_P")?, + BOP_SUCC => write!(f, "BOP_SUCC")?, + BOP_GT => write!(f, "BOP_GT")?, + BOP_GE => write!(f, "BOP_GE")?, + BOP_NOT => write!(f, "BOP_NOT")?, + BOP_NEQ => write!(f, "BOP_NEQ")?, + BOP_MATCH => write!(f, "BOP_MATCH")?, + BOP_FREEZE => write!(f, "BOP_FREEZE")?, + BOP_UMINUS => write!(f, "BOP_UMINUS")?, + BOP_MAX => write!(f, "BOP_MAX")?, + BOP_MIN => write!(f, "BOP_MIN")?, + BOP_HASH => write!(f, "BOP_HASH")?, + BOP_CALL => write!(f, "BOP_CALL")?, + BOP_AND => write!(f, "BOP_AND")?, + BOP_OR => write!(f, "BOP_OR")?, + BOP_CMP => write!(f, "BOP_CMP")?, + BOP_DEFAULT => write!(f, "BOP_DEFAULT")?, + BOP_PACK => write!(f, "BOP_PACK")?, + BOP_INCLUDE_P => write!(f, "BOP_INCLUDE_P")?, _ => write!(f, "{bop}")?, } write!(f, ")") diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 1e058ce11adecf..d0fc582548b1eb 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2123,7 +2123,7 @@ pub mod hir_build_tests { Jump bb2(v8, v9, v10, v11, v12) bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:NilClass, v18:NilClass): v25:BasicObject = SendWithoutBlock v15, :+, v16 - PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, 33) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_INCLUDE_P) v33:BoolExact = ArrayInclude v15, v16 | v16 PatchPoint NoEPEscape(test) v40:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) @@ -2154,7 +2154,7 @@ pub mod hir_build_tests { EntryPoint JIT(0) Jump bb2(v5, v6) bb2(v8:BasicObject, v9:BasicObject): - PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, 33) + PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_INCLUDE_P) v15:BoolExact = DupArrayInclude VALUE(0x1000) | v9 CheckInterrupts Return v15 @@ -2186,7 +2186,7 @@ pub mod hir_build_tests { EntryPoint JIT(0) Jump bb2(v5, v6) bb2(v8:BasicObject, v9:BasicObject): - SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, 33)) + SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_INCLUDE_P)) "); } From 36f1ab967f4b837dfb9856e1c29dd7ebeed8c3d3 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 20 Nov 2025 16:49:26 -0500 Subject: [PATCH 20/29] ZJIT: Add tests for `opt_newarray_send` with target methods redefined --- test/ruby/test_zjit.rb | 53 ++++++++++++++++++++++++++++++++ zjit/src/hir/tests.rs | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index ee046ad9bf1637..5611aea08e3658 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1040,6 +1040,22 @@ def test(x) }, insns: [:opt_newarray_send], call_threshold: 1 end + def test_opt_newarray_send_include_p_redefinition + assert_compiles '[true, false]', %q{ + class Array + alias_method :old_include?, :include? + def include?(x) + old_include?(x) + end + end + + def test(x) + [:y, 1, Object.new].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + def test_opt_duparray_send_include_p assert_compiles '[true, false]', %q{ def test(x) @@ -1049,6 +1065,22 @@ def test(x) }, insns: [:opt_duparray_send], call_threshold: 1 end + def test_opt_duparray_send_include_p_redefinition + assert_compiles '[true, false]', %q{ + class Array + alias_method :old_include?, :include? + def include?(x) + old_include?(x) + end + end + + def test(x) + [:y, 1].include?(x) + end + [test(1), test("n")] + }, insns: [:opt_duparray_send], call_threshold: 1 + end + def test_opt_newarray_send_hash assert_compiles 'Integer', %q{ def test(x) @@ -1069,6 +1101,27 @@ def test(x) }, insns: [:opt_newarray_send], call_threshold: 1 end + def test_opt_newarray_send_max + assert_compiles '[20, 40]', %q{ + def test(a,b) = [a,b].max + [test(10, 20), test(40, 30)] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + + def test_opt_newarray_send_max_redefinition + assert_compiles '[60, 90]', %q{ + class Array + alias_method :old_max, :max + def max + old_max * 2 + end + end + + def test(a,b) = [a,b].max + [test(15, 30), test(45, 35)] + }, insns: [:opt_newarray_send], call_threshold: 1 + end + def test_new_hash_empty assert_compiles '{}', %q{ def test = {} diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index d0fc582548b1eb..76a74c75eb4613 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -1953,6 +1953,35 @@ pub mod hir_build_tests { "); } + #[test] + fn test_opt_newarray_send_max_redefined() { + eval(" + class Array + alias_method :old_max, :max + def max + old_max * 2 + end + end + + def test(a,b) = [a,b].max + "); + assert_contains_opcode("test", YARVINSN_opt_newarray_send); + assert_snapshot!(hir_string("test"), @r" + fn test@:9: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MAX)) + "); + } + #[test] fn test_opt_newarray_send_min() { eval(" @@ -2135,6 +2164,45 @@ pub mod hir_build_tests { "); } + #[test] + fn test_opt_newarray_send_include_p_redefined() { + eval(" + class Array + alias_method :old_include?, :include? + def include?(x) + old_include?(x) + end + end + + def test(a,b) + sum = a+b + result = [a,b].include? b + puts [1,2,3] + result + end + "); + assert_contains_opcode("test", YARVINSN_opt_newarray_send); + assert_snapshot!(hir_string("test"), @r" + fn test@:10: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@7 + v3:BasicObject = GetLocal l0, SP@6 + v4:NilClass = Const Value(nil) + v5:NilClass = Const Value(nil) + Jump bb2(v1, v2, v3, v4, v5) + bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + v11:NilClass = Const Value(nil) + v12:NilClass = Const Value(nil) + Jump bb2(v8, v9, v10, v11, v12) + bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:NilClass, v18:NilClass): + v25:BasicObject = SendWithoutBlock v15, :+, v16 + SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_INCLUDE_P)) + "); + } + #[test] fn test_opt_duparray_send_include_p() { eval(" From 447989e5980510b0bcde34c58363b425c0e78224 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 20 Nov 2025 16:57:46 -0500 Subject: [PATCH 21/29] ZJIT: Update test names to use the same convention as the HIR tests --- test/ruby/test_zjit.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 5611aea08e3658..4e962ac1f50b03 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -983,7 +983,7 @@ def test = Foo.new }, insns: [:opt_new] end - def test_opt_new_with_redefinition + def test_opt_new_with_redefined assert_compiles '"foo"', %q{ class Foo def self.new = "foo" @@ -1040,7 +1040,7 @@ def test(x) }, insns: [:opt_newarray_send], call_threshold: 1 end - def test_opt_newarray_send_include_p_redefinition + def test_opt_newarray_send_include_p_redefined assert_compiles '[true, false]', %q{ class Array alias_method :old_include?, :include? @@ -1065,7 +1065,7 @@ def test(x) }, insns: [:opt_duparray_send], call_threshold: 1 end - def test_opt_duparray_send_include_p_redefinition + def test_opt_duparray_send_include_p_redefined assert_compiles '[true, false]', %q{ class Array alias_method :old_include?, :include? @@ -1090,7 +1090,7 @@ def test(x) }, insns: [:opt_newarray_send], call_threshold: 1 end - def test_opt_newarray_send_hash_redefinition + def test_opt_newarray_send_hash_redefined assert_compiles '42', %q{ Array.class_eval { def hash = 42 } @@ -1108,7 +1108,7 @@ def test(a,b) = [a,b].max }, insns: [:opt_newarray_send], call_threshold: 1 end - def test_opt_newarray_send_max_redefinition + def test_opt_newarray_send_max_redefined assert_compiles '[60, 90]', %q{ class Array alias_method :old_max, :max @@ -2488,7 +2488,7 @@ def entry(flag) }, call_threshold: 2 end - def test_bop_redefinition + def test_bop_redefined assert_runs '[3, :+, 100]', %q{ def test 1 + 2 @@ -2499,7 +2499,7 @@ def test }, call_threshold: 2 end - def test_bop_redefinition_with_adjacent_patch_points + def test_bop_redefined_with_adjacent_patch_points assert_runs '[15, :+, 100]', %q{ def test 1 + 2 + 3 + 4 + 5 @@ -2512,7 +2512,7 @@ def test # ZJIT currently only generates a MethodRedefined patch point when the method # is called on the top-level self. - def test_method_redefinition_with_top_self + def test_method_redefined_with_top_self assert_runs '["original", "redefined"]', %q{ def foo "original" @@ -2535,7 +2535,7 @@ def foo }, call_threshold: 2 end - def test_method_redefinition_with_module + def test_method_redefined_with_module assert_runs '["original", "redefined"]', %q{ module Foo def self.foo = "original" From fb28d4748dc96d581592b0d4c186ca0a8d49fa26 Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Thu, 20 Nov 2025 17:04:45 -0500 Subject: [PATCH 22/29] ZJIT: Change the output on redefined method tests to verify the new definition is used --- test/ruby/test_zjit.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 4e962ac1f50b03..7472ff77156b95 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1041,11 +1041,11 @@ def test(x) end def test_opt_newarray_send_include_p_redefined - assert_compiles '[true, false]', %q{ + assert_compiles '[:true, :false]', %q{ class Array alias_method :old_include?, :include? def include?(x) - old_include?(x) + old_include?(x) ? :true : :false end end @@ -1066,11 +1066,11 @@ def test(x) end def test_opt_duparray_send_include_p_redefined - assert_compiles '[true, false]', %q{ + assert_compiles '[:true, :false]', %q{ class Array alias_method :old_include?, :include? def include?(x) - old_include?(x) + old_include?(x) ? :true : :false end end From 82d8d24e7cdd26123ed4ad478ce6a0bb81d7abb5 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Sat, 8 Nov 2025 16:00:57 -0800 Subject: [PATCH 23/29] [ruby/rubygems] Add support for lockfile in Gemfile This allows you to specify the lockfile to use. This is useful if you want to use different lockfiles for different ruby versions or platforms. You can also skip writing the lockfile by using a false value. https://github.com/ruby/rubygems/commit/2896aa3fc2 Co-authored-by: Colby Swandale <996377+colby-swandale@users.noreply.github.com> --- lib/bundler/definition.rb | 4 +++- lib/bundler/dsl.rb | 14 +++++++++++++- lib/bundler/man/gemfile.5 | 19 ++++++++++++++++++- lib/bundler/man/gemfile.5.ronn | 17 +++++++++++++++++ spec/bundler/commands/install_spec.rb | 22 ++++++++++++++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 21f3760e6d9d3f..ca41d7953d8ea2 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -10,6 +10,8 @@ class << self attr_accessor :no_lock end + attr_writer :lockfile + attr_reader( :dependencies, :locked_checksums, @@ -380,7 +382,7 @@ def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or end def write_lock(file, preserve_unknown_sections) - return if Definition.no_lock || file.nil? + return if Definition.no_lock || !lockfile || file.nil? contents = to_lock diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 998a8134f8e12b..13e0783a436eff 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -9,8 +9,9 @@ class Dsl def self.evaluate(gemfile, lockfile, unlock) builder = new + builder.lockfile(lockfile) builder.eval_gemfile(gemfile) - builder.to_definition(lockfile, unlock) + builder.to_definition(builder.lockfile_path, unlock) end VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze @@ -38,6 +39,7 @@ def initialize @gemspecs = [] @gemfile = nil @gemfiles = [] + @lockfile = nil add_git_sources end @@ -101,6 +103,15 @@ def gem(name, *args) add_dependency(name, version, options) end + # For usage in Dsl.evaluate, since lockfile is used as part of the Gemfile. + def lockfile_path + @lockfile + end + + def lockfile(file) + @lockfile = file + end + def source(source, *args, &blk) options = args.last.is_a?(Hash) ? args.pop.dup : {} options = normalize_hash(options) @@ -175,6 +186,7 @@ def github(repo, options = {}) def to_definition(lockfile, unlock) check_primary_source_safety + lockfile = @lockfile unless @lockfile.nil? Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) end diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 4e1a63578076e2..1dbb2618afba5e 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -469,4 +469,21 @@ For implicit gems (dependencies of explicit gems), any source, git, or path repo .IP "3." 4 If neither of the above conditions are met, the global source will be used\. If multiple global sources are specified, they will be prioritized from last to first, but this is deprecated since Bundler 1\.13, so Bundler prints a warning and will abort with an error in the future\. .IP "" 0 - +.SH "LOCKFILE" +By default, Bundler will create a lockfile by adding \fB\.lock\fR to the end of the Gemfile name\. To change this, use the \fBlockfile\fR method: +.IP "" 4 +.nf +lockfile "/path/to/lockfile\.lock" +.fi +.IP "" 0 +.P +This is useful when you want to use different lockfiles per ruby version or platform\. +.P +To avoid writing a lock file, use \fBfalse\fR as the argument: +.IP "" 4 +.nf +lockfile false +.fi +.IP "" 0 +.P +This is useful for library development and other situations where the code is expected to work with a range of dependency versions\. diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index 802549737e38e4..619151aa8801d7 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -556,3 +556,20 @@ bundler uses the following priority order: If multiple global sources are specified, they will be prioritized from last to first, but this is deprecated since Bundler 1.13, so Bundler prints a warning and will abort with an error in the future. + +## LOCKFILE + +By default, Bundler will create a lockfile by adding `.lock` to the end of the +Gemfile name. To change this, use the `lockfile` method: + + lockfile "/path/to/lockfile.lock" + +This is useful when you want to use different lockfiles per ruby version or +platform. + +To avoid writing a lock file, use `false` as the argument: + + lockfile false + +This is useful for library development and other situations where the code is +expected to work with a range of dependency versions. diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index 0dbe950f87b6d7..68232d92de9b95 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -29,6 +29,28 @@ expect(bundled_app_lock).to exist end + it "creates lockfile based on the lockfile method in Gemfile" do + install_gemfile <<-G + lockfile "OmgFile.lock" + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install" + + expect(bundled_app("OmgFile.lock")).to exist + end + + it "does not make a lockfile if lockfile false is used in Gemfile" do + install_gemfile <<-G + lockfile false + source "https://gem.repo1" + gem 'myrack' + G + + expect(bundled_app_lock).not_to exist + end + it "does not create ./.bundle by default" do install_gemfile <<-G source "https://gem.repo1" From 1562803e5103dea3949027e61672fa82f26782fd Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Sat, 8 Nov 2025 16:03:07 -0800 Subject: [PATCH 24/29] [ruby/rubygems] Add support for bundle install --no-lock This allows for the same behavior as including `lockfile false` in the Gemfile. This allows you to get the behavior without modifying the Gemfile, which is useful if you do not control the Gemfile. This is similar to the --no-lock option already supported by `gem install -g Gemfile`. https://github.com/ruby/rubygems/commit/6c94623881 Co-authored-by: Colby Swandale <996377+colby-swandale@users.noreply.github.com> --- lib/bundler/cli.rb | 1 + lib/bundler/cli/install.rb | 1 + lib/bundler/man/bundle-install.1 | 7 ++++++- lib/bundler/man/bundle-install.1.ronn | 10 ++++++++++ spec/bundler/commands/install_spec.rb | 11 +++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 79f93a7784734c..48178965697667 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -234,6 +234,7 @@ def remove(*gems) method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" method_option "prefer-local", type: :boolean, banner: "Only attempt to fetch gems remotely if not present locally, even if newer versions are available remotely" method_option "no-cache", type: :boolean, banner: "Don't update the existing gem cache." + method_option "no-lock", type: :boolean, banner: "Don't create a lockfile." method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed" method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)." method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)." diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 20e22155de8ee9..85b303eee60900 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -44,6 +44,7 @@ def run # (rather than some optimizations we perform at app runtime). definition = Bundler.definition(strict: true) definition.validate_runtime! + definition.lockfile = false if options["no-lock"] installer = Installer.install(Bundler.root, definition, options) diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index 2d7ef96b4490fd..1acbe430585e22 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" -\fBbundle install\fR [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-no\-cache] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] +\fBbundle install\fR [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-no\-cache] [\-\-no\-lock] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] .SH "DESCRIPTION" Install the gems specified in your Gemfile(5)\. If this is the first time you run bundle install (and a \fBGemfile\.lock\fR does not exist), Bundler will fetch all remote sources, resolve dependencies and install all needed gems\. .P @@ -34,6 +34,11 @@ Force using locally installed gems, or gems already present in Rubygems' cache o \fB\-\-no\-cache\fR Do not update the cache in \fBvendor/cache\fR with the newly bundled gems\. This does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install\. .TP +\fB\-\-no\-lock\fR +Do not create a lockfile\. Useful if you want to install dependencies but not lock versions of gems\. Recommended for library development, and other situations where the code is expected to work with a range of dependency versions\. +.IP +This has the same effect as using \fBlockfile false\fR in the Gemfile\. See gemfile(5) for more information\. +.TP \fB\-\-quiet\fR Do not print progress information to the standard output\. .TP diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn index b946cbf8322917..adb47490d74b2d 100644 --- a/lib/bundler/man/bundle-install.1.ronn +++ b/lib/bundler/man/bundle-install.1.ronn @@ -9,6 +9,7 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile [--jobs=NUMBER] [--local] [--no-cache] + [--no-lock] [--prefer-local] [--quiet] [--retry=NUMBER] @@ -71,6 +72,15 @@ update process below under [CONSERVATIVE UPDATING][]. does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install. +* `--no-lock`: + Do not create a lockfile. Useful if you want to install dependencies but not + lock versions of gems. Recommended for library development, and other + situations where the code is expected to work with a range of dependency + versions. + + This has the same effect as using `lockfile false` in the Gemfile. + See gemfile(5) for more information. + * `--quiet`: Do not print progress information to the standard output. diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index 68232d92de9b95..69d9a860996197 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -89,6 +89,17 @@ expect(bundled_app("OmgFile.lock")).to exist end + it "doesn't create a lockfile if --no-lock option is given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --no-lock" + + expect(bundled_app("OmgFile.lock")).not_to exist + end + it "doesn't delete the lockfile if one already exists" do install_gemfile <<-G source "https://gem.repo1" From 010b23a7cfc4a20371d74406f9f0563331a233fd Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 10 Nov 2025 18:23:58 -0800 Subject: [PATCH 25/29] [ruby/rubygems] Add support for BUNDLE_LOCKFILE environment variable This specifies the lockfile location. This allows for easy support of different lockfiles per Ruby version or platform. https://github.com/ruby/rubygems/commit/b54d65bc0a Co-authored-by: Sutou Kouhei Co-authored-by: Colby Swandale <996377+colby-swandale@users.noreply.github.com> --- lib/bundler/environment_preserver.rb | 1 + lib/bundler/inline.rb | 8 +++++++ lib/bundler/man/bundle-config.1 | 3 +++ lib/bundler/man/bundle-config.1.ronn | 4 ++++ lib/bundler/man/gemfile.5 | 12 +++++++++++ lib/bundler/man/gemfile.5.ronn | 10 +++++++++ lib/bundler/settings.rb | 1 + lib/bundler/shared_helpers.rb | 4 ++++ lib/rubygems/bundler_version_finder.rb | 9 +++++--- spec/bundler/commands/config_spec.rb | 17 +++++++++++++++ spec/bundler/install/gemfile_spec.rb | 29 ++++++++++++++++++++++++++ 11 files changed, 95 insertions(+), 3 deletions(-) diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb index 444ab6fd373d86..bf9478a2990689 100644 --- a/lib/bundler/environment_preserver.rb +++ b/lib/bundler/environment_preserver.rb @@ -6,6 +6,7 @@ class EnvironmentPreserver BUNDLER_KEYS = %w[ BUNDLE_BIN_PATH BUNDLE_GEMFILE + BUNDLE_LOCKFILE BUNDLER_VERSION BUNDLER_SETUP GEM_HOME diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index 4e4b51e7a5dfb4..c861bee1496738 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -44,12 +44,14 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty? old_gemfile = ENV["BUNDLE_GEMFILE"] + old_lockfile = ENV["BUNDLE_LOCKFILE"] Bundler.unbundle_env! begin Bundler.instance_variable_set(:@bundle_path, Pathname.new(Gem.dir)) Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile" + Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", "Gemfile.lock" Bundler::Plugin.gemfile_install(&gemfile) if Bundler.settings[:plugins] builder = Bundler::Dsl.new @@ -94,5 +96,11 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) else ENV["BUNDLE_GEMFILE"] = "" end + + if old_lockfile + ENV["BUNDLE_LOCKFILE"] = old_lockfile + else + ENV["BUNDLE_LOCKFILE"] = "" + end end end diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index b44891f6e92374..05c13e2d0f3213 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -145,6 +145,9 @@ Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init \fBjobs\fR (\fBBUNDLE_JOBS\fR) The number of gems Bundler can install in parallel\. Defaults to the number of available processors\. .TP +\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR) +The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. +.TP \fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR) Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. .TP diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 281ab2da0cd124..7c34f1d1afb26e 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -189,6 +189,10 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). * `jobs` (`BUNDLE_JOBS`): The number of gems Bundler can install in parallel. Defaults to the number of available processors. +* `lockfile` (`BUNDLE_LOCKFILE`): + The path to the lockfile that bundler should use. By default, Bundler adds + `.lock` to the end of the `gemfile` entry. Can be set to `false` in the + Gemfile to disable lockfile creation entirely (see gemfile(5)). * `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true. * `no_install` (`BUNDLE_NO_INSTALL`): diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 1dbb2618afba5e..f345580ed77edb 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -487,3 +487,15 @@ lockfile false .IP "" 0 .P This is useful for library development and other situations where the code is expected to work with a range of dependency versions\. +.SS "LOCKFILE PRECEDENCE" +When determining path to the lockfile or whether to create a lockfile, the following precedence is used: +.IP "1." 4 +The \fBbundle install\fR \fB\-\-no\-lock\fR option (which disables lockfile creation)\. +.IP "2." 4 +The \fBlockfile\fR method in the Gemfile\. +.IP "3." 4 +The \fBBUNDLE_LOCKFILE\fR environment variable\. +.IP "4." 4 +The default behavior of adding \fB\.lock\fR to the end of the Gemfile name\. +.IP "" 0 + diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index 619151aa8801d7..3dea29cb3c6aaf 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -573,3 +573,13 @@ To avoid writing a lock file, use `false` as the argument: This is useful for library development and other situations where the code is expected to work with a range of dependency versions. + +### LOCKFILE PRECEDENCE + +When determining path to the lockfile or whether to create a lockfile, the +following precedence is used: + +1. The `bundle install` `--no-lock` option (which disables lockfile creation). +2. The `lockfile` method in the Gemfile. +3. The `BUNDLE_LOCKFILE` environment variable. +4. The default behavior of adding `.lock` to the end of the Gemfile name. diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index fb1b875b264500..d00a4bb916f692 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -65,6 +65,7 @@ class Settings gem.rubocop gem.test gemfile + lockfile path shebang simulate_version diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 4c914eb1a447a4..6419e4299760b7 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -23,6 +23,9 @@ def default_gemfile end def default_lockfile + given = ENV["BUNDLE_LOCKFILE"] + return Pathname.new(given) if given && !given.empty? + gemfile = default_gemfile case gemfile.basename.to_s @@ -297,6 +300,7 @@ def set_env(key, value) def set_bundle_variables Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", bundle_bin_path Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile.to_s + Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", default_lockfile.to_s Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION Bundler::SharedHelpers.set_env "BUNDLER_SETUP", File.expand_path("setup", __dir__) end diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index ac8988dea577bb..602e00c1d866fb 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -65,9 +65,12 @@ def self.lockfile_contents return unless gemfile - lockfile = case gemfile - when "gems.rb" then "gems.locked" - else "#{gemfile}.lock" + lockfile = ENV["BUNDLE_LOCKFILE"] + lockfile = nil if lockfile&.empty? + + lockfile ||= case gemfile + when "gems.rb" then "gems.locked" + else "#{gemfile}.lock" end return unless File.file?(lockfile) diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb index 1392b1731574a3..954cae09d8464a 100644 --- a/spec/bundler/commands/config_spec.rb +++ b/spec/bundler/commands/config_spec.rb @@ -592,3 +592,20 @@ end end end + +RSpec.describe "setting lockfile via config" do + it "persists the lockfile location to .bundle/config" do + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + bundle "config set --local lockfile #{bundled_app("ReallyNotGemfile.lock")}" + expect(File.exist?(bundled_app(".bundle/config"))).to eq(true) + + bundle "config list" + expect(out).to include("NotGemfile") + expect(out).to include("ReallyNotGemfile.lock") + end +end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb index 0e3b8477678388..87326f67af693c 100644 --- a/spec/bundler/install/gemfile_spec.rb +++ b/spec/bundler/install/gemfile_spec.rb @@ -27,6 +27,35 @@ ENV["BUNDLE_GEMFILE"] = "NotGemfile" expect(the_bundle).to include_gems "myrack 1.0.0" end + + it "respects lockfile and BUNDLE_LOCKFILE" do + gemfile bundled_app("NotGemfile"), <<-G + lockfile "ReallyNotGemfile.lock" + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "respects BUNDLE_LOCKFILE during bundle install" do + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + expect(bundled_app("ReallyNotGemfile.lock")).to exist + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "myrack 1.0.0" + end end context "with gemfile set via config" do From 3ec44a99954a3c4c131f374020b7addcba9bc5f2 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 17 Nov 2025 21:53:30 -0500 Subject: [PATCH 26/29] Add a Ractor test case that causes MMTk to deadlock This was a test case for Ractors discovered that causes MMTk to deadlock. There is a fix for it in https://github.com/ruby/mmtk/pull/49. --- bootstraptest/test_ractor.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 834eb627ef0e90..ed80dd08628e9d 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2350,3 +2350,23 @@ def call_test(obj) end :ok RUBY + +assert_equal 'ok', <<~'RUBY' + begin + 100.times do |i| + Ractor.new(i) do |j| + 1000.times do |i| + "#{j}-#{i}" + end + Ractor.receive + end + pid = fork { } + _, status = Process.waitpid2 pid + raise unless status.success? + end + + :ok + rescue NotImplementedError + :ok + end +RUBY From e15b4e1c56e826655e6fd10e32bdf974edf4b980 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 20 Nov 2025 16:58:36 -0500 Subject: [PATCH 27/29] Bump default compiler to clang-20 in CI clang-18 has a bug that causes the latest Ractor btest to crash. --- .github/actions/compilers/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/compilers/action.yml b/.github/actions/compilers/action.yml index daad5b694766f9..ab5b56a889672a 100644 --- a/.github/actions/compilers/action.yml +++ b/.github/actions/compilers/action.yml @@ -5,7 +5,7 @@ description: >- inputs: tag: required: false - default: clang-18 + default: clang-20 description: >- container image tag to use in this run. From 29d8a50d264be0c9cf1ddfc9fc2ce37724755b38 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 20 Nov 2025 20:02:29 +0900 Subject: [PATCH 28/29] [ruby/rubygems] Keep legacy windows platform, not removed them https://github.com/ruby/rubygems/commit/f360af8e3b --- lib/bundler/dsl.rb | 4 ++-- spec/bundler/bundler/dsl_spec.rb | 8 ++++---- spec/bundler/commands/cache_spec.rb | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 13e0783a436eff..6f06c4e918797d 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -427,8 +427,8 @@ def normalize_options(name, version, opts) windows_platforms = platforms.select {|pl| pl.to_s.match?(/mingw|mswin/) } if windows_platforms.any? windows_platforms = windows_platforms.map! {|pl| ":#{pl}" }.join(", ") - removed_message = "Platform #{windows_platforms} has been removed. Please use platform :windows instead." - Bundler::SharedHelpers.feature_removed! removed_message + deprecated_message = "Platform #{windows_platforms} will be removed in the future. Please use platform :windows instead." + Bundler::SharedHelpers.feature_deprecated! deprecated_message end # Save sources passed in a key diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 286dfa71151877..a19f251be5d946 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -221,8 +221,8 @@ to raise_error(Bundler::GemfileError, /is not a valid platform/) end - it "raises an error for legacy windows platforms" do - expect(Bundler::SharedHelpers).to receive(:feature_removed!).with(/\APlatform :mswin, :x64_mingw has been removed/) + it "warn for legacy windows platforms" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin, :x64_mingw will be removed in the future./) subject.gem("foo", platforms: [:mswin, :jruby, :x64_mingw]) end @@ -291,8 +291,8 @@ end describe "#platforms" do - it "raises an error for legacy windows platforms" do - expect(Bundler::SharedHelpers).to receive(:feature_removed!).with(/\APlatform :mswin64, :mingw has been removed/) + it "warn for legacy windows platforms" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin64, :mingw will be removed in the future./) subject.platforms(:mswin64, :jruby, :mingw) do subject.gem("foo") end diff --git a/spec/bundler/commands/cache_spec.rb b/spec/bundler/commands/cache_spec.rb index 2414e1ca02e439..bd92a84e185315 100644 --- a/spec/bundler/commands/cache_spec.rb +++ b/spec/bundler/commands/cache_spec.rb @@ -207,15 +207,15 @@ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist end - it "prints an error when using legacy windows rubies" do + it "prints a warn when using legacy windows rubies" do gemfile <<-D source "https://gem.repo1" gem 'myrack', :platforms => [:ruby_20, :x64_mingw_20] D bundle "cache --all-platforms", raise_on_error: false - expect(err).to include("removed") - expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).not_to exist + expect(err).to include("will be removed in the future") + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist end it "does not attempt to install gems in without groups" do From aa9e15cb1ecb0de598f816e44a09e016aaa8ef5c Mon Sep 17 00:00:00 2001 From: Alexander Bulancov <6594487+trinistr@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:30:42 +0300 Subject: [PATCH 29/29] Fix multiple bugs in `IO::Buffer.map` and update its documentation. (#15264) - Buffer's size did not account for offset when mapping the file, leading to possible crashes. - Size and offset were not checked properly, leading to many situations raising EINVAL errors with generic messages. - Documentation was wrong. --- io_buffer.c | 64 +++++++++++++++++++++++++------------ test/ruby/test_io_buffer.rb | 54 ++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 87e392b79181d6..abe7832bee829a 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -667,18 +667,25 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags * call-seq: IO::Buffer.map(file, [size, [offset, [flags]]]) -> io_buffer * * Create an IO::Buffer for reading from +file+ by memory-mapping the file. - * +file_io+ should be a +File+ instance, opened for reading. + * +file+ should be a +File+ instance, opened for reading or reading and writing. * * Optional +size+ and +offset+ of mapping can be specified. + * Trying to map an empty file or specify +size+ of 0 will raise an error. + * Valid values for +offset+ are system-dependent. * - * By default, the buffer would be immutable (read only); to create a writable - * mapping, you need to open a file in read-write mode, and explicitly pass - * +flags+ argument without IO::Buffer::IMMUTABLE. + * By default, the buffer is writable and expects the file to be writable. + * It is also shared, so several processes can use the same mapping. + * + * You can pass IO::Buffer::READONLY in +flags+ argument to make a read-only buffer; + * this allows to work with files opened only for reading. + * Specifying IO::Buffer::PRIVATE in +flags+ creates a private mapping, + * which will not impact other processes or the underlying file. + * It also allows updating a buffer created from a read-only file. * * File.write('test.txt', 'test') * * buffer = IO::Buffer.map(File.open('test.txt'), nil, 0, IO::Buffer::READONLY) - * # => # + * # => # * * buffer.readonly? # => true * @@ -686,7 +693,7 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags * # => "test" * * buffer.set_string('b', 0) - * # `set_string': Buffer is not writable! (IO::Buffer::AccessError) + * # 'IO::Buffer#set_string': Buffer is not writable! (IO::Buffer::AccessError) * * # create read/write mapping: length 4 bytes, offset 0, flags 0 * buffer = IO::Buffer.map(File.open('test.txt', 'r+'), 4, 0) @@ -708,31 +715,48 @@ io_buffer_map(int argc, VALUE *argv, VALUE klass) // We might like to handle a string path? VALUE io = argv[0]; + rb_off_t file_size = rb_file_size(io); + // Compiler can confirm that we handled file_size <= 0 case: + if (UNLIKELY(file_size <= 0)) { + rb_raise(rb_eArgError, "Invalid negative or zero file size!"); + } + // Here, we assume that file_size is positive: + else if (UNLIKELY((uintmax_t)file_size > SIZE_MAX)) { + rb_raise(rb_eArgError, "File larger than address space!"); + } + size_t size; if (argc >= 2 && !RB_NIL_P(argv[1])) { size = io_buffer_extract_size(argv[1]); - } - else { - rb_off_t file_size = rb_file_size(io); - - // Compiler can confirm that we handled file_size < 0 case: - if (file_size < 0) { - rb_raise(rb_eArgError, "Invalid negative file size!"); + if (UNLIKELY(size == 0)) { + rb_raise(rb_eArgError, "Size can't be zero!"); } - // Here, we assume that file_size is positive: - else if ((uintmax_t)file_size > SIZE_MAX) { - rb_raise(rb_eArgError, "File larger than address space!"); - } - else { - // This conversion should be safe: - size = (size_t)file_size; + if (UNLIKELY(size > (size_t)file_size)) { + rb_raise(rb_eArgError, "Size can't be larger than file size!"); } } + else { + // This conversion should be safe: + size = (size_t)file_size; + } // This is the file offset, not the buffer offset: rb_off_t offset = 0; if (argc >= 3) { offset = NUM2OFFT(argv[2]); + if (UNLIKELY(offset < 0)) { + rb_raise(rb_eArgError, "Offset can't be negative!"); + } + if (UNLIKELY(offset >= file_size)) { + rb_raise(rb_eArgError, "Offset too large!"); + } + if (RB_NIL_P(argv[1])) { + // Decrease size if it's set from the actual file size: + size = (size_t)(file_size - offset); + } + else if (UNLIKELY((size_t)(file_size - offset) < size)) { + rb_raise(rb_eArgError, "Offset too large!"); + } } enum rb_io_buffer_flags flags = 0; diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 62c46678882a10..997ed52640fb0d 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -73,12 +73,64 @@ def test_new_readonly def test_file_mapped buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)} - contents = buffer.get_string + assert_equal File.size(__FILE__), buffer.size + contents = buffer.get_string assert_include contents, "Hello World" assert_equal Encoding::BINARY, contents.encoding end + def test_file_mapped_with_size + buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, 30, 0, IO::Buffer::READONLY)} + assert_equal 30, buffer.size + + contents = buffer.get_string + assert_equal "# frozen_string_literal: false", contents + assert_equal Encoding::BINARY, contents.encoding + end + + def test_file_mapped_size_too_large + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 200_000, 0, IO::Buffer::READONLY)} + end + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, File.size(__FILE__) + 1, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_size_just_enough + File.open(__FILE__) {|file| + assert_equal File.size(__FILE__), IO::Buffer.map(file, File.size(__FILE__), 0, IO::Buffer::READONLY).size + } + end + + def test_file_mapped_offset_too_large + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, nil, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)} + end + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 20, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_zero_size + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 0, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_negative_size + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, -10, 0, IO::Buffer::READONLY)} + end + end + + def test_file_mapped_negative_offset + assert_raise ArgumentError do + File.open(__FILE__) {|file| IO::Buffer.map(file, 20, -1, IO::Buffer::READONLY)} + end + end + def test_file_mapped_invalid assert_raise TypeError do IO::Buffer.map("foobar")