2021-06-23 19:38:37 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
require 'test/unit'
|
|
|
|
require 'envutil'
|
|
|
|
require 'tmpdir'
|
|
|
|
|
|
|
|
return unless 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_compile_putnil
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles('nil', insns: %i[putnil], result: nil)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_putobject
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles('true', insns: %i[putobject], result: true)
|
|
|
|
assert_compiles('123', insns: %i[putobject], result: 123)
|
|
|
|
assert_compiles(':foo', insns: %i[putobject], result: :foo)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_opt_not
|
2021-07-15 19:49:47 -04:00
|
|
|
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)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_opt_newarray
|
2021-07-15 19:49:47 -04:00
|
|
|
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])
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_opt_duparray
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles('[1]', insns: %i[duparray], result: [1])
|
|
|
|
assert_compiles('[1, 2, 3]', insns: %i[duparray], result: [1, 2, 3])
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_opt_nil_p
|
2021-07-15 19:49:47 -04:00
|
|
|
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)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_eq_fixnum
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles('123 == 123', insns: %i[opt_eq], result: true)
|
|
|
|
assert_compiles('123 == 456', insns: %i[opt_eq], result: false)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_eq_string
|
2021-07-15 19:49:47 -04:00
|
|
|
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)
|
2021-06-23 19:38:37 -04:00
|
|
|
end
|
|
|
|
|
2021-07-20 13:22:08 -04:00
|
|
|
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
|
2021-06-26 02:50:19 -04:00
|
|
|
|
2021-06-23 19:38:37 -04:00
|
|
|
def test_string_then_nil
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: true)
|
2021-06-23 19:38:37 -04:00
|
|
|
def foo(val)
|
|
|
|
val.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
foo("foo")
|
|
|
|
foo(nil)
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_nil_then_string
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: false)
|
2021-06-23 19:38:37 -04:00
|
|
|
def foo(val)
|
|
|
|
val.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
foo(nil)
|
|
|
|
foo("foo")
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_opt_length_in_method
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~RUBY, insns: %i[opt_length], result: 5)
|
2021-06-23 19:38:37 -04:00
|
|
|
def foo(str)
|
|
|
|
str.length
|
|
|
|
end
|
|
|
|
|
|
|
|
foo("hello, ")
|
|
|
|
foo("world")
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_compile_opt_getinlinecache
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~RUBY, insns: %i[opt_getinlinecache], result: 123, min_calls: 2)
|
2021-06-23 19:38:37 -04:00
|
|
|
def get_foo
|
|
|
|
FOO
|
|
|
|
end
|
|
|
|
|
|
|
|
FOO = 123
|
|
|
|
|
|
|
|
get_foo # warm inline cache
|
|
|
|
get_foo
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_string_interpolation
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~'RUBY', insns: %i[checktype concatstrings], result: "foobar", min_calls: 2)
|
2021-06-23 19:38:37 -04:00
|
|
|
def make_str(foo, bar)
|
|
|
|
"#{foo}#{bar}"
|
|
|
|
end
|
|
|
|
|
|
|
|
make_str("foo", "bar")
|
|
|
|
make_str("foo", "bar")
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
2021-06-26 02:50:19 -04:00
|
|
|
def test_fib_recursion
|
2021-07-15 19:49:47 -04:00
|
|
|
assert_compiles(<<~'RUBY', insns: %i[opt_le opt_minus opt_plus opt_send_without_block], result: 34)
|
2021-06-25 17:47:23 -04:00
|
|
|
def fib(n)
|
|
|
|
return n if n <= 1
|
|
|
|
fib(n-1) + fib(n-2)
|
|
|
|
end
|
|
|
|
|
|
|
|
fib(9)
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
2021-07-15 19:38:26 -04:00
|
|
|
def assert_no_exits(script)
|
|
|
|
assert_compiles(script)
|
|
|
|
end
|
|
|
|
|
2021-07-15 19:49:47 -04:00
|
|
|
ANY = Object.new
|
|
|
|
def assert_compiles(test_script, insns: [], min_calls: 1, stdout: nil, exits: {}, result: ANY)
|
2021-06-23 19:38:37 -04:00
|
|
|
reset_stats = <<~RUBY
|
|
|
|
YJIT.runtime_stats
|
|
|
|
YJIT.reset_stats!
|
|
|
|
RUBY
|
|
|
|
|
2021-07-15 19:49:47 -04:00
|
|
|
write_results = <<~RUBY
|
2021-06-23 19:38:37 -04:00
|
|
|
stats = 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 = 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({
|
2021-07-15 19:49:47 -04:00
|
|
|
result: result,
|
2021-06-23 19:38:37 -04:00
|
|
|
stats: stats,
|
|
|
|
iseqs: collect_iseqs(iseq),
|
|
|
|
disasm: iseq.disasm
|
|
|
|
})
|
|
|
|
RUBY
|
|
|
|
|
|
|
|
script = <<~RUBY
|
|
|
|
_test_proc = proc {
|
|
|
|
#{test_script}
|
|
|
|
}
|
|
|
|
#{reset_stats}
|
2021-07-15 19:49:47 -04:00
|
|
|
result = _test_proc.call
|
|
|
|
#{write_results}
|
2021-06-23 19:38:37 -04:00
|
|
|
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
|
|
|
|
|
2021-07-15 19:49:47 -04:00
|
|
|
unless ANY.equal?(result)
|
|
|
|
assert_equal result, stats[:result]
|
|
|
|
end
|
|
|
|
|
2021-06-23 19:38:37 -04:00
|
|
|
runtime_stats = stats[:stats]
|
|
|
|
iseqs = stats[:iseqs]
|
|
|
|
disasm = stats[:disasm]
|
|
|
|
|
2021-07-20 12:34:04 -04:00
|
|
|
# Only available when RUBY_DEBUG enabled
|
|
|
|
if runtime_stats[:all_stats]
|
|
|
|
recorded_exits = runtime_stats.select { |k, v| k.to_s.start_with?("exit_") }
|
2021-06-23 19:38:37 -04:00
|
|
|
recorded_exits = recorded_exits.reject { |k, v| v == 0 }
|
2021-07-20 12:34:04 -04:00
|
|
|
|
2021-06-23 19:38:37 -04:00
|
|
|
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
|
|
|
|
|
2021-07-20 12:34:04 -04:00
|
|
|
# Only available when RUBY_DEBUG enabled
|
|
|
|
if runtime_stats[:all_stats]
|
2021-06-23 19:38:37 -04:00
|
|
|
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
|