diff options
Diffstat (limited to 'test/ruby/test_jit.rb')
| -rw-r--r-- | test/ruby/test_jit.rb | 1273 |
1 files changed, 1273 insertions, 0 deletions
diff --git a/test/ruby/test_jit.rb b/test/ruby/test_jit.rb new file mode 100644 index 0000000000..07ac76210d --- /dev/null +++ b/test/ruby/test_jit.rb @@ -0,0 +1,1273 @@ +# frozen_string_literal: true +require 'test/unit' +require 'tmpdir' +require_relative '../lib/jit_support' + +# Test for --jit option +class TestJIT < Test::Unit::TestCase + include JITSupport + + IGNORABLE_PATTERNS = [ + /\AJIT recompile: .+\n\z/, + /\AJIT inline: .+\n\z/, + /\AJIT cancel: .+\n\z/, + /\ASuccessful MJIT finish\n\z/, + ] + MAX_CACHE_PATTERNS = [ + /\AJIT compaction \([^)]+\): .+\n\z/, + /\AToo many JIT code, but skipped unloading units for JIT compaction\n\z/, + /\ANo units can be unloaded -- .+\n\z/, + ] + + # trace_* insns are not compiled for now... + TEST_PENDING_INSNS = RubyVM::INSTRUCTION_NAMES.select { |n| n.start_with?('trace_') }.map(&:to_sym) + [ + # not supported yet + :defineclass, + + # to be tested + :invokebuiltin, + + # never used + :opt_invokebuiltin_delegate, + ].each do |insn| + if !RubyVM::INSTRUCTION_NAMES.include?(insn.to_s) + warn "instruction #{insn.inspect} is not defined but included in TestJIT::TEST_PENDING_INSNS" + end + end + + def self.untested_insns + @untested_insns ||= (RubyVM::INSTRUCTION_NAMES.map(&:to_sym) - TEST_PENDING_INSNS) + end + + def self.setup + return if defined?(@setup_hooked) + @setup_hooked = true + + # ci.rvm.jp caches its build environment. Clean up temporary files left by SEGV. + if ENV['RUBY_DEBUG']&.include?('ci') + Dir.glob("#{ENV.fetch('TMPDIR', '/tmp')}/_ruby_mjit_p*u*.*").each do |file| + puts "test/ruby/test_jit.rb: removing #{file}" + File.unlink(file) + end + end + + # ruby -w -Itest/lib test/ruby/test_jit.rb + if $VERBOSE + pid = $$ + at_exit do + if pid == $$ && !TestJIT.untested_insns.empty? + warn "you may want to add tests for following insns, when you have a chance: #{TestJIT.untested_insns.join(' ')}" + end + end + end + end + + def setup + unless JITSupport.supported? + skip 'JIT seems not supported on this platform' + end + self.class.setup + end + + def test_compile_insn_nop + assert_compile_once('nil rescue true', result_inspect: 'nil', insns: %i[nop]) + end + + def test_compile_insn_local + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[setlocal_WC_0 getlocal_WC_0]) + begin; + foo = 1 + foo + end; + + insns = %i[setlocal getlocal setlocal_WC_0 getlocal_WC_0 setlocal_WC_1 getlocal_WC_1] + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 3, stdout: '168', insns: insns) + begin; + def foo + a = 0 + [1, 2].each do |i| + a += i + [3, 4].each do |j| + a *= j + end + end + a + end + + print foo + end; + end + + def test_compile_insn_blockparam + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2, insns: %i[getblockparam setblockparam]) + begin; + def foo(&b) + a = b + b = 2 + a.call + 2 + end + + print foo { 1 } + end; + end + + def test_compile_insn_getblockparamproxy + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '4', success_count: 3, insns: %i[getblockparamproxy]) + begin; + def bar(&b) + b.call + end + + def foo(&b) + bar(&b) * bar(&b) + end + + print foo { 2 } + end; + end + + def test_compile_insn_getspecial + assert_compile_once('$1', result_inspect: 'nil', insns: %i[getspecial]) + end + + def test_compile_insn_setspecial + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[setspecial]) + begin; + true if nil.nil?..nil.nil? + end; + end + + def test_compile_insn_instancevariable + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getinstancevariable setinstancevariable]) + begin; + @foo = 1 + @foo + end; + + # optimized getinstancevariable call + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '33', success_count: 1, min_calls: 2) + begin; + class A + def initialize + @a = 1 + @b = 2 + end + + def three + @a + @b + end + end + + a = A.new + print(a.three) # set ic + print(a.three) # inlined ic + end; + end + + def test_compile_insn_classvariable + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 1, insns: %i[getclassvariable setclassvariable]) + begin; + class Foo + def self.foo + @@foo = 1 + @@foo + end + end + + print Foo.foo + end; + end + + def test_compile_insn_constant + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getconstant setconstant]) + begin; + FOO = 1 + FOO + end; + end + + def test_compile_insn_global + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[getglobal setglobal]) + begin; + $foo = 1 + $foo + end; + end + + def test_compile_insn_putnil + assert_compile_once('nil', result_inspect: 'nil', insns: %i[putnil]) + end + + def test_compile_insn_putself + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 1, insns: %i[putself]) + begin; + proc { print "hello" }.call + end; + end + + def test_compile_insn_putobject + assert_compile_once('0', result_inspect: '0', insns: %i[putobject_INT2FIX_0_]) + assert_compile_once('1', result_inspect: '1', insns: %i[putobject_INT2FIX_1_]) + assert_compile_once('2', result_inspect: '2', insns: %i[putobject]) + end + + def test_compile_insn_definemethod_definesmethod + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'helloworld', success_count: 3, insns: %i[definemethod definesmethod]) + begin; + print 1.times.map { + def method_definition + 'hello' + end + + def self.smethod_definition + 'world' + end + + method_definition + smethod_definition + }.join + end; + end + + def test_compile_insn_putspecialobject + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'a', success_count: 2, insns: %i[putspecialobject]) + begin; + print 1.times.map { + def a + 'a' + end + + alias :b :a + + b + }.join + end; + end + + def test_compile_insn_putstring_concatstrings_objtostring + assert_compile_once('"a#{}b" + "c"', result_inspect: '"abc"', insns: %i[putstring concatstrings objtostring]) + end + + def test_compile_insn_toregexp + assert_compile_once('/#{true}/ =~ "true"', result_inspect: '0', insns: %i[toregexp]) + end + + def test_compile_insn_newarray + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '[1, 2, 3]', insns: %i[newarray]) + begin; + a, b, c = 1, 2, 3 + [a, b, c] + end; + end + + def test_compile_insn_newarraykwsplat + assert_compile_once('[**{ x: 1 }]', result_inspect: '[{:x=>1}]', insns: %i[newarraykwsplat]) + end + + def test_compile_insn_intern_duparray + assert_compile_once('[:"#{0}"] + [1,2,3]', result_inspect: '[:"0", 1, 2, 3]', insns: %i[intern duparray]) + end + + def test_compile_insn_expandarray + assert_compile_once('y = [ true, false, nil ]; x, = y; x', result_inspect: 'true', insns: %i[expandarray]) + end + + def test_compile_insn_concatarray + assert_compile_once('["t", "r", *x = "u", "e"].join', result_inspect: '"true"', insns: %i[concatarray]) + end + + def test_compile_insn_splatarray + assert_compile_once('[*(1..2)]', result_inspect: '[1, 2]', insns: %i[splatarray]) + end + + def test_compile_insn_newhash + assert_compile_once('a = 1; { a: a }', result_inspect: '{:a=>1}', insns: %i[newhash]) + end + + def test_compile_insn_duphash + assert_compile_once('{ a: 1 }', result_inspect: '{:a=>1}', insns: %i[duphash]) + end + + def test_compile_insn_newrange + assert_compile_once('a = 1; 0..a', result_inspect: '0..1', insns: %i[newrange]) + end + + def test_compile_insn_pop + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[pop]) + begin; + a = false + b = 1 + a || b + end; + end + + def test_compile_insn_dup + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '3', insns: %i[dup]) + begin; + a = 1 + a&.+(2) + end; + end + + def test_compile_insn_dupn + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[dupn]) + begin; + klass = Class.new + klass::X ||= true + end; + end + + def test_compile_insn_swap_topn + assert_compile_once('{}["true"] = true', result_inspect: 'true', insns: %i[swap topn]) + end + + def test_compile_insn_reput + skip "write test" + end + + def test_compile_insn_setn + assert_compile_once('[nil][0] = 1', result_inspect: '1', insns: %i[setn]) + end + + def test_compile_insn_adjuststack + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'true', insns: %i[adjuststack]) + begin; + x = [true] + x[0] ||= nil + x[0] + end; + end + + def test_compile_insn_defined + assert_compile_once('defined?(a)', result_inspect: 'nil', insns: %i[defined]) + end + + def test_compile_insn_checkkeyword + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'true', success_count: 1, insns: %i[checkkeyword]) + begin; + def test(x: rand) + x + end + print test(x: true) + end; + end + + def test_compile_insn_tracecoverage + skip "write test" + end + + def test_compile_insn_defineclass + skip "support this in mjit_compile (low priority)" + end + + def test_compile_insn_send + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 3, insns: %i[send]) + begin; + print proc { yield_self { 1 } }.call + end; + end + + def test_compile_insn_opt_str_freeze + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"foo"', insns: %i[opt_str_freeze]) + begin; + 'foo'.freeze + end; + end + + def test_compile_insn_opt_nil_p + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: 'false', insns: %i[opt_nil_p]) + begin; + nil.nil?.nil? + end; + end + + def test_compile_insn_opt_str_uminus + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"bar"', insns: %i[opt_str_uminus]) + begin; + -'bar' + end; + end + + def test_compile_insn_opt_newarray_max + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '2', insns: %i[opt_newarray_max]) + begin; + a = 1 + b = 2 + [a, b].max + end; + end + + def test_compile_insn_opt_newarray_min + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '1', insns: %i[opt_newarray_min]) + begin; + a = 1 + b = 2 + [a, b].min + end; + end + + def test_compile_insn_opt_send_without_block + assert_compile_once('print', result_inspect: 'nil', insns: %i[opt_send_without_block]) + end + + def test_compile_insn_invokesuper + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 4, insns: %i[invokesuper]) + begin; + mod = Module.new { + def test + super + 2 + end + } + klass = Class.new { + prepend mod + def test + 1 + end + } + print klass.new.test + end; + end + + def test_compile_insn_invokeblock_leave + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '2', success_count: 2, insns: %i[invokeblock leave]) + begin; + def foo + yield + end + print foo { 2 } + end; + end + + def test_compile_insn_throw + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '4', success_count: 2, insns: %i[throw]) + begin; + def test + proc do + if 1+1 == 1 + return 3 + else + return 4 + end + 5 + end.call + end + print test + end; + end + + def test_compile_insn_jump_branchif + assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: 'nil', insns: %i[jump branchif]) + begin; + a = false + 1 + 1 while a + end; + end + + def test_compile_insn_branchunless + assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '1', insns: %i[branchunless]) + begin; + a = true + if a + 1 + else + 2 + end + end; + end + + def test_compile_insn_branchnil + assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '3', insns: %i[branchnil]) + begin; + a = 2 + a&.+(1) + end; + end + + def test_compile_insn_objtostring + assert_compile_once("#{<<~"begin;"}\n#{<<~'end;'}", result_inspect: '"42"', insns: %i[objtostring]) + begin; + a = '2' + "4#{a}" + end; + end + + def test_compile_insn_inlinecache + assert_compile_once('Struct', result_inspect: 'Struct', insns: %i[opt_getinlinecache opt_setinlinecache]) + end + + def test_compile_insn_once + assert_compile_once('/#{true}/o =~ "true" && $~.to_a', result_inspect: '["true"]', insns: %i[once]) + end + + def test_compile_insn_checkmatch_opt_case_dispatch + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[opt_case_dispatch]) + begin; + case 'hello' + when 'hello' + 'world' + end + end; + end + + def test_compile_insn_opt_calc + assert_compile_once('4 + 2 - ((2 * 3 / 2) % 2)', result_inspect: '5', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) + assert_compile_once('4.0 + 2.0 - ((2.0 * 3.0 / 2.0) % 2.0)', result_inspect: '5.0', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) + assert_compile_once('4 + 2', result_inspect: '6') + end + + def test_compile_insn_opt_cmp + assert_compile_once('(1 == 1) && (1 != 2)', result_inspect: 'true', insns: %i[opt_eq opt_neq]) + end + + def test_compile_insn_opt_rel + assert_compile_once('1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1', result_inspect: 'true', insns: %i[opt_lt opt_le opt_gt opt_ge]) + end + + def test_compile_insn_opt_ltlt + assert_compile_once('[1] << 2', result_inspect: '[1, 2]', insns: %i[opt_ltlt]) + end + + def test_compile_insn_opt_and + assert_compile_once('1 & 3', result_inspect: '1', insns: %i[opt_and]) + end + + def test_compile_insn_opt_or + assert_compile_once('1 | 3', result_inspect: '3', insns: %i[opt_or]) + end + + def test_compile_insn_opt_aref + # optimized call (optimized JIT) -> send call + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '21', success_count: 2, min_calls: 1, insns: %i[opt_aref]) + begin; + obj = Object.new + def obj.[](h) + h + end + + block = proc { |h| h[1] } + print block.call({ 1 => 2 }) + print block.call(obj) + end; + + # send call -> optimized call (send JIT) -> optimized call + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '122', success_count: 2, min_calls: 2) + begin; + obj = Object.new + def obj.[](h) + h + end + + block = proc { |h| h[1] } + print block.call(obj) + print block.call({ 1 => 2 }) + print block.call({ 1 => 2 }) + end; + end + + def test_compile_insn_opt_aref_with + assert_compile_once("{ '1' => 2 }['1']", result_inspect: '2', insns: %i[opt_aref_with]) + end + + def test_compile_insn_opt_aset + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '5', insns: %i[opt_aset opt_aset_with]) + begin; + hash = { '1' => 2 } + (hash['2'] = 2) + (hash[1.to_s] = 3) + end; + end + + def test_compile_insn_opt_length_size + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '4', insns: %i[opt_length opt_size]) + begin; + array = [1, 2] + array.length + array.size + end; + end + + def test_compile_insn_opt_empty_p + assert_compile_once('[].empty?', result_inspect: 'true', insns: %i[opt_empty_p]) + end + + def test_compile_insn_opt_succ + assert_compile_once('1.succ', result_inspect: '2', insns: %i[opt_succ]) + end + + def test_compile_insn_opt_not + assert_compile_once('!!true', result_inspect: 'true', insns: %i[opt_not]) + end + + def test_compile_insn_opt_regexpmatch2 + assert_compile_once("/true/ =~ 'true'", result_inspect: '0', insns: %i[opt_regexpmatch2]) + assert_compile_once("'true' =~ /true/", result_inspect: '0', insns: %i[opt_regexpmatch2]) + end + + def test_compile_insn_opt_invokebuiltin_delegate_leave + iseq = eval(EnvUtil.invoke_ruby(['-e', <<~'EOS'], '', true).first) + p RubyVM::InstructionSequence.of("\x00".method(:unpack)).to_a + EOS + insns = collect_insns(iseq) + mark_tested_insn(:opt_invokebuiltin_delegate_leave, used_insns: insns) + assert_eval_with_jit('print "\x00".unpack("c")', stdout: '[0]', success_count: 1) + end + + def test_compile_insn_checkmatch + assert_compile_once("#{<<~"begin;"}\n#{<<~"end;"}", result_inspect: '"world"', insns: %i[checkmatch]) + begin; + ary = %w(hello good-bye) + case 'hello' + when *ary + 'world' + end + end; + end + + def test_jit_output + out, err = eval_with_jit('5.times { puts "MJIT" }', verbose: 1, min_calls: 5) + assert_equal("MJIT\n" * 5, out) + assert_match(/^#{JIT_SUCCESS_PREFIX}: block in <main>@-e:1 -> .+_ruby_mjit_p\d+u\d+\.c$/, err) + assert_match(/^Successful MJIT finish$/, err) + end + + def test_nothing_to_unload_with_jit_wait + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 11, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS) + begin; + def a1() a2() end + def a2() a3() end + def a3() a4() end + def a4() a5() end + def a5() a6() end + def a6() a7() end + def a7() a8() end + def a8() a9() end + def a9() a10() end + def a10() a11() end + def a11() print('hello') end + a1 + end; + end + + def test_unload_units_on_fiber + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: 'hello', success_count: 12, max_cache: 10, ignorable_patterns: MAX_CACHE_PATTERNS) + begin; + def a1() a2(false); a2(true) end + def a2(a) a3(a) end + def a3(a) a4(a) end + def a4(a) a5(a) end + def a5(a) a6(a) end + def a6(a) a7(a) end + def a7(a) a8(a) end + def a8(a) a9(a) end + def a9(a) a10(a) end + def a10(a) + if a + Fiber.new { a11 }.resume + end + end + def a11() print('hello') end + a1 + end; + end + + def test_unload_units_and_compaction + Dir.mktmpdir("jit_test_unload_units_") do |dir| + # MIN_CACHE_SIZE is 10 + out, err = eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~'end;'}", verbose: 1, min_calls: 1, max_cache: 10) + begin; + i = 0 + while i < 11 + eval(<<-EOS) + def mjit#{i} + print #{i} + end + mjit#{i} + EOS + i += 1 + end + + if defined?(fork) + # test the child does not try to delete files which are deleted by parent, + # and test possible deadlock on fork during MJIT unload and JIT compaction on child + Process.waitpid(Process.fork {}) + end + end; + + debug_info = %Q[stdout:\n"""\n#{out}\n"""\n\nstderr:\n"""\n#{err}"""\n] + assert_equal('012345678910', out, debug_info) + compactions, errs = err.lines.partition do |l| + l.match?(/\AJIT compaction \(\d+\.\dms\): Compacted \d+ methods /) + end + 10.times do |i| + assert_match(/\A#{JIT_SUCCESS_PREFIX}: mjit#{i}@\(eval\):/, errs[i], debug_info) + end + + assert_equal("No units can be unloaded -- incremented max-cache-size to 11 for --jit-wait\n", errs[10], debug_info) + assert_match(/\A#{JIT_SUCCESS_PREFIX}: mjit10@\(eval\):/, errs[11], debug_info) + # On --jit-wait, when the number of JIT-ed code reaches --jit-max-cache, + # it should trigger compaction. + unless RUBY_PLATFORM.match?(/mswin|mingw/) # compaction is not supported on Windows yet + assert_equal(1, compactions.size, debug_info) + end + + if RUBY_PLATFORM.match?(/mswin/) + # "Permission Denied" error is preventing to remove so file on AppVeyor/RubyCI. + skip 'Removing so file is randomly failing on AppVeyor/RubyCI mswin due to Permission Denied.' + else + # verify .c files are deleted on unload_units + assert_send([Dir, :empty?, dir], debug_info) + end + end + end + + def test_newarraykwsplat_on_stack + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "[nil, [{:type=>:development}]]\n", success_count: 1, insns: %i[newarraykwsplat]) + begin; + def arr + [nil, [:type => :development]] + end + p arr + end; + end + + def test_local_stack_on_exception + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '3', success_count: 2) + begin; + def b + raise + rescue + 2 + end + + def a + # Calling #b should be vm_exec, not direct mjit_exec. + # Otherwise `1` on local variable would be purged. + 1 + b + end + + print a + end; + end + + def test_local_stack_with_sp_motion_by_blockargs + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 2) + begin; + def b(base) + 1 + end + + # This method is simple enough to have false in catch_except_p. + # So local_stack_p would be true in JIT compiler. + def a + m = method(:b) + + # ci->flag has VM_CALL_ARGS_BLOCKARG and cfp->sp is moved in vm_caller_setup_arg_block. + # So, for this send insn, JIT-ed code should use cfp->sp instead of local variables for stack. + Module.module_eval(&m) + end + + print a + end; + end + + def test_catching_deep_exception + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '1', success_count: 4) + begin; + def catch_true(paths, prefixes) # catch_except_p: TRUE + prefixes.each do |prefix| # catch_except_p: TRUE + paths.each do |path| # catch_except_p: FALSE + return path + end + end + end + + def wrapper(paths, prefixes) + catch_true(paths, prefixes) + end + + print wrapper(['1'], ['2']) + end; + end + + def test_inlined_builtin_methods + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '', success_count: 1, min_calls: 2) + begin; + def test + float = 0.0 + float.abs + float.-@ + float.zero? + end + test + test + end; + end + + def test_inlined_c_method + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 2, recompile_count: 1, min_calls: 2) + begin; + def test(obj, recursive: nil) + if recursive + test(recursive) + end + obj.to_s + end + + print(test('a')) # set #to_s cc to String#to_s (expecting C method) + print(test('a')) # JIT with #to_s cc: String#to_s + # update #to_s cd->cc to Symbol#to_s, then go through the Symbol#to_s cd->cc + # after checking receiver class using inlined #to_s cc with String#to_s. + print(test('a', recursive: :foo)) + end; + end + + def test_inlined_exivar + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aaa", success_count: 3, recompile_count: 1, min_calls: 2) + begin; + class Foo < Hash + def initialize + @a = :a + end + + def bar + @a + end + end + + print(Foo.new.bar) + print(Foo.new.bar) # compile #initialize, #bar -> recompile #bar + print(Foo.new.bar) # compile #bar with exivar + end; + end + + def test_inlined_undefined_ivar + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "bbb", success_count: 3, min_calls: 3) + begin; + class Foo + def initialize + @a = :a + end + + def bar + if @b.nil? + @b = :b + end + end + end + + print(Foo.new.bar) + print(Foo.new.bar) + print(Foo.new.bar) + end; + end + + def test_inlined_setivar_frozen + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "FrozenError\n", success_count: 2, min_calls: 3) + begin; + class A + def a + @a = 1 + end + end + + a = A.new + a.a + a.a + a.a + a.freeze + begin + a.a + rescue FrozenError => e + p e.class + end + end; + end + + def test_inlined_getconstant + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '11', success_count: 1, min_calls: 2) + begin; + FOO = 1 + def const + FOO + end + print const + print const + end; + end + + def test_attr_reader + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "4nil\nnil\n6", success_count: 2, min_calls: 2) + begin; + class A + attr_reader :a, :b + + def initialize + @a = 2 + end + + def test + a + end + + def undefined + b + end + end + + a = A.new + print(a.test * a.test) + p(a.undefined) + p(a.undefined) + + # redefinition + def a.test + 3 + end + + print(2 * a.test) + end; + + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true", success_count: 1, min_calls: 2) + begin; + class Hoge + attr_reader :foo + + def initialize + @foo = [] + @bar = nil + end + end + + class Fuga < Hoge + def initialize + @bar = nil + @foo = [] + end + end + + def test(recv) + recv.foo.empty? + end + + hoge = Hoge.new + fuga = Fuga.new + + test(hoge) # VM: cc set index=1 + test(hoge) # JIT: compile with index=1 + test(fuga) # JIT -> VM: cc set index=2 + print test(hoge) # JIT: should use index=1, not index=2 in cc + end; + end + + def test_heap_promotion_of_ivar_in_the_middle_of_jit + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\n", success_count: 2, min_calls: 2) + begin; + class A + def initialize + @iv0 = nil + @iv1 = [] + @iv2 = nil + end + + def test(add) + @iv0.nil? + @iv2.nil? + add_ivar if add + @iv1.empty? + end + + def add_ivar + @iv3 = nil + end + end + + a = A.new + p a.test(false) + p a.test(true) + end; + end + + def test_jump_to_precompiled_branch + assert_eval_with_jit("#{<<~'begin;'}\n#{<<~'end;'}", stdout: ".0", success_count: 1, min_calls: 1) + begin; + def test(foo) + ".#{foo unless foo == 1}" if true + end + print test(0) + end; + end + + def test_clean_so + if RUBY_PLATFORM.match?(/mswin/) + skip 'Removing so file is randomly failing on AppVeyor/RubyCI mswin due to Permission Denied.' + end + Dir.mktmpdir("jit_test_clean_so_") do |dir| + code = "x = 0; 10.times {|i|x+=i}" + eval_with_jit({"TMPDIR"=>dir}, code) + assert_send([Dir, :empty?, dir]) + eval_with_jit({"TMPDIR"=>dir}, code, save_temps: true) + assert_not_send([Dir, :empty?, dir]) + end + end + + def test_clean_objects_on_exec + if /mswin|mingw/ =~ RUBY_PLATFORM + # TODO: check call stack and close handle of code which is not on stack, and remove objects on best-effort basis + skip 'Removing so file being used does not work on Windows' + end + Dir.mktmpdir("jit_test_clean_objects_on_exec_") do |dir| + eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 1) + begin; + def a; end; a + exec "true" + end; + error_message = "Undeleted files:\n #{Dir.glob("#{dir}/*").join("\n ")}\n" + assert_send([Dir, :empty?, dir], error_message) + end + end + + def test_lambda_longjmp + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '5', success_count: 1) + begin; + fib = lambda do |x| + return x if x == 0 || x == 1 + fib.call(x-1) + fib.call(x-2) + end + print fib.call(5) + end; + end + + def test_stack_pointer_with_assignment + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "nil\nnil\n", success_count: 1) + begin; + 2.times do + a, _ = nil + p a + end + end; + end + + def test_frame_omitted_inlining + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "true\ntrue\ntrue\n", success_count: 1, min_calls: 2) + begin; + class Integer + remove_method :zero? + def zero? + self == 0 + end + end + + 3.times do + p 0.zero? + end + end; + end + + def test_block_handler_with_possible_frame_omitted_inlining + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "70.0\n70.0\n70.0\n", success_count: 2, min_calls: 2) + begin; + def multiply(a, b) + a *= b + end + + 3.times do + p multiply(7.0, 10.0) + end + end; + end + + def test_builtin_frame_omitted_inlining + assert_eval_with_jit('0.zero?; 0.zero?; 3.times { p 0.zero? }', stdout: "true\ntrue\ntrue\n", success_count: 1, min_calls: 2) + end + + def test_program_counter_with_regexpmatch + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aa", success_count: 1) + begin; + 2.times do + break if /a/ =~ "ab" && !$~[0] + print $~[0] + end + end; + end + + def test_pushed_values_with_opt_aset_with + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "{}{}", success_count: 1) + begin; + 2.times do + print(Thread.current["a"] = {}) + end + end; + end + + def test_pushed_values_with_opt_aref_with + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "nil\nnil\n", success_count: 1) + begin; + 2.times do + p(Thread.current["a"]) + end + end; + end + + def test_mjit_pause_wait + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '', success_count: 0, min_calls: 1) + begin; + RubyVM::MJIT.pause + proc {}.call + end; + end + + def test_not_cancel_by_tracepoint_class + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 1, min_calls: 2) + begin; + TracePoint.new(:class) {}.enable + 2.times {} + end; + end + + def test_cancel_by_tracepoint + assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", success_count: 0, min_calls: 2) + begin; + TracePoint.new(:line) {}.enable + 2.times {} + end; + end + + def test_caller_locations_without_catch_table + out, _ = eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 1) + begin; + def b # 2 + caller_locations.first # 3 + end # 4 + # 5 + def a # 6 + print # <-- don't leave PC here # 7 + b # 8 + end + puts a + puts a + end; + lines = out.lines + assert_equal("-e:8:in `a'\n", lines[0]) + assert_equal("-e:8:in `a'\n", lines[1]) + end + + def test_fork_with_mjit_worker_thread + Dir.mktmpdir("jit_test_fork_with_mjit_worker_thread_") do |dir| + # min_calls: 2 to skip fork block + out, err = eval_with_jit({ "TMPDIR" => dir }, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 2, verbose: 1) + begin; + def before_fork; end + def after_fork; end + + before_fork; before_fork # the child should not delete this .o file + pid = Process.fork do # this child should not delete shared .pch file + sleep 2.0 # to prevent mixing outputs on Solaris + after_fork; after_fork # this child does not share JIT-ed after_fork with parent + end + after_fork; after_fork # this parent does not share JIT-ed after_fork with child + + Process.waitpid(pid) + end; + success_count = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size + debug_info = "stdout:\n```\n#{out}\n```\n\nstderr:\n```\n#{err}```\n" + assert_equal(3, success_count, debug_info) + + # assert no remove error + assert_equal("Successful MJIT finish\n" * 2, err.gsub(/^#{JIT_SUCCESS_PREFIX}:[^\n]+\n/, ''), debug_info) + + # ensure objects are deleted + assert_send([Dir, :empty?, dir], debug_info) + end + end if defined?(fork) + + private + + # The shortest way to test one proc + def assert_compile_once(script, result_inspect:, insns: [], uplevel: 1) + if script.match?(/\A\n.+\n\z/m) + script = script.gsub(/^/, ' ') + else + script = " #{script} " + end + assert_eval_with_jit("p proc {#{script}}.call", stdout: "#{result_inspect}\n", success_count: 1, insns: insns, uplevel: uplevel + 1) + end + + # Shorthand for normal test cases + def assert_eval_with_jit(script, stdout: nil, success_count:, recompile_count: nil, min_calls: 1, max_cache: 1000, insns: [], uplevel: 1, ignorable_patterns: []) + out, err = eval_with_jit(script, verbose: 1, min_calls: min_calls, max_cache: max_cache) + success_actual = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size + recompile_actual = err.scan(/^#{JIT_RECOMPILE_PREFIX}:/).size + # Add --mjit-verbose=2 logs for cl.exe because compiler's error message is suppressed + # for cl.exe with --mjit-verbose=1. See `start_process` in mjit_worker.c. + if RUBY_PLATFORM.match?(/mswin/) && success_count != success_actual + out2, err2 = eval_with_jit(script, verbose: 2, min_calls: min_calls, max_cache: max_cache) + end + + # Make sure that the script has insns expected to be tested + used_insns = method_insns(script) + insns.each do |insn| + mark_tested_insn(insn, used_insns: used_insns, uplevel: uplevel + 3) + end + + suffix = "script:\n#{code_block(script)}\nstderr:\n#{code_block(err)}#{( + "\nstdout(verbose=2 retry):\n#{code_block(out2)}\nstderr(verbose=2 retry):\n#{code_block(err2)}" if out2 || err2 + )}" + assert_equal( + success_count, success_actual, + "Expected #{success_count} times of JIT success, but succeeded #{success_actual} times.\n\n#{suffix}", + ) + if recompile_count + assert_equal( + recompile_count, recompile_actual, + "Expected #{success_count} times of JIT recompile, but recompiled #{success_actual} times.\n\n#{suffix}", + ) + end + if stdout + assert_equal(stdout, out, "Expected stdout #{out.inspect} to match #{stdout.inspect} with script:\n#{code_block(script)}") + end + err_lines = err.lines.reject! do |l| + l.chomp.empty? || l.match?(/\A#{JIT_SUCCESS_PREFIX}/) || (IGNORABLE_PATTERNS + ignorable_patterns).any? { |pat| pat.match?(l) } + end + unless err_lines.empty? + warn err_lines.join(''), uplevel: uplevel + end + end + + def mark_tested_insn(insn, used_insns:, uplevel: 1) + # Currently, this check emits a false-positive warning against opt_regexpmatch2, + # so the insn is excluded explicitly. See https://bugs.ruby-lang.org/issues/18269 + if !used_insns.include?(insn) && insn != :opt_regexpmatch2 + $stderr.puts + warn "'#{insn}' insn is not included in the script. Actual insns are: #{used_insns.join(' ')}\n", uplevel: uplevel + end + TestJIT.untested_insns.delete(insn) + end + + # Collect block's insns or defined method's insns, which are expected to be JIT-ed. + # Note that this intentionally excludes insns in script's toplevel because they are not JIT-ed. + def method_insns(script) + insns = [] + RubyVM::InstructionSequence.compile(script).to_a.last.each do |(insn, *args)| + case insn + when :send + insns += collect_insns(args.last) + when :definemethod, :definesmethod + insns += collect_insns(args[1]) + when :defineclass + insns += collect_insns(args[1]) + end + end + insns.uniq + end + + # Recursively collect insns in iseq_array + def collect_insns(iseq_array) + return [] if iseq_array.nil? + + insns = iseq_array.last.select { |x| x.is_a?(Array) }.map(&:first) + iseq_array.last.each do |(insn, *args)| + case insn + when :definemethod, :definesmethod, :send + insns += collect_insns(args.last) + end + end + insns + end +end |
