From 3ae87b772b46141313eb73c8d4365e476e71c617 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 18 Jul 2026 14:48:58 -0700 Subject: [PATCH 01/10] [ruby/mmtk] Remove MMTK_MAX_OBJ_SIZE https://github.com/ruby/mmtk/commit/2f925b352a --- gc/mmtk/mmtk.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index dbb3545a785e62..7ccf51ff47e39c 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -670,8 +670,6 @@ void rb_gc_impl_set_params(void *objspace_ptr) { } static VALUE gc_verify_internal_consistency(VALUE self) { return Qnil; } -#define MMTK_MAX_OBJ_SIZE 1024 - static inline size_t rb_mmtk_align_obj_size(size_t object_size) { @@ -685,8 +683,6 @@ rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE fl #if USE_ZJIT && RB_GC_OBJ_SUFFIX_SIZE == 0 struct objspace *objspace = objspace_ptr; - if (alloc_size > MMTK_MAX_OBJ_SIZE) return false; - size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE) + RB_GC_OBJ_SUFFIX_SIZE); size_t object_size = total_size - sizeof(VALUE) - RB_GC_OBJ_SUFFIX_SIZE; size_t value_size_shift = sizeof(VALUE) == 8 ? 3 : 2; @@ -730,7 +726,6 @@ rb_gc_impl_init(void) rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_SIZE")), SIZET2NUM(sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), INT2NUM(0)); - rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVARGC_MAX_ALLOCATE_SIZE")), LONG2FIX(MMTK_MAX_OBJ_SIZE)); // TODO: correctly set RVALUE_OLD_AGE when we have generational GC support rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OLD_AGE")), INT2FIX(0)); OBJ_FREEZE(gc_constants); @@ -960,8 +955,8 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags struct objspace *objspace = objspace_ptr; struct MMTk_ractor_cache *ractor_cache = cache_ptr; - if (alloc_size == 0 || alloc_size > MMTK_MAX_OBJ_SIZE) { - rb_bug("rb_gc_impl_new_obj: allocation size too large (size=%"PRIuSIZE")", alloc_size); + if (alloc_size == 0) { + rb_bug("rb_gc_impl_new_obj: allocation size is zero"); } // Layout: [hidden size header (sizeof(VALUE))][payload (alloc_size)][suffix (RB_GC_OBJ_SUFFIX_SIZE)] @@ -1013,7 +1008,7 @@ rb_gc_impl_obj_slot_size(VALUE obj) size_t rb_gc_impl_size_slot_size(void *objspace_ptr, size_t size) { - if (size == 0 || size > MMTK_MAX_OBJ_SIZE) { + if (size == 0) { rb_bug("rb_gc_impl_size_slot_size: size too large (size=%"PRIuSIZE")", size); } @@ -1023,13 +1018,13 @@ rb_gc_impl_size_slot_size(void *objspace_ptr, size_t size) bool rb_gc_impl_size_allocatable_p(size_t size) { - return size <= MMTK_MAX_OBJ_SIZE; + return true; } size_t rb_gc_impl_max_allocation_size(void) { - return MMTK_MAX_OBJ_SIZE; + return SIZE_T_MAX; } // Malloc From d84e57fb38c179682819e63b526c706c74b8f781 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 21 Jul 2026 11:42:42 -0400 Subject: [PATCH 02/10] [ruby/mmtk] Implement large object space https://github.com/ruby/mmtk/commit/b458664cb1 --- gc/mmtk/mmtk.c | 22 ++++++++++++++++------ gc/mmtk/mmtk.h | 2 ++ gc/mmtk/src/api.rs | 8 ++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 7ccf51ff47e39c..60bbbfc9a1b610 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -57,6 +57,7 @@ struct objspace { uintptr_t vo_bit_log_region_size; uintptr_t vo_bit_base_addr; + size_t max_non_los_default_alloc_bytes; }; #define OBJ_FREE_BUF_CAPACITY 128 @@ -110,6 +111,7 @@ RB_THREAD_LOCAL_SPECIFIER VALUE marking_parent_object; #include #define MMTK_ALLOCATION_SEMANTICS_DEFAULT 0 +#define MMTK_ALLOCATION_SEMANTICS_LOS 2 static inline VALUE rb_mmtk_call_object_closure(VALUE obj, bool pin); @@ -604,6 +606,7 @@ rb_gc_impl_objspace_init(void *objspace_ptr) objspace->vo_bit_log_region_size = mmtk_get_vo_bit_log_region_size(); objspace->vo_bit_base_addr = mmtk_get_vo_bit_base_addr(); + objspace->max_non_los_default_alloc_bytes = mmtk_max_non_los_default_alloc_bytes(); } void @@ -687,6 +690,8 @@ rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE fl size_t object_size = total_size - sizeof(VALUE) - RB_GC_OBJ_SUFFIX_SIZE; size_t value_size_shift = sizeof(VALUE) == 8 ? 3 : 2; + if (total_size > objspace->max_non_los_default_alloc_bytes) return false; + struct rb_gc_zjit_mmtk_new_obj_fastpath mmtk_fastpath = { objspace, offsetof(struct objspace, total_allocated_objects), @@ -956,21 +961,26 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags struct MMTk_ractor_cache *ractor_cache = cache_ptr; if (alloc_size == 0) { - rb_bug("rb_gc_impl_new_obj: allocation size is zero"); + rb_bug("rb_gc_impl_new_obj: allocation size out of range (size=%"PRIuSIZE")", alloc_size); } // Layout: [hidden size header (sizeof(VALUE))][payload (alloc_size)][suffix (RB_GC_OBJ_SUFFIX_SIZE)] size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE) + RB_GC_OBJ_SUFFIX_SIZE); size_t object_size = total_size - sizeof(VALUE) - RB_GC_OBJ_SUFFIX_SIZE; + MMTk_AllocationSemantics semantics = total_size > objspace->max_non_los_default_alloc_bytes + ? MMTK_ALLOCATION_SEMANTICS_LOS + : MMTK_ALLOCATION_SEMANTICS_DEFAULT; *actual_alloc_size = object_size; if (objspace->gc_stress) { mmtk_handle_user_collection_request(ractor_cache, false, false); } - VALUE *alloc_obj = (VALUE *)rb_mmtk_alloc_fast_path(objspace, ractor_cache, total_size, MMTk_MIN_OBJ_ALIGN); + VALUE *alloc_obj = semantics == MMTK_ALLOCATION_SEMANTICS_DEFAULT + ? (VALUE *)rb_mmtk_alloc_fast_path(objspace, ractor_cache, total_size, MMTk_MIN_OBJ_ALIGN) + : NULL; if (!alloc_obj) { - alloc_obj = mmtk_alloc(ractor_cache->mutator, total_size, MMTk_MIN_OBJ_ALIGN, 0, MMTK_ALLOCATION_SEMANTICS_DEFAULT); + alloc_obj = mmtk_alloc(ractor_cache->mutator, total_size, MMTk_MIN_OBJ_ALIGN, 0, semantics); // On heap exhaustion raise NoMemoryError. if (RB_UNLIKELY(alloc_obj == NULL)) { @@ -983,8 +993,8 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags alloc_obj[0] = flags; alloc_obj[1] = klass; - if (ractor_cache->bump_pointer == NULL) { - mmtk_post_alloc(ractor_cache->mutator, (void*)alloc_obj, object_size, MMTK_ALLOCATION_SEMANTICS_DEFAULT); + if (semantics == MMTK_ALLOCATION_SEMANTICS_LOS || ractor_cache->bump_pointer == NULL) { + mmtk_post_alloc(ractor_cache->mutator, (void*)alloc_obj, total_size, semantics); } else { // We can use the post alloc fast path if we're using Immix bump pointer allocator @@ -1024,7 +1034,7 @@ rb_gc_impl_size_allocatable_p(size_t size) size_t rb_gc_impl_max_allocation_size(void) { - return SIZE_T_MAX; + return SIZE_MAX; } // Malloc diff --git a/gc/mmtk/mmtk.h b/gc/mmtk/mmtk.h index 42edeb1b1f0e62..7f330b87ec85cd 100644 --- a/gc/mmtk/mmtk.h +++ b/gc/mmtk/mmtk.h @@ -115,6 +115,8 @@ void mmtk_set_gc_enabled(bool enable); bool mmtk_gc_enabled_p(void); +size_t mmtk_max_non_los_default_alloc_bytes(void); + MMTk_Address mmtk_alloc(MMTk_Mutator *mutator, size_t size, size_t align, diff --git a/gc/mmtk/src/api.rs b/gc/mmtk/src/api.rs index 936baf78a8fc28..e8770d48137307 100644 --- a/gc/mmtk/src/api.rs +++ b/gc/mmtk/src/api.rs @@ -314,6 +314,14 @@ pub extern "C" fn mmtk_gc_enabled_p() -> bool { // =============== Object allocation =============== +#[no_mangle] +pub extern "C" fn mmtk_max_non_los_default_alloc_bytes() -> usize { + mmtk() + .get_plan() + .constraints() + .max_non_los_default_alloc_bytes +} + #[no_mangle] pub unsafe extern "C" fn mmtk_alloc( mutator: *mut RubyMutator, From e0e96393b8dce143d528f979aaa2277db2a37235 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 27 Jul 2026 16:53:53 -0400 Subject: [PATCH 03/10] [ruby/mmtk] Skip tests in Test_StringCapacity https://github.com/ruby/mmtk/commit/5767c1d970 --- test/.excludes-mmtk/Test_StringCapacity.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/.excludes-mmtk/Test_StringCapacity.rb diff --git a/test/.excludes-mmtk/Test_StringCapacity.rb b/test/.excludes-mmtk/Test_StringCapacity.rb new file mode 100644 index 00000000000000..59b47a55d983c3 --- /dev/null +++ b/test/.excludes-mmtk/Test_StringCapacity.rb @@ -0,0 +1,7 @@ +exclude(:test_capacity_embedded, "depends on max slot size") +exclude(:test_capacity_frozen, "depends on max slot size") +exclude(:test_capacity_fstring, "depends on max slot size") +exclude(:test_capacity_normal, "depends on max slot size") +exclude(:test_io_read, "depends on max slot size") +exclude(:test_literal_capacity, "depends on max slot size") +exclude(:test_s_new_capacity, "depends on max slot size") From 1cbb55372a0a4d68accd823393ac9bea417d6467 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 28 Jul 2026 11:34:22 -0400 Subject: [PATCH 04/10] [ruby/mmtk] Skip tests in TestStruct https://github.com/ruby/mmtk/commit/62fde9fc52 --- test/.excludes-mmtk/TestStruct/SubStruct.rb | 1 + test/.excludes-mmtk/TestStruct/TopStruct.rb | 1 + 2 files changed, 2 insertions(+) create mode 100644 test/.excludes-mmtk/TestStruct/SubStruct.rb create mode 100644 test/.excludes-mmtk/TestStruct/TopStruct.rb diff --git a/test/.excludes-mmtk/TestStruct/SubStruct.rb b/test/.excludes-mmtk/TestStruct/SubStruct.rb new file mode 100644 index 00000000000000..709cd0fd5c2aa0 --- /dev/null +++ b/test/.excludes-mmtk/TestStruct/SubStruct.rb @@ -0,0 +1 @@ +exclude(:test_larger_than_largest_pool, "depends on max slot size") diff --git a/test/.excludes-mmtk/TestStruct/TopStruct.rb b/test/.excludes-mmtk/TestStruct/TopStruct.rb new file mode 100644 index 00000000000000..709cd0fd5c2aa0 --- /dev/null +++ b/test/.excludes-mmtk/TestStruct/TopStruct.rb @@ -0,0 +1 @@ +exclude(:test_larger_than_largest_pool, "depends on max slot size") From 8ebf16fa7318669001db7e2edd8a876e55549f03 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 29 Jul 2026 18:40:07 +0000 Subject: [PATCH 05/10] id_table.c: keep managed id table live across foreach/dup callbacks rb_managed_id_table stores its rb_id_table body as embedded data (RUBY_TYPED_EMBEDDABLE), so managed_id_table_ptr returns a pointer interior to the wrapper object. rb_managed_id_table_foreach, _foreach_values, and _dup hand that interior pointer to a callback that may allocate and trigger a GC. If the compiler stops keeping the wrapper VALUE live once the interior pointer is taken, the table can be reclaimed while it is still being iterated. Add RB_GC_GUARD on the wrapper across these calls. Co-authored-by: Claude Opus 4.8 --- id_table.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/id_table.c b/id_table.c index a1cc9043b6cff2..90913195c56a82 100644 --- a/id_table.c +++ b/id_table.c @@ -397,6 +397,9 @@ rb_managed_id_table_dup(VALUE old_table) struct rb_id_table *old_tbl = managed_id_table_ptr(old_table); rb_id_table_init(new_tbl, old_tbl->num + 1); rb_id_table_foreach(old_tbl, managed_id_table_dup_i, new_tbl); + /* The table body is embedded (RUBY_TYPED_EMBEDDABLE), so old_tbl is an + * interior pointer. Keep old_table live in case dup_i triggers a GC. */ + RB_GC_GUARD(old_table); return obj; } @@ -422,12 +425,16 @@ void rb_managed_id_table_foreach(VALUE table, rb_id_table_foreach_func_t *func, void *data) { rb_id_table_foreach(managed_id_table_ptr(table), func, data); + /* The table body is embedded (RUBY_TYPED_EMBEDDABLE), so the pointer above + * is interior. Keep table live in case func triggers a GC. */ + RB_GC_GUARD(table); } void rb_managed_id_table_foreach_values(VALUE table, rb_id_table_foreach_values_func_t *func, void *data) { rb_id_table_foreach_values(managed_id_table_ptr(table), func, data); + RB_GC_GUARD(table); } int From a69bb3ab6007122da7a076934718ec49fd83bab4 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 23 Jul 2026 14:38:11 -0400 Subject: [PATCH 06/10] Implement GC fast path for gen_new_range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements GC fast path for newrange instruction when we do not need to call <=> method (when either endpoint is nil or both are fixnums). Benchmark: def run(max) i = 0 while i < max a = i..max i += 1 end end 30.times { run(10) } run(10_000_000) Result: Benchmark 1: master Time (mean ± σ): 221.8 ms ± 5.2 ms [User: 214.4 ms, System: 5.8 ms] Range (min … max): 216.4 ms … 235.8 ms 13 runs Benchmark 2: branch Time (mean ± σ): 62.4 ms ± 1.6 ms [User: 56.0 ms, System: 5.1 ms] Range (min … max): 60.7 ms … 69.7 ms 46 runs Summary branch ran 3.56 ± 0.12 times faster than master --- zjit/src/codegen.rs | 48 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 51b488b9e8a51f..e82bca150caffb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2518,7 +2518,7 @@ fn gen_new_hash( /// Compile a new range instruction fn gen_new_range( - jit: &JITState, + jit: &mut JITState, asm: &mut Assembler, function: &Function, low: lir::Opnd, @@ -2526,11 +2526,49 @@ fn gen_new_range( flag: RangeType, state: &FrameState, ) -> lir::Opnd { - // Sometimes calls `low.<=>(high)` + let hir_block_id = asm.current_block().hir_block_id; + let rpo_idx = asm.current_block().rpo_index; + let fast_block = asm.new_block(hir_block_id, false, rpo_idx); + let slow_block = asm.new_block(hir_block_id, false, rpo_idx); + let result_block = asm.new_block(hir_block_id, false, rpo_idx); + let fast_edge = Target::Block(Box::new(lir::BranchEdge { target: fast_block, args: vec![] })); + let slow_edge = Target::Block(Box::new(lir::BranchEdge { target: slow_block, args: vec![] })); + let result_edge = |range| Target::Block(Box::new(lir::BranchEdge { + target: result_block, + args: vec![range], + })); + + // rb_range_new skips the call to <=> when either endpoint is nil or both are fixnums. + asm.cmp(low, Qnil.into()); + asm.je(jit, fast_edge.clone()); + asm.cmp(high, Qnil.into()); + asm.je(jit, fast_edge.clone()); + asm.test(low, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); + asm.jz(jit, slow_edge.clone()); + asm.test(high, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); + asm.jz(jit, slow_edge.clone()); + asm.jmp(fast_edge); + + asm.set_current_block(fast_block); + let label = jit.get_label(asm, fast_block, hir_block_id); + asm.write_label(label); + let range = gen_new_range_fixnum(jit, asm, low, high, flag, state); + asm.jmp(result_edge(range)); + + asm.set_current_block(slow_block); + let label = jit.get_label(asm, slow_block, hir_block_id); + asm.write_label(label); + // May call `low.<=>(high)`. gen_prepare_non_leaf_call(jit, asm, function, state); - - // Call rb_range_new(low, high, flag) - asm_ccall!(asm, rb_range_new, low, high, (flag as i32).into()) + let range = asm_ccall!(asm, rb_range_new, low, high, (flag as i32).into()); + asm.jmp(result_edge(range)); + + asm.set_current_block(result_block); + let label = jit.get_label(asm, result_block, hir_block_id); + asm.write_label(label); + let param = asm.new_block_param(VALUE_BITS); + asm.current_block().add_parameter(param); + param } fn gen_new_range_fixnum( From e67f02518a3d531079f3ac5e6521b97c1ba344aa Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 29 Jul 2026 16:35:44 -0400 Subject: [PATCH 07/10] [ruby/prism] Pinned back references and pinned numbered references are not allowed https://github.com/ruby/prism/commit/35c587b1c2 --- prism/prism.c | 12 ------------ test/prism/errors/pinned_back_reference.txt | 4 ++++ test/prism/errors/pinned_numbered_reference.txt | 4 ++++ 3 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 test/prism/errors/pinned_back_reference.txt create mode 100644 test/prism/errors/pinned_numbered_reference.txt diff --git a/prism/prism.c b/prism/prism.c index 1f62ea5addc9fd..166f52c98ad1b6 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -17459,18 +17459,6 @@ parse_pattern_primitive(pm_parser_t *parser, pm_constant_id_list_t *captures, pm return UP(pm_pinned_variable_node_create(parser, &operator, variable)); } - case PM_TOKEN_NUMBERED_REFERENCE: { - parser_lex(parser); - pm_node_t *variable = UP(pm_numbered_reference_read_node_create(parser, &parser->previous)); - - return UP(pm_pinned_variable_node_create(parser, &operator, variable)); - } - case PM_TOKEN_BACK_REFERENCE: { - parser_lex(parser); - pm_node_t *variable = UP(pm_back_reference_read_node_create(parser, &parser->previous)); - - return UP(pm_pinned_variable_node_create(parser, &operator, variable)); - } case PM_TOKEN_PARENTHESIS_LEFT: { bool previous_pattern_matching_newlines = parser->pattern_matching_newlines; parser->pattern_matching_newlines = false; diff --git a/test/prism/errors/pinned_back_reference.txt b/test/prism/errors/pinned_back_reference.txt new file mode 100644 index 00000000000000..7d1ef9ea0325ba --- /dev/null +++ b/test/prism/errors/pinned_back_reference.txt @@ -0,0 +1,4 @@ +1 in ^$& + ^ expected a pattern expression after the `^` pin operator + ^~ unexpected back reference, expecting end-of-input + diff --git a/test/prism/errors/pinned_numbered_reference.txt b/test/prism/errors/pinned_numbered_reference.txt new file mode 100644 index 00000000000000..3d6639ab548512 --- /dev/null +++ b/test/prism/errors/pinned_numbered_reference.txt @@ -0,0 +1,4 @@ +1 in ^$1 + ^ expected a pattern expression after the `^` pin operator + ^~ unexpected numbered reference, expecting end-of-input + From ba57693031b01083a7f76fb0e28c5619ae8af05d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 22 Jul 2026 15:09:34 +0900 Subject: [PATCH 08/10] [ruby/rubygems] Fail `bundle check` when frozen mode needs lockfile changes `BUNDLE_FROZEN=1 bundle check` reported that dependencies are satisfied and exited 0 even when the lockfile was missing CHECKSUMS entries, because `write_lock` only warns in frozen mode. Run the same frozen validation as `bundle install` so `bundle check` fails consistently. https://github.com/ruby/rubygems/issues/9546 https://github.com/ruby/rubygems/commit/07337df092 Co-Authored-By: Claude Fable 5 --- lib/bundler/cli/check.rb | 4 +-- spec/bundler/commands/check_spec.rb | 52 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index 493eb3ec6a5ad6..eb2c49ddd20755 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -12,6 +12,7 @@ def run Bundler.settings.set_command_option_if_given :path, options[:path] definition = Bundler.definition + definition.ensure_equivalent_gemfile_and_lockfile definition.validate_runtime! begin @@ -28,9 +29,6 @@ def run not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" } Bundler.ui.warn "Install missing gems with `bundle install`" exit 1 - elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle? - Bundler.ui.error "This bundle has been frozen, but there is no #{SharedHelpers.relative_lockfile_path} present" - exit 1 else definition.lock(true) unless options[:"dry-run"] Bundler.ui.info "The Gemfile's dependencies are satisfied" diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb index 7fe6897ae3b5a9..e168141d1d8cd4 100644 --- a/spec/bundler/commands/check_spec.rb +++ b/spec/bundler/commands/check_spec.rb @@ -257,6 +257,58 @@ bundle :check, raise_on_error: false expect(last_command).to be_failure + expect(err).to include("Frozen mode is set, but there's no lockfile") + end + + it "fails when frozen is set and the lockfile is missing a CHECKSUMS entry" do + system_gems "myrack-1.0.0", path: default_bundle_path + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + CHECKSUMS + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :check, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(exitstatus).to eq(16) + expect(err).to include("Your lockfile is missing a CHECKSUMS entry for \"myrack\", but can't be updated because frozen mode is set") + expect(out).not_to include("The Gemfile's dependencies are satisfied") + end + + it "fails when frozen is set and the Gemfile has changed" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "rails" + G + + bundle :check, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(exitstatus).to eq(16) + expect(err).to include("frozen mode is set") + expect(err).to include("* rails") + expect(out).not_to include("The Gemfile's dependencies are satisfied") end describe "when locked" do From 8495d0ecaeb8a4f8d815d05183a896a2a809753a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 23 Jul 2026 15:08:42 +0900 Subject: [PATCH 09/10] win32/rm.bat: report removal failures via exit code The `error` variable was set on failure but never reached the exit code, so removal failures such as a locked DLL left `nmake clean` green while stale artifacts remained. Exit with `error` at the end. Since `rd /s` does not set ERRORLEVEL on failure, check the entries remaining after it instead. Also pass `/f` to `del` to remove read-only files as `rm -f` does. --- win32/rm.bat | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/win32/rm.bat b/win32/rm.bat index 250cee02c1e42c..8397da3c98171d 100755 --- a/win32/rm.bat +++ b/win32/rm.bat @@ -14,7 +14,7 @@ if "%1" == "-n" (shift & set "dryrun=%1" & goto :optloop) if "%1" == "-r" (shift & set "recursive=%1" & goto :optloop) if "%1" == "--debug" (shift & set "debug=%1" & set PROMPT=$E[34m+$E[m$S & echo on & goto :optloop) :begin -if "%1" == "" goto :EOF +if "%1" == "" exit /b %error% set p=%1 shift set p=%p:/=\% @@ -58,7 +58,7 @@ goto :remove_end rd /q "%p%" 2> nul && goto :remove_end ::- If matching files and directory exist, remove the files only first. - del /q "%p%" 2> nul + del /f /q "%p%" 2> nul ::- `del` exits with 0 even when nothing matched, so its result cannot ::- tell whether directories remain; check if matching entries still @@ -73,7 +73,14 @@ goto :remove_end ::- Remove remained directories recursively. for /D %%I in (%p%) do ( - rd /s /q %%I 2> nul || call set error=%%ERRORLEVEL%% + rd /s /q %%I 2> nul ) + + ::- `rd /s` does not set ERRORLEVEL on failure; check what remains + ::- instead. `if exist "dir\*"` is true even for an empty directory, + ::- so check remaining files and directories separately. + if not exist "%p%" goto :remove_end + for %%I in (%p%) do if not exist "%%I\" set error=1 + for /D %%I in (%p%) do if exist "%%I\" set error=1 :remove_end endlocal & set "error=%error%" & goto :EOF From 94933bfebfc2c967753a423fb4de14f7d95e3af0 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 29 Jul 2026 19:49:57 -0400 Subject: [PATCH 10/10] Don't label bindings-only changes with jit label (#18116) --- .github/labeler.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 6c32852cce236e..9cc3a60e349dc6 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,8 +11,14 @@ jit: - any-glob-to-any-file: # JIT sources (Rust + C + Ruby) - 'jit/**' - - 'yjit/**' - - 'zjit/**' + # All of {y,z}jit/ except the generated cruby_bindings.inc.rs files. + # `!(...)` can't span a `/`, so each pair splits the tree into + # non-Rust files and Rust files not named cruby_bindings.inc.rs; + # their union is everything but the bindings file. + - 'yjit/**/!(*.rs)' + - 'yjit/**/!(cruby_bindings.inc).rs' + - 'zjit/**/!(*.rs)' + - 'zjit/**/!(cruby_bindings.inc).rs' - 'jit.c' - 'jit_hook.rb' - 'jit_undef.rb'