From 5230f835e807b7b6935b758de58ee26da6cb6a60 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Wed, 7 Jan 2026 17:01:56 -0600 Subject: [PATCH 1/8] [DOC] Harmonize #[] methods --- array.c | 34 +++++++++++++++------------------- doc/string/aref.rdoc | 24 ++++++++++++------------ re.c | 10 +++++----- string.c | 14 +++++++------- 4 files changed, 39 insertions(+), 43 deletions(-) diff --git a/array.c b/array.c index e13239ad3daaa1..b4718238763bab 100644 --- a/array.c +++ b/array.c @@ -1789,14 +1789,10 @@ static VALUE rb_ary_aref2(VALUE ary, VALUE b, VALUE e); /* * call-seq: - * self[index] -> object or nil - * self[start, length] -> object or nil + * self[offset] -> object or nil + * self[offset, size] -> object or nil * self[range] -> object or nil * self[aseq] -> object or nil - * slice(index) -> object or nil - * slice(start, length) -> object or nil - * slice(range) -> object or nil - * slice(aseq) -> object or nil * * Returns elements from +self+; does not modify +self+. * @@ -1804,27 +1800,27 @@ static VALUE rb_ary_aref2(VALUE ary, VALUE b, VALUE e); * * a = [:foo, 'bar', 2] * - * # Single argument index: returns one element. + * # Single argument offset: returns one element. * a[0] # => :foo # Zero-based index. * a[-1] # => 2 # Negative index counts backwards from end. * - * # Arguments start and length: returns an array. + * # Arguments offset and size: returns an array. * a[1, 2] # => ["bar", 2] - * a[-2, 2] # => ["bar", 2] # Negative start counts backwards from end. + * a[-2, 2] # => ["bar", 2] # Negative offset counts backwards from end. * * # Single argument range: returns an array. * a[0..1] # => [:foo, "bar"] * a[0..-2] # => [:foo, "bar"] # Negative range-begin counts backwards from end. * a[-2..2] # => ["bar", 2] # Negative range-end counts backwards from end. * - * When a single integer argument +index+ is given, returns the element at offset +index+: + * When a single integer argument +offset+ is given, returns the element at offset +offset+: * * a = [:foo, 'bar', 2] * a[0] # => :foo * a[2] # => 2 * a # => [:foo, "bar", 2] * - * If +index+ is negative, counts backwards from the end of +self+: + * If +offset+ is negative, counts backwards from the end of +self+: * * a = [:foo, 'bar', 2] * a[-1] # => 2 @@ -1832,29 +1828,29 @@ static VALUE rb_ary_aref2(VALUE ary, VALUE b, VALUE e); * * If +index+ is out of range, returns +nil+. * - * When two Integer arguments +start+ and +length+ are given, - * returns a new array of size +length+ containing successive elements beginning at offset +start+: + * When two Integer arguments +offset+ and +size+ are given, + * returns a new array of size +size+ containing successive elements beginning at offset +offset+: * * a = [:foo, 'bar', 2] * a[0, 2] # => [:foo, "bar"] * a[1, 2] # => ["bar", 2] * - * If start + length is greater than self.length, - * returns all elements from offset +start+ to the end: + * If offset + size is greater than self.size, + * returns all elements from offset +offset+ to the end: * * a = [:foo, 'bar', 2] * a[0, 4] # => [:foo, "bar", 2] * a[1, 3] # => ["bar", 2] * a[2, 2] # => [2] * - * If start == self.size and length >= 0, + * If offset == self.size and size >= 0, * returns a new empty array. * - * If +length+ is negative, returns +nil+. + * If +size+ is negative, returns +nil+. * * When a single Range argument +range+ is given, - * treats range.min as +start+ above - * and range.size as +length+ above: + * treats range.min as +offset+ above + * and range.size as +size+ above: * * a = [:foo, 'bar', 2] * a[0..1] # => [:foo, "bar"] diff --git a/doc/string/aref.rdoc b/doc/string/aref.rdoc index 59c6ae97ace01e..a9ab8857bc1fc8 100644 --- a/doc/string/aref.rdoc +++ b/doc/string/aref.rdoc @@ -1,30 +1,30 @@ Returns the substring of +self+ specified by the arguments. -Form self[index] +Form self[offset] -With non-negative integer argument +index+ given, -returns the 1-character substring found in self at character offset index: +With non-negative integer argument +offset+ given, +returns the 1-character substring found in self at character offset +offset+: 'hello'[0] # => "h" 'hello'[4] # => "o" 'hello'[5] # => nil 'こんにちは'[4] # => "は" -With negative integer argument +index+ given, +With negative integer argument +offset+ given, counts backward from the end of +self+: 'hello'[-1] # => "o" 'hello'[-5] # => "h" 'hello'[-6] # => nil -Form self[start, length] +Form self[offset, size] -With integer arguments +start+ and +length+ given, -returns a substring of size +length+ characters (as available) -beginning at character offset specified by +start+. +With integer arguments +offset+ and +size+ given, +returns a substring of size +size+ characters (as available) +beginning at character offset specified by +offset+. -If argument +start+ is non-negative, -the offset is +start+: +If argument +offset+ is non-negative, +the offset is +offset+: 'hello'[0, 1] # => "h" 'hello'[0, 5] # => "hello" @@ -33,7 +33,7 @@ the offset is +start+: 'hello'[2, 0] # => "" 'hello'[2, -1] # => nil -If argument +start+ is negative, +If argument +offset+ is negative, counts backward from the end of +self+: 'hello'[-1, 1] # => "o" @@ -41,7 +41,7 @@ counts backward from the end of +self+: 'hello'[-1, 0] # => "" 'hello'[-6, 5] # => nil -Special case: if +start+ equals the length of +self+, +Special case: if +offset+ equals the size of +self+, returns a new empty string: 'hello'[5, 3] # => "" diff --git a/re.c b/re.c index fe8e93c6a6b96c..027fa597f4da5b 100644 --- a/re.c +++ b/re.c @@ -2213,12 +2213,12 @@ match_ary_aref(VALUE match, VALUE idx, VALUE result) /* * call-seq: - * matchdata[index] -> string or nil - * matchdata[start, length] -> array - * matchdata[range] -> array - * matchdata[name] -> string or nil + * self[offset] -> string or nil + * self[offset, size] -> array + * self[range] -> array + * self[name] -> string or nil * - * When arguments +index+, +start and +length+, or +range+ are given, + * When arguments +offset+, +offset+ and +size+, or +range+ are given, * returns match and captures in the style of Array#[]: * * m = /(.)(.)(\d+)(\d)/.match("THX1138.") diff --git a/string.c b/string.c index c8233be66fd06a..234ef1edc8c64b 100644 --- a/string.c +++ b/string.c @@ -5713,8 +5713,8 @@ rb_str_aref(VALUE str, VALUE indx) /* * call-seq: - * self[index] -> new_string or nil - * self[start, length] -> new_string or nil + * self[offset] -> new_string or nil + * self[offset, size] -> new_string or nil * self[range] -> new_string or nil * self[regexp, capture = 0] -> new_string or nil * self[substring] -> new_string or nil @@ -12493,11 +12493,11 @@ sym_match_m_p(int argc, VALUE *argv, VALUE sym) /* * call-seq: - * symbol[index] -> string or nil - * symbol[start, length] -> string or nil - * symbol[range] -> string or nil - * symbol[regexp, capture = 0] -> string or nil - * symbol[substring] -> string or nil + * self[offset] -> string or nil + * self[offset, size] -> string or nil + * self[range] -> string or nil + * self[regexp, capture = 0] -> string or nil + * self[substring] -> string or nil * * Equivalent to symbol.to_s[]; see String#[]. * From 3ea6ec8344a07e2d10ba248c69039dd4d27fd8fb Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Wed, 7 Jan 2026 17:02:22 -0600 Subject: [PATCH 2/8] [DOC] Harmonize #=~ methods (#15814) --- re.c | 7 +++---- string.c | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/re.c b/re.c index 027fa597f4da5b..b2c1909c153895 100644 --- a/re.c +++ b/re.c @@ -3663,12 +3663,11 @@ reg_match_pos(VALUE re, VALUE *strp, long pos, VALUE* set_match) /* * call-seq: - * regexp =~ string -> integer or nil + * self =~ other -> integer or nil * * Returns the integer index (in characters) of the first match - * for +self+ and +string+, or +nil+ if none; - * also sets the - * {rdoc-ref:Regexp global variables}[rdoc-ref:Regexp@Global+Variables]: + * for +self+ and +other+, or +nil+ if none; + * updates {Regexp-related global variables}[rdoc-ref:Regexp@Global+Variables]. * * /at/ =~ 'input data' # => 7 * $~ # => # diff --git a/string.c b/string.c index 234ef1edc8c64b..6f4ea03fb37a41 100644 --- a/string.c +++ b/string.c @@ -5011,12 +5011,15 @@ rb_str_byterindex_m(int argc, VALUE *argv, VALUE str) /* * call-seq: - * self =~ object -> integer or nil + * self =~ other -> integer or nil * - * When +object+ is a Regexp, returns the index of the first substring in +self+ - * matched by +object+, - * or +nil+ if no match is found; - * updates {Regexp-related global variables}[rdoc-ref:Regexp@Global+Variables]: + * When +other+ is a Regexp: + * + * - Returns the integer index (in characters) of the first match + * for +self+ and +other+, or +nil+ if none; + * - Updates {Regexp-related global variables}[rdoc-ref:Regexp@Global+Variables]. + * + * Examples: * * 'foo' =~ /f/ # => 0 * $~ # => # @@ -5034,8 +5037,8 @@ rb_str_byterindex_m(int argc, VALUE *argv, VALUE str) * /(?\d+)/ =~ 'no. 9' # => 4 * number # => "9" # Assigned. * - * If +object+ is not a Regexp, returns the value - * returned by object =~ self. + * When +other+ is not a Regexp, returns the value + * returned by other =~ self. * * Related: see {Querying}[rdoc-ref:String@Querying]. */ @@ -12445,9 +12448,9 @@ sym_casecmp_p(VALUE sym, VALUE other) /* * call-seq: - * symbol =~ object -> integer or nil + * self =~ other -> integer or nil * - * Equivalent to symbol.to_s =~ object, + * Equivalent to self.to_s =~ other, * including possible updates to global variables; * see String#=~. * From 950ffa90b7939885cc35d376a2e10aabfdc7170d Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:24:29 +0900 Subject: [PATCH 3/8] ZJIT: Add ArrayAset instruction to HIR (#15747) Inline `Array#[]=` into `ArrayAset`. --- test/ruby/test_zjit.rb | 109 ++++++++++++++++++++++++++++++++++++ zjit/src/codegen.rs | 35 ++++++++++++ zjit/src/cruby_methods.rs | 26 +++++++++ zjit/src/hir.rs | 33 +++++++++-- zjit/src/hir/opt_tests.rs | 115 +++++++++++++++++++++++++++++++++++++- zjit/src/stats.rs | 2 + 6 files changed, 313 insertions(+), 7 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 805ecb98b20ff3..43db676d5d1cd3 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1646,6 +1646,115 @@ def test(x) = [1,2,3][x] }, call_threshold: 2, insns: [:opt_aref] end + def test_array_fixnum_aset + assert_compiles '[1, 2, 7]', %q{ + def test(arr, idx) + arr[idx] = 7 + end + arr = [1,2,3] + test(arr, 2) + arr = [1,2,3] + test(arr, 2) + arr + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_fixnum_aset_returns_value + assert_compiles '7', %q{ + def test(arr, idx) + arr[idx] = 7 + end + test([1,2,3], 2) + test([1,2,3], 2) + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_fixnum_aset_out_of_bounds + assert_compiles '[1, 2, 3, nil, nil, 7]', %q{ + def test(arr) + arr[5] = 7 + end + arr = [1,2,3] + test(arr) + arr = [1,2,3] + test(arr) + arr + }, call_threshold: 2 + end + + def test_array_fixnum_aset_negative_index + assert_compiles '[1, 2, 7]', %q{ + def test(arr) + arr[-1] = 7 + end + arr = [1,2,3] + test(arr) + arr = [1,2,3] + test(arr) + arr + }, call_threshold: 2 + end + + def test_array_fixnum_aset_shared + assert_compiles '[10, 999, -1, -2]', %q{ + def test(arr, idx, val) + arr[idx] = val + end + arr = (0..50).to_a + test(arr, 0, -1) + test(arr, 1, -2) + shared = arr[10, 20] + test(shared, 0, 999) + [arr[10], shared[0], arr[0], arr[1]] + }, call_threshold: 2 + end + + def test_array_fixnum_aset_frozen + assert_compiles 'FrozenError', %q{ + def test(arr, idx, val) + arr[idx] = val + end + arr = [1,2,3] + test(arr, 1, 9) + test(arr, 1, 9) + arr.freeze + begin + test(arr, 1, 9) + rescue => e + e.class + end + }, call_threshold: 2 + end + + def test_array_fixnum_aset_array_subclass + assert_compiles '7', %q{ + class MyArray < Array; end + def test(arr, idx) + arr[idx] = 7 + end + arr = MyArray.new + test(arr, 0) + arr = MyArray.new + test(arr, 0) + arr[0] + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_array_aset_non_fixnum_index + assert_compiles 'TypeError', %q{ + def test(arr, idx) + arr[idx] = 7 + end + test([1,2,3], 0) + test([1,2,3], 0) + begin + test([1,2,3], "0") + rescue => e + e.class + end + }, call_threshold: 2 + end + def test_empty_array_pop assert_compiles 'nil', %q{ def test(arr) = arr.pop diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b05c0110909e62..c728df7255c8ba 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -375,6 +375,9 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio 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)), Insn::ArrayArefFixnum { array, index, .. } => gen_aref_fixnum(asm, opnd!(array), opnd!(index)), + Insn::ArrayAset { array, index, val } => { + no_output!(gen_array_aset(asm, opnd!(array), opnd!(index), opnd!(val))) + } 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, opnd!(val), &function.frame_state(*state)), @@ -449,6 +452,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::GuardBitEquals { val, expected, state } => gen_guard_bit_equals(jit, asm, opnd!(val), *expected, &function.frame_state(*state)), &Insn::GuardBlockParamProxy { level, state } => no_output!(gen_guard_block_param_proxy(jit, asm, level, &function.frame_state(state))), Insn::GuardNotFrozen { recv, state } => gen_guard_not_frozen(jit, asm, opnd!(recv), &function.frame_state(*state)), + Insn::GuardNotShared { recv, state } => gen_guard_not_shared(jit, asm, opnd!(recv), &function.frame_state(*state)), &Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), @@ -692,6 +696,15 @@ fn gen_guard_not_frozen(jit: &JITState, asm: &mut Assembler, recv: Opnd, state: recv } +fn gen_guard_not_shared(jit: &JITState, asm: &mut Assembler, recv: Opnd, state: &FrameState) -> Opnd { + let recv = asm.load(recv); + // It's a heap object, so check the shared flag + let flags = asm.load(Opnd::mem(VALUE_BITS, recv, RUBY_OFFSET_RBASIC_FLAGS)); + asm.test(flags, (RUBY_ELTS_SHARED as u64).into()); + asm.jnz(side_exit(jit, state, SideExitReason::GuardNotShared)); + recv +} + fn gen_guard_less(jit: &JITState, asm: &mut Assembler, left: Opnd, right: Opnd, state: &FrameState) -> Opnd { asm.cmp(left, right); asm.jge(side_exit(jit, state, SideExitReason::GuardLess)); @@ -1529,6 +1542,20 @@ fn gen_aref_fixnum( asm_ccall!(asm, rb_ary_entry, array, unboxed_idx) } +fn gen_array_aset( + asm: &mut Assembler, + array: Opnd, + index: Opnd, + val: Opnd, +) { + let unboxed_idx = asm.load(index); + let array = asm.load(array); + let array_ptr = gen_array_ptr(asm, array); + let elem_offset = asm.lshift(unboxed_idx, Opnd::UImm(SIZEOF_VALUE.trailing_zeros() as u64)); + let elem_ptr = asm.add(array_ptr, elem_offset); + asm.store(Opnd::mem(VALUE_BITS, elem_ptr, 0), val); +} + fn gen_array_pop(asm: &mut Assembler, array: Opnd, state: &FrameState) -> lir::Opnd { gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_ary_pop, array) @@ -1545,6 +1572,14 @@ fn gen_array_length(asm: &mut Assembler, array: Opnd) -> lir::Opnd { asm.csel_nz(embedded_len, heap_len) } +fn gen_array_ptr(asm: &mut Assembler, array: Opnd) -> lir::Opnd { + let flags = Opnd::mem(VALUE_BITS, array, RUBY_OFFSET_RBASIC_FLAGS); + asm.test(flags, (RARRAY_EMBED_FLAG as u64).into()); + let heap_ptr = Opnd::mem(usize::BITS as u8, array, RUBY_OFFSET_RARRAY_AS_HEAP_PTR); + let embedded_ptr = asm.lea(Opnd::mem(VALUE_BITS, array, RUBY_OFFSET_RARRAY_AS_ARY)); + asm.csel_nz(embedded_ptr, heap_ptr) +} + /// Compile opt_newarray_hash - create a hash from array elements fn gen_opt_newarray_hash( jit: &JITState, diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 60060f149c850d..4aa9068cb17c16 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -225,6 +225,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable); annotate!(rb_cArray, "join", types::StringExact); annotate!(rb_cArray, "[]", inline_array_aref); + annotate!(rb_cArray, "[]=", inline_array_aset); annotate!(rb_cArray, "<<", inline_array_push); annotate!(rb_cArray, "push", inline_array_push); annotate!(rb_cArray, "pop", inline_array_pop); @@ -332,6 +333,31 @@ fn inline_array_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In None } +fn inline_array_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + if let &[index, val] = args { + if fun.likely_a(recv, types::ArrayExact, state) + && fun.likely_a(index, types::Fixnum, state) + { + let recv = fun.coerce_to(block, recv, types::ArrayExact, state); + let index = fun.coerce_to(block, index, types::Fixnum, state); + let recv = fun.push_insn(block, hir::Insn::GuardNotFrozen { recv, state }); + let recv = fun.push_insn(block, hir::Insn::GuardNotShared { recv, state }); + + // Bounds check: unbox Fixnum index and guard 0 <= idx < length. + let index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index }); + let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv }); + let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, state }); + let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) }); + let index = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: index, right: zero, state }); + + let _ = fun.push_insn(block, hir::Insn::ArrayAset { array: recv, index, val }); + fun.push_insn(block, hir::Insn::WriteBarrier { recv, val }); + return Some(val); + } + } + None +} + fn inline_array_push(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { // Inline only the case of `<<` or `push` when called with a single argument. if let &[val] = args { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 3f1ed83e4bd9b3..2ea6e94c960b6a 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -493,6 +493,7 @@ pub enum SideExitReason { GuardShape(ShapeId), GuardBitEquals(Const), GuardNotFrozen, + GuardNotShared, GuardLess, GuardGreaterEq, PatchPoint(Invariant), @@ -580,6 +581,7 @@ impl std::fmt::Display for SideExitReason { SideExitReason::GuardType(guard_type) => write!(f, "GuardType({guard_type})"), SideExitReason::GuardTypeNot(guard_type) => write!(f, "GuardTypeNot({guard_type})"), SideExitReason::GuardBitEquals(value) => write!(f, "GuardBitEquals({})", value.print(&PtrPrintMap::identity())), + SideExitReason::GuardNotShared => write!(f, "GuardNotShared"), SideExitReason::PatchPoint(invariant) => write!(f, "PatchPoint({invariant})"), _ => write!(f, "{self:?}"), } @@ -728,6 +730,7 @@ pub enum Insn { /// Push `val` onto `array`, where `array` is already `Array`. ArrayPush { array: InsnId, val: InsnId, state: InsnId }, ArrayArefFixnum { array: InsnId, index: InsnId }, + ArrayAset { array: InsnId, index: InsnId, val: InsnId }, ArrayPop { array: InsnId, state: InsnId }, /// Return the length of the array as a C `long` ([`types::CInt64`]) ArrayLength { array: InsnId }, @@ -960,6 +963,9 @@ pub enum Insn { /// Side-exit if val is frozen. Does *not* check if the val is an immediate; assumes that it is /// a heap object. GuardNotFrozen { recv: InsnId, state: InsnId }, + /// Side-exit if val is shared. Does *not* check if the val is an immediate; assumes + /// that it is a heap object. + GuardNotShared { recv: InsnId, state: InsnId }, /// Side-exit if left is not greater than or equal to right (both operands are C long). GuardGreaterEq { left: InsnId, right: InsnId, state: InsnId }, /// Side-exit if left is not less than right (both operands are C long). @@ -992,8 +998,9 @@ impl Insn { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. } | Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } - | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::StoreField { .. } | Insn::WriteBarrier { .. } - | Insn::HashAset { .. } => false, + | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } + | Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } + | Insn::ArrayAset { .. } => false, _ => true, } } @@ -1121,6 +1128,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::ArrayArefFixnum { array, index, .. } => { write!(f, "ArrayArefFixnum {array}, {index}") } + Insn::ArrayAset { array, index, val, ..} => { + write!(f, "ArrayAset {array}, {index}, {val}") + } Insn::ArrayPop { array, .. } => { write!(f, "ArrayPop {array}") } @@ -1332,6 +1342,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { &Insn::GuardShape { val, shape, .. } => { write!(f, "GuardShape {val}, {:p}", self.ptr_map.map_shape(shape)) }, Insn::GuardBlockParamProxy { level, .. } => write!(f, "GuardBlockParamProxy l{level}"), Insn::GuardNotFrozen { recv, .. } => write!(f, "GuardNotFrozen {recv}"), + Insn::GuardNotShared { recv, .. } => write!(f, "GuardNotShared {recv}"), Insn::GuardLess { left, right, .. } => write!(f, "GuardLess {left}, {right}"), Insn::GuardGreaterEq { left, right, .. } => write!(f, "GuardGreaterEq {left}, {right}"), Insn::PatchPoint { invariant, .. } => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) }, @@ -1968,6 +1979,7 @@ impl Function { &GuardShape { val, shape, state } => GuardShape { val: find!(val), shape, state }, &GuardBlockParamProxy { level, state } => GuardBlockParamProxy { level, state: find!(state) }, &GuardNotFrozen { recv, state } => GuardNotFrozen { recv: find!(recv), state }, + &GuardNotShared { recv, state } => GuardNotShared { recv: find!(recv), state }, &GuardGreaterEq { left, right, state } => GuardGreaterEq { left: find!(left), right: find!(right), state }, &GuardLess { left, right, state } => GuardLess { left: find!(left), right: find!(right), state }, &FixnumAdd { left, right, state } => FixnumAdd { left: find!(left), right: find!(right), state }, @@ -2071,6 +2083,7 @@ impl Function { &NewRange { low, high, flag, state } => NewRange { low: find!(low), high: find!(high), flag, state: find!(state) }, &NewRangeFixnum { low, high, flag, state } => NewRangeFixnum { low: find!(low), high: find!(high), flag, state: find!(state) }, &ArrayArefFixnum { array, index } => ArrayArefFixnum { array: find!(array), index: find!(index) }, + &ArrayAset { array, index, val } => ArrayAset { array: find!(array), index: find!(index), val: find!(val) }, &ArrayPop { array, state } => ArrayPop { array: find!(array), state: find!(state) }, &ArrayLength { array } => ArrayLength { array: find!(array) }, &ArrayMax { ref elements, state } => ArrayMax { elements: find_vec!(elements), state: find!(state) }, @@ -2143,7 +2156,7 @@ impl Function { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_) | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::IncrCounterPtr { .. } - | Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } => + | Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } | Insn::ArrayAset { .. } => panic!("Cannot infer type of instruction with no output: {}. See Insn::has_output().", self.insns[insn.0]), Insn::Const { val: Const::Value(val) } => Type::from_value(*val), Insn::Const { val: Const::CBool(val) } => Type::from_cbool(*val), @@ -2197,7 +2210,7 @@ impl Function { Insn::GuardTypeNot { .. } => types::BasicObject, Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_const(*expected)), Insn::GuardShape { val, .. } => self.type_of(*val), - Insn::GuardNotFrozen { recv, .. } => self.type_of(*recv), + Insn::GuardNotFrozen { recv, .. } | Insn::GuardNotShared { recv, .. } => self.type_of(*recv), Insn::GuardLess { left, .. } => self.type_of(*left), Insn::GuardGreaterEq { left, .. } => self.type_of(*left), Insn::FixnumAdd { .. } => types::Fixnum, @@ -3940,6 +3953,7 @@ impl Function { | &Insn::GuardBitEquals { val, state, .. } | &Insn::GuardShape { val, state, .. } | &Insn::GuardNotFrozen { recv: val, state } + | &Insn::GuardNotShared { recv: val, state } | &Insn::ToArray { val, state } | &Insn::IsMethodCfunc { val, state, .. } | &Insn::ToNewArray { val, state } @@ -4004,6 +4018,11 @@ impl Function { worklist.push_back(array); worklist.push_back(index); } + &Insn::ArrayAset { array, index, val } => { + worklist.push_back(array); + worklist.push_back(index); + worklist.push_back(val); + } &Insn::ArrayPop { array, state } => { worklist.push_back(array); worklist.push_back(state); @@ -4628,7 +4647,7 @@ impl Function { | Insn::DefinedIvar { self_val: val, .. } => { self.assert_subtype(insn_id, val, types::BasicObject) } - Insn::GuardNotFrozen { recv, .. } => { + Insn::GuardNotFrozen { recv, .. } | Insn::GuardNotShared { recv, .. } => { self.assert_subtype(insn_id, recv, types::HeapBasicObject) } // Instructions with 2 Ruby object operands @@ -4716,6 +4735,10 @@ impl Function { self.assert_subtype(insn_id, array, types::Array)?; self.assert_subtype(insn_id, index, types::Fixnum) } + Insn::ArrayAset { array, index, .. } => { + self.assert_subtype(insn_id, array, types::ArrayExact)?; + self.assert_subtype(insn_id, index, types::CInt64) + } // Instructions with Hash operands Insn::HashAref { hash, .. } | Insn::HashAset { hash, .. } => self.assert_subtype(insn_id, hash, types::HashExact), diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2a70314fbb7f38..afa97e48f1e4dc 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4772,6 +4772,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_dont_optimize_array_aset_if_redefined() { + eval(r##" + class Array + def []=(*args); :redefined; end + end + + def test(arr) + arr[1] = 10 + end + "##); + assert_snapshot!(hir_string("test"), @r" + fn test@:7: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :arr, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v16:Fixnum[1] = Const Value(1) + v18:Fixnum[10] = Const Value(10) + v22:BasicObject = SendWithoutBlock v9, :[]=, v16, v18 # SendFallbackReason: Uncategorized(opt_aset) + CheckInterrupts + Return v18 + "); + } + #[test] fn test_dont_optimize_array_max_if_redefined() { eval(r##" @@ -6901,7 +6931,7 @@ mod hir_opt_tests { } #[test] - fn test_optimize_array_aset() { + fn test_optimize_array_aset_literal() { eval(" def test(arr) arr[1] = 10 @@ -6924,12 +6954,93 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, []=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v31:ArrayExact = GuardType v9, ArrayExact - v32:BasicObject = CCallVariadic v31, :Array#[]=@0x1038, v16, v18 + v32:ArrayExact = GuardNotFrozen v31 + v33:ArrayExact = GuardNotShared v32 + v34:CInt64 = UnboxFixnum v16 + v35:CInt64 = ArrayLength v33 + v36:CInt64 = GuardLess v34, v35 + v37:CInt64[0] = Const CInt64(0) + v38:CInt64 = GuardGreaterEq v36, v37 + ArrayAset v33, v38, v18 + WriteBarrier v33, v18 + IncrCounter inline_cfunc_optimized_send_count CheckInterrupts Return v18 "); } + #[test] + fn test_optimize_array_aset_profiled() { + eval(" + def test(arr, index, val) + arr[index] = val + end + test([], 0, 1) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :arr, l0, SP@6 + v3:BasicObject = GetLocal :index, l0, SP@5 + v4:BasicObject = GetLocal :val, l0, SP@4 + Jump bb2(v1, v2, v3, v4) + bb1(v7:BasicObject, v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + Jump bb2(v7, v8, v9, v10) + bb2(v12:BasicObject, v13:BasicObject, v14:BasicObject, v15:BasicObject): + PatchPoint MethodRedefined(Array@0x1000, []=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Array@0x1000) + v35:ArrayExact = GuardType v13, ArrayExact + v36:Fixnum = GuardType v14, Fixnum + v37:ArrayExact = GuardNotFrozen v35 + v38:ArrayExact = GuardNotShared v37 + v39:CInt64 = UnboxFixnum v36 + v40:CInt64 = ArrayLength v38 + v41:CInt64 = GuardLess v39, v40 + v42:CInt64[0] = Const CInt64(0) + v43:CInt64 = GuardGreaterEq v41, v42 + ArrayAset v38, v43, v15 + WriteBarrier v38, v15 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v15 + "); + } + + #[test] + fn test_optimize_array_aset_array_subclass() { + eval(" + class MyArray < Array; end + def test(arr, index, val) + arr[index] = val + end + a = MyArray.new + test(a, 0, 1) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :arr, l0, SP@6 + v3:BasicObject = GetLocal :index, l0, SP@5 + v4:BasicObject = GetLocal :val, l0, SP@4 + Jump bb2(v1, v2, v3, v4) + bb1(v7:BasicObject, v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + Jump bb2(v7, v8, v9, v10) + bb2(v12:BasicObject, v13:BasicObject, v14:BasicObject, v15:BasicObject): + PatchPoint MethodRedefined(MyArray@0x1000, []=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(MyArray@0x1000) + v35:ArraySubclass[class_exact:MyArray] = GuardType v13, ArraySubclass[class_exact:MyArray] + v36:BasicObject = CCallVariadic v35, :Array#[]=@0x1038, v14, v15 + CheckInterrupts + Return v15 + "); + } + #[test] fn test_optimize_array_ltlt() { eval(" diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 38e8df170d33fd..68eeac456b38a2 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -191,6 +191,7 @@ make_counters! { exit_guard_int_equals_failure, exit_guard_shape_failure, exit_guard_not_frozen_failure, + exit_guard_not_shared_failure, exit_guard_less_failure, exit_guard_greater_eq_failure, exit_patchpoint_bop_redefined, @@ -511,6 +512,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardBitEquals(_) => exit_guard_bit_equals_failure, GuardShape(_) => exit_guard_shape_failure, GuardNotFrozen => exit_guard_not_frozen_failure, + GuardNotShared => exit_guard_not_shared_failure, GuardLess => exit_guard_less_failure, GuardGreaterEq => exit_guard_greater_eq_failure, CalleeSideExit => exit_callee_side_exit, From 080d66beca71d6cc290a8be4acd49e5a70594f9c Mon Sep 17 00:00:00 2001 From: Misaki Shioi <31817032+shioimm@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:41:42 +0900 Subject: [PATCH 4/8] Avoid flaky test failures by retrying on local port conflicts (#15818) This test obtains an available port number by calling `TCPServer.new`, then closes it and passes the same port number as `local_port` to `TCPSocket.new`. However, `TCPSocket.new` could occasionally fail with `Errno::EADDRINUSE` at the bind(2) step. I believe this happens when tests are run in parallel and another process on the same host happens to bind the same port in the short window between closing the `TCPServer` and calling `TCPSocket.new`. To address this race condition, the test now retries with a newly selected available port when such a conflict occurs. --- .../library/socket/tcpsocket/shared/new.rb | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/spec/ruby/library/socket/tcpsocket/shared/new.rb b/spec/ruby/library/socket/tcpsocket/shared/new.rb index 5280eb790080f8..0e405253c84324 100644 --- a/spec/ruby/library/socket/tcpsocket/shared/new.rb +++ b/spec/ruby/library/socket/tcpsocket/shared/new.rb @@ -53,14 +53,23 @@ end it "connects to a server when passed local_host and local_port arguments" do - server = TCPServer.new(SocketSpecs.hostname, 0) + retries = 0 + max_retries = 3 + begin - available_port = server.addr[1] - ensure - server.close + retries += 1 + server = TCPServer.new(SocketSpecs.hostname, 0) + begin + available_port = server.addr[1] + ensure + server.close + end + @socket = TCPSocket.send(@method, @hostname, @server.port, + @hostname, available_port) + rescue Errno::EADDRINUSE + raise if retries >= max_retries + retry end - @socket = TCPSocket.send(@method, @hostname, @server.port, - @hostname, available_port) @socket.should be_an_instance_of(TCPSocket) end From 768862868472fb1800e556effb0e37be2fbaec52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Thu, 8 Jan 2026 01:13:38 +0100 Subject: [PATCH 5/8] Fix incorrect bundled gems warning for hyphenated gem names When requiring a file like "benchmark/ips", the warning system would incorrectly warn about the "benchmark" gem not being a default gem, even when the user has "benchmark-ips" (a separate third-party gem) in their Gemfile. The fix checks if a hyphenated version of the require path exists in the bundle specs before issuing a warning. For example, requiring "benchmark/ips" now checks for both "benchmark" and "benchmark-ips" in the Gemfile. [Bug #21828] --- lib/bundled_gems.rb | 10 ++++++++++ test/test_bundled_gems.rb | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb index 49fb90249dd6ae..3ac10aa25606d3 100644 --- a/lib/bundled_gems.rb +++ b/lib/bundled_gems.rb @@ -124,6 +124,16 @@ def self.warning?(name, specs: nil) return if specs.include?(name) + # Don't warn if a hyphenated gem provides this feature + # (e.g., benchmark-ips provides benchmark/ips, not the benchmark gem) + if subfeature + feature_parts = feature.split("/") + if feature_parts.size >= 2 + hyphenated_gem = "#{feature_parts[0]}-#{feature_parts[1]}" + return if specs.include?(hyphenated_gem) + end + end + return if WARNED[name] WARNED[name] = true diff --git a/test/test_bundled_gems.rb b/test/test_bundled_gems.rb index 19546dd29606af..6e25df9b01f82d 100644 --- a/test/test_bundled_gems.rb +++ b/test/test_bundled_gems.rb @@ -32,4 +32,17 @@ def test_warning_archdir assert Gem::BUNDLED_GEMS.warning?(path, specs: {}) assert_nil Gem::BUNDLED_GEMS.warning?(path, specs: {}) end + + def test_no_warning_for_hyphenated_gem + # When benchmark-ips gem is in specs, requiring "benchmark/ips" should not warn + # about the benchmark gem (Bug #21828) + assert_nil Gem::BUNDLED_GEMS.warning?("benchmark/ips", specs: {"benchmark-ips" => true}) + end + + def test_warning_without_hyphenated_gem + # When benchmark-ips is NOT in specs, requiring "benchmark/ips" should warn + warning = Gem::BUNDLED_GEMS.warning?("benchmark/ips", specs: {}) + assert warning + assert_match(/benchmark/, warning) + end end From 725e3d0aa7ffe47765a973904b72eac65763834b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 8 Jan 2026 12:26:27 +0900 Subject: [PATCH 6/8] Fluent and/or is supported by Prism too now --- test/ruby/test_syntax.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index 94a2e03940bf5b..585e691765c66a 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -1260,8 +1260,6 @@ def test_fluent_dot end def test_fluent_and - omit if /\+PRISM\b/ =~ RUBY_DESCRIPTION - assert_valid_syntax("a\n" "&& foo") assert_valid_syntax("a\n" "and foo") @@ -1285,8 +1283,6 @@ def test_fluent_and end def test_fluent_or - omit if /\+PRISM\b/ =~ RUBY_DESCRIPTION - assert_valid_syntax("a\n" "|| foo") assert_valid_syntax("a\n" "or foo") From 1852ef43778d59a64881b293b0b887e2bc5af37a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 7 Jan 2026 09:39:55 +0900 Subject: [PATCH 7/8] Fail if the Ruby specified with `--with-baseruby` is too old If the baseruby is explicitly specified, fail because the option is not accepted if it does not meet the requirements. If the option is not specified, just display the warning and continue, in the hope that it is not needed. Follow up GH-15809 --- configure.ac | 12 ++++++++++-- win32/configure.bat | 2 +- win32/setup.mak | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/configure.ac b/configure.ac index a276783b6c2f63..90326e2926fc33 100644 --- a/configure.ac +++ b/configure.ac @@ -83,9 +83,17 @@ AC_ARG_WITH(baseruby, ], [ AC_PATH_PROG([BASERUBY], [ruby], [false]) + HAVE_BASERUBY= ]) -AS_IF([test "$HAVE_BASERUBY" != no], [ - RUBYOPT=- $BASERUBY --disable=gems "${tooldir}/missing-baseruby.bat" --verbose || HAVE_BASERUBY=no +AS_IF([test "$HAVE_BASERUBY" = no], [ + # --without-baseruby +], [error=`RUBYOPT=- $BASERUBY --disable=gems "${tooldir}/missing-baseruby.bat" --verbose 2>&1`], [ + HAVE_BASERUBY=yes +], [test "$HAVE_BASERUBY" = ""], [ # no --with-baseruby option + AC_MSG_WARN($error) # just warn and continue + HAVE_BASERUBY=no +], [ # the ruby given by --with-baseruby is too old + AC_MSG_ERROR($error) # bail out ]) AS_IF([test "${HAVE_BASERUBY:=no}" != no], [ AS_CASE(["$build_os"], [mingw*], [ diff --git a/win32/configure.bat b/win32/configure.bat index 8f767ede73256a..181813f4ad1581 100755 --- a/win32/configure.bat +++ b/win32/configure.bat @@ -208,7 +208,7 @@ goto :loop ; shift goto :loop ; :baseruby - echo>> %config_make% HAVE_BASERUBY = + echo>> %config_make% HAVE_BASERUBY = yes echo>> %config_make% BASERUBY = %~2 echo>>%confargs% %1=%2 \ shift diff --git a/win32/setup.mak b/win32/setup.mak index b06081cab99d98..de8db870ba69eb 100644 --- a/win32/setup.mak +++ b/win32/setup.mak @@ -24,6 +24,9 @@ MAKEFILE = Makefile CPU = PROCESSOR_LEVEL CC = $(CC) -nologo -source-charset:utf-8 CPP = $(CC) -EP +!if "$(HAVE_BASERUBY)" != "no" && "$(BASERUBY)" == "" +BASERUBY = ruby +!endif all: -prologue- -generic- -epilogue- i386-mswin32: -prologue- -i386- -epilogue- @@ -46,8 +49,8 @@ prefix = $(prefix:\=/) << @type $(config_make) >>$(MAKEFILE) @del $(config_make) > nul -!if "$(HAVE_BASERUBY)" != "no" && "$(BASERUBY)" != "" - $(BASERUBY:/=\) "$(srcdir)/tool/missing-baseruby.bat" --verbose +!if "$(HAVE_BASERUBY)" != "no" + @$(BASERUBY:/=\) "$(srcdir)/tool/missing-baseruby.bat" --verbose $(HAVE_BASERUBY:yes=|| exit )|| exit 0 !endif !if "$(WITH_GMP)" != "no" @($(CC) $(XINCFLAGS) < nul && (echo USE_GMP = yes) || exit /b 0) >>$(MAKEFILE) From 946b1c1ba19e708d54a3f9eee00d4ea06434876c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 8 Jan 2026 13:41:07 +0900 Subject: [PATCH 8/8] Move parentheses around macro arguments --- internal/error.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/error.h b/internal/error.h index 4b41aee77b00ec..ae9a13fceced27 100644 --- a/internal/error.h +++ b/internal/error.h @@ -75,8 +75,8 @@ PRINTF_ARGS(void rb_warn_deprecated_to_remove(const char *removal, const char *f PRINTF_ARGS(void rb_warn_reserved_name(const char *removal, const char *fmt, ...), 2, 3); #if RUBY_DEBUG # include "ruby/version.h" -# define RUBY_VERSION_SINCE(major, minor) (RUBY_API_VERSION_CODE >= (major * 10000) + (minor) * 100) -# define RUBY_VERSION_BEFORE(major, minor) (RUBY_API_VERSION_CODE < (major * 10000) + (minor) * 100) +# define RUBY_VERSION_SINCE(major, minor) (RUBY_API_VERSION_CODE >= (major) * 10000 + (minor) * 100) +# define RUBY_VERSION_BEFORE(major, minor) (RUBY_API_VERSION_CODE < (major) * 10000 + (minor) * 100) # if defined(RBIMPL_WARNING_PRAGMA0) # define RBIMPL_TODO0(x) RBIMPL_WARNING_PRAGMA0(message(x)) # elif RBIMPL_COMPILER_IS(MSVC)