mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
e53d07f583
Since the YJIT Ruby module is CRuby specific and not meant for general use, it should live under RubyVM instead of at top level.
634 lines
16 KiB
Ruby
634 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
require 'test/unit'
|
|
require 'envutil'
|
|
require 'tmpdir'
|
|
|
|
return unless defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
|
|
|
# Tests for YJIT with assertions on compilation and side exits
|
|
# insipired by the MJIT tests in test/ruby/test_jit.rb
|
|
class TestYJIT < Test::Unit::TestCase
|
|
def test_yjit_in_ruby_description
|
|
assert_includes(RUBY_DESCRIPTION, '+YJIT')
|
|
end
|
|
|
|
def test_yjit_in_version
|
|
[
|
|
%w(--version --yjit),
|
|
%w(--version --disable-yjit --yjit),
|
|
%w(--version --disable-yjit --enable-yjit),
|
|
%w(--version --disable-yjit --enable=yjit),
|
|
%w(--version --disable=yjit --yjit),
|
|
%w(--version --disable=yjit --enable-yjit),
|
|
%w(--version --disable=yjit --enable=yjit),
|
|
].each do |version_args|
|
|
assert_in_out_err(version_args) do |stdout, stderr|
|
|
assert_equal(RUBY_DESCRIPTION, stdout.first)
|
|
assert_equal([], stderr)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_command_line_switches
|
|
assert_in_out_err('--yjit-', '', [], /invalid option --yjit-/)
|
|
assert_in_out_err('--yjithello', '', [], /invalid option --yjithello/)
|
|
assert_in_out_err('--yjit-call-threshold', '', [], /--yjit-call-threshold needs an argument/)
|
|
assert_in_out_err('--yjit-call-threshold=', '', [], /--yjit-call-threshold needs an argument/)
|
|
assert_in_out_err('--yjit-greedy-versioning=1', '', [], /warning: argument to --yjit-greedy-versioning is ignored/)
|
|
end
|
|
|
|
def test_yjit_stats_and_v_no_error
|
|
_stdout, stderr, _status = EnvUtil.invoke_ruby(%w(-v --yjit-stats), '', true, true)
|
|
refute_includes(stderr, "NoMethodError")
|
|
end
|
|
|
|
def test_enable_from_env_var
|
|
yjit_child_env = {'RUBY_YJIT_ENABLE' => '1'}
|
|
assert_in_out_err([yjit_child_env, '--version'], '') do |stdout, stderr|
|
|
assert_equal(RUBY_DESCRIPTION, stdout.first)
|
|
assert_equal([], stderr)
|
|
end
|
|
assert_in_out_err([yjit_child_env, '-e puts RUBY_DESCRIPTION'], '', [RUBY_DESCRIPTION])
|
|
assert_in_out_err([yjit_child_env, '-e p RubyVM::YJIT.enabled?'], '', ['true'])
|
|
end
|
|
|
|
def test_compile_getclassvariable
|
|
script = 'class Foo; @@foo = 1; def self.foo; @@foo; end; end; Foo.foo'
|
|
assert_compiles(script, insns: %i[getclassvariable], result: 1)
|
|
end
|
|
|
|
def test_compile_putnil
|
|
assert_compiles('nil', insns: %i[putnil], result: nil)
|
|
end
|
|
|
|
def test_compile_putobject
|
|
assert_compiles('true', insns: %i[putobject], result: true)
|
|
assert_compiles('123', insns: %i[putobject], result: 123)
|
|
assert_compiles(':foo', insns: %i[putobject], result: :foo)
|
|
end
|
|
|
|
def test_compile_opt_not
|
|
assert_compiles('!false', insns: %i[opt_not], result: true)
|
|
assert_compiles('!nil', insns: %i[opt_not], result: true)
|
|
assert_compiles('!true', insns: %i[opt_not], result: false)
|
|
assert_compiles('![]', insns: %i[opt_not], result: false)
|
|
end
|
|
|
|
def test_compile_opt_newarray
|
|
assert_compiles('[]', insns: %i[newarray], result: [])
|
|
assert_compiles('[1+1]', insns: %i[newarray opt_plus], result: [2])
|
|
assert_compiles('[1,1+1,3,4,5,6]', insns: %i[newarray opt_plus], result: [1, 2, 3, 4, 5, 6])
|
|
end
|
|
|
|
def test_compile_opt_duparray
|
|
assert_compiles('[1]', insns: %i[duparray], result: [1])
|
|
assert_compiles('[1, 2, 3]', insns: %i[duparray], result: [1, 2, 3])
|
|
end
|
|
|
|
def test_compile_newrange
|
|
assert_compiles('s = 1; (s..5)', insns: %i[newrange], result: 1..5)
|
|
assert_compiles('s = 1; e = 5; (s..e)', insns: %i[newrange], result: 1..5)
|
|
assert_compiles('s = 1; (s...5)', insns: %i[newrange], result: 1...5)
|
|
assert_compiles('s = 1; (s..)', insns: %i[newrange], result: 1..)
|
|
assert_compiles('e = 5; (..e)', insns: %i[newrange], result: ..5)
|
|
end
|
|
|
|
def test_compile_duphash
|
|
assert_compiles('{ two: 2 }', insns: %i[duphash], result: { two: 2 })
|
|
end
|
|
|
|
def test_compile_newhash
|
|
assert_compiles('{}', insns: %i[newhash], result: {})
|
|
assert_compiles('{ two: 1 + 1 }', insns: %i[newhash], result: { two: 2 })
|
|
assert_compiles('{ 1 + 1 => :two }', insns: %i[newhash], result: { 2 => :two })
|
|
end
|
|
|
|
def test_compile_opt_nil_p
|
|
assert_compiles('nil.nil?', insns: %i[opt_nil_p], result: true)
|
|
assert_compiles('false.nil?', insns: %i[opt_nil_p], result: false)
|
|
assert_compiles('true.nil?', insns: %i[opt_nil_p], result: false)
|
|
assert_compiles('(-"").nil?', insns: %i[opt_nil_p], result: false)
|
|
assert_compiles('123.nil?', insns: %i[opt_nil_p], result: false)
|
|
end
|
|
|
|
def test_compile_eq_fixnum
|
|
assert_compiles('123 == 123', insns: %i[opt_eq], result: true)
|
|
assert_compiles('123 == 456', insns: %i[opt_eq], result: false)
|
|
end
|
|
|
|
def test_compile_eq_string
|
|
assert_compiles('-"" == -""', insns: %i[opt_eq], result: true)
|
|
assert_compiles('-"foo" == -"foo"', insns: %i[opt_eq], result: true)
|
|
assert_compiles('-"foo" == -"bar"', insns: %i[opt_eq], result: false)
|
|
end
|
|
|
|
def test_compile_eq_symbol
|
|
assert_compiles(':foo == :foo', insns: %i[opt_eq], result: true)
|
|
assert_compiles(':foo == :bar', insns: %i[opt_eq], result: false)
|
|
assert_compiles(':foo == "foo".to_sym', insns: %i[opt_eq], result: true)
|
|
end
|
|
|
|
def test_compile_eq_object
|
|
assert_compiles(<<~RUBY, insns: %i[opt_eq], result: false)
|
|
def eq(a, b)
|
|
a == b
|
|
end
|
|
|
|
eq(Object.new, Object.new)
|
|
RUBY
|
|
|
|
assert_compiles(<<~RUBY, insns: %i[opt_eq], result: true)
|
|
def eq(a, b)
|
|
a == b
|
|
end
|
|
|
|
obj = Object.new
|
|
eq(obj, obj)
|
|
RUBY
|
|
end
|
|
|
|
def test_compile_eq_arbitrary_class
|
|
assert_compiles(<<~RUBY, insns: %i[opt_eq], result: "yes")
|
|
def eq(a, b)
|
|
a == b
|
|
end
|
|
|
|
class Foo
|
|
def ==(other)
|
|
"yes"
|
|
end
|
|
end
|
|
|
|
eq(Foo.new, Foo.new)
|
|
eq(Foo.new, Foo.new)
|
|
RUBY
|
|
end
|
|
|
|
def test_compile_opt_lt
|
|
assert_compiles('1 < 2', insns: %i[opt_lt])
|
|
assert_compiles('"a" < "b"', insns: %i[opt_lt])
|
|
end
|
|
|
|
def test_compile_opt_le
|
|
assert_compiles('1 <= 2', insns: %i[opt_le])
|
|
assert_compiles('"a" <= "b"', insns: %i[opt_le])
|
|
end
|
|
|
|
def test_compile_opt_gt
|
|
assert_compiles('1 > 2', insns: %i[opt_gt])
|
|
assert_compiles('"a" > "b"', insns: %i[opt_gt])
|
|
end
|
|
|
|
def test_compile_opt_ge
|
|
assert_compiles('1 >= 2', insns: %i[opt_ge])
|
|
assert_compiles('"a" >= "b"', insns: %i[opt_ge])
|
|
end
|
|
|
|
def test_compile_opt_plus
|
|
assert_compiles('1 + 2', insns: %i[opt_plus])
|
|
assert_compiles('"a" + "b"', insns: %i[opt_plus])
|
|
assert_compiles('[:foo] + [:bar]', insns: %i[opt_plus])
|
|
end
|
|
|
|
def test_compile_opt_minus
|
|
assert_compiles('1 - 2', insns: %i[opt_minus])
|
|
assert_compiles('[:foo, :bar] - [:bar]', insns: %i[opt_minus])
|
|
end
|
|
|
|
def test_compile_opt_or
|
|
assert_compiles('1 | 2', insns: %i[opt_or])
|
|
assert_compiles('[:foo] | [:bar]', insns: %i[opt_or])
|
|
end
|
|
|
|
def test_compile_opt_and
|
|
assert_compiles('1 & 2', insns: %i[opt_and])
|
|
assert_compiles('[:foo, :bar] & [:bar]', insns: %i[opt_and])
|
|
end
|
|
|
|
def test_compile_set_and_get_global
|
|
assert_compiles('$foo = 123; $foo', insns: %i[setglobal], result: 123)
|
|
end
|
|
|
|
def test_compile_putspecialobject
|
|
assert_compiles('-> {}', insns: %i[putspecialobject])
|
|
end
|
|
|
|
def test_compile_tostring
|
|
assert_no_exits('"i am a string #{true}"')
|
|
end
|
|
|
|
def test_compile_opt_aset
|
|
assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset])
|
|
assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset])
|
|
assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset])
|
|
assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset])
|
|
end
|
|
|
|
def test_compile_attr_set
|
|
assert_no_exits(<<~EORB)
|
|
class Foo
|
|
attr_accessor :bar
|
|
end
|
|
|
|
foo = Foo.new
|
|
foo.bar = 3
|
|
foo.bar = 3
|
|
foo.bar = 3
|
|
foo.bar = 3
|
|
EORB
|
|
end
|
|
|
|
def test_compile_regexp
|
|
assert_no_exits('/#{true}/')
|
|
end
|
|
|
|
def test_getlocal_with_level
|
|
assert_compiles(<<~RUBY, insns: %i[getlocal opt_plus], result: [[7]])
|
|
def foo(foo, bar)
|
|
[1].map do |x|
|
|
[1].map do |y|
|
|
foo + bar
|
|
end
|
|
end
|
|
end
|
|
|
|
foo(5, 2)
|
|
RUBY
|
|
end
|
|
|
|
def test_setlocal_with_level
|
|
assert_no_exits(<<~RUBY)
|
|
def sum(arr)
|
|
sum = 0
|
|
arr.each do |x|
|
|
sum += x
|
|
end
|
|
sum
|
|
end
|
|
|
|
sum([1,2,3])
|
|
RUBY
|
|
end
|
|
|
|
def test_string_then_nil
|
|
assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: true)
|
|
def foo(val)
|
|
val.nil?
|
|
end
|
|
|
|
foo("foo")
|
|
foo(nil)
|
|
RUBY
|
|
end
|
|
|
|
def test_nil_then_string
|
|
assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: false)
|
|
def foo(val)
|
|
val.nil?
|
|
end
|
|
|
|
foo(nil)
|
|
foo("foo")
|
|
RUBY
|
|
end
|
|
|
|
def test_opt_length_in_method
|
|
assert_compiles(<<~RUBY, insns: %i[opt_length], result: 5)
|
|
def foo(str)
|
|
str.length
|
|
end
|
|
|
|
foo("hello, ")
|
|
foo("world")
|
|
RUBY
|
|
end
|
|
|
|
def test_opt_regexpmatch2
|
|
assert_compiles(<<~RUBY, insns: %i[opt_regexpmatch2], result: 0)
|
|
def foo(str)
|
|
str =~ /foo/
|
|
end
|
|
|
|
foo("foobar")
|
|
RUBY
|
|
end
|
|
|
|
def test_expandarray
|
|
assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [1, 2])
|
|
a, b = [1, 2]
|
|
RUBY
|
|
end
|
|
|
|
def test_expandarray_nil
|
|
assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [nil, nil])
|
|
a, b = nil
|
|
[a, b]
|
|
RUBY
|
|
end
|
|
|
|
def test_getspecial_backref
|
|
assert_compiles("'foo' =~ /(o)./; $&", insns: %i[getspecial], result: "oo")
|
|
assert_compiles("'foo' =~ /(o)./; $`", insns: %i[getspecial], result: "f")
|
|
assert_compiles("'foo' =~ /(o)./; $'", insns: %i[getspecial], result: "")
|
|
assert_compiles("'foo' =~ /(o)./; $+", insns: %i[getspecial], result: "o")
|
|
assert_compiles("'foo' =~ /(o)./; $1", insns: %i[getspecial], result: "o")
|
|
assert_compiles("'foo' =~ /(o)./; $2", insns: %i[getspecial], result: nil)
|
|
end
|
|
|
|
def test_compile_opt_getinlinecache
|
|
assert_compiles(<<~RUBY, insns: %i[opt_getinlinecache], result: 123, min_calls: 2)
|
|
def get_foo
|
|
FOO
|
|
end
|
|
|
|
FOO = 123
|
|
|
|
get_foo # warm inline cache
|
|
get_foo
|
|
RUBY
|
|
end
|
|
|
|
def test_opt_getinlinecache_slowpath
|
|
assert_compiles(<<~RUBY, exits: { opt_getinlinecache: 1 }, result: [42, 42, 1, 1], min_calls: 2)
|
|
class A
|
|
FOO = 42
|
|
class << self
|
|
def foo
|
|
_foo = nil
|
|
FOO
|
|
end
|
|
end
|
|
end
|
|
|
|
result = []
|
|
|
|
result << A.foo
|
|
result << A.foo
|
|
|
|
class << A
|
|
FOO = 1
|
|
end
|
|
|
|
result << A.foo
|
|
result << A.foo
|
|
|
|
result
|
|
RUBY
|
|
end
|
|
|
|
def test_string_interpolation
|
|
assert_compiles(<<~'RUBY', insns: %i[checktype concatstrings], result: "foobar", min_calls: 2)
|
|
def make_str(foo, bar)
|
|
"#{foo}#{bar}"
|
|
end
|
|
|
|
make_str("foo", "bar")
|
|
make_str("foo", "bar")
|
|
RUBY
|
|
end
|
|
|
|
def test_string_interpolation_cast
|
|
assert_compiles(<<~'RUBY', insns: %i[checktype concatstrings tostring], result: "123")
|
|
def make_str(foo, bar)
|
|
"#{foo}#{bar}"
|
|
end
|
|
|
|
make_str(1, 23)
|
|
RUBY
|
|
end
|
|
|
|
def test_invokebuiltin
|
|
assert_compiles(<<~RUBY)
|
|
def foo(obj)
|
|
obj.foo = 123
|
|
obj.bar = 123
|
|
end
|
|
|
|
Foo = Struct.new(:foo, :bar)
|
|
foo(Foo.new(123))
|
|
foo(Foo.new(123))
|
|
RUBY
|
|
end
|
|
|
|
def test_super_iseq
|
|
assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15)
|
|
class A
|
|
def foo
|
|
1 + 2
|
|
end
|
|
end
|
|
|
|
class B < A
|
|
def foo
|
|
super * 5
|
|
end
|
|
end
|
|
|
|
B.new.foo
|
|
RUBY
|
|
end
|
|
|
|
def test_super_cfunc
|
|
assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Hello")
|
|
class Gnirts < String
|
|
def initialize
|
|
super(-"olleH")
|
|
end
|
|
|
|
def to_s
|
|
super().reverse
|
|
end
|
|
end
|
|
|
|
Gnirts.new.to_s
|
|
RUBY
|
|
end
|
|
|
|
# Tests calling a variadic cfunc with many args
|
|
def test_build_large_struct
|
|
assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], min_calls: 2)
|
|
::Foo = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h)
|
|
|
|
def build_foo
|
|
::Foo.new(:a, :b, :c, :d, :e, :f, :g, :h)
|
|
end
|
|
|
|
build_foo
|
|
build_foo
|
|
RUBY
|
|
end
|
|
|
|
def test_fib_recursion
|
|
assert_compiles(<<~'RUBY', insns: %i[opt_le opt_minus opt_plus opt_send_without_block], result: 34)
|
|
def fib(n)
|
|
return n if n <= 1
|
|
fib(n-1) + fib(n-2)
|
|
end
|
|
|
|
fib(9)
|
|
RUBY
|
|
end
|
|
|
|
def test_ctx_different_mappings
|
|
# regression test simplified from URI::Generic#hostname=
|
|
assert_compiles(<<~'RUBY', frozen_string_literal: true)
|
|
def foo(v)
|
|
!(v&.start_with?('[')) && v&.index(':')
|
|
end
|
|
|
|
foo(nil)
|
|
foo("example.com")
|
|
RUBY
|
|
end
|
|
|
|
def test_no_excessive_opt_getinlinecache_invalidation
|
|
assert_compiles(<<~'RUBY', exits: :any, result: :ok)
|
|
objects = [Object.new, Object.new]
|
|
|
|
objects.each do |o|
|
|
class << o
|
|
def foo
|
|
Object
|
|
end
|
|
end
|
|
end
|
|
|
|
9000.times {
|
|
objects[0].foo
|
|
objects[1].foo
|
|
}
|
|
|
|
stats = RubyVM::YJIT.runtime_stats
|
|
return :ok unless stats[:all_stats]
|
|
return :ok if stats[:invalidation_count] < 10
|
|
|
|
:fail
|
|
RUBY
|
|
end
|
|
|
|
def assert_no_exits(script)
|
|
assert_compiles(script)
|
|
end
|
|
|
|
ANY = Object.new
|
|
def assert_compiles(test_script, insns: [], min_calls: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil)
|
|
reset_stats = <<~RUBY
|
|
RubyVM::YJIT.runtime_stats
|
|
RubyVM::YJIT.reset_stats!
|
|
RUBY
|
|
|
|
write_results = <<~RUBY
|
|
stats = RubyVM::YJIT.runtime_stats
|
|
|
|
def collect_blocks(blocks)
|
|
blocks.sort_by(&:address).map { |b| [b.iseq_start_index, b.iseq_end_index] }
|
|
end
|
|
|
|
def collect_iseqs(iseq)
|
|
iseq_array = iseq.to_a
|
|
insns = iseq_array.last.grep(Array)
|
|
blocks = RubyVM::YJIT.blocks_for(iseq)
|
|
h = {
|
|
name: iseq_array[5],
|
|
insns: insns,
|
|
blocks: collect_blocks(blocks),
|
|
}
|
|
arr = [h]
|
|
iseq.each_child { |c| arr.concat collect_iseqs(c) }
|
|
arr
|
|
end
|
|
|
|
iseq = RubyVM::InstructionSequence.of(_test_proc)
|
|
IO.open(3).write Marshal.dump({
|
|
result: #{result == ANY ? "nil" : "result"},
|
|
stats: stats,
|
|
iseqs: collect_iseqs(iseq),
|
|
disasm: iseq.disasm
|
|
})
|
|
RUBY
|
|
|
|
script = <<~RUBY
|
|
#{"# frozen_string_literal: true" if frozen_string_literal}
|
|
_test_proc = -> {
|
|
#{test_script}
|
|
}
|
|
#{reset_stats}
|
|
result = _test_proc.call
|
|
#{write_results}
|
|
RUBY
|
|
|
|
status, out, err, stats = eval_with_jit(script, min_calls: min_calls)
|
|
|
|
assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}"
|
|
|
|
assert_equal stdout.chomp, out.chomp if stdout
|
|
|
|
unless ANY.equal?(result)
|
|
assert_equal result, stats[:result]
|
|
end
|
|
|
|
runtime_stats = stats[:stats]
|
|
iseqs = stats[:iseqs]
|
|
disasm = stats[:disasm]
|
|
|
|
# Only available when RUBY_DEBUG enabled
|
|
if runtime_stats[:all_stats]
|
|
recorded_exits = runtime_stats.select { |k, v| k.to_s.start_with?("exit_") }
|
|
recorded_exits = recorded_exits.reject { |k, v| v == 0 }
|
|
|
|
recorded_exits.transform_keys! { |k| k.to_s.gsub("exit_", "").to_sym }
|
|
if exits != :any && exits != recorded_exits
|
|
flunk "Expected #{exits.empty? ? "no" : exits.inspect} exits" \
|
|
", but got\n#{recorded_exits.inspect}"
|
|
end
|
|
end
|
|
|
|
# Only available when RUBY_DEBUG enabled
|
|
if runtime_stats[:all_stats]
|
|
missed_insns = insns.dup
|
|
all_compiled_blocks = {}
|
|
iseqs.each do |iseq|
|
|
compiled_blocks = iseq[:blocks].map { |from, to| (from...to) }
|
|
all_compiled_blocks[iseq[:name]] = compiled_blocks
|
|
compiled_insns = iseq[:insns]
|
|
next_idx = 0
|
|
compiled_insns.map! do |insn|
|
|
# TODO: not sure this is accurate for determining insn size
|
|
idx = next_idx
|
|
next_idx += insn.length
|
|
[idx, *insn]
|
|
end
|
|
|
|
compiled_insns.each do |idx, op, *arguments|
|
|
next unless missed_insns.include?(op)
|
|
next unless compiled_blocks.any? { |block| block === idx }
|
|
|
|
# This instruction was compiled
|
|
missed_insns.delete(op)
|
|
end
|
|
end
|
|
|
|
unless missed_insns.empty?
|
|
flunk "Expected to compile instructions #{missed_insns.join(", ")} but didn't.\nCompiled ranges: #{all_compiled_blocks.inspect}\niseq:\n#{disasm}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def eval_with_jit(script, min_calls: 1, timeout: 1000)
|
|
args = [
|
|
"--disable-gems",
|
|
"--yjit-call-threshold=#{min_calls}",
|
|
"--yjit-stats"
|
|
]
|
|
args << "-e" << script
|
|
stats_r, stats_w = IO.pipe
|
|
out, err, status = EnvUtil.invoke_ruby(args,
|
|
'', true, true, timeout: timeout, ios: {3 => stats_w}
|
|
)
|
|
stats_w.close
|
|
stats = stats_r.read
|
|
stats = Marshal.load(stats) if !stats.empty?
|
|
stats_r.close
|
|
[status, out, err, stats]
|
|
end
|
|
end
|