diff options
Diffstat (limited to 'bootstraptest/test_yjit.rb')
-rw-r--r-- | bootstraptest/test_yjit.rb | 2203 |
1 files changed, 2173 insertions, 30 deletions
diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index d124d180d1..8b8f7d1e39 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -1,32 +1,318 @@ -assert_equal '2022', %q{ - def contrivance(hash, key) - # Expect this to compile to an `opt_aref`. - hash[key] +# To run the tests in this file only, with YJIT enabled: +# make btest BTESTS=bootstraptest/test_yjit.rb RUN_OPTS="--yjit-call-threshold=1" - # The [] call above tracks that the `hash` local has a VALUE that - # is a heap pointer and the guard for the Kernel#itself call below - # doesn't check that it's a heap pointer VALUE. - # - # As you can see from the crash, the call to rb_hash_aref() can set the - # `hash` local, making eliding the heap object guard unsound. - hash.itself +# regression test for popping before side exit +assert_equal "ok", %q{ + def foo(a, *) = a + + def call(args, &) + foo(1) # spill at where the block arg will be + foo(*args, &) + end + + call([1, 2]) + + begin + call([]) + rescue ArgumentError + :ok + end +} + +# regression test for send processing before side exit +assert_equal "ok", %q{ + def foo(a, *) = :foo + + def call(args) + send(:foo, *args) + end + + call([1, 2]) + + begin + call([]) + rescue ArgumentError + :ok + end +} + +# test discarding extra yield arguments +assert_equal "2210150001501015", %q{ + def splat_kw(ary) = yield *ary, a: 1 + + def splat(ary) = yield *ary + + def kw = yield 1, 2, a: 0 + + def simple = yield 0, 1 + + def calls + [ + splat([1, 1, 2]) { |x, y| x + y }, + splat([1, 1, 2]) { |y, opt = raise| opt + y}, + splat_kw([0, 1]) { |a:| a }, + kw { |a:| a }, + kw { |a| a }, + simple { 5.itself }, + simple { |a| a }, + simple { |opt = raise| opt }, + simple { |*rest| rest }, + simple { |opt_kw: 5| opt_kw }, + # autosplat ineractions + [0, 1, 2].yield_self { |a, b| [a, b] }, + [0, 1, 2].yield_self { |a, opt = raise| [a, opt] }, + [1].yield_self { |a, opt = 4| a + opt }, + ] + end + + calls.join +} + +# test autosplat with empty splat +assert_equal "ok", %q{ + def m(pos, splat) = yield pos, *splat + + m([:ok], []) {|v0,| v0 } +} + +# regression test for send stack shifting +assert_normal_exit %q{ + def foo(a, b) + a.singleton_methods(b) + end + + def call_foo + [1, 1, 1, 1, 1, 1, send(:foo, 1, 1)] + end + + call_foo +} + +# regression test for keyword splat with yield +assert_equal 'nil', %q{ + def splat_kw(kwargs) = yield(**kwargs) + + splat_kw({}) { _1 }.inspect +} + +# regression test for arity check with splat +assert_equal '[:ae, :ae]', %q{ + def req_one(a_, b_ = 1) = raise + + def test(args) + req_one *args + rescue ArgumentError + :ae + end + + [test(Array.new 5), test([])] +} unless rjit_enabled? # Not yet working on RJIT + +# regression test for arity check with splat and send +assert_equal '[:ae, :ae]', %q{ + def two_reqs(a, b_, _ = 1) = a.gsub(a, a) + + def test(name, args) + send(name, *args) + rescue ArgumentError + :ae + end + + [test(:two_reqs, ["g", nil, nil, nil]), test(:two_reqs, ["g"])] +} + +# regression test for GC marking stubs in invalidated code +assert_normal_exit %q{ + garbage = Array.new(10_000) { [] } # create garbage to cause iseq movement + eval(<<~RUBY) + def foo(n, garbage) + if n == 2 + # 1.times.each to create a cfunc frame to preserve the JIT frame + # which will return to a stub housed in an invalidated block + return 1.times.each do + Object.define_method(:foo) {} + garbage.clear + GC.verify_compaction_references(toward: :empty, expand_heap: true) + end + end + + foo(n + 1, garbage) + end + RUBY + + foo(1, garbage) +} + +# regression test for callee block handler overlapping with arguments +assert_equal '3', %q{ + def foo(_req, *args) = args.last + + def call_foo = foo(0, 1, 2, 3, &->{}) + + call_foo +} + +# call leaf builtin with a block argument +assert_equal '0', "0.abs(&nil)" + +# regression test for invokeblock iseq guard +assert_equal 'ok', %q{ + return :ok unless defined?(GC.compact) + def foo = yield + 10.times do |i| + ret = eval("foo { #{i} }") + raise "failed at #{i}" unless ret == i + GC.compact + end + :ok +} unless rjit_enabled? # Not yet working on RJIT + +# regression test for overly generous guard elision +assert_equal '[0, :sum, 0, :sum]', %q{ + # In faulty versions, the following happens: + # 1. YJIT puts object on the temp stack with type knowledge + # (CArray or CString) about RBASIC_CLASS(object). + # 2. In iter=0, due to the type knowledge, YJIT generates + # a call to sum() without any guard on RBASIC_CLASS(object). + # 3. In iter=1, a singleton class is added to the object, + # changing RBASIC_CLASS(object), falsifying the type knowledge. + # 4. Because the code from (1) has no class guard, it is incorrectly + # reused and the wrong method is invoked. + # Putting a literal is important for gaining type knowledge. + def carray(iter) + array = [] + array.sum(iter.times { def array.sum(_) = :sum }) + end + + def cstring(iter) + string = "".dup + string.sum(iter.times { def string.sum(_) = :sum }) + end + + [carray(0), carray(1), cstring(0), cstring(1)] +} + +# regression test for return type of Integer#/ +# It can return a T_BIGNUM when inputs are T_FIXNUM. +assert_equal 0x3fffffffffffffff.to_s, %q{ + def call(fixnum_min) + (fixnum_min / -1) - 1 + end + + call(-(2**62)) +} + +# regression test for return type of String#<< +assert_equal 'Sub', %q{ + def call(sub) = (sub << sub).itself + + class Sub < String; end + + call(Sub.new('o')).class +} + +# test splat filling required and feeding rest +assert_equal '[0, 1, 2, [3, 4]]', %q{ + public def lead_rest(a, b, *rest) + [self, a, b, rest] + end + + def call(args) = 0.lead_rest(*args) + + call([1, 2, 3, 4]) +} + +# test missing opts are nil initialized +assert_equal '[[0, 1, nil, 3], [0, 1, nil, 3], [0, 1, nil, 3, []], [0, 1, nil, 3, []]]', %q{ + public def lead_opts(a, b=binding.local_variable_get(:c), c=3) + [self, a, b, c] + end + + public def opts_rest(a=raise, b=binding.local_variable_get(:c), c=3, *rest) + [self, a, b, c, rest] + end + + def call(args) + [ + 0.lead_opts(1), + 0.lead_opts(*args), + + 0.opts_rest(1), + 0.opts_rest(*args), + ] + end + + call([1]) +} + +# test filled optionals with unspecified keyword param +assert_equal 'ok', %q{ + def opt_rest_opt_kw(_=1, *, k: :ok) = k + + def call = opt_rest_opt_kw(0) + + call +} + +# test splat empty array with rest param +assert_equal '[0, 1, 2, []]', %q{ + public def foo(a=1, b=2, *rest) + [self, a, b, rest] + end + + def call(args) = 0.foo(*args) + + call([]) +} + +# Regression test for yielding with autosplat to block with +# optional parameters. https://github.com/Shopify/yjit/issues/313 +assert_equal '[:a, :b, :a, :b]', %q{ + def yielder(arg) = yield(arg) + yield(arg) + + yielder([:a, :b]) do |c = :c, d = :d| + [c, d] + end +} + +# Regression test for GC mishap while doing shape transition +assert_equal '[:ok]', %q{ + # [Bug #19601] + class RegressionTest + def initialize + @a = @b = @fourth_ivar_does_shape_transition = nil + end + + def extender + @first_extended_ivar = [:ok] + end end - # This is similar to ->(recv, mid) { send(recv, mid).local_variable_set(...) }. - # By composing we avoid creating new Ruby frames and so sending :binding - # captures the environment of the frame that does the missing key lookup. - # We use it to capture the environment inside of `contrivance`. - cap_then_set = - Kernel.instance_method(:send).method(:bind_call).to_proc >> - ->(binding) { binding.local_variable_set(:hash, 2022) } - special_missing = Hash.new(&cap_then_set) + GC.stress = true + + # Used to crash due to GC run in rb_ensure_iv_list_size() + # not marking the newly allocated [:ok]. + RegressionTest.new.extender.itself +} unless rjit_enabled? # Skip on RJIT since this uncovers a crash + +assert_equal 'true', %q{ + # regression test for tracking type of locals for too long + def local_setting_cmp(five) + victim = 5 + five.define_singleton_method(:respond_to?) do |_, _| + victim = nil + end - # Make YJIT speculate that it's a hash and generate code - # that calls rb_hash_aref(). - contrivance({}, :warmup) - contrivance({}, :warmup) + # +1 makes YJIT track that victim is a number and + # defined? calls respond_to? from above indirectly + unless (victim + 1) && defined?(five.something) + # Would return wrong result if we still think `five` is a number + victim.nil? + end + end - contrivance(special_missing, :binding) + local_setting_cmp(Object.new) + local_setting_cmp(Object.new) } assert_equal '18374962167983112447', %q{ @@ -45,7 +331,7 @@ assert_equal '18374962167983112447', %q{ } assert_normal_exit %q{ - # regression test for a leak caught by an asert on --yjit-call-threshold=2 + # regression test for a leak caught by an assert on --yjit-call-threshold=2 Foo = 1 eval("def foo = [#{(['Foo,']*256).join}]") @@ -56,6 +342,29 @@ assert_normal_exit %q{ Object.send(:remove_const, :Foo) } +assert_normal_exit %q{ + # Test to ensure send on overridden c functions + # doesn't corrupt the stack + class Bar + def bar(x) + x + end + end + + class Foo + def bar + Bar.new + end + end + + foo = Foo.new + # before this change, this line would error + # because "s" would still be on the stack + # String.to_s is the overridden method here + p foo.bar.bar("s".__send__(:to_s)) +} + + assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ [NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass| klass.class_eval("def foo = @foo") @@ -67,6 +376,80 @@ assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ end } +assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ + # Tests defined? on non-heap objects + [NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass| + klass.class_eval("def foo = defined?(@foo)") + end + + [nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance| + instance.foo + instance.foo + end +} + +assert_equal '[nil, "instance-variable", nil, "instance-variable"]', %q{ + # defined? on object that changes shape between calls + class Foo + def foo + defined?(@foo) + end + + def add + @foo = 1 + end + + def remove + self.remove_instance_variable(:@foo) + end + end + + obj = Foo.new + [obj.foo, (obj.add; obj.foo), (obj.remove; obj.foo), (obj.add; obj.foo)] +} + +assert_equal '["instance-variable", 5]', %q{ + # defined? on object too complex for shape information + class Foo + def initialize + 100.times { |i| instance_variable_set("@foo#{i}", i) } + end + + def foo + [defined?(@foo5), @foo5] + end + end + + Foo.new.foo +} + +# getinstancevariable with shape too complex +assert_normal_exit %q{ + class Foo + def initialize + @a = 1 + end + + def getter + @foobar + end + end + + # Initialize ivars in changing order, making the Foo + # class have shape too complex + 100.times do |x| + foo = Foo.new + foo.instance_variable_set(:"@a#{x}", 1) + foo.instance_variable_set(:"@foobar", 777) + + # The getter method eventually sees shape too complex + r = foo.getter + if r != 777 + raise "error" + end + end +} + assert_equal '0', %q{ # This is a regression test for incomplete invalidation from # opt_setinlinecache. This test might be brittle, so @@ -362,6 +745,45 @@ assert_equal 'false', %q{ less_than 2 } +# BOP redefinition works on Integer#<= +assert_equal 'false', %q{ + def le(x, y) = x <= y + + le(2, 2) + + class Integer + def <=(_) = false + end + + le(2, 2) +} + +# BOP redefinition works on Integer#> +assert_equal 'false', %q{ + def gt(x, y) = x > y + + gt(3, 2) + + class Integer + def >(_) = false + end + + gt(3, 2) +} + +# BOP redefinition works on Integer#>= +assert_equal 'false', %q{ + def ge(x, y) = x >= y + + ge(2, 2) + + class Integer + def >=(_) = false + end + + ge(2, 2) +} + # Putobject, less-than operator, fixnums assert_equal '2', %q{ def check_index(index) @@ -817,7 +1239,7 @@ assert_equal "good", %q{ foo begin - GC.verify_compaction_references(double_heap: true, toward: :empty) + GC.verify_compaction_references(expand_heap: true, toward: :empty) rescue NotImplementedError # in case compaction isn't supported end @@ -828,7 +1250,7 @@ assert_equal "good", %q{ # Test polymorphic getinstancevariable. T_OBJECT -> T_STRING assert_equal 'ok', %q{ @hello = @h1 = @h2 = @h3 = @h4 = 'ok' - str = "" + str = +"" str.instance_variable_set(:@hello, 'ok') public def get @@ -972,6 +1394,18 @@ assert_equal '[42, :default]', %q{ ] } +# Test default value block for Hash with opt_aref_with +assert_equal "false", <<~RUBY, frozen_string_literal: false + def index_with_string(h) + h["foo"] + end + + h = Hash.new { |h, k| k.frozen? } + + index_with_string(h) + index_with_string(h) +RUBY + # A regression test for making sure cfp->sp is proper when # hitting stubs. See :stub-sp-flush: assert_equal 'ok', %q{ @@ -1129,6 +1563,38 @@ assert_equal '42', %q{ run } +# splatting an empty array on a specialized method +assert_equal 'ok', %q{ + def run + "ok".to_s(*[]) + end + + run + run +} + +# splatting an single element array on a specialized method +assert_equal '[1]', %q{ + def run + [].<<(*[1]) + end + + run + run +} + +# specialized method with wrong args +assert_equal 'ok', %q{ + def run(x) + "bad".to_s(123) if x + rescue + :ok + end + + run(false) + run(true) +} + # getinstancevariable on Symbol assert_equal '[nil, nil]', %q{ # @foo to exercise the getinstancevariable instruction @@ -1235,6 +1701,19 @@ assert_equal '[1, 2, 42]', %q{ [foo {1}, foo {2}, foo {42}] } +# test calling without block param +assert_equal '[1, false, 2, false]', %q{ + def bar + block_given? && yield + end + + def foo(&block) + bar(&block) + end + + [foo { 1 }, foo, foo { 2 }, foo] +} + # test calling block param failing assert_equal '42', %q{ def foo(&block) @@ -1368,6 +1847,46 @@ assert_equal 'foo123', %q{ make_str("foo", 123) } +# test that invalidation of String#to_s doesn't crash +assert_equal 'meh', %q{ + def inval_method + "".to_s + end + + inval_method + + class String + def to_s + "meh" + end + end + + inval_method +} + +# test that overriding to_s on a String subclass works consistently +assert_equal 'meh', %q{ + class MyString < String + def to_s + "meh" + end + end + + def test_to_s(obj) + obj.to_s + end + + OBJ = MyString.new + + # Should return '' both times + test_to_s("") + test_to_s("") + + # Can return '' if YJIT optimises String#to_s too aggressively + test_to_s(OBJ) + test_to_s(OBJ) +} + # test string interpolation with overridden to_s assert_equal 'foo', %q{ class String @@ -1384,6 +1903,228 @@ assert_equal 'foo', %q{ make_str("foo") } +# Test that String unary plus returns the same object ID for an unfrozen string. +assert_equal 'true', <<~RUBY, frozen_string_literal: false + def jittable_method + str = "bar" + + old_obj_id = str.object_id + uplus_str = +str + + uplus_str.object_id == old_obj_id + end + jittable_method +RUBY + +# Test that String unary plus returns a different unfrozen string when given a frozen string +assert_equal 'false', %q{ + # Logic needs to be inside an ISEQ, such as a method, for YJIT to compile it + def jittable_method + frozen_str = "foo".freeze + + old_obj_id = frozen_str.object_id + uplus_str = +frozen_str + + uplus_str.object_id == old_obj_id || uplus_str.frozen? + end + + jittable_method +} + +# String-subclass objects should behave as expected inside string-interpolation via concatstrings +assert_equal 'monkeys / monkeys, yo!', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def to_s + super + ", yo!" + end + end + + def jittable_method + m = MyString.new('monkeys') + "#{m} / #{m.to_s}" + end + + jittable_method +} + +# String-subclass objects should behave as expected for string equality +assert_equal 'false', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def ==(b) + "#{self}_" == b + end + end + + def jittable_method + ma = MyString.new("a") + + # Check equality with string-subclass receiver + ma == "a" || ma != "a_" || + # Check equality with string receiver + "a_" == ma || "a" != ma || + # Check equality between string subclasses + ma != MyString.new("a_") || + # Make sure "string always equals itself" check isn't used with overridden equality + ma == ma + end + jittable_method +} + +# Test to_s duplicates a string subclass object but not a string +assert_equal 'false', %q{ + class MyString < String; end + + def jittable_method + a = "a" + ma = MyString.new("a") + + a.object_id != a.to_s.object_id || + ma.object_id == ma.to_s.object_id + end + jittable_method +} + +# Test freeze on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Freezing a string subclass should not duplicate it + fma.object_id == fma.freeze.object_id + end + jittable_method +} + +# Test unary minus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + ma = MyString.new("a") + fma = MyString.new("a").freeze + + # Unary minus on frozen string subclass should not duplicate it + fma.object_id == (-fma).object_id && + # Unary minus on unfrozen string subclass should duplicate it + ma.object_id != (-ma).object_id + end + jittable_method +} + +# Test unary plus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Unary plus on frozen string subclass should not duplicate it + fma.object_id != (+fma).object_id + end + jittable_method +} + +# test getbyte on string class +assert_equal '[97, :nil, 97, :nil, :raised]', %q{ + def getbyte(s, i) + byte = begin + s.getbyte(i) + rescue TypeError + :raised + end + + byte || :nil + end + + getbyte("a", 0) + getbyte("a", 0) + + [getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")] +} unless rjit_enabled? # Not yet working on RJIT + +# Basic test for String#setbyte +assert_equal 'AoZ', %q{ + s = +"foo" + s.setbyte(0, 65) + s.setbyte(-1, 90) + s +} + +# String#setbyte IndexError +assert_equal 'String#setbyte', %q{ + def ccall = "".setbyte(1, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# String#setbyte TypeError +assert_equal 'String#setbyte', %q{ + def ccall = "".setbyte(nil, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# String#setbyte FrozenError +assert_equal 'String#setbyte', %q{ + def ccall = "a".freeze.setbyte(0, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# non-leaf String#setbyte +assert_equal 'String#setbyte', %q{ + def to_int + @caller = caller + 0 + end + + def ccall = "a".dup.setbyte(self, 98) + ccall + + @caller.first.split("'").last +} + +# non-leaf String#byteslice +assert_equal 'TypeError', %q{ + def ccall = "".byteslice(nil, nil) + begin + ccall + rescue => e + e.class + end +} + +# Test << operator on string subclass +assert_equal 'abab', %q{ + class MyString < String; end + + def jittable_method + a = -"a" + mb = MyString.new("b") + + buf = String.new + mbuf = MyString.new + + buf << a << mb + mbuf << a << mb + + buf + mbuf + end + jittable_method +} # test invokebuiltin as used in struct assignment assert_equal '123', %q{ @@ -1576,6 +2317,19 @@ assert_equal '123', %q{ foo(Foo) } +# Test EP == BP invalidation with moving ISEQs +assert_equal 'ok', %q{ + def entry + ok = proc { :ok } # set #entry as an EP-escaping ISEQ + [nil].reverse_each do # avoid exiting the JIT frame on the constant + GC.compact # move #entry ISEQ + end + ok # should be read off of escaped EP + end + + entry.call +} + # invokesuper edge case assert_equal '[:A, [:A, :B]]', %q{ class B @@ -1732,6 +2486,50 @@ assert_equal '[:A, :Btwo]', %q{ ins.foo } +# invokesuper with a block +assert_equal 'true', %q{ + class A + def foo = block_given? + end + + class B < A + def foo = super() + end + + B.new.foo { } + B.new.foo { } +} + +# invokesuper in a block +assert_equal '[0, 2]', %q{ + class A + def foo(x) = x * 2 + end + + class B < A + def foo + 2.times.map do |x| + super(x) + end + end + end + + B.new.foo + B.new.foo +} + +# invokesuper zsuper in a bmethod +assert_equal 'ok', %q{ + class Foo + define_method(:itself) { super } + end + begin + Foo.new.itself + rescue RuntimeError + :ok + end +} + # Call to fixnum assert_equal '[true, false]', %q{ def is_odd(obj) @@ -1772,6 +2570,16 @@ assert_equal '[true, false, true, false]', %q{ [is_odd(123), is_odd(456), is_odd(bignum), is_odd(bignum+1)] } +# Flonum and Flonum +assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{ + [1.0 + 1.0, 1.0 - 1.0, 1.0 * 1.0, 8.0 / 2.0] +} + +# Flonum and Fixnum +assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{ + [1.0 + 1, 1.0 - 1, 1.0 * 1, 8.0 / 2] +} + # Call to static and dynamic symbol assert_equal 'bar', %q{ def to_string(obj) @@ -1812,6 +2620,30 @@ assert_equal '[1, 2, 3, 4, 5]', %q{ splatarray } +# splatkw +assert_equal '[1, 2]', %q{ + def foo(a:) = [a, yield] + + def entry(&block) + a = { a: 1 } + foo(**a, &block) + end + + entry { 2 } +} +assert_equal '[1, 2]', %q{ + def foo(a:) = [a, yield] + + def entry(obj, &block) + foo(**obj, &block) + end + + entry({ a: 3 }) { 2 } + obj = Object.new + def obj.to_hash = { a: 1 } + entry(obj) { 2 } +} + assert_equal '[1, 1, 2, 1, 2, 3]', %q{ def expandarray arr = [1, 2, 3] @@ -1866,6 +2698,23 @@ assert_equal '[:not_array, nil, nil]', %q{ expandarray_not_array(obj) } +assert_equal '[1, 2]', %q{ + class NilClass + private + def to_ary + [1, 2] + end + end + + def expandarray_redefined_nilclass + a, b = nil + [a, b] + end + + expandarray_redefined_nilclass + expandarray_redefined_nilclass +} unless rjit_enabled? + assert_equal '[1, 2, nil]', %q{ def expandarray_rhs_too_small a, b, c = [1, 2] @@ -1876,6 +2725,17 @@ assert_equal '[1, 2, nil]', %q{ expandarray_rhs_too_small } +assert_equal '[nil, 2, nil]', %q{ + def foo(arr) + a, b, c = arr + end + + a, b, c1 = foo([0, 1]) + a, b, c2 = foo([0, 1, 2]) + a, b, c3 = foo([0, 1]) + [c1, c2, c3] +} + assert_equal '[1, [2]]', %q{ def expandarray_splat a, *b = [1, 2] @@ -1965,7 +2825,7 @@ assert_equal '[[:c_return, :String, :string_alias, "events_to_str"]]', %q{ events.compiled(events) events -} +} unless rjit_enabled? # RJIT calls extra Ruby methods # test enabling a TracePoint that targets a particular line in a C method call assert_equal '[true]', %q{ @@ -2047,7 +2907,7 @@ assert_equal '[[:c_call, :itself]]', %q{ tp.enable { shouldnt_compile } events -} +} unless rjit_enabled? # RJIT calls extra Ruby methods # test enabling c_return tracing before compiling assert_equal '[[:c_return, :itself, main]]', %q{ @@ -2062,6 +2922,26 @@ assert_equal '[[:c_return, :itself, main]]', %q{ tp.enable { shouldnt_compile } events +} unless rjit_enabled? # RJIT calls extra Ruby methods + +# test c_call invalidation +assert_equal '[[:c_call, :itself]]', %q{ + # enable the event once to make sure invalidation + # happens the second time we enable it + TracePoint.new(:c_call) {}.enable{} + + def compiled + itself + end + + # assume first call compiles + compiled + + events = [] + tp = TracePoint.new(:c_call) { |tp| events << [tp.event, tp.method_id] } + tp.enable { compiled } + + events } # test enabling tracing for a suspended fiber @@ -2088,7 +2968,6 @@ assert_equal '[:itself]', %q{ itself end - tracing_ractor = Ractor.new do # 1: start tracing events = [] @@ -2717,11 +3596,20 @@ assert_equal 'new', %q{ foo end + def bar + :bar + end + + test test RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + # Old simulat_omm! leaves one byte of space and this fills it up + bar + bar + def foo :new end @@ -2837,3 +3725,1258 @@ assert_equal '', %q{ foo foo } + +# Make sure we're correctly reading RStruct's as.ary union for embedded RStructs +assert_equal '3,12', %q{ + pt_struct = Struct.new(:x, :y) + p = pt_struct.new(3, 12) + def pt_inspect(pt) + "#{pt.x},#{pt.y}" + end + + # Make sure pt_inspect is JITted + 10.times { pt_inspect(p) } + + # Make sure it's returning '3,12' instead of e.g. '3,false' + pt_inspect(p) +} + +# Regression test for deadlock between branch_stub_hit and ractor_receive_if +assert_equal '10', %q{ + r = Ractor.new Ractor.current do |main| + main << 1 + main << 2 + main << 3 + main << 4 + main << 5 + main << 6 + main << 7 + main << 8 + main << 9 + main << 10 + end + + a = [] + a << Ractor.receive_if{|msg| msg == 10} + a << Ractor.receive_if{|msg| msg == 9} + a << Ractor.receive_if{|msg| msg == 8} + a << Ractor.receive_if{|msg| msg == 7} + a << Ractor.receive_if{|msg| msg == 6} + a << Ractor.receive_if{|msg| msg == 5} + a << Ractor.receive_if{|msg| msg == 4} + a << Ractor.receive_if{|msg| msg == 3} + a << Ractor.receive_if{|msg| msg == 2} + a << Ractor.receive_if{|msg| msg == 1} + + a.length +} + +# checktype +assert_equal 'false', %q{ + def function() + [1, 2] in [Integer, String] + end + function() +} + +# opt_send_without_block (VM_METHOD_TYPE_ATTRSET) +assert_equal 'foo', %q{ + class Foo + attr_writer :foo + + def foo() + self.foo = "foo" + end + end + foo = Foo.new + foo.foo +} + +# anytostring, intern +assert_equal 'true', %q{ + def foo() + :"#{true}" + end + foo() +} + +# toregexp, objtostring +assert_equal '/true/', %q{ + def foo() + /#{true}/ + end + foo().inspect +} + +# concatstrings, objtostring +assert_equal '9001', %q{ + def foo() + "#{9001}" + end + foo() +} + +# opt_send_without_block (VM_METHOD_TYPE_CFUNC) +assert_equal 'nil', %q{ + def foo + nil.inspect # argc: 0 + end + foo +} +assert_equal '4', %q{ + def foo + 2.pow(2) # argc: 1 + end + foo +} +assert_equal 'aba', %q{ + def foo + "abc".tr("c", "a") # argc: 2 + end + foo +} +assert_equal 'true', %q{ + def foo + respond_to?(:inspect) # argc: -1 + end + foo +} +assert_equal '["a", "b"]', %q{ + def foo + "a\nb".lines(chomp: true) # kwargs + end + foo +} + +# invokebuiltin +assert_equal '123', %q{ + def foo(obj) + obj.foo = 123 + end + + struct = Struct.new(:foo) + obj = struct.new + foo(obj) +} + +# invokebuiltin_delegate +assert_equal '.', %q{ + def foo(path) + Dir.open(path).path + end + foo(".") +} + +# opt_invokebuiltin_delegate_leave +assert_equal '[0]', %q{"\x00".unpack("c")} + +# opt_send_without_block (VM_METHOD_TYPE_ISEQ) +assert_equal '1', %q{ + def foo = 1 + def bar = foo + bar +} +assert_equal '[1, 2, 3]', %q{ + def foo(a, b) = [1, a, b] + def bar = foo(2, 3) + bar +} +assert_equal '[1, 2, 3, 4, 5, 6]', %q{ + def foo(a, b, c:, d:, e: 0, f: 6) = [a, b, c, d, e, f] + def bar = foo(1, 2, c: 3, d: 4, e: 5) + bar +} +assert_equal '[1, 2, 3, 4]', %q{ + def foo(a, b = 2) = [a, b] + def bar = foo(1) + foo(3, 4) + bar +} + +assert_equal '1', %q{ + def foo(a) = a + def bar = foo(1) { 2 } + bar +} +assert_equal '[1, 2]', %q{ + def foo(a, &block) = [a, block.call] + def bar = foo(1) { 2 } + bar +} + +# opt_send_without_block (VM_METHOD_TYPE_IVAR) +assert_equal 'foo', %q{ + class Foo + attr_reader :foo + + def initialize + @foo = "foo" + end + end + Foo.new.foo +} + +# opt_send_without_block (VM_METHOD_TYPE_OPTIMIZED) +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("bar").bar = "foo" +} +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("foo").bar +} + +# getblockparamproxy +assert_equal 'foo', %q{ + def foo(&block) + block.call + end + foo { "foo" } +} + +# getblockparam +assert_equal 'foo', %q{ + def foo(&block) + block + end + foo { "foo" }.call +} + +assert_equal '[1, 2]', %q{ + def foo + x = [2] + [1, *x] + end + + foo + foo +} + +# respond_to? with changing symbol +assert_equal 'false', %q{ + def foo(name) + :sym.respond_to?(name) + end + foo(:to_s) + foo(:to_s) + foo(:not_exist) +} + +# respond_to? with method being defined +assert_equal 'true', %q{ + def foo + :sym.respond_to?(:not_yet_defined) + end + foo + foo + module Kernel + def not_yet_defined = true + end + foo +} + +# respond_to? with undef method +assert_equal 'false', %q{ + module Kernel + def to_be_removed = true + end + def foo + :sym.respond_to?(:to_be_removed) + end + foo + foo + class Object + undef_method :to_be_removed + end + foo +} + +# respond_to? with respond_to_missing? +assert_equal 'true', %q{ + class Foo + end + def foo(x) + x.respond_to?(:bar) + end + foo(Foo.new) + foo(Foo.new) + class Foo + def respond_to_missing?(*) = true + end + foo(Foo.new) +} + +# bmethod +assert_equal '[1, 2, 3]', %q{ + one = 1 + define_method(:foo) do + one + end + + 3.times.map { |i| foo + i } +} + +# return inside bmethod +assert_equal 'ok', %q{ + define_method(:foo) do + 1.tap { return :ok } + end + + foo +} + +# bmethod optional and keywords +assert_equal '[[1, nil, 2]]', %q{ + define_method(:opt_and_kwargs) do |a = {}, b: nil, c: nil| + [a, b, c] + end + + 5.times.map { opt_and_kwargs(1, c: 2) }.uniq +} + +# bmethod with forwarded block +assert_equal '2', %q{ + define_method(:foo) do |&block| + block.call + end + + def bar(&block) + foo(&block) + end + + bar { 1 } + bar { 2 } +} + +# bmethod with forwarded block and arguments +assert_equal '5', %q{ + define_method(:foo) do |n, &block| + n + block.call + end + + def bar(n, &block) + foo(n, &block) + end + + bar(0) { 1 } + bar(3) { 2 } +} + +# bmethod with forwarded unwanted block +assert_equal '1', %q{ + one = 1 + define_method(:foo) do + one + end + + def bar(&block) + foo(&block) + end + + bar { } + bar { } +} + +# test for return stub lifetime issue +assert_equal '1', %q{ + def foo(n) + if n == 2 + return 1.times { Object.define_method(:foo) {} } + end + + foo(n + 1) + end + + foo(1) +} + +# case-when with redefined === +assert_equal 'ok', %q{ + class Symbol + def ===(a) + true + end + end + + def cw(arg) + case arg + when :b + :ok + when 4 + :ng + end + end + + cw(4) +} + +assert_equal 'threw', %q{ + def foo(args) + wrap(*args) + rescue ArgumentError + 'threw' + end + + def wrap(a) + [a] + end + + foo([Hash.ruby2_keywords_hash({})]) +} + +assert_equal 'threw', %q{ + # C call + def bar(args) + Array(*args) + rescue ArgumentError + 'threw' + end + + bar([Hash.ruby2_keywords_hash({})]) +} + +# Test instance_of? and is_a? +assert_equal 'true', %q{ + 1.instance_of?(Integer) && 1.is_a?(Integer) +} + +# Test instance_of? and is_a? for singleton classes +assert_equal 'true', %q{ + a = [] + def a.test = :test + a.instance_of?(Array) && a.is_a?(Array) +} + +# Test instance_of? for singleton_class +# Yes this does really return false +assert_equal 'false', %q{ + a = [] + def a.test = :test + a.instance_of?(a.singleton_class) +} + +# Test is_a? for singleton_class +assert_equal 'true', %q{ + a = [] + def a.test = :test + a.is_a?(a.singleton_class) +} + +# Test send with splat to a cfunc +assert_equal 'true', %q{ + 1.send(:==, 1, *[]) +} + +# Test empty splat with cfunc +assert_equal '2', %q{ + def foo + Integer.sqrt(4, *[]) + end + # call twice to deal with constant exiting + foo + foo +} + +# Test non-empty splat with cfunc +assert_equal 'Hello World', %q{ + def bar + args = ["Hello "] + greeting = +"World" + greeting.insert(0, *args) + greeting + end + bar +} + +# Regression: this creates a temp stack with > 127 elements +assert_normal_exit %q{ + def foo(a) + [ + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, + ] + end + + def entry + foo(1) + end + + entry +} + +# Test that splat and rest combined +# properly dupe the array +assert_equal "[]", %q{ + def foo(*rest) + rest << 1 + end + + def test(splat) + foo(*splat) + end + + EMPTY = [] + custom = Object.new + def custom.to_a + EMPTY + end + + test(custom) + test(custom) + EMPTY +} + +# Rest with send +assert_equal '[1, 2, 3]', %q{ + def bar(x, *rest) + rest.insert(0, x) + end + send(:bar, 1, 2, 3) +} + +# Fix splat block arg bad compilation +assert_equal "foo", %q{ + def literal(*args, &block) + s = ''.dup + literal_append(s, *args, &block) + s + end + + def literal_append(sql, v) + sql << v + end + + literal("foo") +} + +# regression test for accidentally having a parameter truncated +# due to Rust/C signature mismatch. Used to crash with +# > [BUG] rb_vm_insn_addr2insn: invalid insn address ... +# or +# > ... `Err` value: TryFromIntError(())' +assert_normal_exit %q{ + n = 16384 + eval( + "def foo(arg); " + "_=arg;" * n + '_=1;' + "Object; end" + ) + foo 1 +} + +# Regression test for CantCompile not using starting_ctx +assert_normal_exit %q{ + class Integer + def ===(other) + false + end + end + + def my_func(x) + case x + when 1 + 1 + when 2 + 2 + else + 3 + end + end + + my_func(1) +} + +# Regression test for CantCompile not using starting_ctx +assert_equal "ArgumentError", %q{ + def literal(*args, &block) + s = ''.dup + args = [1, 2, 3] + literal_append(s, *args, &block) + s + end + + def literal_append(sql, v) + [sql.inspect, v.inspect] + end + + begin + literal("foo") + rescue ArgumentError + "ArgumentError" + end +} + +# Rest with block +# Simplified code from railsbench +assert_equal '[{"/a"=>"b", :as=>:c, :via=>:post}, [], nil]', %q{ + def match(path, *rest, &block) + [path, rest, block] + end + + def map_method(method, args, &block) + options = args.last + args.pop + options[:via] = method + match(*args, options, &block) + end + + def post(*args, &block) + map_method(:post, args, &block) + end + + post "/a" => "b", as: :c +} + +# Test rest and kw_args +assert_equal '[true, true, true, true]', %q{ + def my_func(*args, base: nil, sort: true) + [args, base, sort] + end + + def calling_my_func + results = [] + results << (my_func("test") == [["test"], nil, true]) + results << (my_func("test", base: :base) == [["test"], :base, true]) + results << (my_func("test", sort: false) == [["test"], nil, false]) + results << (my_func("test", "other", base: :base) == [["test", "other"], :base, true]) + results + end + calling_my_func +} + +# Test Integer#[] with 2 args +assert_equal '0', %q{ + 3[0, 0] +} + +# unspecified_bits + checkkeyword +assert_equal '2', %q{ + def callee = 1 + + # checkkeyword should see unspecified_bits=0 (use bar), not Integer 1 (set bar = foo). + def foo(foo, bar: foo) = bar + + def entry(&block) + # write 1 at stack[3]. Calling #callee spills stack[3]. + 1 + (1 + (1 + (1 + callee))) + # &block is written to a register instead of stack[3]. When &block is popped and + # unspecified_bits is pushed, it must be written to stack[3], not to a register. + foo(1, bar: 2, &block) + end + + entry # call branch_stub_hit (spill temps) + entry # doesn't call branch_stub_hit (not spill temps) +} + +# Test rest and optional_params +assert_equal '[true, true, true, true]', %q{ + def my_func(stuff, base=nil, sort=true, *args) + [stuff, base, sort, args] + end + + def calling_my_func + results = [] + results << (my_func("test") == ["test", nil, true, []]) + results << (my_func("test", :base) == ["test", :base, true, []]) + results << (my_func("test", :base, false) == ["test", :base, false, []]) + results << (my_func("test", :base, false, "other", "other") == ["test", :base, false, ["other", "other"]]) + results + end + calling_my_func +} + +# Test rest and optional_params and splat +assert_equal '[true, true, true, true, true]', %q{ + def my_func(stuff, base=nil, sort=true, *args) + [stuff, base, sort, args] + end + + def calling_my_func + results = [] + splat = ["test"] + results << (my_func(*splat) == ["test", nil, true, []]) + splat = [:base] + results << (my_func("test", *splat) == ["test", :base, true, []]) + splat = [:base, false] + results << (my_func("test", *splat) == ["test", :base, false, []]) + splat = [:base, false, "other", "other"] + results << (my_func("test", *splat) == ["test", :base, false, ["other", "other"]]) + splat = ["test", :base, false, "other", "other"] + results << (my_func(*splat) == ["test", :base, false, ["other", "other"]]) + results + end + calling_my_func +} + +# Regression test: rest and optional and splat +assert_equal 'true', %q{ + def my_func(base=nil, *args) + [base, args] + end + + def calling_my_func + array = [] + my_func(:base, :rest1, *array) == [:base, [:rest1]] + end + + calling_my_func +} + +# Fix failed case for large splat +assert_equal 'true', %q{ + def d(a, b=:b) + end + + def calling_func + ary = 1380888.times; + d(*ary) + end + begin + calling_func + rescue ArgumentError + true + end +} unless rjit_enabled? # Not yet working on RJIT + +# Regression test: register allocator on expandarray +assert_equal '[]', %q{ + func = proc { [] } + proc do + _x, _y = func.call + end.call +} + +# Catch TAG_BREAK in a non-FINISH frame with JIT code +assert_equal '1', %q{ + def entry + catch_break + end + + def catch_break + while_true do + break + end + 1 + end + + def while_true + while true + yield + end + end + + entry +} + +assert_equal '6', %q{ + class Base + def number = 1 + yield + end + + class Sub < Base + def number = super + 2 + end + + Sub.new.number { 3 } +} + +# Integer multiplication and overflow +assert_equal '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ + def foo(a, b) + a * b + end + + r1 = foo(2, 3) + r2 = foo(2, -3) + r3 = foo(2 << 40, 2 << 41) + r4 = foo(2 << 40, -2 << 41) + r5 = foo(1 << 62, 1 << 62) + + [r1, r2, r3, r4, r5] +} + +# Integer multiplication and overflow (minimized regression test from test-basic) +assert_equal '8515157028618240000', %q{2128789257154560000 * 4} + +# Inlined method calls +assert_equal 'nil', %q{ + def putnil = nil + def entry = putnil + entry.inspect +} +assert_equal '1', %q{ + def putobject_1 = 1 + def entry = putobject_1 + entry +} +assert_equal 'false', %q{ + def putobject(_unused_arg1) = false + def entry = putobject(nil) + entry +} +assert_equal 'true', %q{ + def entry = yield + entry { true } +} +assert_equal 'sym', %q{ + def entry = :sym.to_sym + entry +} + +assert_normal_exit %q{ + ivars = 1024.times.map { |i| "@iv_#{i} = #{i}\n" }.join + Foo = Class.new + Foo.class_eval "def initialize() #{ivars} end" + Foo.new +} + +assert_equal '0', %q{ + def spill + 1.to_i # not inlined + end + + def inline(_stack1, _stack2, _stack3, _stack4, _stack5) + 0 # inlined + end + + def entry + # RegTemps is 00111110 prior to the #inline call. + # Its return value goes to stack_idx=0, which conflicts with stack_idx=5. + inline(spill, 2, 3, 4, 5) + end + + entry +} + +# Integer succ and overflow +assert_equal '[2, 4611686018427387904]', %q{ + [1.succ, 4611686018427387903.succ] +} + +# Integer right shift +assert_equal '[0, 1, -4]', %q{ + [0 >> 1, 2 >> 1, -7 >> 1] +} + +# Integer XOR +assert_equal '[0, 0, 4]', %q{ + [0 ^ 0, 1 ^ 1, 7 ^ 3] +} + +assert_equal '[nil, "yield"]', %q{ + def defined_yield = defined?(yield) + [defined_yield, defined_yield {}] +} + +# splat with ruby2_keywords into rest parameter +assert_equal '[[{:a=>1}], {}]', %q{ + ruby2_keywords def foo(*args) = args + + def bar(*args, **kw) = [args, kw] + + def pass_bar(*args) = bar(*args) + + def body + args = foo(a: 1) + pass_bar(*args) + end + + body +} + +# concatarray +assert_equal '[1, 2]', %q{ + def foo(a, b) = [a, b] + arr = [2] + foo(*[1], *arr) +} + +# pushtoarray +assert_equal '[1, 2]', %q{ + def foo(a, b) = [a, b] + arr = [1] + foo(*arr, 2) +} + +# pop before fallback +assert_normal_exit %q{ + class Foo + attr_reader :foo + + def try = foo(0, &nil) + end + + Foo.new.try +} + +# a kwrest case +assert_equal '[1, 2, {:complete=>false}]', %q{ + def rest(foo: 1, bar: 2, **kwrest) + [foo, bar, kwrest] + end + + def callsite = rest(complete: false) + + callsite +} + +# splat+kw_splat+opt+rest +assert_equal '[1, []]', %q{ + def opt_rest(a = 0, *rest) = [a, rest] + + def call_site(args) = opt_rest(*args, **nil) + + call_site([1]) +} + +# splat and nil kw_splat +assert_equal 'ok', %q{ + def identity(x) = x + + def splat_nil_kw_splat(args) = identity(*args, **nil) + + splat_nil_kw_splat([:ok]) +} + +# empty splat and kwsplat into leaf builtins +assert_equal '[1, 1, 1]', %q{ + empty = [] + [1.abs(*empty), 1.abs(**nil), 1.bit_length(*empty, **nil)] +} + +# splat into C methods with -1 arity +assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{ + class Foo < Array + def push(args) = super(1, *args) + end + + def test_cfunc_vargs_splat(sub_instance, array_class, empty_kw_hash) + splat = [2, 3] + kw_splat = [empty_kw_hash] + [ + sub_instance.push(splat), + array_class[0, *splat, **nil], + array_class[1, *splat, &nil], + array_class[2, *splat, **nil, &nil], + array_class.send(:[], *kw_splat), + # kw_splat disables keywords hash handling + array_class[*kw_splat], + array_class[*kw_splat, **nil], + ] + end + + test_cfunc_vargs_splat(Foo.new, Array, Hash.ruby2_keywords_hash({})) +} + +# Class#new (arity=-1), splat, and ruby2_keywords +assert_equal '[0, {1=>1}]', %q{ + class KwInit + attr_reader :init_args + def initialize(x = 0, **kw) + @init_args = [x, kw] + end + end + + def test(klass, args) + klass.new(*args).init_args + end + + test(KwInit, [Hash.ruby2_keywords_hash({1 => 1})]) +} + +# Chilled string setivar trigger warning +assert_equal 'literal string will be frozen in the future', %q{ + Warning[:deprecated] = true + $VERBOSE = true + $warning = "no-warning" + module ::Warning + def self.warn(message) + $warning = message.split("warning: ").last.strip + end + end + + class String + def setivar! + @ivar = 42 + end + end + + def setivar!(str) + str.setivar! + end + + 10.times { setivar!("mutable".dup) } + 10.times do + setivar!("frozen".freeze) + rescue FrozenError + end + + setivar!("chilled") # Emit warning + $warning +} + +# arity=-2 cfuncs +assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{ + def test_cases(file, chain) + new_chain = chain.allocate # to call initialize directly + new_chain.send(:initialize, [0], ok: 1) + + [ + file.join, + file.join("1", "2"), + new_chain.to_a, + ] + end + + test_cases(File, Enumerator::Chain) +} + +# singleton class should invalidate Type::CString assumption +assert_equal 'foo', %q{ + def define_singleton(str, define) + if define + # Wrap a C method frame to avoid exiting JIT code on defineclass + [nil].reverse_each do + class << str + def +(_) + "foo" + end + end + end + end + "bar" + end + + def entry(define) + str = "" + # When `define` is false, #+ compiles to rb_str_plus() without a class guard. + # When the code is reused with `define` is true, the class of `str` is changed + # to a singleton class, so the block should be invalidated. + str + define_singleton(str, define) + end + + entry(false) + entry(true) +} + +assert_equal '[:ok, :ok, :ok]', %q{ + def identity(x) = x + def foo(x, _) = x + def bar(_, _, _, _, x) = x + + def tests + [ + identity(:ok), + foo(:ok, 2), + bar(1, 2, 3, 4, :ok), + ] + end + + tests +} + +# regression test for invalidating an empty block +assert_equal '0', %q{ + def foo = (* = 1).pred + + foo # compile it + + class Integer + def to_ary = [] # invalidate + end + + foo # try again +} unless rjit_enabled? # doesn't work on RJIT + +# test integer left shift with constant rhs +assert_equal [0x80000000000, 'a+', :ok].inspect, %q{ + def shift(val) = val << 43 + + def tests + int = shift(1) + str = shift("a") + + Integer.define_method(:<<) { |_| :ok } + redef = shift(1) + + [int, str, redef] + end + + tests +} + +# test String#stebyte with arguments that need conversion +assert_equal "abc", %q{ + str = +"a00" + def change_bytes(str, one, two) + str.setbyte(one, "b".ord) + str.setbyte(2, two) + end + + to_int_1 = Object.new + to_int_99 = Object.new + def to_int_1.to_int = 1 + def to_int_99.to_int = 99 + + change_bytes(str, to_int_1, to_int_99) + str +} + +# test --yjit-verify-ctx for arrays with a singleton class +assert_equal "ok", %q{ + class Array + def foo + self.singleton_class.define_method(:first) { :ok } + first + end + end + + def test = [].foo + + test +} + +assert_equal '["raised", "Module", "Object"]', %q{ + def foo(obj) + obj.superclass.name + end + + ret = [] + + begin + foo(Class.allocate) + rescue TypeError + ret << 'raised' + end + + ret += [foo(Class), foo(Class.new)] +} + +# test TrueClass#=== before and after redefining TrueClass#== +assert_equal '[[true, false, false], [true, true, false], [true, :error, :error]]', %q{ + def true_eqq(x) + true === x + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + true_eqq(true), + # these will use TrueClass#== + true_eqq(false), + true_eqq(:truthy), + ] + end + + results = [test] + + class TrueClass + def ==(x) + !x + end + end + + results << test + + class TrueClass + undef_method :== + end + + results << test +} unless rjit_enabled? # Not yet working on RJIT + +# test FalseClass#=== before and after redefining FalseClass#== +assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ + def case_equal(x, y) + x === y + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + case_equal(false, false), + # these will use #== + case_equal(false, true), + case_equal(false, nil), + ] + end + + results = [test] + + class FalseClass + def ==(x) + !x + end + end + + results << test + + class FalseClass + undef_method :== + end + + results << test +} unless rjit_enabled? # Not yet working on RJIT + +# test NilClass#=== before and after redefining NilClass#== +assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ + def case_equal(x, y) + x === y + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + case_equal(nil, nil), + # these will use #== + case_equal(nil, true), + case_equal(nil, false), + ] + end + + results = [test] + + class NilClass + def ==(x) + !x + end + end + + results << test + + class NilClass + undef_method :== + end + + results << test +} unless rjit_enabled? # Not yet working on RJIT + +# test struct accessors fire c_call events +assert_equal '[[:c_call, :x=], [:c_call, :x]]', %q{ + c = Struct.new(:x) + obj = c.new + + events = [] + TracePoint.new(:c_call) do + events << [_1.event, _1.method_id] + end.enable do + obj.x = 100 + obj.x + end + + events +} |