From 1fcccaec8c852b87db4e92187259eb3700fe9112 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 9 Jul 2026 16:17:50 +0100 Subject: [PATCH 01/25] ZJIT: Inline GC fastpath in ZJIT for object allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speeds up object allocation by 70% ``` master: ruby 4.1.0dev (2026-07-09T11:46:27Z master 835ad5c09e) +ZJIT +PRISM [x86_64-linux] experiment: ruby 4.1.0dev (2026-07-09T17:43:21Z mvh-zjit-inline-fa.. 60534f3b33) +ZJIT +PRISM [x86_64-linux] --------------------- ----------- --------------- ------------------ ----------------- bench master (ms) experiment (ms) experiment 1st itr master/experiment object-new 17.9 ± 1.6% 10.3 ± 2.7% 1.002 1.729 object-new-initialize 51.4 ± 1.4% 44.9 ± 1.7% 1.095 1.144 object-new-no-escape 91.0 ± 0.4% 77.0 ± 0.5% 1.150 1.182 --------------------- ----------- --------------- ------------------ ----------------- Legend: - experiment 1st itr: ratio of master/experiment time for the first benchmarking iteration. - master/experiment: ratio of master/experiment time. Higher is better for experiment. Above 1 represents a speedup. ``` --- depend | 2 ++ gc.c | 34 ++++++++++++++++++++++++++++++---- zjit.h | 3 +++ zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 19 +++++++++++++++---- zjit/src/codegen_tests.rs | 26 ++++++++++++++++++++++++++ zjit/src/cruby_bindings.inc.rs | 5 +++++ 7 files changed, 82 insertions(+), 8 deletions(-) diff --git a/depend b/depend index 96d0485d527d51..8db2d7a6759f4f 100644 --- a/depend +++ b/depend @@ -16094,6 +16094,7 @@ scheduler.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h scheduler.$(OBJEXT): $(top_srcdir)/internal/serial.h scheduler.$(OBJEXT): $(top_srcdir)/internal/set_table.h scheduler.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +scheduler.$(OBJEXT): $(top_srcdir)/internal/struct.h scheduler.$(OBJEXT): $(top_srcdir)/internal/thread.h scheduler.$(OBJEXT): $(top_srcdir)/internal/variable.h scheduler.$(OBJEXT): $(top_srcdir)/internal/vm.h @@ -16897,6 +16898,7 @@ signal.$(OBJEXT): $(top_srcdir)/internal/set_table.h signal.$(OBJEXT): $(top_srcdir)/internal/signal.h signal.$(OBJEXT): $(top_srcdir)/internal/static_assert.h signal.$(OBJEXT): $(top_srcdir)/internal/string.h +signal.$(OBJEXT): $(top_srcdir)/internal/struct.h signal.$(OBJEXT): $(top_srcdir)/internal/thread.h signal.$(OBJEXT): $(top_srcdir)/internal/variable.h signal.$(OBJEXT): $(top_srcdir)/internal/vm.h diff --git a/gc.c b/gc.c index e521369caa72ef..b29c72d7dfb9b1 100644 --- a/gc.c +++ b/gc.c @@ -1106,6 +1106,16 @@ VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) return obj; } +static inline size_t +robject_embedded_size(uint32_t fields_count) +{ + size_t size = rb_obj_embedded_size(fields_count); + if (!rb_gc_size_allocatable_p(size)) { + size = sizeof(struct RObject); + } + return size; +} + VALUE rb_class_allocate_instance(VALUE klass) { @@ -1118,10 +1128,7 @@ rb_class_allocate_instance(VALUE klass) obj = class_allocate_complex_instance(klass, index_tbl_num_entries); } else { - size_t size = rb_obj_embedded_size(index_tbl_num_entries); - if (!rb_gc_size_allocatable_p(size)) { - size = sizeof(struct RObject); - } + size_t size = robject_embedded_size(index_tbl_num_entries); // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. @@ -1145,6 +1152,25 @@ rb_class_allocate_instance(VALUE klass) return obj; } +#if USE_ZJIT +bool +rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out) +{ + uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); + + RUBY_ASSERT(rb_shape_max_capacity() > 0); + if (RB_UNLIKELY(index_tbl_num_entries > rb_shape_max_capacity())) { + return false; + } + + size_t size = robject_embedded_size(index_tbl_num_entries); + *size_out = size; + *shape_id_out = rb_shape_transition_slot_size(rb_shape_transition_robject(0), + rb_gc_size_slot_size(size)); + return true; +} +#endif + void rb_gc_register_pinning_obj(VALUE obj) { diff --git a/zjit.h b/zjit.h index 586cf28a20c598..2bbac39b7c7348 100644 --- a/zjit.h +++ b/zjit.h @@ -4,6 +4,8 @@ // This file contains definitions ZJIT exposes to the CRuby codebase // +#include "shape.h" // for shape_id_t + // ZJIT_STATS controls whether to support runtime counters in the interpreter #ifndef ZJIT_STATS # define ZJIT_STATS (USE_ZJIT && RUBY_DEBUG) @@ -91,6 +93,7 @@ void rb_zjit_invalidate_root_box(void); void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame); void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp); size_t rb_zjit_hash_new_size(void); +bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out); // Special value for cfp->jit_return that means "this is a C method frame, use // rb_zjit_c_frame as the JITFrame". We don't control the native stack layout diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index cc28a6e05e8ceb..943ae22e30b708 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -112,6 +112,7 @@ fn main() { .allowlist_function("rb_zjit_profile_disable") .allowlist_function("rb_zjit_insn_to_bare_insn") .allowlist_function("rb_zjit_hash_new_size") + .allowlist_function("rb_zjit_class_allocate_instance_fastpath") // For crashing .allowlist_function("rb_bug") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 7f62d83ae09b21..25f3d77be20364 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -627,7 +627,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::ArrayPop { array, state } => gen_array_pop(asm, opnd!(array), &function.frame_state(*state)), Insn::ArrayLength { array } => gen_array_length(asm, opnd!(array)), Insn::ObjectAlloc { val, state } => gen_object_alloc(jit, asm, function, opnd!(val), &function.frame_state(*state)), - &Insn::ObjectAllocClass { class, state } => gen_object_alloc_class(asm, class, &function.frame_state(state)), + &Insn::ObjectAllocClass { class, state } => gen_object_alloc_class(jit, asm, class, &function.frame_state(state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, function, opnds!(strings), &function.frame_state(*state)), &Insn::StringGetbyte { string, index } => gen_string_getbyte(asm, opnd!(string), opnd!(index)), @@ -2344,13 +2344,24 @@ fn gen_object_alloc(jit: &JITState, asm: &mut Assembler, function: &Function, va asm_ccall!(asm, rb_obj_alloc, val) } -fn gen_object_alloc_class(asm: &mut Assembler, class: VALUE, state: &FrameState) -> lir::Opnd { +fn gen_object_alloc_class(jit: &mut JITState, asm: &mut Assembler, class: VALUE, state: &FrameState) -> lir::Opnd { // Allocating an object for a known class with default allocator is leaf; see doc for // `ObjectAllocClass`. gen_prepare_leaf_call_with_gc(asm, state); if unsafe { rb_zjit_class_has_default_allocator(class) } { - // TODO(max): inline code to allocate an instance - asm_ccall!(asm, rb_class_allocate_instance, class.into()) + let mut alloc_size: usize = 0; + let mut shape_id: shape_id_t = 0; + let has_fastpath = unsafe { + rb_zjit_class_allocate_instance_fastpath(class, &mut alloc_size, &mut shape_id) + }; + if has_fastpath { + let flags = (RUBY_T_OBJECT as u64) | ((shape_id as u64) << RB_SHAPE_FLAG_SHIFT as u64); + gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, class, |asm| { + asm_ccall!(asm, rb_class_allocate_instance, class.into()) + }) + } else { + asm_ccall!(asm, rb_class_allocate_instance, class.into()) + } } else { assert!(class_has_leaf_allocator(class), "class passed to ObjectAllocClass must have a leaf allocator"); let alloc_func = unsafe { rb_zjit_class_get_alloc_func(class) }; diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 51055badf29cad..ff4cf77891b930 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2920,6 +2920,32 @@ fn test_new_hash_empty_gc_stress() { "#), @"[Hash, 1, nil, {a: 1}]"); } +#[test] +fn test_object_alloc_gc_stress() { + eval(" + class Foo + def initialize + @a = 1 + @b = 2 + end + def sum = @a + @b + end + def make = Foo.new + "); + assert_contains_opcode("make", YARVINSN_opt_new); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make + foo = make + foo.instance_variable_set(:@c, 3) + [foo.class, foo.sum, foo.instance_variables] + ensure + GC.stress = false + end + "#), @"[Foo, 3, [:@a, :@b, :@c]]"); +} + #[test] fn test_new_hash_nonempty() { eval(r#" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 7b000daa7393cb..e54937dd9463a2 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2234,6 +2234,11 @@ unsafe extern "C" { pub fn rb_iseq_label(iseq: *const rb_iseq_t) -> VALUE; pub fn rb_iseq_defined_string(type_: defined_type) -> VALUE; pub fn rb_zjit_hash_new_size() -> usize; + pub fn rb_zjit_class_allocate_instance_fastpath( + klass: VALUE, + size_out: *mut usize, + shape_id_out: *mut shape_id_t, + ) -> bool; pub fn rb_profile_frames( start: ::std::os::raw::c_int, limit: ::std::os::raw::c_int, From 9660b88060b2274574493340029225a265ea0518 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Jul 2026 12:30:50 -0700 Subject: [PATCH 02/25] ZJIT: Make IsBlockGiven just a comparison (#17756) Do the load from the EP in HIR instead. This allows it to be optimized in the future when we de-duplicate GetEP. --- zjit/src/codegen.rs | 5 ++--- zjit/src/cruby_methods.rs | 3 ++- zjit/src/hir.rs | 10 +++++----- zjit/src/hir/opt_tests.rs | 5 +++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 25f3d77be20364..7f441fb8b856dc 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -771,7 +771,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::LoadField { recv, id, offset, return_type: _, num_bits } => gen_load_field(asm, opnd!(recv), id, offset, num_bits), &Insn::StoreField { recv, id, offset, val, num_bits } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), num_bits)), &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))), - &Insn::IsBlockGiven { lep } => gen_is_block_given(asm, opnd!(lep)), + &Insn::IsBlockGiven { block_handler } => gen_is_block_given(asm, opnd!(block_handler)), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, function, opnds!(elements), opnd!(target), &function.frame_state(*state)), Insn::ArrayPackBuffer { elements, fmt, buffer, state } => gen_array_pack_buffer(jit, asm, function, opnds!(elements), opnd!(fmt), (*buffer).map(|buffer| opnd!(buffer)), &function.frame_state(*state)), &Insn::DupArrayInclude { ary, target, state } => gen_dup_array_include(jit, asm, function, ary, opnd!(target), &function.frame_state(state)), @@ -837,8 +837,7 @@ fn gen_defined(jit: &JITState, asm: &mut Assembler, function: &Function, op_type } /// Similar to gen_defined for DEFINED_YIELD -fn gen_is_block_given(asm: &mut Assembler, lep: Opnd) -> Opnd { - let block_handler = asm.load(Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL)); +fn gen_is_block_given(asm: &mut Assembler, block_handler: Opnd) -> Opnd { asm.cmp(block_handler, VM_BLOCK_HANDLER_NONE.into()); asm.csel_e(Qfalse.into(), Qtrue.into()) } diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 29c32fe9aab5e9..4ff3eb759199be 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -352,7 +352,8 @@ fn inline_kernel_block_given_p(fun: &mut hir::Function, block: hir::BlockId, _re // Equivalent of GET_LEP() macro. let level = crate::cruby::get_lvar_level(call_site_iseq); let lep = fun.push_insn(block, hir::Insn::GetEP { level }); - Some(fun.push_insn(block, hir::Insn::IsBlockGiven { lep })) + let block_handler = fun.load_field(block, lep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, types::RubyValue); + Some(fun.push_insn(block, hir::Insn::IsBlockGiven { block_handler })) } else { Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) })) } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 4f1b8750fa00e8..1dd357e76bbc0f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -992,7 +992,7 @@ pub enum Insn { GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId }, /// Kernel#block_given? but without pushing a frame. Similar to [`Insn::Defined`] with /// `DEFINED_YIELD` - IsBlockGiven { lep: InsnId }, + IsBlockGiven { block_handler: InsnId }, /// Test the bit at index of val, a Fixnum. /// Return Qtrue if the bit is set, else Qfalse. FixnumBitCheck { val: InsnId, index: u8 }, @@ -1259,8 +1259,8 @@ macro_rules! for_each_operand_impl { | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } => {} - Insn::IsBlockGiven { lep } => { - $visit_one!(*lep); + Insn::IsBlockGiven { block_handler } => { + $visit_one!(*block_handler); } Insn::IsBlockParamModified { flags } => { $visit_one!(*flags); @@ -1686,7 +1686,7 @@ impl Insn { Insn::Defined { .. } => effects::Any, Insn::GetConstant { .. } => effects::Any, Insn::GetConstantPath { .. } => effects::Any, - Insn::IsBlockGiven { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty), + Insn::IsBlockGiven { .. } => effects::Empty, Insn::FixnumBitCheck { .. } => effects::Empty, // IsA needs to read the class of the value and traverse the class hierarchy, which we model as reading from Memory. Insn::IsA { .. } => Effect::read_write(abstract_heaps::Memory, abstract_heaps::Empty), @@ -2200,7 +2200,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { write!(f, "GetConstant {klass}, :{}, {allow_nil}", id.contents_lossy()) } Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) }, - Insn::IsBlockGiven { lep } => { write!(f, "IsBlockGiven {lep}") }, + Insn::IsBlockGiven { block_handler } => { write!(f, "IsBlockGiven {block_handler}") }, Insn::FixnumBitCheck {val, index} => { write!(f, "FixnumBitCheck {val}, {index}") }, Insn::CCall { cfunc, recv, args, name, owner, return_type: _, elidable: _ } => { let display_name = if *owner == Qnil { name.contents_lossy().to_string() } else { qualified_method_name(*owner, *name) }; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 6db2f1d74e0b99..2a59144b2d6c77 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3581,9 +3581,10 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:CPtr = GetEP 0 - v21:BoolExact = IsBlockGiven v20 + v21:RubyValue = LoadField v20, :VM_ENV_DATA_INDEX_SPECVAL@0x1038 + v22:BoolExact = IsBlockGiven v21 CheckInterrupts - Return v21 + Return v22 "); } From d939495a8fb878a3134089ab9f47a9d6d770eb8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 01:55:55 +0000 Subject: [PATCH 03/25] Fix unused variable warning in thread_sched_set_running on non-DTrace platforms --- thread_pthread.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 1b664afbb1456a..1dc68318495f20 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -709,8 +709,7 @@ thread_sched_set_running(struct rb_thread_sched *sched, rb_thread_t *th) VM_ASSERT(sched->running != th); if (RUBY_DTRACE_RTS_SET_RUNNING_ENABLED()) { - rb_thread_t *old = sched->running; - RUBY_DTRACE_RTS_SET_RUNNING(sched, old, th); + RUBY_DTRACE_RTS_SET_RUNNING(sched, sched->running, th); } sched->running = th; From 924f3fb6470ce65d78615b0ff85ed993536ca714 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 9 Jul 2026 13:51:10 -0700 Subject: [PATCH 04/25] ZJIT: Fix CFP publication for direct calls (#17760) Co-authored-by: Alan Wu --- zjit/src/codegen.rs | 10 ++++++++-- zjit/src/codegen_tests.rs | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 7f441fb8b856dc..295f227411cccf 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1756,8 +1756,7 @@ fn gen_send_iseq_direct( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); - asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.mov(CFP, new_cfp); // will be published at `ec->cfp` after callee's entrypoint let params = unsafe { iseq.params() }; @@ -2401,6 +2400,11 @@ fn gen_entry_point(jit: &mut JITState, asm: &mut Assembler, jit_entry_idx: Optio let jit_frame = JITFrame::new_iseq(entry_pc(jit.iseq(), jit_entry_idx), jit.iseq(), 0); asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), Opnd::const_ptr(jit_frame)); asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), NATIVE_BASE_PTR); + + // Direct JIT-to-JIT callers switch the CFP register before calling this entry + // point, but they leave ec->cfp pointing at the caller until cfp->jit_return + // is valid so signal-based frame walkers never observe a half-published callee. + asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); } /// Compile code that exits from JIT code with a return value @@ -3384,6 +3388,8 @@ c_callable! { unsafe { rb_set_cfp_pc(cfp, pc) }; unsafe { (*cfp)._iseq = iseq }; unsafe { (*cfp).jit_return = std::ptr::null_mut() }; + let ec_cfp = unsafe { ec.byte_add(RUBY_OFFSET_EC_CFP as usize) as *mut CfpPtr }; + unsafe { *ec_cfp = cfp }; } with_vm_lock(src_loc!(), || { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index ff4cf77891b930..56de74a8104937 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -4755,6 +4755,46 @@ fn test_profile_frames_from_signal_handler() { assert!(profiler.samples() > 0, "rb_profile_frames was not called from SIGPROF handler"); } +// A direct JIT-to-JIT call switches the CFP register before entering the callee. +// Signal profilers must not observe the callee through ec->cfp until the callee's +// cfp->jit_return points at a valid JITFrame. +#[cfg(all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64"), +))] +#[test] +fn test_profile_frames_during_direct_jit_to_jit_entry() { + with_inlining_threshold(0, || { + eval(r#" + def profiled_direct_callee(value) + value + 1 + end + + def profiled_direct_loop(n) + i = 0 + value = 0 + while i < n + value = profiled_direct_callee(value) + i += 1 + end + value + end + + # Compile both methods and patch the caller's SendDirect site before + # arming the sampler. + profiled_direct_callee(0) + profiled_direct_callee(0) + profiled_direct_loop(1) + profiled_direct_loop(1) + profiled_direct_loop(1) + "#); + + let profiler = signal_profiler::Profiler::start(10); + assert_snapshot!(assert_compiles("profiled_direct_loop(1_000_000)"), @"1000000"); + assert!(profiler.samples() > 0, "rb_profile_frames was not called from SIGPROF handler"); + }); +} + #[test] fn test_profile_under_nested_jit_call() { assert_snapshot!(inspect(" From a8a6890b71e0d1f7741b0706d1cff5b984374946 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 9 Jul 2026 14:01:12 -0700 Subject: [PATCH 05/25] Remove obj_free_object_id This empty function is no longer needed now that _id2ref is gone. --- gc.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gc.c b/gc.c index b29c72d7dfb9b1..223467db4ab72b 100644 --- a/gc.c +++ b/gc.c @@ -2185,16 +2185,10 @@ object_id(VALUE obj) return object_id0(obj); } -static inline void -obj_free_object_id(VALUE obj) -{ -} - void rb_gc_obj_free_vm_weak_references(VALUE obj) { ASSUME(!RB_SPECIAL_CONST_P(obj)); - obj_free_object_id(obj); if (rb_obj_gen_fields_p(obj)) { rb_free_generic_ivar(obj); From 2e5922e112f231444217732f234cd67fe088a055 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 9 Jul 2026 14:12:19 -0400 Subject: [PATCH 06/25] ZJIT: GC fastpath alloc for hashes with symbol keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ZJIT can prove that we'll always receive symbols as keys for NewHash, we can spill less because `rb_hash_bulk_insert` is now leaf (cmp and hash are leaf). ``` HashNew with symbol keys (unit: millions of IPS) ┌───────────────────┬────────┬───────┬───────┬──────────────────────┐ │ op │ before │ after │ raw Δ │ control-normalized Δ │ ├───────────────────┼────────┼───────┼───────┼──────────────────────┤ │ hash 2 (ar_table) │ 27.95 │ 29.96 │ +7.2% │ +10.3% │ ├───────────────────┼────────┼───────┼───────┼──────────────────────┤ │ hash 6 (ar_table) │ 15.64 │ 16.84 │ +7.7% │ +10.9% │ ├───────────────────┼────────┼───────┼───────┼──────────────────────┤ │ hash 9 (st_table) │ 7.15 │ 7.25 │ +1.5% │ +4.5% │ ├───────────────────┼────────┼───────┼───────┼──────────────────────┤ │ array 4 (control) │ 33.90 │ 32.93 │ −2.9% │ — (reference) │ └───────────────────┴────────┴───────┴───────┴──────────────────────┘ ``` --- jit.c | 3 ++ yjit/src/cruby_bindings.inc.rs | 1 + zjit/src/codegen.rs | 33 +++++++++++++++++- zjit/src/codegen_tests.rs | 64 ++++++++++++++++++++++++++++++++++ zjit/src/cruby_bindings.inc.rs | 1 + 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/jit.c b/jit.c index 5def0f89b14ec7..534a07c848abfd 100644 --- a/jit.c +++ b/jit.c @@ -41,6 +41,9 @@ enum jit_bindgen_constants { // Field offset for the RHash struct RUBY_OFFSET_RHASH_IFNONE = offsetof(struct RHash, ifnone), + // Max pairs an embedded ar_table hash holds before it converts to an st_table + RUBY_RHASH_AR_TABLE_MAX_SIZE = RHASH_AR_TABLE_MAX_SIZE, + // Field offsets for the RString struct RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len), diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 29149bbb044836..8ec0c495bbeb9a 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -989,6 +989,7 @@ pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; pub const TDATA_OFFSET_FIELDS_OBJ: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RHASH_IFNONE: jit_bindgen_constants = 16; +pub const RUBY_RHASH_AR_TABLE_MAX_SIZE: jit_bindgen_constants = 8; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; pub const RB_SHAPE_FLAG_SHIFT: jit_bindgen_constants = 32; pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 295f227411cccf..1a4f84d60beb4d 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -615,7 +615,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio } Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"), Insn::NewArray { elements, state } => gen_new_array(jit, asm, opnds!(elements), &function.frame_state(*state)), - Insn::NewHash { elements, state } => gen_new_hash(jit, asm, function, opnds!(elements), &function.frame_state(*state)), + Insn::NewHash { elements, state } => { + let sym_keys = elements.iter().step_by(2).all(|&key| function.type_of(key).is_subtype(types::Symbol)); + gen_new_hash(jit, asm, function, opnds!(elements), sym_keys, &function.frame_state(*state)) + } Insn::NewRange { low, high, flag, state } => gen_new_range(jit, asm, function, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::NewRangeFixnum { low, high, flag, state } => gen_new_range_fixnum(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), @@ -2283,6 +2286,7 @@ fn gen_new_hash( asm: &mut Assembler, function: &Function, elements: Vec, + sym_keys: bool, state: &FrameState, ) -> lir::Opnd { if elements.is_empty() { @@ -2300,6 +2304,33 @@ fn gen_new_hash( // the redundant store and be reusable for other types. asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); hash + // TODO: we should use effects_of for this (we would need to add it). + } else if sym_keys { + // Symbols hash and compare without running Ruby and those operations never raise so + // the bulk insert is leaf. + gen_prepare_leaf_call_with_gc(asm, state); + + let num_pairs = elements.len() / 2; + let hash = if num_pairs <= RUBY_RHASH_AR_TABLE_MAX_SIZE as usize { + let alloc_size = unsafe { rb_zjit_hash_new_size() }; + let flags = RUBY_T_HASH as u64; + let klass = unsafe { rb_cHash }; + + let hash = gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, klass, |asm| { + asm_ccall!(asm, rb_hash_new_with_size, num_pairs.into()) + }); + // TODO: this runs on the slow path too, where rb_hash_new already set + // ifnone. A fast-path-only init hook in gc_fast_path_new_obj would avoid + // the redundant store and be reusable for other types. + asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); + hash + } else { + asm_ccall!(asm, rb_hash_new_with_size, num_pairs.into()) + }; + + let argv = gen_push_opnds(jit, asm, &elements); + asm_ccall!(asm, rb_hash_bulk_insert, elements.len().into(), argv, hash); + hash } else { gen_prepare_non_leaf_call(jit, asm, function, state); diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 56de74a8104937..eed7e280e9a15a 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2920,6 +2920,70 @@ fn test_new_hash_empty_gc_stress() { "#), @"[Hash, 1, nil, {a: 1}]"); } +// Static-symbol keys hash and compare without running Ruby, so NewHash takes the +// leaf bulk-insert fast path into an inline-allocated ar_table. Runs under GC +// stress to guard the leaf-call preparation. +#[test] +fn test_new_hash_static_sym_keys_gc_stress() { + eval(" + def make(a, b) = {x: a, y: b} + "); + assert_contains_opcode("make", YARVINSN_newhash); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make(1, 2) + h = make(:foo, [3]) + [h.class, h.size, h[:x], h[:y], h[:z], h.default, h] + ensure + GC.stress = false + end + "#), @"[Hash, 2, :foo, [3], nil, nil, {x: :foo, y: [3]}]"); +} + +// Eight pairs fills an inline embedded ar_table (the fast path); nine crosses +// RHASH_AR_TABLE_MAX_SIZE, so it's built as a pre-sized st_table instead. Both stay +// on the static-symbol leaf path, so this guards the ar_table and st_table routes. +#[test] +fn test_new_hash_static_sym_ar_table_boundary() { + eval(" + def eight(v) = {a:v,b:v,c:v,d:v,e:v,f:v,g:v,h:v} + def nine(v) = {a:v,b:v,c:v,d:v,e:v,f:v,g:v,h:v,i:v} + "); + assert_contains_opcode("eight", YARVINSN_newhash); + assert_contains_opcode("nine", YARVINSN_newhash); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + [eight(7).size, eight(7)[:h], nine([9]).size, nine([9])[:i]] + ensure + GC.stress = false + end + "#), @"[8, 7, 9, [9]]"); +} + +// Dynamic symbol keys hash and compare without running Ruby, so NewHash takes the +// leaf bulk-insert fast path into an inline-allocated ar_table. Runs under GC +// stress to guard the leaf-call preparation. +#[test] +fn test_new_hash_dynamic_sym_keys_gc_stress() { + eval(r#" + def make(k, v) = { :"x_#{k}" => v, :"y_#{k}" => v } + "#); + assert_contains_opcode("make", YARVINSN_newhash); + assert_contains_opcode("make", YARVINSN_intern); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make("warm", 0) + h = make("k", [3]) + [h.class, h.size, h[:"x_k"], h[:"y_k"]] + ensure + GC.stress = false + end + "#), @r#"[Hash, 2, [3], [3]]"#); +} + #[test] fn test_object_alloc_gc_stress() { eval(" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index e54937dd9463a2..6b2c6c0e5f078f 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1958,6 +1958,7 @@ pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; pub const TDATA_OFFSET_FIELDS_OBJ: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RHASH_IFNONE: jit_bindgen_constants = 16; +pub const RUBY_RHASH_AR_TABLE_MAX_SIZE: jit_bindgen_constants = 8; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; pub const RB_SHAPE_FLAG_SHIFT: jit_bindgen_constants = 32; pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; From e2403acbca36399a1a1280ae04dd70974e68a757 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:04:05 +0900 Subject: [PATCH 07/25] win32/rm.bat: fix `-r` failing to remove directories `del` exits successfully even when it removes nothing, so a directory containing no plain files was never passed to `rd /s`, and a directory containing files kept its subdirectories. Remove directories before falling back to `del`. --- win32/rm.bat | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/win32/rm.bat b/win32/rm.bat index c41ebfa5ee0177..95d889ee486e15 100755 --- a/win32/rm.bat +++ b/win32/rm.bat @@ -55,10 +55,13 @@ goto :remove_end ) ::- Try `rd` first for symlink to a directory; `del` attemps to remove all ::- files under the target directory, instead of the symlink itself. - (rd /q "%p%" || del /q "%p%") 2> nul && goto :remove_end + rd /q "%p%" 2> nul && goto :remove_end + ::- `del` exits with 0 even when nothing matched, so its result cannot + ::- tell whether directories remain; remove them first. if "%recursive%" == "-r" for /D %%I in (%p%) do ( rd /s /q %%I || call set error=%%ERRORLEVEL%% ) + if exist "%p%" del /q "%p%" 2> nul :remove_end endlocal & set "error=%error%" & goto :EOF From 2bd6bf4c3ea054c80d78f0b6990c67c517947441 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:04:24 +0900 Subject: [PATCH 08/25] win32/rm.bat: fix exist check for paths expanded from wildcards The check used the path relative to the expanded wildcard parent instead of the full path, so removal through a wildcard in the middle of a path was silently skipped. --- win32/rm.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win32/rm.bat b/win32/rm.bat index 95d889ee486e15..8d5aaba14734df 100755 --- a/win32/rm.bat +++ b/win32/rm.bat @@ -48,7 +48,7 @@ if "%sub:\=%" == "%sub%" goto :remove_plain goto :remove_end :remove_plain set p=%2%1 - if not exist "%1" goto :remove_end + if not exist "%p%" goto :remove_end if not "%dryrun%" == "" ( echo Removing %p:\=/% goto :remove_end From 3a372f02b15cdf44a48122decd5df28ce9625fa4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:09:41 +0900 Subject: [PATCH 09/25] [DOC] Restore Windows backslash note for filename globbing Dir.glob documentation had noted since [Bug #2625] that a backslash cannot be used as a path separator in patterns on Windows, but the note was dropped when the documentation was consolidated into doc/file/filename_globbing.md (GH-17265). Restore it, and also note that File::FNM_NOESCAPE does not turn the backslash into a separator. --- doc/file/filename_globbing.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doc/file/filename_globbing.md b/doc/file/filename_globbing.md index 8c691cccda50d1..273dd46b938c62 100644 --- a/doc/file/filename_globbing.md +++ b/doc/file/filename_globbing.md @@ -325,6 +325,22 @@ Pathname('.').glob('\[k-m][h-j][a-c]') # => [] Pathname('.').glob('\**/*.rb') # => [] ``` +Note that the backslash escapes the following character on Windows as well, +and therefore cannot be used as a path separator in the pattern; +`Dir.glob('C:\Users\*')` matches nothing. +Write the pattern with forward slashes instead: + +```ruby +Dir.glob('C:/Users/*') +``` + +A Windows path taken from an external source (such as an environment variable) +may be converted for use as a pattern: + +```ruby +pattern = path.tr('\\', '/') +``` + ## Keyword Arguments | Keyword | Value | Default | Meaning | @@ -417,6 +433,11 @@ Pathname('.').glob('\*').size # => 0 Pathname('.').glob('\*', File::FNM_NOESCAPE).size # => 0 ``` +Note that on Windows this flag does not make the backslash usable +as a path separator in the pattern; +the backslash is then matched as an ordinary character, +which cannot occur in an entry name. + #### Constant File::FNM_SHORTNAME By default, Windows shortname matching is disabled; From fdad6b5dcf0de57c79e064f4189a2b3be5514e84 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:58:02 +0900 Subject: [PATCH 10/25] [DOC] Restore glob documentation lost in consolidation Dir.glob's block form, Dir[]'s multiple pattern arguments, matching only directories by a trailing slash, and the note that '**' without a following slash is equivalent to '*' were dropped when the glob documentation was consolidated into doc/file/filename_globbing.md (GH-17265). Restore them, with examples verified against the current source tree. --- dir.rb | 15 +++++++++++++-- doc/file/filename_globbing.md | 29 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/dir.rb b/dir.rb index 461b344d180ead..8917a8f79e4691 100644 --- a/dir.rb +++ b/dir.rb @@ -213,18 +213,29 @@ def initialize(name, encoding: nil) end # call-seq: - # Dir[patterns, base: nil, sort: true] -> array + # Dir[*patterns, base: nil, sort: true] -> array + # + # Like Dir.glob, but does not accept keyword argument +flags+, + # and may take multiple patterns as arguments: + # + # Dir['*.rb', '*.h'].take(3) # => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] # - # Like Dir.glob, but does not accept keyword argument +flags+. def self.[](*args, base: nil, sort: true) Primitive.dir_s_aref(args, base, sort) end # call-seq: # Dir.glob(patterns, flags: 0, base: '.', sort: true) -> array_of_entries + # Dir.glob(patterns, flags: 0, base: '.', sort: true) {|entry_name| ... } -> nil # # Returns an array of filesystem entries; # see {Filename Globbing}[rdoc-ref:file/filename_globbing.md]. + # + # With a block given, calls the block with each of the selected entry names + # and returns +nil+: + # + # Dir.glob('*.rb') {|entry_name| puts entry_name } # => nil + # def self.glob(pattern, _flags = 0, flags: _flags, base: nil, sort: true) Primitive.attr! :use_block Primitive.dir_s_glob(pattern, flags, base, sort) diff --git a/doc/file/filename_globbing.md b/doc/file/filename_globbing.md index 273dd46b938c62..7e21abeaa20dff 100644 --- a/doc/file/filename_globbing.md +++ b/doc/file/filename_globbing.md @@ -33,7 +33,10 @@ Inputs to the filename-globbing methods: Their return values: -- Each of the methods `Dir[]` and Dir.glob returns an array of the selected string entries. +- \Method `Dir[]` returns an array of the selected string entries. +- \Method Dir.glob with no block returns an array of the selected string entries. +- \Method Dir.glob with a block calls the block with each selected string entry + and returns `nil`. - \Method Pathname#glob with no block returns an array of Pathname objects each based on a selected string entry. - \Method Pathname#glob with a block calls the block with each pathname @@ -44,8 +47,13 @@ Examples: ```ruby Dir['*'].take(3) # => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir['*.rb', '*.h'].take(3) +# => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] Dir.glob('*').take(3) # => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir.glob(['*.rb', '*.h']).take(3) +# => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] +Dir.glob('*.rb') {|entry_name| puts entry_name } # => nil Pathname('.').glob('*').take(3) # => [#, #, #] a = [] @@ -299,6 +307,25 @@ Pathname('.').glob('test/ruby/**/*.rb').take(3) # #] ``` +The double-asterisk pattern matches directories recursively +only when followed by the slash character `'/'`; +otherwise it is equivalent to pattern `'*'`: + +```ruby +Dir.glob('**') == Dir.glob('*') # => true +``` + +A pattern ending with `'/'` matches directory names only; +each matched name ends with `'/'`: + +```ruby +# Find the directories at the top level. +Dir.glob('*/').take(3) # => ["basictest/", "benchmark/", "bin/"] + +# Find all directories everywhere. +Dir.glob('**/').take(3) # => ["basictest/", "benchmark/", "benchmark/gc/"] +``` + The pattern may be escaped: ```ruby From 07490d1e52706611064f82021e3c403ab0575ed7 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 9 Jul 2026 15:15:58 -0700 Subject: [PATCH 11/25] tool/redmine-backporter.rb: Authenticate GET requests bugs.ruby-lang.org now rejects anonymous JSON API requests with 403 Forbidden. The backporter already requires a Redmine API key and uses it for write requests, but the read-only issue and journal lookups were still sent without credentials. Send the existing X-Redmine-API-Key header for those GET requests too, so ls/show/done/last can keep using the JSON API. --- tool/redmine-backporter.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tool/redmine-backporter.rb b/tool/redmine-backporter.rb index 95a9688cb2cef9..bd4a4114354cd0 100755 --- a/tool/redmine-backporter.rb +++ b/tool/redmine-backporter.rb @@ -33,6 +33,7 @@ BACKPORT_CF_KEY = 'cf_5' STATUS_CLOSE = 5 REDMINE_API_KEY = api_key || ENV['REDMINE_API_KEY'] || (puts opts.help; raise 'need to specify REDMINE_API_KEY') +REDMINE_API_KEY_HEADER = {'X-Redmine-API-Key' => REDMINE_API_KEY}.freeze REDMINE_BASE = 'https://bugs.ruby-lang.org' @query = { @@ -164,8 +165,16 @@ def has_commit(commit, branch) system("git", *base, "merge-base", "--is-ancestor", commit, branch) end +def redmine_read_json(uri) + JSON(uri.read($openuri_options.merge(REDMINE_API_KEY_HEADER))) +end + +def redmine_get(http, uri) + http.get(uri.respond_to?(:request_uri) ? uri.request_uri : uri, REDMINE_API_KEY_HEADER) +end + def show_last_journal(http, uri) - res = http.get("#{uri.path}?include=journals") + res = redmine_get(http, "#{uri.path}?include=journals") res.value h = JSON(res.body) x = h["issue"] @@ -219,7 +228,7 @@ class CommandSyntaxError < RuntimeError; end raise CommandSyntaxError unless /\A(\d+)?\z/ =~ args uri = URI(REDMINE_BASE+'/projects/ruby-master/issues.json?'+URI.encode_www_form(@query.dup.merge('page' => ($1 ? $1.to_i : 1)))) # puts uri - res = JSON(uri.read($openuri_options)) + res = redmine_read_json(uri) @issues = issues = res["issues"] from = res["offset"] + 1 total = res["total_count"] @@ -244,7 +253,7 @@ class CommandSyntaxError < RuntimeError; end end uri = "#{REDMINE_BASE}/issues/#{id}" uri = URI(uri+".json?include=children,attachments,relations,changesets,journals") - res = JSON(uri.read($openuri_options)) + res = redmine_read_json(uri) i = res["issue"] unless i["changesets"] abort "You don't have view_changesets permission" @@ -375,7 +384,8 @@ class << @changesets uri = URI("#{REDMINE_BASE}/issues/#{@issue}.json") Net::HTTP.start(uri.host, uri.port, http_options) do |http| - res = http.get(uri.path) + res = redmine_get(http, uri) + res.value data = JSON(res.body) h = data["issue"]["custom_fields"].find{|x|x["id"]==5} if h and val = h["value"] and val != "" From 014f7f1aa8337b1c35ab9191c09975fb6c351c4c Mon Sep 17 00:00:00 2001 From: XrXr Date: Thu, 9 Jul 2026 18:47:00 -0400 Subject: [PATCH 12/25] YJIT: Fix `warn(unused_assignments)` in test_gen_opt_reverse Previously: ``` warning: value assigned to `value_array` is never read --> yjit/src/codegen.rs:11446:9 | 11446 | value_array[1] = 4; | ^^^^^^^^^^^^^^^^^^ | = help: maybe it is overwritten before being read? = note: `#[warn(unused_assignments)]` (part of `#[warn(unused)]`) on by default ``` It can't see the alias through `jit.pc`! --- yjit/src/codegen.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 1cd4c6c70df8a3..1f8b320c4d6749 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -11429,9 +11429,8 @@ mod tests { asm.stack_push(Type::Flonum); asm.stack_push(Type::CString); - let mut value_array: [u64; 2] = [0, 3]; - let pc: *mut VALUE = &mut value_array as *mut u64 as *mut VALUE; - jit.pc = pc; + let mut fake_encoded_iseq: [VALUE; 2] = [VALUE(0), VALUE(3)]; + jit.pc = &mut fake_encoded_iseq[0]; let mut status = gen_opt_reverse(&mut jit, &mut asm); @@ -11442,8 +11441,9 @@ mod tests { assert_eq!(Type::Fixnum, asm.ctx.get_opnd_type(StackOpnd(0))); // Try again with an even number of elements. + fake_encoded_iseq[1] = VALUE(4); + let _ = fake_encoded_iseq; asm.stack_push(Type::Nil); - value_array[1] = 4; status = gen_opt_reverse(&mut jit, &mut asm); assert_eq!(status, Some(KeepCompiling)); From 0ef01bc18e99104d5c2db816733c0ef601af49f8 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 6 Jul 2026 12:00:37 -0400 Subject: [PATCH 13/25] [ruby/rubygems] Skip the make jobserver when using BSD make Like nmake, BSD make can't parse the GNU `--jobserver-auth` flag in MAKEFLAGS and aborts native extension builds with: make: argument 'observer-auth=6,7' to option '-j' must be a positive number Skip the jobserver on BSD platforms, unless an gmake is explicitly being used. https://github.com/ruby/rubygems/commit/d682fd3370 --- lib/bundler/installer/parallel_installer.rb | 8 +++- .../installer/parallel_installer_spec.rb | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb index f65f171cb8a8f2..09540c3f568306 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -125,7 +125,7 @@ def with_jobserver # every native extension build with `fatal error U1065: invalid option # '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains # such as mingw use GNU make and keep working through the inherited pipe. - return yield if nmake? + return yield if nmake? || bsd_make? begin r, w = IO.pipe @@ -155,6 +155,12 @@ def nmake? /\bnmake/i.match?(make.to_s) end + def bsd_make? + return false unless Gem.freebsd_platform? + make = ENV["MAKE"] || ENV["make"] || "make" + !/\bgmake/i.match?(make) + end + def install_serially until finished_installing? raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb index 1f057cc4061145..22c880952aefbc 100644 --- a/spec/bundler/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -251,4 +251,50 @@ def redefine_build_jobs expect(makeflags_during).to eq(makeflags_before) end end + + describe "make jobserver on BSD" do + # BSD make (the default `make` on FreeBSD) can't parse the GNU + # `--jobserver-auth` and aborts every native extension build, so the + # jobserver must be skipped there. + it "leaves MAKEFLAGS untouched" do + parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) + + makeflags_before = ENV["MAKEFLAGS"] + makeflags_during = :not_yielded + + old_make = ENV["MAKE"] + ENV.delete("MAKE") + allow(Gem).to receive(:freebsd_platform?).and_return(true) + begin + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + ensure + ENV["MAKE"] = old_make + end + + expect(makeflags_during).to eq(makeflags_before) + end + + # A BSD user who opts into gmake gets a make that understands the + # jobserver, so it should still be set up. + it "sets up the jobserver when gmake is used" do + parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) + + makeflags_during = :not_yielded + + old_make = ENV["MAKE"] + ENV["MAKE"] = "gmake" + allow(Gem).to receive(:freebsd_platform?).and_return(true) + begin + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + ensure + ENV["MAKE"] = old_make + end + + expect(makeflags_during).to include("--jobserver-auth=") + end + end end From b226c591082bc78d5f0a506e69b4ed31ed421be3 Mon Sep 17 00:00:00 2001 From: Simon Quigley Date: Thu, 9 Jul 2026 19:58:00 -0500 Subject: [PATCH 14/25] [DOC] Fix man page markup for groff 1.24 Two issues caused groff/lintian warnings on the mdoc pages: 1. man/ruby.1: the MISC ENVIRONMENT list for RUBY_TCP_NO_FAST_FALLBACK opened a .Bl that was never closed before .Sh SEE ALSO, producing "A .Bl directive has no matching .El". 2. man/erb.1 and man/ruby.1: .Bl -tag -width "1234567890123" uses a long all-digit width string. Under groff 1.24 that is mishandled and floods "cannot adjust line; overset by ~5e7n" warnings, and mangled OPTIONS rendering. Replace it with -width "--encoding" (a representative tag). Verified with man --warnings: 0 warnings on erb.1/ruby.1/goruby.1 after this change (was 249 / 1399 / 0 on master). --- man/erb.1 | 4 ++-- man/goruby.1 | 2 +- man/ruby.1 | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/man/erb.1 b/man/erb.1 index 9d8cfbb1bb361d..f518d86678e44b 100644 --- a/man/erb.1 +++ b/man/erb.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd July 10, 2026 .Dt ERB 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME @@ -31,7 +31,7 @@ is a part of .Nm Ruby . .Pp .Sh OPTIONS -.Bl -tag -width "1234567890123" -compact +.Bl -tag -width "--encoding" -compact .Pp .It Fl -version Prints the version of diff --git a/man/goruby.1 b/man/goruby.1 index fb72f6d5b845a3..7465c20f1f5904 100644 --- a/man/goruby.1 +++ b/man/goruby.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd May 24, 2026 .Dt GORUBY 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME diff --git a/man/ruby.1 b/man/ruby.1 index 803e922c394296..84ec097dbf8ccd 100644 --- a/man/ruby.1 +++ b/man/ruby.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd July 10, 2026 .Dt RUBY 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME @@ -141,7 +141,7 @@ to see how they are being developed and used. The Ruby interpreter accepts the following command-line options (switches). They are quite similar to those of .Xr perl 1 . -.Bl -tag -width "1234567890123" -compact +.Bl -tag -width "--encoding" -compact .Pp .It Fl -copyright Prints the copyright notice, and quits immediately without running any @@ -808,6 +808,7 @@ When set to .Li 0 or left unset, the fast fallback feature is enabled. Introduced in Ruby 3.4, default: unset. +.El .Sh SEE ALSO .Bl -hang -compact -width "https://www.ruby-toolbox.com/" .It Lk https://www.ruby-lang.org/ From c62bfaf72101236fb2e20988177810a592e924a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:09:20 +0000 Subject: [PATCH 15/25] Bump the github-actions group across 1 directory with 5 updates Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` | | [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` | | [actions/labeler](https://github.com/actions/labeler) | `6.1.0` | `6.2.0` | | [taiki-e/install-action](https://github.com/taiki-e/install-action) | `2.82.10` | `2.83.0` | Updates `github/codeql-action/init` from 4.36.3 to 4.37.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) Updates `github/codeql-action/analyze` from 4.36.3 to 4.37.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) Updates `github/codeql-action/upload-sarif` from 4.36.3 to 4.37.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) Updates `actions/labeler` from 6.1.0 to 6.2.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/f27b608878404679385c85cfa523b85ccb86e213...b8dd2d9be0f68b860e7dae5dae7d772984eacd6d) Updates `taiki-e/install-action` from 2.82.10 to 2.83.0 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/50414676f9f5d50a65992c6dd2ed02641263226c...c7eb1735f09259a5035e8e5d44b1406b1cddc0fb) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/labeler dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.83.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/labeler.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 537432bd407920..dbe2f43bb475c8 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d0a8024b053b0d..f0fa12c456ef08 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,4 +12,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 + - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index e63a22e5d262b9..d2c8b7a4f1a929 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 82cef518c3ea51..3ff9076d57bca0 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2.82.10 + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 81aa9dd4beaca7..223712af7cb87d 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -141,7 +141,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2.82.10 + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 59d4afaade45c4d8d4c226a348243c7155d5c254 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Mon, 6 Jul 2026 11:03:19 -0400 Subject: [PATCH 16/25] [ruby/net-http] Add support for HTTP `QUERY` method Closes https://github.com/ruby/net-http/issues/202 As of June 2026, [RFC 10008][] is a proposed standard. From the RFC Abstract: > This specification defines the QUERY method for HTTP. A QUERY requests > that the request target process the enclosed content in a safe and > idempotent manner and then respond with the result of that processing. > This is similar to POST requests, but QUERY requests can be > automatically repeated or restarted without concern for partial state > changes. This commit proposes support for the HTTP Query method by way of the `Net::HTTP::Query` class along with the corresponding `#query` and `#request_query` methods. [RFC 10008]: https://www.rfc-editor.org/rfc/rfc10008.html https://github.com/ruby/net-http/commit/76b11b3a26 --- lib/net/http.rb | 55 +++++++++++++++++++++++++++++++++++++- lib/net/http/request.rb | 1 + lib/net/http/requests.rb | 35 ++++++++++++++++++++++++ test/net/http/test_http.rb | 28 +++++++++++++++++++ test/net/http/utils.rb | 9 +++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/lib/net/http.rb b/lib/net/http.rb index 0e8d8bf3fa869f..4e2e6c9263f1d9 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -82,6 +82,7 @@ class HTTPHeaderSyntaxError < StandardError; end # body = 'Some text' # http.post(path, body) # Can also have a block. # http.put(path, body) + # http.query(path, body) # Can also have a block. # http.delete(path) # http.options(path) # http.trace(path) @@ -207,6 +208,7 @@ class HTTPHeaderSyntaxError < StandardError; end # - #options: OPTIONS. # - #trace: TRACE. # - #patch: PATCH. + # - #query, #request_query: QUERY. # # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: # @@ -553,6 +555,8 @@ class HTTPHeaderSyntaxError < StandardError; end # Sends a PROPPATCH request and returns a response object. # - {#put}[rdoc-ref:Net::HTTP#put]: # Sends a PUT request and returns a response object. + # - {#query}[rdoc-ref:Net::HTTP#query]: + # Sends a QUERY request and returns a response object. # - {#request}[rdoc-ref:Net::HTTP#request]: # Sends a request and returns a response object. # - {#request_get}[rdoc-ref:Net::HTTP#request_get]: @@ -2101,6 +2105,25 @@ def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segm send_entity(path, data, initheader, dest, Patch, &block) end + # Sends a QUERY request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Query object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.query('/todos/1', data) # => # + # + # Related: + # + # - Net::HTTP::Query: request class for \HTTP method QUERY. + # - Net::HTTP.query: sends QUERY request, returns response body. + # + def query(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Query, &block) + end + # Sends a PUT request to the server; # returns an instance of a subclass of Net::HTTPResponse. # @@ -2335,6 +2358,36 @@ def request_put(path, data, initheader = nil, &block) #:nodoc: request Put.new(path, initheader), data, &block end + # Sends a QUERY request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Query object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.query('/todos/1', 'xyzzy') + # # => # + # + # With no block given, returns the response object: + # + # http = Net::HTTP.new(hostname) + # http.request_query('/todos') # => # + # + # With a block given, calls the block with the response object + # and returns the response object: + # + # http.request_query('/todos') do |res| + # p res + # end # => # + # + # Output: + # + # # + # + def request_query(path, data, initheader = nil, &block) # :yield: +response+ + request Query.new(path, initheader), data, &block + end + alias get2 request_get #:nodoc: obsolete alias head2 request_head #:nodoc: obsolete alias post2 request_post #:nodoc: obsolete @@ -2430,7 +2483,7 @@ def send_entity(path, data, initheader, dest, type, &block) # :stopdoc: - IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE QUERY/.freeze # :nodoc: def transport_request(req) count = 0 diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb index 4a138572e905f4..db7641a7b24dff 100644 --- a/lib/net/http/request.rb +++ b/lib/net/http/request.rb @@ -61,6 +61,7 @@ # - Net::HTTP::Options # - Net::HTTP::Trace # - Net::HTTP::Patch +# - Net::HTTP::Query # # Subclasses for WebDAV requests: # diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb index 8dc79a9f665d52..2685b797ee09bd 100644 --- a/lib/net/http/requests.rb +++ b/lib/net/http/requests.rb @@ -271,6 +271,41 @@ class Net::HTTP::Patch < Net::HTTPRequest RESPONSE_HAS_BODY = true end +# \Class for representing +# {HTTP method QUERY}[https://www.rfc-editor.org/rfc/rfc10008.html]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Net::HTTP::Query.new(uri) # => # +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: yes. +# +# Related: +# +# - Net::HTTP#query: sends +QUERY+ request, returns response object. +# +class Net::HTTP::Query < Net::HTTPRequest + # :stopdoc: + METHOD = 'QUERY' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + # # WebDAV methods --- RFC2518 # diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 477c3415b48f53..e5028d4ef6a0ca 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -636,6 +636,7 @@ module TestNetHTTP_version_1_2_methods def test_request start {|http| _test_request__GET http + _test_request__QUERY http _test_request__accept_encoding http _test_request__file http # _test_request__range http # WEBrick does not support Range: header. @@ -665,6 +666,21 @@ def _test_request__GET(http) } end + def _test_request__QUERY(http) + data = 'query data' + req = Net::HTTP::Query.new('/') + req['Accept'] = $test_net_http_data_type + req['Content-Type'] = 'application/x-www-form-urlencoded' + http.request(req, data) {|res| + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal data, res.body + } + end + def _test_request__accept_encoding(http) req = Net::HTTP::Get.new('/', 'accept-encoding' => 'deflate') http.request(req) {|res| @@ -791,6 +807,7 @@ def _test_request__uri_host(http) def test_send_request start {|http| _test_send_request__GET http + _test_send_request__QUERY http _test_send_request__HEAD http _test_send_request__POST http } @@ -806,6 +823,17 @@ def _test_send_request__GET(http) assert_equal $test_net_http_data, res.body end + def _test_send_request__QUERY(http) + data = 'aaabbb cc ddddddddddd lkjoiu4j3qlkuoa' + res = http.send_request('QUERY', '/', data, 'content-type' => 'application/x-www-form-urlencoded') + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal data, res.body + end + def _test_send_request__HEAD(http) res = http.send_request('HEAD', '/') assert_kind_of Net::HTTPResponse, res diff --git a/test/net/http/utils.rb b/test/net/http/utils.rb index f499963d418729..3618a92b774535 100644 --- a/test/net/http/utils.rb +++ b/test/net/http/utils.rb @@ -282,6 +282,8 @@ def spawn_server handle_post(path, headers, socket) when 'PATCH' handle_patch(path, headers, socket) + when 'QUERY' + handle_query(path, headers, socket) else socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" end @@ -330,6 +332,13 @@ def handle_patch(path, headers, socket) socket.print(response) end + def handle_query(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + content_type = headers['Content-Type'] || 'application/octet-stream' + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}" + socket.print(response) + end + def parse_content_type(content_type) return [nil, nil] unless content_type type, *params = content_type.split(';').map(&:strip) From b9be3d6b6dbc190af64fb95a8263076d3f4f19db Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 10 Jul 2026 00:12:29 -0500 Subject: [PATCH 17/25] [DOC] Minor updates to Set documentation --- set.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/set.c b/set.c index e5853e52b44a93..4e76750a9f0400 100644 --- a/set.c +++ b/set.c @@ -416,7 +416,7 @@ set_s_alloc(VALUE klass) * call-seq: * Set[*objects] -> new_set * - * Returns a new \Set object populated with the given +objects+: + * Returns a new set populated with the given +objects+: * * Set[1, 'one', :one, 1.0, %w[a b c], {foo: 0, bar: 1}] * # => Set[1, "one", :one, 1.0, ["a", "b", "c"], {foo: 0, bar: 1}] @@ -503,11 +503,11 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new(object = nil) -> new_set * Set.new(object = nil) {|element| ... } -> new_set * - * Returns a new \Set object based on the given +object+, + * Returns a new set based on the given +object+, * which must be an Enumerable or +nil+. * * With argument +object+ given as +nil+, - * returns a new empty \Set object: + * returns a new empty set: * * Set.new # => Set[] * Set.new { fail 'Cannot happen' } # => Set[] # Block not called. @@ -695,7 +695,7 @@ set_i_to_a(VALUE set) * set.to_set.equal?(set) # => true * * With no block given, when +self+ is an instance of a subclass of +Set+, - * returns a \Set object containing the elements of +self+: + * returns a set containing the elements of +self+: * * class MySet < Set; end * my_set = MySet[*0..9] # => # @@ -749,7 +749,7 @@ set_i_join(int argc, VALUE *argv, VALUE set) * call-seq: * add(object) -> self * - * Adds the given +object+ to +self+, returns +self+: + * Adds the given +object+ to +self+; returns +self+: * * set = Set[0, 1, 2] * set.add(%w[a b c]) # => Set[0, 1, 2, ["a", "b", "c"]] @@ -1439,7 +1439,7 @@ set_xor_i(st_data_t key, st_data_t data) * call-seq: * self ^ enumerable -> new_set * - * Returns a new \Set object containing + * Returns a new set containing * the {exclusive OR}[https://en.wikipedia.org/wiki/Exclusive_or] * of +self+ and the given +enumerable+; * that is, containing each element that is in either +self+ or +enumerable+, @@ -1479,7 +1479,7 @@ set_i_xor(VALUE set, VALUE other) * call-seq: * self | enumerable -> new_set * - * Returns a new \Set object containing + * Returns a new set containing * the {union}[https://en.wikipedia.org/wiki/Union_(set_theory)] * of +self+ and the given +enumerable+; * that is, containing the elements of both +self+ and +enumerable+. @@ -1738,7 +1738,7 @@ set_i_replace(VALUE set, VALUE other) * call-seq: * reset -> self * - * Resets the internal state of +self+; return +self+. + * Resets the internal state of +self+; returns +self+. * * A set relies on the #hash results of each element being consistent. * Modifying an element in a way that changes the results of #hash @@ -2501,8 +2501,8 @@ rb_set_size(VALUE set) * - #each: * Calls the block with each successive element of +self+; returns +self+. * - #reset: - * Resets the internal state; useful if an element - * has been modified while an element in the set. + * Resets the internal state of +self+; returns +self+. + * Useful if an element has been modified while an element in the set. * */ void From 64450290f3b8e3f3653cc399f388e473ea8fb66a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 30 Jun 2026 15:40:09 +0200 Subject: [PATCH 18/25] sprintf.c: don't raise "too many arguments" errors when $DEBUG is set [Feature #22136] This is as far as I can tell the only method that changes its behavior in debug mode, and that's all but helpful because it might cause the program to abort. Usually when you run a program with $DEBUG set, it is to find some error that may have been swallowed, so aside from the extra output you would expect Ruby to behave the same than in normal mode. --- spec/ruby/core/string/modulo_spec.rb | 26 ++++++++++++++------------ sprintf.c | 1 - 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spec/ruby/core/string/modulo_spec.rb b/spec/ruby/core/string/modulo_spec.rb index 45381fd92f5cb0..9c5eed8cf690fd 100644 --- a/spec/ruby/core/string/modulo_spec.rb +++ b/spec/ruby/core/string/modulo_spec.rb @@ -67,18 +67,20 @@ end end - it "raises an ArgumentError for unused arguments when $DEBUG is true" do - begin - old_debug = $DEBUG - $DEBUG = true - s = $stderr - $stderr = IOStub.new - - -> { "" % [1, 2, 3] }.should.raise(ArgumentError) - -> { "%s" % [1, 2, 3] }.should.raise(ArgumentError) - ensure - $DEBUG = old_debug - $stderr = s + ruby_version_is ""..."4.1" do + it "raises an ArgumentError for unused arguments when $DEBUG is true" do + begin + old_debug = $DEBUG + $DEBUG = true + s = $stderr + $stderr = IOStub.new + + -> { "" % [1, 2, 3] }.should.raise(ArgumentError) + -> { "%s" % [1, 2, 3] }.should.raise(ArgumentError) + ensure + $DEBUG = old_debug + $stderr = s + end end end diff --git a/sprintf.c b/sprintf.c index d380053fa8fc37..9ff8498143127c 100644 --- a/sprintf.c +++ b/sprintf.c @@ -947,7 +947,6 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) */ if (posarg >= 0 && nextarg < argc && !(argc == 2 && RB_TYPE_P(argv[1], T_HASH))) { const char *mesg = "too many arguments for format string"; - if (RTEST(ruby_debug)) rb_raise(rb_eArgError, "%s", mesg); if (RTEST(ruby_verbose)) rb_warn("%s", mesg); } rb_str_resize(result, blen); From 52f58888037ce87f404010ca0417c83d94f8e520 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 10 Jul 2026 04:02:42 +0000 Subject: [PATCH 19/25] Make the object-traversal mark redirect per-Ractor rb_objspace_reachable_objects_from and rb_objspace_reachable_objects_from_root install a redirect so that marking a child hands it to a callback instead of the real mark routine. It lived in vm->gc.mark_func_data, a single slot shared by the whole VM. Move it to rb_ractor_t: an installed redirect belongs to the Ractor doing the traversal, and no other Ractor should observe it. A modular GC (e.g. MMTk) may mark on worker threads that have no current EC, where the Ractor is not reachable, so on a modular build it keeps living in the VM. A non-modular build pays nothing extra over the previous GET_VM() lookup. Behaviour is unchanged with a single Ractor. Co-Authored-By: Claude Opus 4.8 (1M context) --- gc.c | 37 ++++++++++++++++++++++++------------- ractor_core.h | 7 +++++++ vm_core.h | 21 +++++++++++++++++---- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/gc.c b/gc.c index 223467db4ab72b..4bdc164347271b 100644 --- a/gc.c +++ b/gc.c @@ -2656,11 +2656,22 @@ ruby_stack_check(void) /* ==================== Marking ==================== */ +/* The traversal mark redirect is per-Ractor, except on a modular GC where + * marking can run on worker threads with no current EC and it lives in the VM. + * GC_MARK_FUNC_DATA_SLOTP() points at the active slot; a non-modular build pays + * nothing extra over a plain GET_VM() (one GET_RACTOR(), no NULL check). */ +#if USE_MODULAR_GC +# define GC_MARK_FUNC_DATA_SLOTP() (&GET_VM()->gc.mark_func_data) +#else +# define GC_MARK_FUNC_DATA_SLOTP() (&GET_RACTOR()->mark_func_data) +#endif + #define RB_GC_MARK_OR_TRAVERSE(func, obj_or_ptr, obj, check_obj) do { \ if (!RB_SPECIAL_CONST_P(obj)) { \ - rb_vm_t *vm = GET_VM(); \ - void *objspace = vm->gc.objspace; \ - if (LIKELY(vm->gc.mark_func_data == NULL)) { \ + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); \ + struct gc_mark_func_data_struct *mark_func_data = *mfdp; \ + void *objspace = GET_VM()->gc.objspace; \ + if (LIKELY(mark_func_data == NULL)) { \ GC_ASSERT(rb_gc_impl_during_gc_p(objspace)); \ (func)(objspace, (obj_or_ptr)); \ } \ @@ -2669,10 +2680,9 @@ ruby_stack_check(void) !rb_gc_impl_garbage_object_p(objspace, obj) : \ true) { \ GC_ASSERT(!rb_gc_impl_during_gc_p(objspace)); \ - struct gc_mark_func_data_struct *mark_func_data = vm->gc.mark_func_data; \ - vm->gc.mark_func_data = NULL; \ + *mfdp = NULL; \ mark_func_data->mark_func((obj), mark_func_data->data); \ - vm->gc.mark_func_data = mark_func_data; \ + *mfdp = mark_func_data; \ } \ } \ } while (0) @@ -4539,16 +4549,16 @@ rb_objspace_reachable_objects_from(VALUE obj, void (func)(VALUE, void *), void * if (rb_gc_impl_during_gc_p(rb_gc_get_objspace())) rb_bug("rb_objspace_reachable_objects_from() is not supported while during GC"); if (!RB_SPECIAL_CONST_P(obj)) { - rb_vm_t *vm = GET_VM(); - struct gc_mark_func_data_struct *prev_mfd = vm->gc.mark_func_data; + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); + struct gc_mark_func_data_struct *prev_mfd = *mfdp; struct gc_mark_func_data_struct mfd = { .mark_func = func, .data = data, }; - vm->gc.mark_func_data = &mfd; + *mfdp = &mfd; rb_gc_mark_children(rb_gc_get_objspace(), obj); - vm->gc.mark_func_data = prev_mfd; + *mfdp = prev_mfd; } } } @@ -4578,16 +4588,17 @@ rb_objspace_reachable_objects_from_root(void (func)(const char *category, VALUE, .data = passing_data, }; - struct gc_mark_func_data_struct *prev_mfd = vm->gc.mark_func_data; + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); + struct gc_mark_func_data_struct *prev_mfd = *mfdp; struct gc_mark_func_data_struct mfd = { .mark_func = root_objects_from, .data = &data, }; - vm->gc.mark_func_data = &mfd; + *mfdp = &mfd; rb_gc_save_machine_context(); rb_gc_mark_roots(vm->gc.objspace, &data.category); - vm->gc.mark_func_data = prev_mfd; + *mfdp = prev_mfd; } /* diff --git a/ractor_core.h b/ractor_core.h index 032e578603a398..9d31072406a4ae 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -69,6 +69,13 @@ struct rb_ractor_struct { struct rb_ractor_pub pub; struct rb_ractor_sync sync; +#if !USE_MODULAR_GC + /* traversal-API mark redirect (NULL outside a traversal). Per Ractor so a + * concurrent traversal on another Ractor is never observed. A modular GC + * keeps this in the VM instead (vm->gc.mark_func_data). */ + struct gc_mark_func_data_struct *mark_func_data; +#endif + // thread management struct { struct ccan_list_head set; diff --git a/vm_core.h b/vm_core.h index c72933ae21f1e5..1c3c6dd1588185 100644 --- a/vm_core.h +++ b/vm_core.h @@ -679,6 +679,16 @@ typedef struct rb_hook_list_struct { // see builtin.h for definition typedef const struct rb_builtin_function *RB_BUILTIN; +/* The mark redirect used by the object-traversal APIs + * (rb_objspace_reachable_objects_from etc.). It is installed while a traversal + * runs and is NULL during a real GC. Storage is per-Ractor + * (rb_ractor_t.mark_func_data), except on a modular GC where it lives in the VM + * (rb_vm_struct's gc sub-struct; see gc.c). */ +struct gc_mark_func_data_struct { + void *data; + void (*mark_func)(VALUE v, void *data); +}; + typedef struct rb_vm_struct { VALUE self; @@ -799,10 +809,13 @@ typedef struct rb_vm_struct { struct { struct rb_objspace *objspace; - struct gc_mark_func_data_struct { - void *data; - void (*mark_func)(VALUE v, void *data); - } *mark_func_data; +#if USE_MODULAR_GC + /* A modular GC (e.g. MMTk) may mark on worker threads that have no + * current EC, so the traversal mark redirect must be reachable without + * a Ractor and lives here. Otherwise it is per-Ractor + * (rb_ractor_t.mark_func_data). */ + struct gc_mark_func_data_struct *mark_func_data; +#endif } gc; rb_at_exit_list *at_exit; From a1392b966535f60d06d0da1db03df5890d325f18 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 10 Jul 2026 04:02:42 +0000 Subject: [PATCH 20/25] Make mark_object_ary per-Ractor Objects registered with rb_gc_register_mark_object were pinned in a single vm->mark_object_ary. Give each Ractor its own (rb_ractor_t.mark_object_ary, created lazily) and mark it from ractor_mark; the main Ractor's list is also marked from rb_vm_mark so early-boot registrations are safe before the main Ractor joins vm->ractor.set (e.g. under RUBY_DEBUG=gc_stress). When a Ractor terminates, hand its objects to the main Ractor so these process-lifetime pins stay alive. global_object_list (rb_gc_register_address) stays VM-global: a registered address is a slot whose value changes over time, so it has no single owner and every root scan must keep seeing it. Behaviour is unchanged with a single Ractor. Co-Authored-By: Claude Opus 4.8 (1M context) --- ractor.c | 21 ++++++++++++++++++++- ractor_core.h | 5 +++++ vm.c | 40 ++++++++++++++++++++++++++++++++++------ vm_core.h | 1 - 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/ractor.c b/ractor.c index 22b6432c4b1e81..e58a85d100ea7d 100644 --- a/ractor.c +++ b/ractor.c @@ -237,6 +237,12 @@ ractor_mark(void *ptr) if (!checking_shareable) { // may unshareable objects + + /* objects this Ractor pinned via rb_gc_register_mark_object (the + * pin_array_list wrapper itself is an unshareable internal object; + * updated in ractor_update_references) */ + if (r->mark_object_ary) rb_gc_mark_movable(r->mark_object_ary); + rb_gc_mark(r->r_stdin); rb_gc_mark(r->r_stdout); rb_gc_mark(r->r_stderr); @@ -313,13 +319,23 @@ ractor_memsize(const void *ptr) return sizeof(rb_ractor_t) + ractor_sync_memsize(r); } +static void +ractor_update_references(void *ptr) +{ + rb_ractor_t *r = (rb_ractor_t *)ptr; + /* the registered mark objects list is marked movable in ractor_mark */ + if (r->mark_object_ary) { + r->mark_object_ary = rb_gc_location(r->mark_object_ary); + } +} + static const rb_data_type_t ractor_data_type = { "ractor", { ractor_mark, ractor_free, ractor_memsize, - NULL, // update + ractor_update_references, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY /* | RUBY_TYPED_WB_PROTECTED */ }; @@ -437,6 +453,9 @@ vm_remove_ractor(rb_vm_t *vm, rb_ractor_t *cr) RB_VM_LOCK(); { + /* keep this Ractor's registered mark objects alive under the main Ractor */ + if (cr->mark_object_ary) rb_vm_ractor_migrate_mark_objects(vm->ractor.main_ractor, cr); + RUBY_DEBUG_LOG("ractor.cnt:%u-- terminate_waiting:%d", vm->ractor.cnt, vm->ractor.sync.terminate_waiting); diff --git a/ractor_core.h b/ractor_core.h index 9d31072406a4ae..ce43ca9e91943c 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -69,6 +69,10 @@ struct rb_ractor_struct { struct rb_ractor_pub pub; struct rb_ractor_sync sync; + /* objects pinned via rb_gc_register_mark_object; this Ractor owns them and + * marks them, and hands them to the main Ractor when it terminates. */ + VALUE mark_object_ary; + #if !USE_MODULAR_GC /* traversal-API mark redirect (NULL outside a traversal). Per Ractor so a * concurrent traversal on another Ractor is never observed. A modular GC @@ -143,6 +147,7 @@ rb_ractor_self(const rb_ractor_t *r) rb_ractor_t *rb_ractor_main_alloc(void); void rb_ractor_main_setup(rb_vm_t *vm, rb_ractor_t *main_ractor, rb_thread_t *main_thread); +void rb_vm_ractor_migrate_mark_objects(rb_ractor_t *dst, rb_ractor_t *src); void rb_ractor_atexit(rb_execution_context_t *ec, VALUE result); void rb_ractor_atexit_exception(rb_execution_context_t *ec); void rb_ractor_teardown(rb_execution_context_t *ec); diff --git a/vm.c b/vm.c index d0f944592ae55a..58eb41d9113acb 100644 --- a/vm.c +++ b/vm.c @@ -3290,7 +3290,6 @@ rb_vm_update_references(void *ptr) rb_vm_t *vm = ptr; vm->self = rb_gc_location(vm->self); - vm->mark_object_ary = rb_gc_location(vm->mark_object_ary); vm->orig_progname = rb_gc_location(vm->orig_progname); vm->cc_refinement_set = rb_gc_location(vm->cc_refinement_set); @@ -3376,7 +3375,14 @@ rb_vm_mark(void *ptr) rb_box_entry_mark(vm->main_box); } - rb_gc_mark_movable(vm->mark_object_ary); + /* The main Ractor's registered mark objects (rb_gc_register_mark_object) + * are process-lifetime pins. Mark them here as well as from ractor_mark + * so they stay live before the main Ractor joins vm->ractor.set, e.g. + * during early boot under GC.stress. */ + if (vm->ractor.main_ractor && vm->ractor.main_ractor->mark_object_ary) { + rb_gc_mark_movable(vm->ractor.main_ractor->mark_object_ary); + } + rb_gc_mark_movable(vm->orig_progname); rb_gc_mark_movable(vm->coverages); rb_gc_mark_movable(vm->me2counter); @@ -4824,15 +4830,37 @@ rb_vm_register_global_object(VALUE obj) break; } RB_VM_LOCKING() { - VALUE list = GET_VM()->mark_object_ary; + rb_ractor_t *cr = GET_RACTOR(); + if (!cr->mark_object_ary) cr->mark_object_ary = pin_array_list_new(Qnil); + VALUE list = cr->mark_object_ary; VALUE head = pin_array_list_append(list, obj); if (head != list) { - GET_VM()->mark_object_ary = head; + cr->mark_object_ary = head; } RB_GC_GUARD(obj); } } +/* Hand src's registered mark objects to dst (used when a Ractor terminates: + * these are process-lifetime pins, so the main Ractor keeps them alive). */ +void +rb_vm_ractor_migrate_mark_objects(rb_ractor_t *dst, rb_ractor_t *src) +{ + ASSERT_vm_locking(); + VALUE list = src->mark_object_ary; + while (!NIL_P(list) && list) { + struct pin_array_list *array_list; + TypedData_Get_Struct(list, struct pin_array_list, &pin_array_list_type, array_list); + for (long i = 0; i < array_list->len; i++) { + if (!dst->mark_object_ary) dst->mark_object_ary = pin_array_list_new(Qnil); + VALUE head = pin_array_list_append(dst->mark_object_ary, array_list->array[i]); + if (head != dst->mark_object_ary) dst->mark_object_ary = head; + } + list = array_list->next; + } + src->mark_object_ary = 0; +} + VALUE rb_cc_refinement_set_create(void); void @@ -4840,8 +4868,8 @@ Init_vm_objects(void) { rb_vm_t *vm = GET_VM(); - /* initialize mark object array, hash */ - vm->mark_object_ary = pin_array_list_new(Qnil); + /* mark object arrays are per-Ractor (rb_ractor_t.mark_object_ary), + * lazily created on first rb_gc_register_mark_object */ st_init_existing_table_with_size(&vm->ci_table, &vm_ci_hashtype, 0); vm->cc_refinement_set = rb_cc_refinement_set_create(); } diff --git a/vm_core.h b/vm_core.h index 1c3c6dd1588185..008193b2e7b43f 100644 --- a/vm_core.h +++ b/vm_core.h @@ -773,7 +773,6 @@ typedef struct rb_vm_struct { unsigned int thread_ignore_deadlock: 1; /* object management */ - VALUE mark_object_ary; VALUE **global_object_list; size_t global_object_list_size; size_t global_object_list_capa; From ef67c377508ea3a07516c1fa413a038b5268fc1c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 10 Jul 2026 18:08:46 +1200 Subject: [PATCH 21/25] Respect `handle_interrupt` for default `SIGINT`. (#17533) [[Bug #22133]] --- signal.c | 2 +- .../ruby/core/thread/handle_interrupt_spec.rb | 46 +++++++++++++++++++ test/ruby/test_thread.rb | 34 ++++++++++++++ thread.c | 23 ++++++++++ vm_core.h | 1 + 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/signal.c b/signal.c index f37aaf971ad997..6ec02a5fe8ad3a 100644 --- a/signal.c +++ b/signal.c @@ -1112,7 +1112,7 @@ rb_signal_exec(rb_thread_t *th, int sig) if (cmd == 0) { switch (sig) { case SIGINT: - rb_interrupt(); + rb_threadptr_interrupt_raise(th); break; #ifdef SIGHUP case SIGHUP: diff --git a/spec/ruby/core/thread/handle_interrupt_spec.rb b/spec/ruby/core/thread/handle_interrupt_spec.rb index aa03d4c66d2570..c9de57aaa1ce60 100644 --- a/spec/ruby/core/thread/handle_interrupt_spec.rb +++ b/spec/ruby/core/thread/handle_interrupt_spec.rb @@ -122,4 +122,50 @@ def make_handle_interrupt_thread(interrupt_config, blocking = true) it "supports multiple pairs in the Hash" do make_handle_interrupt_thread(ArgumentError => :never, RuntimeError => :never).should == [:deferred] end + + ruby_version_is "4.1" do + platform_is_not :windows do + def signal_exception_deferred_by_handle_interrupt(signal) + ruby_exe(<<-RUBY) + waiting = Thread::Queue.new + release = Thread::Queue.new + inner = false + + Thread.new do + waiting.pop + Process.kill(:#{signal}, Process.pid) + release.push(true) + end + + begin + Thread.handle_interrupt(SignalException => :never) do + begin + waiting.push(true) + release.pop + rescue SignalException + inner = true + raise + end + end + rescue SignalException => exception + puts exception.class + puts exception.signo + puts inner + end + RUBY + end + + it "defers the default SIGINT Interrupt until exiting the handle_interrupt block" do + out = signal_exception_deferred_by_handle_interrupt(:INT) + + out.should == "Interrupt\n#{Signal.list.fetch("INT")}\nfalse\n" + end + + it "defers the default SIGTERM SignalException until exiting the handle_interrupt block" do + out = signal_exception_deferred_by_handle_interrupt(:TERM) + + out.should == "SignalException\n#{Signal.list.fetch("TERM")}\nfalse\n" + end + end + end end diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 475bdc7558cdfe..9f509cc4f220c1 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -847,6 +847,40 @@ def test_handle_interrupt_blocking assert_equal(:ok, r) end + def test_handle_interrupt_masks_sigint + if /mswin|mingw/ =~ RUBY_PLATFORM + omit "SIGINT handling differs on Windows" + end + + assert_in_out_err([], <<-INPUT, %w(outer false), []) + waiting = Thread::Queue.new + release = Thread::Queue.new + inner = false + + Thread.new do + waiting.pop + Process.kill(:INT, Process.pid) + release.push(true) + end + + begin + Thread.handle_interrupt(SignalException => :never) do + begin + waiting.push(true) + release.pop + rescue Interrupt + inner = true + raise + end + end + rescue Interrupt + puts "outer" + end + + puts inner + INPUT + end + def test_handle_interrupt_and_io assert_in_out_err([], <<-INPUT, %w(ok), []) th_waiting = true diff --git a/thread.c b/thread.c index 72839418fc5074..3cfbe6beff199c 100644 --- a/thread.c +++ b/thread.c @@ -2812,6 +2812,29 @@ rb_threadptr_signal_raise(rb_thread_t *th, int sig) rb_threadptr_raise(th->vm->ractor.main_thread, 2, argv); } +void +rb_threadptr_interrupt_raise(rb_thread_t *th) +{ + rb_thread_t *target_th = th->vm->ractor.main_thread; + + if (rb_threadptr_dead(target_th)) { + return; + } + + /* Preserve the traditional no-message Interrupt from default SIGINT. */ + VALUE exc = rb_exc_new(rb_eInterrupt, 0, 0); + + /* making an exception object can switch thread, + so we need to check thread deadness again */ + if (rb_threadptr_dead(target_th)) { + return; + } + + rb_ec_setup_exception(GET_EC(), exc, Qundef); + rb_threadptr_pending_interrupt_enque(target_th, exc); + rb_threadptr_interrupt(target_th); +} + void rb_threadptr_signal_exit(rb_thread_t *th) { diff --git a/vm_core.h b/vm_core.h index 008193b2e7b43f..fba04b7f56ac5c 100644 --- a/vm_core.h +++ b/vm_core.h @@ -2266,6 +2266,7 @@ int rb_signal_buff_size(void); int rb_signal_exec(rb_thread_t *th, int sig); void rb_threadptr_check_signal(rb_thread_t *mth); void rb_threadptr_signal_raise(rb_thread_t *th, int sig); +void rb_threadptr_interrupt_raise(rb_thread_t *th); void rb_threadptr_signal_exit(rb_thread_t *th); int rb_threadptr_execute_interrupts(rb_thread_t *, int); void rb_threadptr_interrupt(rb_thread_t *th); From 5f9613168c8fad4aedacea0c23e15fe4aa4dc96f Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 10 Jul 2026 01:33:42 -0500 Subject: [PATCH 22/25] [DOC] Doc for Pathname#readlines --- io.c | 11 ++++++-- pathname_builtin.rb | 67 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/io.c b/io.c index fb66a2f27fd51b..91119735508474 100644 --- a/io.c +++ b/io.c @@ -12141,7 +12141,7 @@ io_s_foreach(VALUE v) * * - {Open Options}[rdoc-ref:IO@Open+Options]. * - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. - * - {Line Options}[rdoc-ref:IO@Line+IO]. + * - {Line Input Options}[rdoc-ref:IO@Line+Input+Options]. * * Returns an Enumerator if no block is given. * @@ -12217,7 +12217,7 @@ io_s_readlines(VALUE v) * * - {Open Options}[rdoc-ref:IO@Open+Options]. * - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. - * - {Line Options}[rdoc-ref:IO@Line+IO]. + * - {Line Input Options}[rdoc-ref:IO@Line+Input+Options]. * */ @@ -15253,7 +15253,12 @@ set_LAST_READ_LINE(VALUE val, ID _x, VALUE *_y) * === Line Input * * Class \IO supports line-oriented input for - * {files}[rdoc-ref:IO@File+Line+Input] and {IO streams}[rdoc-ref:IO@Stream+Line+Input] + * {files}[rdoc-ref:IO@File+Line+Input] and {IO streams}[rdoc-ref:IO@Stream+Line+Input]. + * + * ==== Line Input Options + * + * Optional keyword argument +chomp+ (default: +false+) + * specifies whether line separators are to be excluded from the result of a read. * * ==== \File Line Input * diff --git a/pathname_builtin.rb b/pathname_builtin.rb index ab3132725db541..91f2252e6aec4a 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -982,10 +982,10 @@ class Pathname # * File * # :markup: markdown # # call-seq: - # each_line(sep = $/, **opts) {|line| ... } → nil - # each_line(limit, **opts) {|line| ... } → nil - # each_line(sep, limit, **opts) {|line| ... } → nil - # each_line(...) → new_enumerator + # each_line(sep = $/, **opts) {|line| ... } -> nil + # each_line(limit, **opts) {|line| ... } -> nil + # each_line(sep, limit, **opts) {|line| ... } -> nil + # each_line(...) -> new_enumerator # # With a block given, calls the block with each line # from the file represented by `self`; @@ -1077,7 +1077,60 @@ def read(...) File.read(@path, ...) end # def binread(...) File.binread(@path, ...) end - # See File.readlines. Returns all the lines from the file. + # :markup: markdown + # + # call-seq: + # readlines(sep = $/, **options) -> array + # readlines(limit, **options) -> array + # readlines(sep, limit, **options) -> array + # + # Returns an array of all lines read from the source at the path in `self`, + # which must be the path to a file. + # + # Examples here use a file defined + # at [IO Example Files](rdoc-ref:IO@Example+Files). + # + # With no arguments given, parses lines from the file at the given path, + # as determined by the default line separator, and returns those lines in an array: + # + # ```ruby + # pn = Pathname('t.txt') + # ppn.readlines + # # => ["First line\n", "Second line\n", "\n", "Fourth line\n", "Fifth line\n"] + # ``` + # + # With argument `sep` given, + # parses lines as determined by that line separator + # (see [IO Line Separator](rdoc-ref:IO@Line+Separator)): + # + # ```ruby + # pn.readlines('li') + # # => ["First li", "ne\nSecond li", "ne\n\nFourth li", "ne\nFifth li", "ne\n"] + # pn.readlines('') # Special "paragraphs" separator value. + # # => ["First line\nSecond line\n\n", "Fourth line\nFifth line\n"] + # pn.readlines(nil) # Special "slurp" separator value. + # # => ["First line\nSecond line\n\nFourth line\nFifth line\n"] + # ``` + # + # With argument `limit` given, parses lines as determined by the default line separator + # and the given line-length `limit` + # (see [IO Line Separator](rdoc-ref:IO@Line+Separator) + # and [IO Line Limit](rdoc-ref:IO@Line+Limit)): + # + # ```ruby + # pn.readlines(7) + # # => ["First l", "ine\n", "Second ", "line\n", "\n", "Fourth ", "line\n", "Fifth l", "ine\n"] + # ``` + # + # With arguments `sep` and `limit` given, combines the two behaviors + # (see [IO Line Separator and Line Limit](rdoc-ref:IO@Line+Separator+and+Line+Limit)). + # + # Optional keyword arguments `options` specify: + # + # - {IO Open Options}[rdoc-ref:IO@Open+Options]. + # - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. + # - {IO Line Input Options}[rdoc-ref:IO@Line+Input+Options]. + # def readlines(...) File.readlines(@path, ...) end # See File.sysopen. @@ -2087,8 +2140,8 @@ def zero?() FileTest.zero?(@path) end class Pathname # call-seq: - # glob(patterns, base: '.', flags: 0, sort: true) → array_of_pathnames - # glob(patterns, base: '.', flags: 0, sort: true) {|pathname| ... } → nil + # glob(patterns, base: '.', flags: 0, sort: true) -> array_of_pathnames + # glob(patterns, base: '.', flags: 0, sort: true) {|pathname| ... } -> nil # # Selects filesystem entries # based on the given keyword arguments +base+, +flags+, and +sort+; From b416f906cf29a3586d3378e147db24f7c39cae3f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 10 Jul 2026 07:26:04 +0200 Subject: [PATCH 23/25] Remove `String#to_f` and `Kernel#Float` out of range warning [Feature #22085] This warning is only actionable for float literals in source code. When parsing user input into a float, most of the time this warning isn't actionable and is pure noise. --- object.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/object.c b/object.c index 5a3203d4e1a567..919a2fd2e7cdec 100644 --- a/object.c +++ b/object.c @@ -3571,7 +3571,6 @@ rb_cstr_to_dbl_raise(const char *p, rb_encoding *enc, int badcheck, int raise, i d = strtod(p, &end); if (errno == ERANGE) { OutOfRange(); - rb_warning("Float %.*s%s out of range", w, p, ellipsis); errno = 0; } if (p == end) { @@ -3654,7 +3653,6 @@ rb_cstr_to_dbl_raise(const char *p, rb_encoding *enc, int badcheck, int raise, i d = strtod(p, &end); if (errno == ERANGE) { OutOfRange(); - rb_warning("Float %.*s%s out of range", w, p, ellipsis); errno = 0; } if (badcheck) { From 9eb455cdd11a7f1cdb78dcffadaaf83aa0982000 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Thu, 9 Jul 2026 16:02:22 -0500 Subject: [PATCH 24/25] [DOC] Doc for Pathname.rename --- pathname_builtin.rb | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 91f2252e6aec4a..17c924cf8e41db 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1563,7 +1563,47 @@ def open(...) # :yield: file # def readlink() self.class.new(File.readlink(@path)) end - # See File.rename. Rename the file. + # :markup: markdown + # + # call-seq: + # rename(new_name) + # + # Renames the entry at the path in `self` to the entry given in `new_name`, + # which may be either a path or another pathname: + # + # ```ruby + # # Create source and destination pathnames and directories. + # pn_srcdir = Pathname('/tmp/src') # => # + # pn_srcdir.mkdir + # pn_dstdir = Pathname('/tmp/dst') # => # + # pn_dstdir.mkdir + # # Create source file pathname and file. + # pn_srcfile = pn_srcdir.join('t.tmp') # => # + # pn_srcfile.write('foo') + # # Create destination file pathname. + # pn_dstfile = pn_dstdir.join('u.tmp') # => # + # # Rename source file as destination file. + # pn_srcfile.rename(pn_dstfile) + # pn_srcfile.exist? # => false + # pn_dstfile.exist? # => true + # ``` + # + # Works for directories, too: + # + # ```ruby + # pn_dstdir.rename('/tmp/foo') + # pn_dstdir.exist? # => false + # Pathname('/tmp/foo').exist? # => true + # ``` + # + # Clean up. + # + # ```ruby + # pn_srcdir.rmtree + # Pathname('/tmp/foo').rmtree + # ``` + # + # Raises SystemCallError if the entry cannot be renamed. def rename(to) File.rename(@path, to) end # See File.stat. Returns a File::Stat object. From 79da507032b0efb8189bf4aa19d4104263601de8 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 9 Jul 2026 15:54:03 +0900 Subject: [PATCH 25/25] Prevent overflow of length bits for embedded arrays Embedded arrays only use 7 bits for the length, so it will overflow if the GC can allocate large slots. We need to add the cap to ary_embeddable_p. --- array.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/array.c b/array.c index a05d57b83105fa..58f41287bdb3cd 100644 --- a/array.c +++ b/array.c @@ -208,7 +208,9 @@ ary_embed_size(long capa) static bool ary_embeddable_p(long capa) { - return rb_gc_size_allocatable_p(ary_embed_size(capa)); + const long embed_len_max = RARRAY_EMBED_LEN_MASK >> RARRAY_EMBED_LEN_SHIFT; + + return capa <= embed_len_max && rb_gc_size_allocatable_p(ary_embed_size(capa)); } bool