1 # frozen_string_literal: true
5 require_relative '../lib/jit_support'
7 return unless defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
9 # Tests for YJIT with assertions on compilation and side exits
10 # insipired by the MJIT tests in test/ruby/test_jit.rb
11 class TestYJIT < Test::Unit::TestCase
12 def test_yjit_in_ruby_description
13 assert_includes(RUBY_DESCRIPTION, '+YJIT')
16 def test_yjit_in_version
19 %w(--version --disable-yjit --yjit),
20 %w(--version --disable-yjit --enable-yjit),
21 %w(--version --disable-yjit --enable=yjit),
22 %w(--version --disable=yjit --yjit),
23 %w(--version --disable=yjit --enable-yjit),
24 %w(--version --disable=yjit --enable=yjit),
27 %w(--version --disable-jit --jit),
28 %w(--version --disable-jit --enable-jit),
29 %w(--version --disable-jit --enable=jit),
30 %w(--version --disable=jit --yjit),
31 %w(--version --disable=jit --enable-jit),
32 %w(--version --disable=jit --enable=jit),
33 ] if JITSupport.yjit_supported?),
34 ].each do |version_args|
35 assert_in_out_err(version_args) do |stdout, stderr|
36 assert_equal(RUBY_DESCRIPTION, stdout.first)
37 assert_equal([], stderr)
42 def test_command_line_switches
43 assert_in_out_err('--yjit-', '', [], /invalid option --yjit-/)
44 assert_in_out_err('--yjithello', '', [], /invalid option --yjithello/)
45 assert_in_out_err('--yjit-call-threshold', '', [], /--yjit-call-threshold needs an argument/)
46 assert_in_out_err('--yjit-call-threshold=', '', [], /--yjit-call-threshold needs an argument/)
47 assert_in_out_err('--yjit-greedy-versioning=1', '', [], /warning: argument to --yjit-greedy-versioning is ignored/)
50 def test_yjit_stats_and_v_no_error
51 _stdout, stderr, _status = EnvUtil.invoke_ruby(%w(-v --yjit-stats), '', true, true)
52 refute_includes(stderr, "NoMethodError")
55 def test_enable_from_env_var
56 yjit_child_env = {'RUBY_YJIT_ENABLE' => '1'}
57 assert_in_out_err([yjit_child_env, '--version'], '') do |stdout, stderr|
58 assert_equal(RUBY_DESCRIPTION, stdout.first)
59 assert_equal([], stderr)
61 assert_in_out_err([yjit_child_env, '-e puts RUBY_DESCRIPTION'], '', [RUBY_DESCRIPTION])
62 assert_in_out_err([yjit_child_env, '-e p RubyVM::YJIT.enabled?'], '', ['true'])
65 def test_compile_setclassvariable
66 script = 'class Foo; def self.foo; @@foo = 1; end; end; Foo.foo'
67 assert_compiles(script, insns: %i[setclassvariable], result: 1)
70 def test_compile_getclassvariable
71 script = 'class Foo; @@foo = 1; def self.foo; @@foo; end; end; Foo.foo'
72 assert_compiles(script, insns: %i[getclassvariable], result: 1)
75 def test_compile_putnil
76 assert_compiles('nil', insns: %i[putnil], result: nil)
79 def test_compile_putobject
80 assert_compiles('true', insns: %i[putobject], result: true)
81 assert_compiles('123', insns: %i[putobject], result: 123)
82 assert_compiles(':foo', insns: %i[putobject], result: :foo)
85 def test_compile_opt_not
86 assert_compiles('!false', insns: %i[opt_not], result: true)
87 assert_compiles('!nil', insns: %i[opt_not], result: true)
88 assert_compiles('!true', insns: %i[opt_not], result: false)
89 assert_compiles('![]', insns: %i[opt_not], result: false)
92 def test_compile_opt_newarray
93 assert_compiles('[]', insns: %i[newarray], result: [])
94 assert_compiles('[1+1]', insns: %i[newarray opt_plus], result: [2])
95 assert_compiles('[1,1+1,3,4,5,6]', insns: %i[newarray opt_plus], result: [1, 2, 3, 4, 5, 6])
98 def test_compile_opt_duparray
99 assert_compiles('[1]', insns: %i[duparray], result: [1])
100 assert_compiles('[1, 2, 3]', insns: %i[duparray], result: [1, 2, 3])
103 def test_compile_newrange
104 assert_compiles('s = 1; (s..5)', insns: %i[newrange], result: 1..5)
105 assert_compiles('s = 1; e = 5; (s..e)', insns: %i[newrange], result: 1..5)
106 assert_compiles('s = 1; (s...5)', insns: %i[newrange], result: 1...5)
107 assert_compiles('s = 1; (s..)', insns: %i[newrange], result: 1..)
108 assert_compiles('e = 5; (..e)', insns: %i[newrange], result: ..5)
111 def test_compile_duphash
112 assert_compiles('{ two: 2 }', insns: %i[duphash], result: { two: 2 })
115 def test_compile_newhash
116 assert_compiles('{}', insns: %i[newhash], result: {})
117 assert_compiles('{ two: 1 + 1 }', insns: %i[newhash], result: { two: 2 })
118 assert_compiles('{ 1 + 1 => :two }', insns: %i[newhash], result: { 2 => :two })
121 def test_compile_opt_nil_p
122 assert_compiles('nil.nil?', insns: %i[opt_nil_p], result: true)
123 assert_compiles('false.nil?', insns: %i[opt_nil_p], result: false)
124 assert_compiles('true.nil?', insns: %i[opt_nil_p], result: false)
125 assert_compiles('(-"").nil?', insns: %i[opt_nil_p], result: false)
126 assert_compiles('123.nil?', insns: %i[opt_nil_p], result: false)
129 def test_compile_eq_fixnum
130 assert_compiles('123 == 123', insns: %i[opt_eq], result: true)
131 assert_compiles('123 == 456', insns: %i[opt_eq], result: false)
134 def test_compile_eq_string
135 assert_compiles('-"" == -""', insns: %i[opt_eq], result: true)
136 assert_compiles('-"foo" == -"foo"', insns: %i[opt_eq], result: true)
137 assert_compiles('-"foo" == -"bar"', insns: %i[opt_eq], result: false)
140 def test_compile_eq_symbol
141 assert_compiles(':foo == :foo', insns: %i[opt_eq], result: true)
142 assert_compiles(':foo == :bar', insns: %i[opt_eq], result: false)
143 assert_compiles(':foo == "foo".to_sym', insns: %i[opt_eq], result: true)
146 def test_compile_eq_object
147 assert_compiles(<<~RUBY, insns: %i[opt_eq], result: false)
152 eq(Object.new, Object.new)
155 assert_compiles(<<~RUBY, insns: %i[opt_eq], result: true)
165 def test_compile_eq_arbitrary_class
166 assert_compiles(<<~RUBY, insns: %i[opt_eq], result: "yes")
182 def test_compile_opt_lt
183 assert_compiles('1 < 2', insns: %i[opt_lt])
184 assert_compiles('"a" < "b"', insns: %i[opt_lt])
187 def test_compile_opt_le
188 assert_compiles('1 <= 2', insns: %i[opt_le])
189 assert_compiles('"a" <= "b"', insns: %i[opt_le])
192 def test_compile_opt_gt
193 assert_compiles('1 > 2', insns: %i[opt_gt])
194 assert_compiles('"a" > "b"', insns: %i[opt_gt])
197 def test_compile_opt_ge
198 assert_compiles('1 >= 2', insns: %i[opt_ge])
199 assert_compiles('"a" >= "b"', insns: %i[opt_ge])
202 def test_compile_opt_plus
203 assert_compiles('1 + 2', insns: %i[opt_plus])
204 assert_compiles('"a" + "b"', insns: %i[opt_plus])
205 assert_compiles('[:foo] + [:bar]', insns: %i[opt_plus])
208 def test_compile_opt_minus
209 assert_compiles('1 - 2', insns: %i[opt_minus])
210 assert_compiles('[:foo, :bar] - [:bar]', insns: %i[opt_minus])
213 def test_compile_opt_or
214 assert_compiles('1 | 2', insns: %i[opt_or])
215 assert_compiles('[:foo] | [:bar]', insns: %i[opt_or])
218 def test_compile_opt_and
219 assert_compiles('1 & 2', insns: %i[opt_and])
220 assert_compiles('[:foo, :bar] & [:bar]', insns: %i[opt_and])
223 def test_compile_set_and_get_global
224 assert_compiles('$foo = 123; $foo', insns: %i[setglobal], result: 123)
227 def test_compile_putspecialobject
228 assert_compiles('-> {}', insns: %i[putspecialobject])
231 def test_compile_tostring
232 assert_no_exits('"i am a string #{true}"')
235 def test_compile_opt_aset
236 assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset])
237 assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset])
238 assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset])
239 assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset])
242 def test_compile_attr_set
243 assert_no_exits(<<~EORB)
256 def test_compile_regexp
257 assert_no_exits('/#{true}/')
260 def test_compile_dynamic_symbol
261 assert_compiles(':"#{"foo"}"', insns: %i[intern])
262 assert_compiles('s = "bar"; :"foo#{s}"', insns: %i[intern])
265 def test_getlocal_with_level
266 assert_compiles(<<~RUBY, insns: %i[getlocal opt_plus], result: [[7]])
279 def test_setlocal_with_level
280 assert_no_exits(<<~RUBY)
293 def test_string_then_nil
294 assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: true)
304 def test_nil_then_string
305 assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: false)
315 def test_opt_length_in_method
316 assert_compiles(<<~RUBY, insns: %i[opt_length], result: 5)
326 def test_opt_regexpmatch2
327 assert_compiles(<<~RUBY, insns: %i[opt_regexpmatch2], result: 0)
337 assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [1, 2])
342 def test_expandarray_nil
343 assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [nil, nil])
349 def test_getspecial_backref
350 assert_compiles("'foo' =~ /(o)./; $&", insns: %i[getspecial], result: "oo")
351 assert_compiles("'foo' =~ /(o)./; $`", insns: %i[getspecial], result: "f")
352 assert_compiles("'foo' =~ /(o)./; $'", insns: %i[getspecial], result: "")
353 assert_compiles("'foo' =~ /(o)./; $+", insns: %i[getspecial], result: "o")
354 assert_compiles("'foo' =~ /(o)./; $1", insns: %i[getspecial], result: "o")
355 assert_compiles("'foo' =~ /(o)./; $2", insns: %i[getspecial], result: nil)
358 def test_compile_opt_getinlinecache
359 assert_compiles(<<~RUBY, insns: %i[opt_getinlinecache], result: 123, min_calls: 2)
366 get_foo # warm inline cache
371 def test_opt_getinlinecache_slowpath
372 assert_compiles(<<~RUBY, exits: { opt_getinlinecache: 1 }, result: [42, 42, 1, 1], min_calls: 2)
399 def test_string_interpolation
400 assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "foobar", min_calls: 2)
401 def make_str(foo, bar)
405 make_str("foo", "bar")
406 make_str("foo", "bar")
410 def test_string_interpolation_cast
411 assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "123")
412 def make_str(foo, bar)
420 def test_checkkeyword
421 assert_compiles(<<~'RUBY', insns: %i[checkkeyword], result: [2, 5])
431 assert_compiles(<<~RUBY)
437 Foo = Struct.new(:foo, :bar)
444 assert_compiles(<<~RUBY)
450 Foo = Struct.new(:foo, :bar)
457 assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15)
475 assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Hello")
476 class Gnirts < String
490 # Tests calling a variadic cfunc with many args
491 def test_build_large_struct
492 assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], min_calls: 2)
493 ::Foo = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h)
496 ::Foo.new(:a, :b, :c, :d, :e, :f, :g, :h)
504 def test_fib_recursion
505 assert_compiles(<<~'RUBY', insns: %i[opt_le opt_minus opt_plus opt_send_without_block], result: 34)
515 def test_optarg_and_kwarg
516 assert_no_exits(<<~'RUBY')
517 def opt_and_kwarg(a, b=nil, c: nil)
521 opt_and_kwarg(1, 2, c: 3)
527 assert_no_exits('{}.store(:value, foo: 123)')
528 assert_no_exits('{}.store(:value, foo: 123, bar: 456, baz: 789)')
529 assert_no_exits('{}.merge(foo: 123)')
530 assert_no_exits('{}.merge(foo: 123, bar: 456, baz: 789)')
533 def test_ctx_different_mappings
534 # regression test simplified from URI::Generic#hostname=
535 assert_compiles(<<~'RUBY', frozen_string_literal: true)
537 !(v&.start_with?('[')) && v&.index(':')
545 def test_no_excessive_opt_getinlinecache_invalidation
546 assert_compiles(<<~'RUBY', exits: :any, result: :ok)
547 objects = [Object.new, Object.new]
562 stats = RubyVM::YJIT.runtime_stats
563 return :ok unless stats[:all_stats]
564 return :ok if stats[:invalidation_count] < 10
570 def assert_no_exits(script)
571 assert_compiles(script)
575 def assert_compiles(test_script, insns: [], min_calls: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil)
576 reset_stats = <<~RUBY
577 RubyVM::YJIT.runtime_stats
578 RubyVM::YJIT.reset_stats!
581 write_results = <<~RUBY
582 stats = RubyVM::YJIT.runtime_stats
584 def collect_blocks(blocks)
585 blocks.sort_by(&:address).map { |b| [b.iseq_start_index, b.iseq_end_index] }
588 def collect_iseqs(iseq)
589 iseq_array = iseq.to_a
590 insns = iseq_array.last.grep(Array)
591 blocks = RubyVM::YJIT.blocks_for(iseq)
595 blocks: collect_blocks(blocks),
598 iseq.each_child { |c| arr.concat collect_iseqs(c) }
602 iseq = RubyVM::InstructionSequence.of(_test_proc)
603 IO.open(3).write Marshal.dump({
604 result: #{result == ANY ? "nil" : "result"},
606 iseqs: collect_iseqs(iseq),
612 #{"# frozen_string_literal: true" if frozen_string_literal}
617 result = _test_proc.call
621 status, out, err, stats = eval_with_jit(script, min_calls: min_calls)
623 assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}"
625 assert_equal stdout.chomp, out.chomp if stdout
627 unless ANY.equal?(result)
628 assert_equal result, stats[:result]
631 runtime_stats = stats[:stats]
632 iseqs = stats[:iseqs]
633 disasm = stats[:disasm]
635 # Only available when RUBY_DEBUG enabled
636 if runtime_stats[:all_stats]
637 recorded_exits = runtime_stats.select { |k, v| k.to_s.start_with?("exit_") }
638 recorded_exits = recorded_exits.reject { |k, v| v == 0 }
640 recorded_exits.transform_keys! { |k| k.to_s.gsub("exit_", "").to_sym }
641 if exits != :any && exits != recorded_exits
642 flunk "Expected #{exits.empty? ? "no" : exits.inspect} exits" \
643 ", but got\n#{recorded_exits.inspect}"
647 # Only available when RUBY_DEBUG enabled
648 if runtime_stats[:all_stats]
649 missed_insns = insns.dup
650 all_compiled_blocks = {}
652 compiled_blocks = iseq[:blocks].map { |from, to| (from...to) }
653 all_compiled_blocks[iseq[:name]] = compiled_blocks
654 compiled_insns = iseq[:insns]
656 compiled_insns.map! do |insn|
657 # TODO: not sure this is accurate for determining insn size
659 next_idx += insn.length
663 compiled_insns.each do |idx, op, *arguments|
664 next unless missed_insns.include?(op)
665 next unless compiled_blocks.any? { |block| block === idx }
667 # This instruction was compiled
668 missed_insns.delete(op)
672 unless missed_insns.empty?
673 flunk "Expected to compile instructions #{missed_insns.join(", ")} but didn't.\nCompiled ranges: #{all_compiled_blocks.inspect}\niseq:\n#{disasm}"
678 def eval_with_jit(script, min_calls: 1, timeout: 1000)
681 "--yjit-call-threshold=#{min_calls}",
684 args << "-e" << script
685 stats_r, stats_w = IO.pipe
686 out, err, status = EnvUtil.invoke_ruby(args,
687 '', true, true, timeout: timeout, ios: {3 => stats_w}
691 stats = Marshal.load(stats) if !stats.empty?
693 [status, out, err, stats]