From 33db313e855dfa83d7c66c2de6d63b9b401c32a0 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 25 Jul 2026 15:49:23 +0900 Subject: [PATCH 1/6] [Feature #17056] Add offset specs for `Array#index` and `rindex` --- NEWS.md | 4 ++++ spec/ruby/core/array/find_index_spec.rb | 21 +++++++++++++++++++++ spec/ruby/core/array/rindex_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/NEWS.md b/NEWS.md index 64ffe9d4d6d535..5394fb60ed9761 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,6 +27,9 @@ Note: We're only listing outstanding class updates. * Array + * `Array#index`, `Array#find_index`, and `Array#rindex` accept an + `offset` keyword argument to specify where to start searching. + [[Feature #17056]] * `Array#pack` accepts new formats `R` and `r` for unsigned and signed LEB128 encoded integers. [[Feature #21785]] * `Array#pack` accepts new formats `x!` and `@!` to align the current @@ -240,6 +243,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 +[Feature #17056]: https://bugs.ruby-lang.org/issues/17056 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21768]: https://bugs.ruby-lang.org/issues/21768 [Feature #21781]: https://bugs.ruby-lang.org/issues/21781 diff --git a/spec/ruby/core/array/find_index_spec.rb b/spec/ruby/core/array/find_index_spec.rb index 17ff6c04a7515f..cc307f587ad0fc 100644 --- a/spec/ruby/core/array/find_index_spec.rb +++ b/spec/ruby/core/array/find_index_spec.rb @@ -22,6 +22,21 @@ def x.==(obj) 3 == obj; end [2, 1, 1, 1, 1].find_index(3).should == nil end + it "starts searching at a non-negative offset" do + [1, 2, 1, 2].find_index(1, offset: 1).should == 2 + end + + it "starts searching at a negative offset relative to the end" do + [1, 2, 1, 2].find_index(2, offset: -2).should == 3 + end + + it "returns nil when the offset is outside the array" do + array = [1, 2, 1, 2] + + array.find_index(1, offset: array.size).should == nil + array.find_index(1, offset: -array.size - 1).should == nil + end + it "accepts a block instead of an argument" do [4, 2, 1, 5, 1, 3].find_index {|x| x < 2}.should == 2 end @@ -32,6 +47,12 @@ def x.==(obj) 3 == obj; end }.should complain(/given block not used/) end + it "ignores the block if there is an argument and an offset" do + -> { + [4, 2, 1, 5, 1, 3].find_index(1, offset: 3) { |x| x < 2 }.should == 4 + }.should complain(/given block not used/) + end + describe "given no argument and no block" do it "produces an Enumerator" do [].find_index.should.instance_of?(Enumerator) diff --git a/spec/ruby/core/array/rindex_spec.rb b/spec/ruby/core/array/rindex_spec.rb index 858c39dc9280b7..fc24eae95bb346 100644 --- a/spec/ruby/core/array/rindex_spec.rb +++ b/spec/ruby/core/array/rindex_spec.rb @@ -32,6 +32,22 @@ [1, 1, 3, 2, 1, 3].rindex(4).should == nil end + it "does not search before a non-negative offset" do + [1, 2, 1, 2].rindex(1, offset: 1).should == 2 + [1, 2, 1, 2].rindex(1, offset: 3).should == nil + end + + it "starts searching at a negative offset relative to the end" do + [1, 2, 1, 2].rindex(2, offset: -2).should == 1 + end + + it "returns nil when the offset is outside the array" do + array = [1, 2, 1, 2] + + array.rindex(1, offset: array.size).should == nil + array.rindex(1, offset: -array.size - 1).should == nil + end + it "returns correct index even after delete_at" do array = ["fish", "bird", "lion", "cat"] array.delete_at(0) @@ -60,6 +76,12 @@ }.should complain(/given block not used/) end + it "ignores the block if there is an argument and an offset" do + -> { + [4, 2, 1, 5, 1, 3].rindex(1, offset: 2) { |x| x < 2 }.should == 4 + }.should complain(/given block not used/) + end + it "rechecks the array size during iteration" do ary = [4, 2, 1, 5, 1, 3] seen = [] From 1b560fae2a64d7a6ed4d8f3129945a59e04da43f Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 25 Jul 2026 17:07:12 +0900 Subject: [PATCH 2/6] Revert [Feature #17056] offset specs for `Array#index` and `rindex`" This reverts commits: * fa055b6d8bfa2510d546c2a3eb346744660073bd Allow specifying the position to start search from in Array#index and Array#rindex * 33db313e855dfa83d7c66c2de6d63b9b401c32a0. Add offset specs for `Array#index` and `rindex` --- NEWS.md | 4 -- array.c | 96 +++++-------------------- spec/ruby/core/array/find_index_spec.rb | 21 ------ spec/ruby/core/array/rindex_spec.rb | 22 ------ test/ruby/test_array.rb | 15 ---- 5 files changed, 16 insertions(+), 142 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5394fb60ed9761..64ffe9d4d6d535 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,9 +27,6 @@ Note: We're only listing outstanding class updates. * Array - * `Array#index`, `Array#find_index`, and `Array#rindex` accept an - `offset` keyword argument to specify where to start searching. - [[Feature #17056]] * `Array#pack` accepts new formats `R` and `r` for unsigned and signed LEB128 encoded integers. [[Feature #21785]] * `Array#pack` accepts new formats `x!` and `@!` to align the current @@ -243,7 +240,6 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 -[Feature #17056]: https://bugs.ruby-lang.org/issues/17056 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21768]: https://bugs.ruby-lang.org/issues/21768 [Feature #21781]: https://bugs.ruby-lang.org/issues/21781 diff --git a/array.c b/array.c index 5af5c856a8dc21..4b1343b6fa51bc 100644 --- a/array.c +++ b/array.c @@ -2167,12 +2167,12 @@ rb_ary_rfind(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * find_index(object, offset: 0) -> integer or nil + * find_index(object) -> integer or nil * find_index {|element| ... } -> integer or nil - * find_index(offset: 0) -> new_enumerator - * index(object, offset: 0) -> integer or nil + * find_index -> new_enumerator + * index(object) -> integer or nil * index {|element| ... } -> integer or nil - * index(offset: 0) -> new_enumerator + * index -> new_enumerator * * Returns the zero-based integer index of a specified element, or +nil+. * @@ -2194,20 +2194,6 @@ rb_ary_rfind(int argc, VALUE *argv, VALUE ary) * * Returns +nil+ if the block never returns a truthy value. * - * When +offset+ is non-negative, begins the search at position +offset+; - * the returned index is relative to the beginning of +self+: - * - * a = [:foo, 'bar', 2, 'bar'] - * a.index('bar', offset: 1) # => 1 - * a.index('bar', offset: 2) # => 3 - * a.index('bar', offset: 4) # => nil - * - * With negative integer argument +offset+, selects the search position by counting backward - * from the end of +self+: - * - * a = [:foo, 'bar', 2, 'bar'] - * a.index('bar', offset: -2) # => 3 - * * With neither an argument nor a block given, returns a new Enumerator. * * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. @@ -2216,8 +2202,7 @@ rb_ary_rfind(int argc, VALUE *argv, VALUE ary) static VALUE rb_ary_index(int argc, VALUE *argv, VALUE ary) { - VALUE val, opts = Qnil, initpos = Qnil; - long pos = 0; + VALUE val; long i; if (argc == 0) { @@ -2229,24 +2214,11 @@ rb_ary_index(int argc, VALUE *argv, VALUE ary) } return Qnil; } - rb_check_arity(argc, 0, 2); - rb_scan_args(argc, argv, "1:", &val, &opts); - if (!NIL_P(opts)) { - static ID keywords[1]; - if (!keywords[0]) { - keywords[0] = rb_intern_const("offset"); - } - rb_get_kwargs(opts, keywords, 0, 1, &initpos); - if (!UNDEF_P(initpos)) - pos = NUM2LONG(initpos); - } + rb_check_arity(argc, 0, 1); + val = argv[0]; if (rb_block_given_p()) rb_warn("given block not used"); - if (pos < 0) - pos += RARRAY_LEN(ary); - if (pos < 0) - return Qnil; - for (i=pos; i integer or nil + * rindex(object) -> integer or nil * rindex {|element| ... } -> integer or nil - * rindex(offset: nil) -> new_enumerator + * rindex -> new_enumerator * * Returns the index of the last element for which object == element. * @@ -2278,23 +2250,6 @@ rb_ary_index(int argc, VALUE *argv, VALUE ary) * * Returns +nil+ if the block never returns a truthy value. * - * When +offset+ is non-negative, it specifies the maximum starting position in the - * array to end the search: - * - * a = [:foo, 'bar', 2, 'bar'] - * a.rindex('bar', offset: 0) # => 3 - * a.rindex(2, offset: 1) # => 2 - * a.rindex(2, offset: 3) # => nil - * - * With negative integer argument +offset+, - * selects the search position by counting backward from the end of +self+: - * - * a = [:foo, 'bar', 2, 'bar'] - * a.rindex('bar', -1) # => 3 - * a.rindex('bar', -2) # => 1 - * a.rindex('bar', -3) # => 1 - * a.rindex('bar', -4) # => nil - * * When neither an argument nor a block is given, returns a new Enumerator. * * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. @@ -2303,44 +2258,25 @@ rb_ary_index(int argc, VALUE *argv, VALUE ary) static VALUE rb_ary_rindex(int argc, VALUE *argv, VALUE ary) { - VALUE val, opts = Qnil, offset_arg = Qnil; - long i = RARRAY_LEN(ary) - 1, len, end_pos = 0, offset; + VALUE val; + long i = RARRAY_LEN(ary), len; if (argc == 0) { RETURN_ENUMERATOR(ary, 0, 0); - while (i >= 0) { + while (i--) { if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) return LONG2NUM(i); if (i > (len = RARRAY_LEN(ary))) { i = len; } - i--; } return Qnil; } - rb_check_arity(argc, 0, 2); - rb_scan_args(argc, argv, "1:", &val, &opts); - if (!NIL_P(opts)) { - static ID keywords[1]; - if (!keywords[0]) { - keywords[0] = rb_intern_const("offset"); - } - rb_get_kwargs(opts, keywords, 0, 1, &offset_arg); - if (!UNDEF_P(offset_arg)){ - offset = NUM2LONG(offset_arg); - if (offset < 0) { - i += offset + 1; - } - else { - end_pos = offset; - } - } - } + rb_check_arity(argc, 0, 1); + val = argv[0]; if (rb_block_given_p()) rb_warn("given block not used"); - if (i < 0 || end_pos >= RARRAY_LEN(ary)) - return Qnil; - for (; i >= end_pos; i--) { + while (i--) { VALUE e = RARRAY_AREF(ary, i); if (rb_equal(e, val)) { return LONG2NUM(i); diff --git a/spec/ruby/core/array/find_index_spec.rb b/spec/ruby/core/array/find_index_spec.rb index cc307f587ad0fc..17ff6c04a7515f 100644 --- a/spec/ruby/core/array/find_index_spec.rb +++ b/spec/ruby/core/array/find_index_spec.rb @@ -22,21 +22,6 @@ def x.==(obj) 3 == obj; end [2, 1, 1, 1, 1].find_index(3).should == nil end - it "starts searching at a non-negative offset" do - [1, 2, 1, 2].find_index(1, offset: 1).should == 2 - end - - it "starts searching at a negative offset relative to the end" do - [1, 2, 1, 2].find_index(2, offset: -2).should == 3 - end - - it "returns nil when the offset is outside the array" do - array = [1, 2, 1, 2] - - array.find_index(1, offset: array.size).should == nil - array.find_index(1, offset: -array.size - 1).should == nil - end - it "accepts a block instead of an argument" do [4, 2, 1, 5, 1, 3].find_index {|x| x < 2}.should == 2 end @@ -47,12 +32,6 @@ def x.==(obj) 3 == obj; end }.should complain(/given block not used/) end - it "ignores the block if there is an argument and an offset" do - -> { - [4, 2, 1, 5, 1, 3].find_index(1, offset: 3) { |x| x < 2 }.should == 4 - }.should complain(/given block not used/) - end - describe "given no argument and no block" do it "produces an Enumerator" do [].find_index.should.instance_of?(Enumerator) diff --git a/spec/ruby/core/array/rindex_spec.rb b/spec/ruby/core/array/rindex_spec.rb index fc24eae95bb346..858c39dc9280b7 100644 --- a/spec/ruby/core/array/rindex_spec.rb +++ b/spec/ruby/core/array/rindex_spec.rb @@ -32,22 +32,6 @@ [1, 1, 3, 2, 1, 3].rindex(4).should == nil end - it "does not search before a non-negative offset" do - [1, 2, 1, 2].rindex(1, offset: 1).should == 2 - [1, 2, 1, 2].rindex(1, offset: 3).should == nil - end - - it "starts searching at a negative offset relative to the end" do - [1, 2, 1, 2].rindex(2, offset: -2).should == 1 - end - - it "returns nil when the offset is outside the array" do - array = [1, 2, 1, 2] - - array.rindex(1, offset: array.size).should == nil - array.rindex(1, offset: -array.size - 1).should == nil - end - it "returns correct index even after delete_at" do array = ["fish", "bird", "lion", "cat"] array.delete_at(0) @@ -76,12 +60,6 @@ }.should complain(/given block not used/) end - it "ignores the block if there is an argument and an offset" do - -> { - [4, 2, 1, 5, 1, 3].rindex(1, offset: 2) { |x| x < 2 }.should == 4 - }.should complain(/given block not used/) - end - it "rechecks the array size during iteration" do ary = [4, 2, 1, 5, 1, 3] seen = [] diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 5f47b652f5754f..36f88016149e2a 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1162,14 +1162,7 @@ def test_index assert_nil(a.index('ca')) assert_nil(a.index([1,2])) - assert_equal(1, a.index(99, offset: 0)) - assert_equal(3, a.index(99, offset: 2)) - assert_equal(2, a.index(/a/, offset: -4)) - assert_nil(a.index(99, offset: a.size)) - assert_nil(a.index(99, offset: -a.size - 1)) - assert_equal(1, assert_warn(/given block not used/) {a.index(99) {|x| x == 'cat' }}) - assert_equal(3, assert_warn(/given block not used/) {a.index(99, offset: 2) {|x| x == 'cat' }}) end def test_values_at @@ -1544,15 +1537,7 @@ def test_rindex assert_nil(a.rindex('ca')) assert_nil(a.rindex([1,2])) - assert_equal(3, a.rindex(99, offset: 0)) - assert_equal(3, a.rindex(99, offset: 2)) - assert_equal(1, a.rindex(99, offset: -3)) - assert_nil(a.rindex(/a/, offset: -4)) - assert_nil(a.index(99, offset: a.size)) - assert_nil(a.index(99, offset: -a.size - 1)) - assert_equal(3, assert_warning(/given block not used/) {a.rindex(99) {|x| x == [1,2,3] }}) - assert_equal(3, assert_warning(/given block not used/) {a.rindex(99, offset: 2) {|x| x == [1,2,3] }}) bug15951 = "[Bug #15951]" o2 = Object.new From e09a12b1160342def19430ecd703ca4e71c462cd Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Sat, 25 Jul 2026 19:40:41 +0900 Subject: [PATCH 3/6] [DOC] Clarify when IO::Buffer#readonly? is true (#18062) The documentation says "Frozen strings and read-only files create read-only buffers", but this omits the most common case: a buffer created by IO::Buffer.for without a block is always read-only, even when the source string is not frozen. This is because the no-block form of IO::Buffer.for uses an internal frozen copy of the string as the buffer source (see rb_io_buffer_type_for, which passes RB_IO_BUFFER_READONLY unconditionally), and its call-seq is already documented as `IO::Buffer.for(string) -> readonly io_buffer`. p IO::Buffer.for("test").readonly? # => true (not frozen) p IO::Buffer.for("test") {|b| b.readonly? } # => false (block form) p IO::Buffer.new(4).readonly? # => false --- io_buffer.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/io_buffer.c b/io_buffer.c index 98c82bd6c7821d..a64fa506ec04c8 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1490,7 +1490,8 @@ rb_io_buffer_readonly_p(VALUE self) * If the buffer is read only, meaning the buffer cannot be modified using * #set_value, #set_string or #copy and similar. * - * Frozen strings and read-only files create read-only buffers. + * A buffer created by IO::Buffer.for without a block is read-only, as is one + * backed by a frozen string or a read-only file. */ static VALUE io_buffer_readonly_p(VALUE self) From 1cf2a652218d4f2c57b9ec8102329c412a4b3f8c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 25 Jul 2026 18:13:32 +0900 Subject: [PATCH 4/6] Remove obsolete VM guard macros These two optimization macros in `internal/vm.h` were "temporarily" removed in commit f2286925f084 for [Feature #16614]. However, since they have not been restored for over six years, the corresponding guards in `internal.h` can now be considered obsolete. --- internal.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal.h b/internal.h index 002044cfa1a590..a08fab10dc2355 100644 --- a/internal.h +++ b/internal.h @@ -64,10 +64,6 @@ /* internal/symbol.h */ #define rb_sym_intern_ascii_cstr(...) rb_nonexistent_symbol(__VA_ARGS__) -/* internal/vm.h */ -#define rb_funcallv(...) rb_nonexistent_symbol(__VA_ARGS__) -#define rb_method_basic_definition_p(...) rb_nonexistent_symbol(__VA_ARGS__) - /* MRI debug support */ From 9e6839488c5c0385064293ab45573215a852f721 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 19 Jul 2026 19:31:55 +0900 Subject: [PATCH 5/6] YJIT: Add a test of argument types for forwarding callees Forwarding callees keep call information, not arguments, in their local table. Mapping caller argument types to those slots could make YJIT carry a `CString` type for an uninitialized `nil` local and fail context verification. --- test/ruby/test_yjit.rb | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 97bf24bcb8a685..9b9a59c397f8e5 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -853,6 +853,27 @@ def jit_method RUBY end + def test_forwarding_callee_does_not_map_arguments_to_locals + assert_compiles( + <<~'RUBY', call_threshold: 1, code_gc: true, result: [:ok, :ok], verify_ctx: true + class ForwardingReceiver + def foo(...) = :ok + end + + def delegate(...) + receiver = ForwardingReceiver.new + if defined?(receiver.foo) + receiver.foo(...) + else + receiver.__send__(:foo, ...) + end + end + + 2.times.map { delegate(nil, "string") } + RUBY + ) + end + def test_send_block # Setlocal_wc_0 sometimes side-exits on write barrier assert_compiles(<<~'RUBY', result: "b:n/b:y/b:y/b:n") @@ -1996,7 +2017,8 @@ def assert_compiles( frozen_string_literal: nil, mem_size: nil, code_gc: false, - no_send_fallbacks: false + no_send_fallbacks: false, + verify_ctx: false ) reset_stats = <<~RUBY RubyVM::YJIT.runtime_stats @@ -2031,7 +2053,7 @@ def collect_insns(iseq) #{write_results} RUBY - status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:, code_gc:) + status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:, code_gc:, verify_ctx:) assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}" @@ -2096,7 +2118,9 @@ def script_shell_encode(s) s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join end - def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil, code_gc: false) + def eval_with_jit( + script, call_threshold: 1, timeout: 1000, mem_size: nil, code_gc: false, verify_ctx: false + ) args = [ "--disable-gems", "--yjit-call-threshold=#{call_threshold}", @@ -2104,6 +2128,7 @@ def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil, code_ ] args << "--yjit-exec-mem-size=#{mem_size}" if mem_size args << "--yjit-code-gc" if code_gc + args << "--yjit-verify-ctx" if verify_ctx args << "-e" << script_shell_encode(script) stats_r, stats_w = IO.pipe # Separate thread so we don't deadlock when From f8b0105256b29f276ef4a6a60212e4dd2fc0fac1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 19 Jul 2026 19:32:21 +0900 Subject: [PATCH 6/6] YJIT: Avoid argument types for forwarding callees Skip local argument type propagation for forwarding callees and cover the `defined?` delegator shape with context verification and code GC enabled. --- yjit/src/codegen.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 0a9a0a796eb7a5..c8178e12842cbb 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -8290,17 +8290,14 @@ fn gen_send_iseq( callee_ctx.set_inline_block(iseq); } - // Set the argument types in the callee's context - for arg_idx in 0..argc { - let stack_offs: u8 = (argc - arg_idx - 1).try_into().unwrap(); - let arg_type = asm.ctx.get_opnd_type(StackOpnd(stack_offs)); - callee_ctx.set_local_type(arg_idx.try_into().unwrap(), arg_type); - } - - // If we're in a forwarding callee, there will be one unknown type - // written in to the local table (the caller's CI object) - if forwarding { - callee_ctx.set_local_type(0, Type::Unknown) + // Forwarding callees store a callinfo object rather than arguments in + // their local table, so their argument types do not map to local types. + if !forwarding { + for arg_idx in 0..argc { + let stack_offs: u8 = (argc - arg_idx - 1).try_into().unwrap(); + let arg_type = asm.ctx.get_opnd_type(StackOpnd(stack_offs)); + callee_ctx.set_local_type(arg_idx.try_into().unwrap(), arg_type); + } } // Set the receiver type in the callee's context