ruby/bootstraptest/test_yjit.rb

4184 строки
68 KiB
Ruby

# 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 defined?(RubyVM::RJIT) && RubyVM::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 = ""
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
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 RUBY_DESCRIPTION.include?('+RJIT') # 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
# +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
local_setting_cmp(Object.new)
local_setting_cmp(Object.new)
}
assert_equal '18374962167983112447', %q{
# regression test for incorrectly discarding 32 bits of a pointer when it
# comes to default values.
def large_literal_default(n: 0xff00_fabcafe0_00ff)
n
end
def call_graph_root
large_literal_default
end
call_graph_root
call_graph_root
}
assert_normal_exit %q{
# regression test for a leak caught by an assert on --yjit-call-threshold=2
Foo = 1
eval("def foo = [#{(['Foo,']*256).join}]")
foo
foo
Object.send(:remove_const, :Foo)
}
assert_normal_exit %q{
# Test to ensure send on overriden 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")
end
[nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance|
instance.foo
instance.foo
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
}
assert_equal '0', %q{
# This is a regression test for incomplete invalidation from
# opt_setinlinecache. This test might be brittle, so
# feel free to remove it in the future if it's too annoying.
# This test assumes --yjit-call-threshold=2.
module M
Foo = 1
def foo
Foo
end
def pin_self_type_then_foo
_ = @foo
foo
end
def only_ints
1 + self
foo
end
end
class Integer
include M
end
class Sub
include M
end
foo_method = M.instance_method(:foo)
dbg = ->(message) do
return # comment this out to get printouts
$stderr.puts RubyVM::YJIT.disasm(foo_method)
$stderr.puts message
end
2.times { 42.only_ints }
dbg["There should be two versions of getinlineache"]
module M
remove_const(:Foo)
end
dbg["There should be no getinlinecaches"]
2.times do
42.only_ints
rescue NameError => err
_ = "caught name error #{err}"
end
dbg["There should be one version of getinlineache"]
2.times do
Sub.new.pin_self_type_then_foo
rescue NameError
_ = 'second specialization'
end
dbg["There should be two versions of getinlineache"]
module M
Foo = 1
end
dbg["There should still be two versions of getinlineache"]
42.only_ints
dbg["There should be no getinlinecaches"]
# Find name of the first VM instruction in M#foo.
insns = RubyVM::InstructionSequence.of(foo_method).to_a
if defined?(RubyVM::YJIT.blocks_for) && (insns.last.find { Array === _1 }&.first == :opt_getinlinecache)
RubyVM::YJIT.blocks_for(RubyVM::InstructionSequence.of(foo_method))
.filter { _1.iseq_start_index == 0 }.count
else
0 # skip the test
end
}
# Check that frozen objects are respected
assert_equal 'great', %q{
class Foo
attr_accessor :bar
def initialize
@bar = 1
freeze
end
end
foo = Foo.new
5.times do
begin
foo.bar = 2
rescue FrozenError
end
end
foo.bar == 1 ? "great" : "NG"
}
# Check that global variable set works
assert_equal 'string', %q{
def foo
$foo = "string"
end
foo
}
# Check that exceptions work when setting global variables
assert_equal 'rescued', %q{
def set_var
$var = 100
rescue
:rescued
end
set_var
trace_var(:$var) { raise }
set_var
}
# Check that global variables work
assert_equal 'string', %q{
$foo = "string"
def foo
$foo
end
foo
}
# Check that exceptions work when getting global variable
assert_equal 'rescued', %q{
module Warning
def warn(message)
raise
end
end
def get_var
$=
rescue
:rescued
end
$VERBOSE = true
get_var
get_var
}
# Check that global tracepoints work
assert_equal 'true', %q{
def foo
1
end
foo
foo
foo
called = false
tp = TracePoint.new(:return) { |event|
if event.method_id == :foo
called = true
end
}
tp.enable
foo
tp.disable
called
}
# Check that local tracepoints work
assert_equal 'true', %q{
def foo
1
end
foo
foo
foo
called = false
tp = TracePoint.new(:return) { |_| called = true }
tp.enable(target: method(:foo))
foo
tp.disable
called
}
# Make sure that optional param methods return the correct value
assert_equal '1', %q{
def m(ary = [])
yield(ary)
end
# Warm the JIT with a 0 param call
2.times { m { } }
m(1) { |v| v }
}
# Test for topn
assert_equal 'array', %q{
def threequals(a)
case a
when Array
"array"
when Hash
"hash"
else
"unknown"
end
end
threequals([])
threequals([])
threequals([])
}
# Test for opt_mod
assert_equal '2', %q{
def mod(a, b)
a % b
end
mod(7, 5)
mod(7, 5)
}
# Test for opt_mult
assert_equal '12', %q{
def mult(a, b)
a * b
end
mult(6, 2)
mult(6, 2)
}
# Test for opt_div
assert_equal '3', %q{
def div(a, b)
a / b
end
div(6, 2)
div(6, 2)
}
# BOP redefined methods work when JIT compiled
assert_equal 'false', %q{
def less_than x
x < 10
end
class Integer
def < x
false
end
end
less_than 2
less_than 2
less_than 2
}
# BOP redefinition works on Integer#<
assert_equal 'false', %q{
def less_than x
x < 10
end
less_than 2
less_than 2
class Integer
def < x
false
end
end
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)
if 0x40000000 < index
raise "wat? #{index}"
end
index
end
check_index 2
check_index 2
}
# foo leaves a temp on the stack before the call
assert_equal '6', %q{
def bar
return 5
end
def foo
return 1 + bar
end
foo()
retval = foo()
}
# Method with one arguments
# foo leaves a temp on the stack before the call
assert_equal '7', %q{
def bar(a)
return a + 1
end
def foo
return 1 + bar(5)
end
foo()
retval = foo()
}
# Method with two arguments
# foo leaves a temp on the stack before the call
assert_equal '0', %q{
def bar(a, b)
return a - b
end
def foo
return 1 + bar(1, 2)
end
foo()
retval = foo()
}
# Passing argument types to callees
assert_equal '8.5', %q{
def foo(x, y)
x + y
end
def bar
foo(7, 1.5)
end
bar
bar
}
# Recursive Ruby-to-Ruby calls
assert_equal '21', %q{
def fib(n)
if n < 2
return n
end
return fib(n-1) + fib(n-2)
end
r = fib(8)
}
# Ruby-to-Ruby call and C call
assert_normal_exit %q{
def bar
puts('hi!')
end
def foo
bar
end
foo()
foo()
}
# Method aliasing
assert_equal '42', %q{
class Foo
def method_a
42
end
alias method_b method_a
def method_a
:somethingelse
end
end
@obj = Foo.new
def test
@obj.method_b
end
test
test
}
# Method aliasing with method from parent class
assert_equal '777', %q{
class A
def method_a
777
end
end
class B < A
alias method_b method_a
end
@obj = B.new
def test
@obj.method_b
end
test
test
}
# The hash method is a C function and uses the self argument
assert_equal 'true', %q{
def lehashself
hash
end
a = lehashself
b = lehashself
a == b
}
# Method redefinition (code invalidation) test
assert_equal '1', %q{
def ret1
return 1
end
klass = Class.new do
def alias_then_hash(klass, method_to_redefine)
# Redefine the method to be ret1
klass.alias_method(method_to_redefine, :ret1)
hash
end
end
instance = klass.new
i = 0
while i < 12
if i < 11
# Redefine the bar method
instance.alias_then_hash(klass, :bar)
else
# Redefine the hash method to be ret1
retval = instance.alias_then_hash(klass, :hash)
end
i += 1
end
retval
}
# Code invalidation and opt_getinlinecache
assert_normal_exit %q{
class Foo; end
# Uses the class constant Foo
def use_constant(arg)
[Foo.new, arg]
end
def propagate_type
i = Array.new
i.itself # make it remember that i is on-heap
use_constant(i)
end
propagate_type
propagate_type
use_constant(Foo.new)
class Jo; end # bump global constant state
use_constant(3)
}
# Method redefinition (code invalidation) and GC
assert_equal '7', %q{
def bar()
return 5
end
def foo()
bar()
end
foo()
foo()
def bar()
return 7
end
4.times { GC.start }
foo()
foo()
}
# Method redefinition with two block versions
assert_equal '7', %q{
def bar()
return 5
end
def foo(n)
return ((n < 5)? 5:false), bar()
end
foo(4)
foo(4)
foo(10)
foo(10)
def bar()
return 7
end
4.times { GC.start }
foo(4)
foo(4)[1]
}
# Method redefinition while the method is on the stack
assert_equal '[777, 1]', %q{
def foo
redef()
777
end
def redef
# Redefine the global foo
eval("def foo; 1; end", TOPLEVEL_BINDING)
# Collect dead code
GC.stress = true
GC.start
# But we will return to the original foo,
# which remains alive because it's on the stack
end
# Must produce [777, 1]
[foo, foo]
}
# Test for GC safety. Don't invalidate dead iseqs.
assert_normal_exit %q{
Class.new do
def foo
itself
end
new.foo
new.foo
new.foo
new.foo
end
4.times { GC.start }
def itself
self
end
}
# test setinstancevariable on extended objects
assert_equal '1', %q{
class Extended
attr_reader :one
def write_many
@a = 1
@b = 2
@c = 3
@d = 4
@one = 1
end
end
foo = Extended.new
foo.write_many
foo.write_many
foo.write_many
}
# test setinstancevariable on embedded objects
assert_equal '1', %q{
class Embedded
attr_reader :one
def write_one
@one = 1
end
end
foo = Embedded.new
foo.write_one
foo.write_one
foo.write_one
}
# test setinstancevariable after extension
assert_equal '[10, 11, 12, 13, 1]', %q{
class WillExtend
attr_reader :one
def make_extended
@foo1 = 10
@foo2 = 11
@foo3 = 12
@foo4 = 13
end
def write_one
@one = 1
end
def read_all
[@foo1, @foo2, @foo3, @foo4, @one]
end
end
foo = WillExtend.new
foo.write_one
foo.write_one
foo.make_extended
foo.write_one
foo.read_all
}
# test setinstancevariable on frozen object
assert_equal 'object was not modified', %q{
class WillFreeze
def write
@ivar = 1
end
end
wf = WillFreeze.new
wf.write
wf.write
wf.freeze
begin
wf.write
rescue FrozenError
"object was not modified"
end
}
# Test getinstancevariable and inline caches
assert_equal '6', %q{
class Foo
def initialize
@x1 = 1
@x2 = 1
@x2 = 1
@x3 = 1
@x4 = 3
end
def bar
x = 1
@x4 + @x4
end
end
f = Foo.new
f.bar
f.bar
}
# Test that getinstancevariable codegen checks for extended table size
assert_equal "nil\n", %q{
class A
def read
@ins1000
end
end
ins = A.new
other = A.new
10.times { other.instance_variable_set(:"@otr#{_1}", 'value') }
1001.times { ins.instance_variable_set(:"@ins#{_1}", 'value') }
ins.read
ins.read
ins.read
p other.read
}
# Test that opt_aref checks the class of the receiver
assert_equal 'special', %q{
def foo(array)
array[30]
end
foo([])
foo([])
special = []
def special.[](idx)
'special'
end
foo(special)
}
# Test that object references in generated code get marked and moved
assert_equal "good", %q{
def bar
"good"
end
def foo
bar
end
foo
foo
begin
GC.verify_compaction_references(expand_heap: true, toward: :empty)
rescue NotImplementedError
# in case compaction isn't supported
end
foo
}
# Test polymorphic getinstancevariable. T_OBJECT -> T_STRING
assert_equal 'ok', %q{
@hello = @h1 = @h2 = @h3 = @h4 = 'ok'
str = ""
str.instance_variable_set(:@hello, 'ok')
public def get
@hello
end
get
get
str.get
str.get
}
# Test polymorphic getinstancevariable, two different classes
assert_equal 'ok', %q{
class Embedded
def initialize
@ivar = 0
end
def get
@ivar
end
end
class Extended < Embedded
def initialize
@v1 = @v2 = @v3 = @v4 = @ivar = 'ok'
end
end
embed = Embedded.new
extend = Extended.new
embed.get
embed.get
extend.get
extend.get
}
# Test megamorphic getinstancevariable
assert_equal 'ok', %q{
parent = Class.new do
def initialize
@hello = @h1 = @h2 = @h3 = @h4 = 'ok'
end
def get
@hello
end
end
subclasses = 300.times.map { Class.new(parent) }
subclasses.each { _1.new.get }
parent.new.get
}
# Test polymorphic opt_aref. array -> hash
assert_equal '[42, :key]', %q{
def index(obj, idx)
obj[idx]
end
index([], 0) # get over compilation threshold
[
index([42], 0),
index({0=>:key}, 0),
]
}
# Test polymorphic opt_aref. hash -> array -> custom class
assert_equal '[nil, nil, :custom]', %q{
def index(obj, idx)
obj[idx]
end
custom = Object.new
def custom.[](_idx)
:custom
end
index({}, 0) # get over compilation threshold
[
index({}, 0),
index([], 0),
index(custom, 0)
]
}
# Test polymorphic opt_aref. array -> custom class
assert_equal '[42, :custom]', %q{
def index(obj, idx)
obj[idx]
end
custom = Object.new
def custom.[](_idx)
:custom
end
index([], 0) # get over compilation threshold
[
index([42], 0),
index(custom, 0)
]
}
# Test custom hash method with opt_aref
assert_equal '[nil, :ok]', %q{
def index(obj, idx)
obj[idx]
end
custom = Object.new
def custom.hash
42
end
h = {custom => :ok}
[
index(h, 0),
index(h, custom)
]
}
# Test default value block for Hash with opt_aref
assert_equal '[42, :default]', %q{
def index(obj, idx)
obj[idx]
end
h = Hash.new { :default }
h[0] = 42
[
index(h, 0),
index(h, 1)
]
}
# Test default value block for Hash with opt_aref_with
assert_equal "false", %q{
def index_with_string(h)
h["foo"]
end
h = Hash.new { |h, k| k.frozen? }
index_with_string(h)
index_with_string(h)
}
# A regression test for making sure cfp->sp is proper when
# hitting stubs. See :stub-sp-flush:
assert_equal 'ok', %q{
class D
def foo
Object.new
end
end
GC.stress = true
10.times do
D.new.foo
# ^
# This hits a stub with sp_offset > 0
end
:ok
}
# Test polymorphic callsite, cfunc -> iseq
assert_equal '[Cfunc, Iseq]', %q{
public def call_itself
itself # the polymorphic callsite
end
class Cfunc; end
class Iseq
def itself
self
end
end
call_itself # cross threshold
[Cfunc.call_itself, Iseq.call_itself]
}
# Test polymorphic callsite, iseq -> cfunc
assert_equal '[Iseq, Cfunc]', %q{
public def call_itself
itself # the polymorphic callsite
end
class Cfunc; end
class Iseq
def itself
self
end
end
call_itself # cross threshold
[Iseq.call_itself, Cfunc.call_itself]
}
# attr_reader method
assert_equal '[100, 299]', %q{
class A
attr_reader :foo
def initialize
@foo = 100
end
# Make it extended
def fill!
@bar = @jojo = @as = @sdfsdf = @foo = 299
end
end
def bar(ins)
ins.foo
end
ins = A.new
oth = A.new
oth.fill!
bar(ins)
bar(oth)
[bar(ins), bar(oth)]
}
# get ivar on object, then on hash
assert_equal '[42, 100]', %q{
class Hash
attr_accessor :foo
end
class A
attr_reader :foo
def initialize
@foo = 42
end
end
def use(val)
val.foo
end
h = {}
h.foo = 100
obj = A.new
use(obj)
[use(obj), use(h)]
}
# get ivar on String
assert_equal '[nil, nil, 42, 42]', %q{
# @foo to exercise the getinstancevariable instruction
public def get_foo
@foo
end
get_foo
get_foo # compile it for the top level object
class String
attr_reader :foo
end
def run
str = String.new
getter = str.foo
insn = str.get_foo
str.instance_variable_set(:@foo, 42)
[getter, insn, str.foo, str.get_foo]
end
run
run
}
# splatting an empty array on a getter
assert_equal '42', %q{
@foo = 42
module Kernel
attr_reader :foo
end
def run
foo(*[])
end
run
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
public def get_foo
@foo
end
dyn_sym = ("a" + "b").to_sym
sym = :static
# compile get_foo
dyn_sym.get_foo
dyn_sym.get_foo
[dyn_sym.get_foo, sym.get_foo]
}
# attr_reader on Symbol
assert_equal '[nil, nil]', %q{
class Symbol
attr_reader :foo
end
public def get_foo
foo
end
dyn_sym = ("a" + "b").to_sym
sym = :static
# compile get_foo
dyn_sym.get_foo
dyn_sym.get_foo
[dyn_sym.get_foo, sym.get_foo]
}
# passing too few arguments to method with optional parameters
assert_equal 'raised', %q{
def opt(a, b = 0)
end
def use
opt
end
use rescue nil
begin
use
:ng
rescue ArgumentError
:raised
end
}
# passing too many arguments to method with optional parameters
assert_equal 'raised', %q{
def opt(a, b = 0)
end
def use
opt(1, 2, 3, 4)
end
use rescue nil
begin
use
:ng
rescue ArgumentError
:raised
end
}
# test calling Ruby method with a block
assert_equal '[1, 2, 42]', %q{
def thing(a, b)
[a, b, yield]
end
def use
thing(1,2) { 42 }
end
use
use
}
# test calling C method with a block
assert_equal '[42, 42]', %q{
def use(array, initial)
array.reduce(initial) { |a, b| a + b }
end
use([], 0)
[use([2, 2], 38), use([14, 14, 14], 0)]
}
# test calling block param
assert_equal '[1, 2, 42]', %q{
def foo(&block)
block.call
end
[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)
block.call
end
foo {} # warmup
begin
foo
rescue NoMethodError => e
42 if nil == e.receiver
end
}
# test calling method taking block param
assert_equal '[Proc, 1, 2, 3, Proc]', %q{
def three(a, b, c, &block)
[a, b, c, block.class]
end
def zero(&block)
block.class
end
def use_three
three(1, 2, 3) {}
end
def use_zero
zero {}
end
use_three
use_zero
[use_zero] + use_three
}
# test building empty array
assert_equal '[]', %q{
def build_arr
[]
end
build_arr
build_arr
}
# test building array of one element
assert_equal '[5]', %q{
def build_arr(val)
[val]
end
build_arr(5)
build_arr(5)
}
# test building array of several element
assert_equal '[5, 5, 5, 5, 5]', %q{
def build_arr(val)
[val, val, val, val, val]
end
build_arr(5)
build_arr(5)
}
# test building empty hash
assert_equal '{}', %q{
def build_hash
{}
end
build_hash
build_hash
}
# test building hash with values
assert_equal '{:foo=>:bar}', %q{
def build_hash(val)
{ foo: val }
end
build_hash(:bar)
build_hash(:bar)
}
# test string interpolation with known types
assert_equal 'foobar', %q{
def make_str
foo = -"foo"
bar = -"bar"
"#{foo}#{bar}"
end
make_str
make_str
}
# test string interpolation with unknown types
assert_equal 'foobar', %q{
def make_str(foo, bar)
"#{foo}#{bar}"
end
make_str("foo", "bar")
make_str("foo", "bar")
}
# test string interpolation with known non-strings
assert_equal 'foo123', %q{
def make_str
foo = -"foo"
bar = 123
"#{foo}#{bar}"
end
make_str
make_str
}
# test string interpolation with unknown non-strings
assert_equal 'foo123', %q{
def make_str(foo, bar)
"#{foo}#{bar}"
end
make_str("foo", 123)
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
def to_s
"bad"
end
end
def make_str(foo)
"#{foo}"
end
make_str("foo")
make_str("foo")
}
# Test that String unary plus returns the same object ID for an unfrozen string.
assert_equal 'true', %q{
def jittable_method
str = "bar"
old_obj_id = str.object_id
uplus_str = +str
uplus_str.object_id == old_obj_id
end
jittable_method
}
# 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 defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # Not yet working on RJIT
# 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{
def foo(obj)
obj.foo = 123
end
struct = Struct.new(:foo)
obj = struct.new
foo(obj)
foo(obj)
}
# test invokebuiltin_delegate as used inside Dir.open
assert_equal '.', %q{
def foo(path)
Dir.open(path).path
end
foo(".")
foo(".")
}
# test invokebuiltin_delegate_leave in method called from jit
assert_normal_exit %q{
def foo(obj)
obj.clone
end
foo(Object.new)
foo(Object.new)
}
# test invokebuiltin_delegate_leave in method called from cfunc
assert_normal_exit %q{
def foo(obj)
[obj].map(&:clone)
end
foo(Object.new)
foo(Object.new)
}
# defining TrueClass#!
assert_equal '[false, false, :ok]', %q{
def foo(obj)
!obj
end
x = foo(true)
y = foo(true)
class TrueClass
def !
:ok
end
end
z = foo(true)
[x, y, z]
}
# defining FalseClass#!
assert_equal '[true, true, :ok]', %q{
def foo(obj)
!obj
end
x = foo(false)
y = foo(false)
class FalseClass
def !
:ok
end
end
z = foo(false)
[x, y, z]
}
# defining NilClass#!
assert_equal '[true, true, :ok]', %q{
def foo(obj)
!obj
end
x = foo(nil)
y = foo(nil)
class NilClass
def !
:ok
end
end
z = foo(nil)
[x, y, z]
}
# polymorphic opt_not
assert_equal '[true, true, false, false, false, false, false]', %q{
def foo(obj)
!obj
end
foo(0)
[foo(nil), foo(false), foo(true), foo([]), foo(0), foo(4.2), foo(:sym)]
}
# getlocal with 2 levels
assert_equal '7', %q{
def foo(foo, bar)
while foo > 0
while bar > 0
return foo + bar
end
end
end
foo(5,2)
foo(5,2)
}
# test pattern matching
assert_equal '[:ok, :ok]', %q{
class C
def destructure_keys
{}
end
end
pattern_match = ->(i) do
case i
in a: 0
:ng
else
:ok
end
end
[{}, C.new].map(&pattern_match)
}
# Call to object with singleton
assert_equal '123', %q{
obj = Object.new
def obj.foo
123
end
def foo(obj)
obj.foo()
end
foo(obj)
foo(obj)
}
# Call method on an object that has a non-material
# singleton class.
# TODO: assert that it takes no side exits? This
# test case revealed that we were taking exits unnecessarily.
assert_normal_exit %q{
def foo(obj)
obj.itself
end
o = Object.new.singleton_class
foo(o)
foo(o)
}
# Call to singleton class
assert_equal '123', %q{
class Foo
def self.foo
123
end
end
def foo(obj)
obj.foo()
end
foo(Foo)
foo(Foo)
}
# invokesuper edge case
assert_equal '[:A, [:A, :B]]', %q{
class B
def foo = :B
end
class A < B
def foo = [:A, super()]
end
A.new.foo
A.new.foo # compile A#foo
class C < A
define_method(:bar, A.instance_method(:foo))
end
C.new.bar
}
# Same invokesuper bytecode, multiple destinations
assert_equal '[:Forward, :SecondTerminus]', %q{
module Terminus
def foo = :Terminus
end
module SecondTerminus
def foo = :SecondTerminus
end
module Forward
def foo = [:Forward, super]
end
class B
include SecondTerminus
end
class A < B
include Terminus
include Forward
end
A.new.foo
A.new.foo # compile
class B
include Forward
alias bar foo
end
# A.ancestors.take(5) == [A, Forward, Terminus, B, Forward, SecondTerminus]
A.new.bar
}
# invokesuper calling into itself
assert_equal '[:B, [:B, :m]]', %q{
module M
def foo = :m
end
class B
include M
def foo = [:B, super]
end
ins = B.new
ins.singleton_class # materialize the singleton class
ins.foo
ins.foo # compile
ins.singleton_class.define_method(:bar, B.instance_method(:foo))
ins.bar
}
# invokesuper changed ancestor
assert_equal '[:A, [:M, :B]]', %q{
class B
def foo
:B
end
end
class A < B
def foo
[:A, super]
end
end
module M
def foo
[:M, super]
end
end
ins = A.new
ins.foo
ins.foo
A.include(M)
ins.foo
}
# invokesuper changed ancestor via prepend
assert_equal '[:A, [:M, :B]]', %q{
class B
def foo
:B
end
end
class A < B
def foo
[:A, super]
end
end
module M
def foo
[:M, super]
end
end
ins = A.new
ins.foo
ins.foo
B.prepend(M)
ins.foo
}
# invokesuper replaced method
assert_equal '[:A, :Btwo]', %q{
class B
def foo
:B
end
end
class A < B
def foo
[:A, super]
end
end
ins = A.new
ins.foo
ins.foo
class B
def foo
:Btwo
end
end
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
}
# Call to fixnum
assert_equal '[true, false]', %q{
def is_odd(obj)
obj.odd?
end
is_odd(1)
is_odd(1)
[is_odd(123), is_odd(456)]
}
# Call to bignum
assert_equal '[true, false]', %q{
def is_odd(obj)
obj.odd?
end
bignum = 99999999999999999999
is_odd(bignum)
is_odd(bignum)
[is_odd(bignum), is_odd(bignum+1)]
}
# Call to fixnum and bignum
assert_equal '[true, false, true, false]', %q{
def is_odd(obj)
obj.odd?
end
bignum = 99999999999999999999
is_odd(bignum)
is_odd(bignum)
is_odd(123)
is_odd(123)
[is_odd(123), is_odd(456), is_odd(bignum), is_odd(bignum+1)]
}
# Call to static and dynamic symbol
assert_equal 'bar', %q{
def to_string(obj)
obj.to_s
end
to_string(:foo)
to_string(:foo)
to_string((-"bar").to_sym)
to_string((-"bar").to_sym)
}
# Call to flonum and heap float
assert_equal '[nil, nil, nil, 1]', %q{
def is_inf(obj)
obj.infinite?
end
is_inf(0.0)
is_inf(0.0)
is_inf(1e256)
is_inf(1e256)
[
is_inf(0.0),
is_inf(1.0),
is_inf(1e256),
is_inf(1.0/0.0)
]
}
assert_equal '[1, 2, 3, 4, 5]', %q{
def splatarray
[*(1..5)]
end
splatarray
splatarray
}
assert_equal '[1, 1, 2, 1, 2, 3]', %q{
def expandarray
arr = [1, 2, 3]
a, = arr
b, c, = arr
d, e, f = arr
[a, b, c, d, e, f]
end
expandarray
expandarray
}
assert_equal '[1, 1]', %q{
def expandarray_useless_splat
arr = (1..10).to_a
a, * = arr
b, (*) = arr
[a, b]
end
expandarray_useless_splat
expandarray_useless_splat
}
assert_equal '[:not_heap, nil, nil]', %q{
def expandarray_not_heap
a, b, c = :not_heap
[a, b, c]
end
expandarray_not_heap
expandarray_not_heap
}
assert_equal '[:not_array, nil, nil]', %q{
def expandarray_not_array(obj)
a, b, c = obj
[a, b, c]
end
obj = Object.new
def obj.to_ary
[:not_array]
end
expandarray_not_array(obj)
expandarray_not_array(obj)
}
assert_equal '[1, 2, nil]', %q{
def expandarray_rhs_too_small
a, b, c = [1, 2]
[a, b, c]
end
expandarray_rhs_too_small
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]
[a, b]
end
expandarray_splat
expandarray_splat
}
assert_equal '2', %q{
def expandarray_postarg
*, a = [1, 2]
a
end
expandarray_postarg
expandarray_postarg
}
assert_equal '10', %q{
obj = Object.new
val = nil
obj.define_singleton_method(:to_ary) { val = 10; [] }
def expandarray_always_call_to_ary(object)
* = object
end
expandarray_always_call_to_ary(obj)
expandarray_always_call_to_ary(obj)
val
}
# regression test of local type change
assert_equal '1.1', %q{
def bar(baz, quux)
if baz.integer?
baz, quux = quux, nil
end
baz.to_s
end
bar(123, 1.1)
bar(123, 1.1)
}
# test enabling a line TracePoint in a C method call
assert_equal '[[:line, true]]', %q{
events = []
events.instance_variable_set(
:@tp,
TracePoint.new(:line) { |tp| events << [tp.event, tp.lineno] if tp.path == __FILE__ }
)
def events.to_str
@tp.enable; ''
end
# Stay in generated code while enabling tracing
def events.compiled(obj)
String(obj)
@tp.disable; __LINE__
end
line = events.compiled(events)
events[0][-1] = (events[0][-1] == line)
events
}
# test enabling a c_return TracePoint in a C method call
assert_equal '[[:c_return, :String, :string_alias, "events_to_str"]]', %q{
events = []
events.instance_variable_set(:@tp, TracePoint.new(:c_return) { |tp| events << [tp.event, tp.method_id, tp.callee_id, tp.return_value] })
def events.to_str
@tp.enable; 'events_to_str'
end
# Stay in generated code while enabling tracing
alias string_alias String
def events.compiled(obj)
string_alias(obj)
@tp.disable
end
events.compiled(events)
events
} unless defined?(RubyVM::RJIT) && RubyVM::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{
events = []
events.instance_variable_set(:@tp, TracePoint.new(:line) { |tp| events << tp.lineno })
def events.to_str
@tp.enable(target: method(:compiled))
''
end
# Stay in generated code while enabling tracing
def events.compiled(obj)
String(obj)
__LINE__
end
line = events.compiled(events)
events[0] = (events[0] == line)
events
}
# test enabling tracing in the middle of splatarray
assert_equal '[true]', %q{
events = []
obj = Object.new
obj.instance_variable_set(:@tp, TracePoint.new(:line) { |tp| events << tp.lineno })
def obj.to_a
@tp.enable(target: method(:compiled))
[]
end
# Enable tracing in the middle of the splatarray instruction
def obj.compiled(obj)
* = *obj
__LINE__
end
obj.compiled([])
line = obj.compiled(obj)
events[0] = (events[0] == line)
events
}
# test enabling tracing in the middle of opt_aref. Different since the codegen
# for it ends in a jump.
assert_equal '[true]', %q{
def lookup(hash, tp)
hash[42]
tp.disable; __LINE__
end
lines = []
tp = TracePoint.new(:line) { lines << _1.lineno if _1.path == __FILE__ }
lookup(:foo, tp)
lookup({}, tp)
enable_tracing_on_missing = Hash.new { tp.enable }
expected_line = lookup(enable_tracing_on_missing, tp)
lines[0] = true if lines[0] == expected_line
lines
}
# test enabling c_call tracing before compiling
assert_equal '[[:c_call, :itself]]', %q{
def shouldnt_compile
itself
end
events = []
tp = TracePoint.new(:c_call) { |tp| events << [tp.event, tp.method_id] }
# assume first call compiles
tp.enable { shouldnt_compile }
events
} unless defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # RJIT calls extra Ruby methods
# test enabling c_return tracing before compiling
assert_equal '[[:c_return, :itself, main]]', %q{
def shouldnt_compile
itself
end
events = []
tp = TracePoint.new(:c_return) { |tp| events << [tp.event, tp.method_id, tp.return_value] }
# assume first call compiles
tp.enable { shouldnt_compile }
events
} unless defined?(RubyVM::RJIT) && RubyVM::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
assert_equal '[[:return, 42]]', %q{
def traced_method
Fiber.yield
42
end
events = []
tp = TracePoint.new(:return) { events << [_1.event, _1.return_value] }
# assume first call compiles
fiber = Fiber.new { traced_method }
fiber.resume
tp.enable(target: method(:traced_method))
fiber.resume
events
}
# test compiling on non-tracing ractor then running on a tracing one
assert_equal '[:itself]', %q{
def traced_method
itself
end
tracing_ractor = Ractor.new do
# 1: start tracing
events = []
tp = TracePoint.new(:c_call) { events << _1.method_id }
tp.enable
Ractor.yield(nil)
# 3: run compiled method on tracing ractor
Ractor.yield(nil)
traced_method
events
ensure
tp&.disable
end
tracing_ractor.take
# 2: compile on non tracing ractor
traced_method
tracing_ractor.take
tracing_ractor.take
}
# Try to hit a lazy branch stub while another ractor enables tracing
assert_equal '42', %q{
def compiled(arg)
if arg
arg + 1
else
itself
itself
end
end
ractor = Ractor.new do
compiled(false)
Ractor.yield(nil)
compiled(41)
end
tp = TracePoint.new(:line) { itself }
ractor.take
tp.enable
ractor.take
}
# Test equality with changing types
assert_equal '[true, false, false, false]', %q{
def eq(a, b)
a == b
end
[
eq("foo", "foo"),
eq("foo", "bar"),
eq(:foo, "bar"),
eq("foo", :bar)
]
}
# Redefined String eq
assert_equal 'true', %q{
class String
def ==(other)
true
end
end
def eq(a, b)
a == b
end
eq("foo", "bar")
eq("foo", "bar")
}
# Redefined Integer eq
assert_equal 'true', %q{
class Integer
def ==(other)
true
end
end
def eq(a, b)
a == b
end
eq(1, 2)
eq(1, 2)
}
# aset on array with invalid key
assert_normal_exit %q{
def foo(arr)
arr[:foo] = 123
end
foo([1]) rescue nil
foo([1]) rescue nil
}
# test ractor exception on when setting ivar
assert_equal '42', %q{
class A
def self.foo
_foo = 1
_bar = 2
begin
@bar = _foo + _bar
rescue Ractor::IsolationError
42
end
end
end
A.foo
A.foo
Ractor.new { A.foo }.take
}
assert_equal '["plain", "special", "sub", "plain"]', %q{
def foo(arg)
arg.to_s
end
class Sub < String
end
special = String.new("special")
special.singleton_class
[
foo("plain"),
foo(special),
foo(Sub.new("sub")),
foo("plain")
]
}
assert_equal '["sub", "sub"]', %q{
def foo(arg)
arg.to_s
end
class Sub < String
def to_s
super
end
end
sub = Sub.new("sub")
[foo(sub), foo(sub)]
}
assert_equal '[1]', %q{
def kwargs(value:)
value
end
5.times.map { kwargs(value: 1) }.uniq
}
assert_equal '[:ok]', %q{
def kwargs(value:)
value
end
5.times.map { kwargs() rescue :ok }.uniq
}
assert_equal '[:ok]', %q{
def kwargs(a:, b: nil)
value
end
5.times.map { kwargs(b: 123) rescue :ok }.uniq
}
assert_equal '[[1, 2]]', %q{
def kwargs(left:, right:)
[left, right]
end
5.times.flat_map do
[
kwargs(left: 1, right: 2),
kwargs(right: 2, left: 1)
]
end.uniq
}
assert_equal '[[1, 2]]', %q{
def kwargs(lead, kwarg:)
[lead, kwarg]
end
5.times.map { kwargs(1, kwarg: 2) }.uniq
}
# optional and keyword args
assert_equal '[[1, 2, 3]]', %q{
def opt_and_kwargs(a, b=2, c: nil)
[a,b,c]
end
5.times.map { opt_and_kwargs(1, c: 3) }.uniq
}
assert_equal '[[1, 2, 3]]', %q{
def opt_and_kwargs(a, b=nil, c: nil)
[a,b,c]
end
5.times.map { opt_and_kwargs(1, 2, c: 3) }.uniq
}
# Bug #18453
assert_equal '[[1, nil, 2]]', %q{
def opt_and_kwargs(a = {}, b: nil, c: nil)
[a, b, c]
end
5.times.map { opt_and_kwargs(1, c: 2) }.uniq
}
assert_equal '[[{}, nil, 1]]', %q{
def opt_and_kwargs(a = {}, b: nil, c: nil)
[a, b, c]
end
5.times.map { opt_and_kwargs(c: 1) }.uniq
}
# leading and keyword arguments are swapped into the right order
assert_equal '[[1, 2, 3, 4, 5, 6]]', %q{
def kwargs(five, six, a:, b:, c:, d:)
[a, b, c, d, five, six]
end
5.times.flat_map do
[
kwargs(5, 6, a: 1, b: 2, c: 3, d: 4),
kwargs(5, 6, a: 1, b: 2, d: 4, c: 3),
kwargs(5, 6, a: 1, c: 3, b: 2, d: 4),
kwargs(5, 6, a: 1, c: 3, d: 4, b: 2),
kwargs(5, 6, a: 1, d: 4, b: 2, c: 3),
kwargs(5, 6, a: 1, d: 4, c: 3, b: 2),
kwargs(5, 6, b: 2, a: 1, c: 3, d: 4),
kwargs(5, 6, b: 2, a: 1, d: 4, c: 3),
kwargs(5, 6, b: 2, c: 3, a: 1, d: 4),
kwargs(5, 6, b: 2, c: 3, d: 4, a: 1),
kwargs(5, 6, b: 2, d: 4, a: 1, c: 3),
kwargs(5, 6, b: 2, d: 4, c: 3, a: 1),
kwargs(5, 6, c: 3, a: 1, b: 2, d: 4),
kwargs(5, 6, c: 3, a: 1, d: 4, b: 2),
kwargs(5, 6, c: 3, b: 2, a: 1, d: 4),
kwargs(5, 6, c: 3, b: 2, d: 4, a: 1),
kwargs(5, 6, c: 3, d: 4, a: 1, b: 2),
kwargs(5, 6, c: 3, d: 4, b: 2, a: 1),
kwargs(5, 6, d: 4, a: 1, b: 2, c: 3),
kwargs(5, 6, d: 4, a: 1, c: 3, b: 2),
kwargs(5, 6, d: 4, b: 2, a: 1, c: 3),
kwargs(5, 6, d: 4, b: 2, c: 3, a: 1),
kwargs(5, 6, d: 4, c: 3, a: 1, b: 2),
kwargs(5, 6, d: 4, c: 3, b: 2, a: 1)
]
end.uniq
}
# implicit hashes get skipped and don't break compilation
assert_equal '[[:key]]', %q{
def implicit(hash)
hash.keys
end
5.times.map { implicit(key: :value) }.uniq
}
# default values on keywords don't mess up argument order
assert_equal '[2]', %q{
def default_value
1
end
def default_expression(value: default_value)
value
end
5.times.map { default_expression(value: 2) }.uniq
}
# constant default values on keywords
assert_equal '[3]', %q{
def default_expression(value: 3)
value
end
5.times.map { default_expression }.uniq
}
# non-constant default values on keywords
assert_equal '[3]', %q{
def default_value
3
end
def default_expression(value: default_value)
value
end
5.times.map { default_expression }.uniq
}
# reordered optional kwargs
assert_equal '[[100, 1]]', %q{
def foo(capacity: 100, max: nil)
[capacity, max]
end
5.times.map { foo(max: 1) }.uniq
}
# invalid lead param
assert_equal 'ok', %q{
def bar(baz: 2)
baz
end
def foo
bar(1, baz: 123)
end
begin
foo
foo
rescue ArgumentError => e
print "ok"
end
}
# reordered required kwargs
assert_equal '[[1, 2, 3, 4]]', %q{
def foo(default1: 1, required1:, default2: 3, required2:)
[default1, required1, default2, required2]
end
5.times.map { foo(required1: 2, required2: 4) }.uniq
}
# reordered default expression kwargs
assert_equal '[[:one, :two, 3]]', %q{
def foo(arg1: (1+0), arg2: (2+0), arg3: (3+0))
[arg1, arg2, arg3]
end
5.times.map { foo(arg2: :two, arg1: :one) }.uniq
}
# complex kwargs
assert_equal '[[1, 2, 3, 4]]', %q{
def foo(required:, specified: 999, simple_default: 3, complex_default: "4".to_i)
[required, specified, simple_default, complex_default]
end
5.times.map { foo(specified: 2, required: 1) }.uniq
}
# cfunc kwargs
assert_equal '{:foo=>123}', %q{
def foo(bar)
bar.store(:value, foo: 123)
bar[:value]
end
foo({})
foo({})
}
# cfunc kwargs
assert_equal '{:foo=>123}', %q{
def foo(bar)
bar.replace(foo: 123)
end
foo({})
foo({})
}
# cfunc kwargs
assert_equal '{:foo=>123, :bar=>456}', %q{
def foo(bar)
bar.replace(foo: 123, bar: 456)
end
foo({})
foo({})
}
# variadic cfunc kwargs
assert_equal '{:foo=>123}', %q{
def foo(bar)
bar.merge(foo: 123)
end
foo({})
foo({})
}
# optimized cfunc kwargs
assert_equal 'false', %q{
def foo
:foo.eql?(foo: :foo)
end
foo
foo
}
# attr_reader on frozen object
assert_equal 'false', %q{
class Foo
attr_reader :exception
def failed?
!exception.nil?
end
end
foo = Foo.new.freeze
foo.failed?
foo.failed?
}
# regression test for doing kwarg shuffle before checking for interrupts
assert_equal 'ok', %q{
def new_media_drop(attributes:, product_drop:, context:, sources:)
nil.nomethod rescue nil # force YJIT to bail to side exit
[attributes, product_drop, context, sources]
end
def load_medias(product_drop: nil, raw_medias:, context:)
raw_medias.map do |raw_media|
case new_media_drop(context: context, attributes: raw_media, product_drop: product_drop, sources: [])
in [Hash, ProductDrop, Context, Array]
else
raise "bad shuffle"
end
end
end
class Context; end
class ProductDrop
attr_reader :title
def initialize(title)
@title = title
end
end
# Make a thread so we have thread switching interrupts
th = Thread.new do
while true; end
end
1_000.times do |i|
load_medias(product_drop: ProductDrop.new("foo"), raw_medias: [{}, {}], context: Context.new)
end
th.kill.join
:ok
}
# regression test for tracing attr_accessor methods.
assert_equal "true", %q{
c = Class.new do
attr_accessor :x
alias y x
alias y= x=
end
obj = c.new
ar_meth = obj.method(:x)
aw_meth = obj.method(:x=)
aar_meth = obj.method(:y)
aaw_meth = obj.method(:y=)
events = []
trace = TracePoint.new(:c_call, :c_return){|tp|
next if tp.path != __FILE__
next if tp.method_id == :call
case tp.event
when :c_call
events << [tp.event, tp.method_id, tp.callee_id]
when :c_return
events << [tp.event, tp.method_id, tp.callee_id, tp.return_value]
end
}
test_proc = proc do
obj.x = 1
obj.x
obj.y = 2
obj.y
aw_meth.call(1)
ar_meth.call
aaw_meth.call(2)
aar_meth.call
end
test_proc.call # populate call caches
trace.enable(&test_proc)
expected = [
[:c_call, :x=, :x=],
[:c_return, :x=, :x=, 1],
[:c_call, :x, :x],
[:c_return, :x, :x, 1],
[:c_call, :x=, :y=],
[:c_return, :x=, :y=, 2],
[:c_call, :x, :y],
[:c_return, :x, :y, 2],
] * 2
expected == events
}
# duphash
assert_equal '{:foo=>123}', %q{
def foo
{foo: 123}
end
foo
foo
}
# newhash
assert_equal '{:foo=>2}', %q{
def foo
{foo: 1+1}
end
foo
foo
}
# block invalidation edge case
assert_equal 'undef', %q{
class A
def foo(arg)
arg.times { A.remove_method(:bar) }
self
end
def bar
4
end
def use(arg)
# two consecutive sends. When bar is removed, the return address
# for calling it is already on foo's control frame
foo(arg).bar
rescue NoMethodError
:undef
end
end
A.new.use 0
A.new.use 0
A.new.use 1
}
# block invalidation edge case
assert_equal 'ok', %q{
class A
Good = :ng
def foo(arg)
arg.times { A.const_set(:Good, :ok) }
self
end
def id(arg)
arg
end
def use(arg)
# send followed by an opt_getinlinecache.
# The return address remains on the control frame
# when opt_getinlinecache is invalidated.
foo(arg).id(Good)
end
end
A.new.use 0
A.new.use 0
A.new.use 1
}
assert_equal 'ok', %q{
# test hitting a branch stub when out of memory
def nimai(jita)
if jita
:ng
else
:ok
end
end
nimai(true)
nimai(true)
RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT)
nimai(false)
}
assert_equal 'new', %q{
# test block invalidation while out of memory
def foo
:old
end
def test
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
test
}
assert_equal 'ok', %q{
# Try to compile new method while OOM
def foo
:ok
end
RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT)
foo
foo
}
# struct aref embedded
assert_equal '2', %q{
def foo(s)
s.foo
end
S = Struct.new(:foo)
foo(S.new(1))
foo(S.new(2))
}
# struct aref non-embedded
assert_equal '4', %q{
def foo(s)
s.d
end
S = Struct.new(:a, :b, :c, :d, :e)
foo(S.new(1,2,3,4,5))
foo(S.new(1,2,3,4,5))
}
# struct aset embedded
assert_equal '123', %q{
def foo(s)
s.foo = 123
end
s = Struct.new(:foo).new
foo(s)
s = Struct.new(:foo).new
foo(s)
s.foo
}
# struct aset non-embedded
assert_equal '[1, 2, 3, 4, 5]', %q{
def foo(s)
s.a = 1
s.b = 2
s.c = 3
s.d = 4
s.e = 5
end
S = Struct.new(:a, :b, :c, :d, :e)
s = S.new
foo(s)
s = S.new
foo(s)
[s.a, s.b, s.c, s.d, s.e]
}
# struct aref too many args
assert_equal 'ok', %q{
def foo(s)
s.foo(:bad)
end
s = Struct.new(:foo).new
foo(s) rescue :ok
foo(s) rescue :ok
}
# struct aset too many args
assert_equal 'ok', %q{
def foo(s)
s.set_foo(123, :bad)
end
s = Struct.new(:foo) do
alias :set_foo :foo=
end
foo(s) rescue :ok
foo(s) rescue :ok
}
# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2)
assert_equal 'foo/bar', %q{
def foo
File.join("foo", "bar")
end
foo
foo
}
# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2)
assert_equal '', %q{
def foo
File.join()
end
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
}
# Regresssion 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 defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # Not yet working on RJIT
# Regresssion 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]
}