* 2022-01-18 [ci skip]
[ruby-80x24.org.git] / test / ruby / test_yjit.rb
blob88f8e428135a69a5171e8eb0fcd394870e4641d9
1 # frozen_string_literal: true
2 require 'test/unit'
3 require 'envutil'
4 require 'tmpdir'
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')
14   end
16   def test_yjit_in_version
17     [
18       %w(--version --yjit),
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),
25       *([
26         %w(--version --jit),
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)
38       end
39     end
40   end
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/)
48   end
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")
53   end
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)
60     end
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'])
63   end
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)
68   end
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)
73   end
75   def test_compile_putnil
76     assert_compiles('nil', insns: %i[putnil], result: nil)
77   end
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)
83   end
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)
90   end
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])
96   end
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])
101   end
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)
109   end
111   def test_compile_duphash
112     assert_compiles('{ two: 2 }', insns: %i[duphash], result: { two: 2 })
113   end
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 })
119   end
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)
127   end
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)
132   end
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)
138   end
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)
144   end
146   def test_compile_eq_object
147     assert_compiles(<<~RUBY, insns: %i[opt_eq], result: false)
148       def eq(a, b)
149         a == b
150       end
152       eq(Object.new, Object.new)
153     RUBY
155     assert_compiles(<<~RUBY, insns: %i[opt_eq], result: true)
156       def eq(a, b)
157         a == b
158       end
160       obj = Object.new
161       eq(obj, obj)
162     RUBY
163   end
165   def test_compile_eq_arbitrary_class
166     assert_compiles(<<~RUBY, insns: %i[opt_eq], result: "yes")
167       def eq(a, b)
168         a == b
169       end
171       class Foo
172         def ==(other)
173           "yes"
174         end
175       end
177       eq(Foo.new, Foo.new)
178       eq(Foo.new, Foo.new)
179     RUBY
180   end
182   def test_compile_opt_lt
183     assert_compiles('1 < 2', insns: %i[opt_lt])
184     assert_compiles('"a" < "b"', insns: %i[opt_lt])
185   end
187   def test_compile_opt_le
188     assert_compiles('1 <= 2', insns: %i[opt_le])
189     assert_compiles('"a" <= "b"', insns: %i[opt_le])
190   end
192   def test_compile_opt_gt
193     assert_compiles('1 > 2', insns: %i[opt_gt])
194     assert_compiles('"a" > "b"', insns: %i[opt_gt])
195   end
197   def test_compile_opt_ge
198     assert_compiles('1 >= 2', insns: %i[opt_ge])
199     assert_compiles('"a" >= "b"', insns: %i[opt_ge])
200   end
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])
206   end
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])
211   end
213   def test_compile_opt_or
214     assert_compiles('1 | 2', insns: %i[opt_or])
215     assert_compiles('[:foo] | [:bar]', insns: %i[opt_or])
216   end
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])
221   end
223   def test_compile_set_and_get_global
224     assert_compiles('$foo = 123; $foo', insns: %i[setglobal], result: 123)
225   end
227   def test_compile_putspecialobject
228     assert_compiles('-> {}', insns: %i[putspecialobject])
229   end
231   def test_compile_tostring
232     assert_no_exits('"i am a string #{true}"')
233   end
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])
240   end
242   def test_compile_attr_set
243     assert_no_exits(<<~EORB)
244     class Foo
245       attr_accessor :bar
246     end
248     foo = Foo.new
249     foo.bar = 3
250     foo.bar = 3
251     foo.bar = 3
252     foo.bar = 3
253     EORB
254   end
256   def test_compile_regexp
257     assert_no_exits('/#{true}/')
258   end
260   def test_compile_dynamic_symbol
261     assert_compiles(':"#{"foo"}"', insns: %i[intern])
262     assert_compiles('s = "bar"; :"foo#{s}"', insns: %i[intern])
263   end
265   def test_getlocal_with_level
266     assert_compiles(<<~RUBY, insns: %i[getlocal opt_plus], result: [[7]])
267       def foo(foo, bar)
268         [1].map do |x|
269           [1].map do |y|
270             foo + bar
271           end
272         end
273       end
275       foo(5, 2)
276     RUBY
277   end
279   def test_setlocal_with_level
280     assert_no_exits(<<~RUBY)
281       def sum(arr)
282         sum = 0
283         arr.each do |x|
284           sum += x
285         end
286         sum
287       end
289       sum([1,2,3])
290     RUBY
291   end
293   def test_string_then_nil
294     assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: true)
295       def foo(val)
296         val.nil?
297       end
299       foo("foo")
300       foo(nil)
301     RUBY
302   end
304   def test_nil_then_string
305     assert_compiles(<<~RUBY, insns: %i[opt_nil_p], result: false)
306       def foo(val)
307         val.nil?
308       end
310       foo(nil)
311       foo("foo")
312     RUBY
313   end
315   def test_opt_length_in_method
316     assert_compiles(<<~RUBY, insns: %i[opt_length], result: 5)
317       def foo(str)
318         str.length
319       end
321       foo("hello, ")
322       foo("world")
323     RUBY
324   end
326   def test_opt_regexpmatch2
327     assert_compiles(<<~RUBY, insns: %i[opt_regexpmatch2], result: 0)
328       def foo(str)
329         str =~ /foo/
330       end
332       foo("foobar")
333     RUBY
334   end
336   def test_expandarray
337     assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [1, 2])
338       a, b = [1, 2]
339     RUBY
340   end
342   def test_expandarray_nil
343     assert_compiles(<<~'RUBY', insns: %i[expandarray], result: [nil, nil])
344       a, b = nil
345       [a, b]
346     RUBY
347   end
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)
356   end
358   def test_compile_opt_getinlinecache
359     assert_compiles(<<~RUBY, insns: %i[opt_getinlinecache], result: 123, min_calls: 2)
360       def get_foo
361         FOO
362       end
364       FOO = 123
366       get_foo # warm inline cache
367       get_foo
368     RUBY
369   end
371   def test_opt_getinlinecache_slowpath
372     assert_compiles(<<~RUBY, exits: { opt_getinlinecache: 1 }, result: [42, 42, 1, 1], min_calls: 2)
373       class A
374         FOO = 42
375         class << self
376           def foo
377             _foo = nil
378             FOO
379           end
380         end
381       end
383       result = []
385       result << A.foo
386       result << A.foo
388       class << A
389         FOO = 1
390       end
392       result << A.foo
393       result << A.foo
395       result
396     RUBY
397   end
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)
402         "#{foo}#{bar}"
403       end
405       make_str("foo", "bar")
406       make_str("foo", "bar")
407     RUBY
408   end
410   def test_string_interpolation_cast
411     assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "123")
412       def make_str(foo, bar)
413         "#{foo}#{bar}"
414       end
416       make_str(1, 23)
417     RUBY
418   end
420   def test_checkkeyword
421     assert_compiles(<<~'RUBY', insns: %i[checkkeyword], result: [2, 5])
422       def foo(foo: 1+1)
423         foo
424       end
426       [foo, foo(foo: 5)]
427     RUBY
428   end
430   def test_struct_aref
431     assert_compiles(<<~RUBY)
432       def foo(obj)
433         obj.foo
434         obj.bar
435       end
437       Foo = Struct.new(:foo, :bar)
438       foo(Foo.new(123))
439       foo(Foo.new(123))
440     RUBY
441   end
443   def test_struct_aset
444     assert_compiles(<<~RUBY)
445       def foo(obj)
446         obj.foo = 123
447         obj.bar = 123
448       end
450       Foo = Struct.new(:foo, :bar)
451       foo(Foo.new(123))
452       foo(Foo.new(123))
453     RUBY
454   end
456   def test_super_iseq
457     assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15)
458       class A
459         def foo
460           1 + 2
461         end
462       end
464       class B < A
465         def foo
466           super * 5
467         end
468       end
470       B.new.foo
471     RUBY
472   end
474   def test_super_cfunc
475     assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Hello")
476       class Gnirts < String
477         def initialize
478           super(-"olleH")
479         end
481         def to_s
482           super().reverse
483         end
484       end
486       Gnirts.new.to_s
487     RUBY
488   end
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)
495       def build_foo
496         ::Foo.new(:a, :b, :c, :d, :e, :f, :g, :h)
497       end
499       build_foo
500       build_foo
501     RUBY
502   end
504   def test_fib_recursion
505     assert_compiles(<<~'RUBY', insns: %i[opt_le opt_minus opt_plus opt_send_without_block], result: 34)
506       def fib(n)
507         return n if n <= 1
508         fib(n-1) + fib(n-2)
509       end
511       fib(9)
512     RUBY
513   end
515   def test_optarg_and_kwarg
516     assert_no_exits(<<~'RUBY')
517       def opt_and_kwarg(a, b=nil, c: nil)
518       end
520       2.times do
521         opt_and_kwarg(1, 2, c: 3)
522       end
523     RUBY
524   end
526   def test_cfunc_kwarg
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)')
531   end
533   def test_ctx_different_mappings
534     # regression test simplified from URI::Generic#hostname=
535     assert_compiles(<<~'RUBY', frozen_string_literal: true)
536       def foo(v)
537         !(v&.start_with?('[')) && v&.index(':')
538       end
540       foo(nil)
541       foo("example.com")
542     RUBY
543   end
545   def test_no_excessive_opt_getinlinecache_invalidation
546     assert_compiles(<<~'RUBY', exits: :any, result: :ok)
547       objects = [Object.new, Object.new]
549       objects.each do |o|
550         class << o
551           def foo
552             Object
553           end
554         end
555       end
557       9000.times {
558         objects[0].foo
559         objects[1].foo
560       }
562       stats = RubyVM::YJIT.runtime_stats
563       return :ok unless stats[:all_stats]
564       return :ok if stats[:invalidation_count] < 10
566       :fail
567     RUBY
568   end
570   def assert_no_exits(script)
571     assert_compiles(script)
572   end
574   ANY = Object.new
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!
579     RUBY
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] }
586       end
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)
592         h = {
593           name: iseq_array[5],
594           insns: insns,
595           blocks: collect_blocks(blocks),
596         }
597         arr = [h]
598         iseq.each_child { |c| arr.concat collect_iseqs(c) }
599         arr
600       end
602       iseq = RubyVM::InstructionSequence.of(_test_proc)
603       IO.open(3).write Marshal.dump({
604         result: #{result == ANY ? "nil" : "result"},
605         stats: stats,
606         iseqs: collect_iseqs(iseq),
607         disasm: iseq.disasm
608       })
609     RUBY
611     script = <<~RUBY
612       #{"# frozen_string_literal: true" if frozen_string_literal}
613       _test_proc = -> {
614         #{test_script}
615       }
616       #{reset_stats}
617       result = _test_proc.call
618       #{write_results}
619     RUBY
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]
629     end
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}"
644       end
645     end
647     # Only available when RUBY_DEBUG enabled
648     if runtime_stats[:all_stats]
649       missed_insns = insns.dup
650       all_compiled_blocks = {}
651       iseqs.each do |iseq|
652         compiled_blocks = iseq[:blocks].map { |from, to| (from...to) }
653         all_compiled_blocks[iseq[:name]] = compiled_blocks
654         compiled_insns = iseq[:insns]
655         next_idx = 0
656         compiled_insns.map! do |insn|
657           # TODO: not sure this is accurate for determining insn size
658           idx = next_idx
659           next_idx += insn.length
660           [idx, *insn]
661         end
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)
669         end
670       end
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}"
674       end
675     end
676   end
678   def eval_with_jit(script, min_calls: 1, timeout: 1000)
679     args = [
680       "--disable-gems",
681       "--yjit-call-threshold=#{min_calls}",
682       "--yjit-stats"
683     ]
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}
688     )
689     stats_w.close
690     stats = stats_r.read
691     stats = Marshal.load(stats) if !stats.empty?
692     stats_r.close
693     [status, out, err, stats]
694   end