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/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/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/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) 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/re.c b/re.c index fe8e93c6a6b96c..b2c1909c153895 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.") @@ -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/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 diff --git a/string.c b/string.c index c8233be66fd06a..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]. */ @@ -5713,8 +5716,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 @@ -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#=~. * @@ -12493,11 +12496,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#[]. * 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") 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/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 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) 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,