1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/bootstraptest/test_yjit.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

3312 lines
52 KiB
Ruby
Raw Normal View History

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_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 '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)
}
2021-07-06 16:26:56 -04:00
# 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
}
# 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
}
2021-02-12 14:35:57 -05:00
# foo leaves a temp on the stack before the call
assert_equal '6', %q{
def bar
return 5
end
2021-02-12 14:35:57 -05:00
def foo
return 1 + bar
end
2021-02-12 14:35:57 -05:00
foo()
retval = foo()
}
2021-02-12 14:35:57 -05:00
# Method with one arguments
# foo leaves a temp on the stack before the call
2021-02-12 14:35:57 -05:00
assert_equal '7', %q{
def bar(a)
return a + 1
end
def foo
2021-02-12 14:35:57 -05:00
return 1 + bar(5)
end
foo()
retval = foo()
}
2021-02-12 14:35:57 -05:00
# Method with two arguments
2021-02-09 17:34:02 -05:00
# 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
}
2021-02-09 17:34:02 -05:00
# 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
}
2021-02-12 14:35:57 -05:00
# 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)
}
2021-03-11 14:15:01 -05:00
# Method redefinition (code invalidation) and GC
2021-02-12 14:35:57 -05:00
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]
}
2021-08-31 14:26:18 -04:00
# 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)
]
}
2021-03-22 20:12:34 -04:00
# 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
}
# 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 << 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
}
2021-07-09 02:33:55 -04:00
# 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
}
2021-08-30 23:57:31 -04:00
# 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
}
# 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)
}
2021-06-24 11:15:53 -04:00
# 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)
]
}
2021-07-14 15:16:56 -04:00
assert_equal '[1, 2, 3, 4, 5]', %q{
def splatarray
[*(1..5)]
end
splatarray
splatarray
}
2021-07-14 13:46:18 -04:00
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 '[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
}
# 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
}
# 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
}
# 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)
2021-08-25 17:08:00 -04:00
# 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
}
2021-08-27 21:35:34 -04:00
# 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)
]
}
2021-09-12 02:29:24 -04:00
# Redefined String eq
2021-08-27 21:35:34 -04:00
assert_equal 'true', %q{
class String
def ==(other)
true
end
end
2021-09-12 02:29:24 -04:00
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)
2021-08-27 21:35:34 -04:00
}
2021-09-17 11:38:14 -04:00
# 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)]
}
2021-09-28 15:51:33 -04:00
2021-10-08 14:40:38 -04:00
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
}
2021-10-08 14:40:38 -04:00
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
}
2021-10-07 11:01:37 -04:00
# 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]
2021-09-28 15:51:33 -04:00
end
5.times.flat_map do
[
2021-10-07 11:01:37 -04:00
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)
2021-09-28 15:51:33 -04:00
]
end.uniq
}
2021-10-07 11:01:37 -04:00
# 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
}
YJIT: Add ability to exit to interpreter from stubs Previously, YJIT assumed that it's always possible to generate a new basic block when servicing a stub in branch_stub_hit(). When YJIT is out of executable memory, for example, this assumption doesn't hold up. Add handling to branch_stub_hit() for servicing stubs without consuming more executable memory by adding a code path that exits to the interpreter at the location the branch stub represents. The new code path reconstructs interpreter state in branch_stub_hit() and then exits with a new snippet called `code_for_exit_from_stub` that returns `Qundef` from the YJIT native stack frame. As this change adds another place where we regenerate code from `branch_t`, extract the logic for it into a new function and call it regenerate_branch(). While we are at it, make the branch shrinking code path in branch_stub_hit() more explicit. This new functionality is hard to test without full support for out of memory conditions. To verify this change, I ran `RUBY_YJIT_ENABLE=1 make check -j12` with the following patch to stress test the new code path: ```diff diff --git a/yjit_core.c b/yjit_core.c index 4ab63d9806..5788b8c5ed 100644 --- a/yjit_core.c +++ b/yjit_core.c @@ -878,8 +878,12 @@ branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_contex cb_set_write_ptr(cb, branch->end_addr); } +if (rand() < RAND_MAX/2) { // Compile the new block version p_block = gen_block_version(target, target_ctx, ec); +}else{ + p_block = NULL; +} if (!p_block && branch_modified) { // We couldn't generate a new block for the branch, but we modified the branch. ``` We can enable the new test along with other OOM tests once full support lands. Other small changes: * yjit_utils.c (print_str): Update to work with new native frame shape. Follow up for 8fa0ee4d404. * yjit_iface.c (rb_yjit_init): Run yjit_init_core() after yjit_init_codegen() so `cb` and `ocb` are available.
2021-11-26 18:00:42 -05:00
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)
}
YJIT: Add ability to exit to interpreter from stubs Previously, YJIT assumed that it's always possible to generate a new basic block when servicing a stub in branch_stub_hit(). When YJIT is out of executable memory, for example, this assumption doesn't hold up. Add handling to branch_stub_hit() for servicing stubs without consuming more executable memory by adding a code path that exits to the interpreter at the location the branch stub represents. The new code path reconstructs interpreter state in branch_stub_hit() and then exits with a new snippet called `code_for_exit_from_stub` that returns `Qundef` from the YJIT native stack frame. As this change adds another place where we regenerate code from `branch_t`, extract the logic for it into a new function and call it regenerate_branch(). While we are at it, make the branch shrinking code path in branch_stub_hit() more explicit. This new functionality is hard to test without full support for out of memory conditions. To verify this change, I ran `RUBY_YJIT_ENABLE=1 make check -j12` with the following patch to stress test the new code path: ```diff diff --git a/yjit_core.c b/yjit_core.c index 4ab63d9806..5788b8c5ed 100644 --- a/yjit_core.c +++ b/yjit_core.c @@ -878,8 +878,12 @@ branch_stub_hit(branch_t *branch, const uint32_t target_idx, rb_execution_contex cb_set_write_ptr(cb, branch->end_addr); } +if (rand() < RAND_MAX/2) { // Compile the new block version p_block = gen_block_version(target, target_ctx, ec); +}else{ + p_block = NULL; +} if (!p_block && branch_modified) { // We couldn't generate a new block for the branch, but we modified the branch. ``` We can enable the new test along with other OOM tests once full support lands. Other small changes: * yjit_utils.c (print_str): Update to work with new native frame shape. Follow up for 8fa0ee4d404. * yjit_iface.c (rb_yjit_init): Run yjit_init_core() after yjit_init_codegen() so `cb` and `ocb` are available.
2021-11-26 18:00:42 -05:00
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
}
Rust YJIT In December 2021, we opened an [issue] to solicit feedback regarding the porting of the YJIT codebase from C99 to Rust. There were some reservations, but this project was given the go ahead by Ruby core developers and Matz. Since then, we have successfully completed the port of YJIT to Rust. The new Rust version of YJIT has reached parity with the C version, in that it passes all the CRuby tests, is able to run all of the YJIT benchmarks, and performs similarly to the C version (because it works the same way and largely generates the same machine code). We've even incorporated some design improvements, such as a more fine-grained constant invalidation mechanism which we expect will make a big difference in Ruby on Rails applications. Because we want to be careful, YJIT is guarded behind a configure option: ```shell ./configure --enable-yjit # Build YJIT in release mode ./configure --enable-yjit=dev # Build YJIT in dev/debug mode ``` By default, YJIT does not get compiled and cargo/rustc is not required. If YJIT is built in dev mode, then `cargo` is used to fetch development dependencies, but when building in release, `cargo` is not required, only `rustc`. At the moment YJIT requires Rust 1.60.0 or newer. The YJIT command-line options remain mostly unchanged, and more details about the build process are documented in `doc/yjit/yjit.md`. The CI tests have been updated and do not take any more resources than before. The development history of the Rust port is available at the following commit for interested parties: https://github.com/Shopify/ruby/commit/1fd9573d8b4b65219f1c2407f30a0a60e537f8be Our hope is that Rust YJIT will be compiled and included as a part of system packages and compiled binaries of the Ruby 3.2 release. We do not anticipate any major problems as Rust is well supported on every platform which YJIT supports, but to make sure that this process works smoothly, we would like to reach out to those who take care of building systems packages before the 3.2 release is shipped and resolve any issues that may come up. [issue]: https://bugs.ruby-lang.org/issues/18481 Co-authored-by: Maxime Chevalier-Boisvert <maximechevalierb@gmail.com> Co-authored-by: Noah Gibbs <the.codefolio.guy@gmail.com> Co-authored-by: Kevin Newton <kddnewton@gmail.com>
2022-04-19 14:40:21 -04:00
# 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)
}