diff options
Diffstat (limited to 'test/ruby/test_yjit.rb')
| -rw-r--r-- | test/ruby/test_yjit.rb | 382 |
1 files changed, 352 insertions, 30 deletions
diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 35036aad4e..0d7fe66e1c 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -56,14 +56,26 @@ class TestYJIT < Test::Unit::TestCase def test_yjit_enable args = [] args << "--disable=yjit" if RubyVM::YJIT.enabled? - assert_separately(args, <<~RUBY) - assert_false RubyVM::YJIT.enabled? - assert_false RUBY_DESCRIPTION.include?("+YJIT") + assert_separately(args, <<~'RUBY') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" RubyVM::YJIT.enable - assert_true RubyVM::YJIT.enabled? - assert_true RUBY_DESCRIPTION.include?("+YJIT") + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+YJIT" + RUBY + end + + def test_yjit_disable + assert_separately(["--yjit", "--yjit-disable"], <<~'RUBY') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" + + RubyVM::YJIT.enable + + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+YJIT" RUBY end @@ -121,7 +133,7 @@ class TestYJIT < Test::Unit::TestCase end def test_yjit_enable_with_monkey_patch - assert_separately(%w[--yjit-disable], <<~RUBY) + assert_ruby_status(%w[--yjit-disable], <<~RUBY) # This lets rb_method_entry_at(rb_mKernel, ...) return NULL Kernel.prepend(Module.new) @@ -130,6 +142,36 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_yjit_enable_with_valid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', + 'RubyVM::YJIT.enable(call_threshold: 1); puts RubyVM::YJIT.enabled?']) do |stdout, stderr, _status| + assert_empty stderr + assert_include stdout.join, "true" + end + end + + def test_yjit_enable_with_invalid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + end + end + + def test_yjit_enable_with_invalid_runtime_mem_size_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + end + end + + if JITSupport.zjit_supported? + def test_yjit_enable_with_zjit_enabled + assert_in_out_err(['--zjit'], 'puts RubyVM::YJIT.enable', ['false'], ['Only one JIT can be enabled at the same time.']) + end + end + def test_yjit_stats_and_v_no_error _stdout, stderr, _status = invoke_ruby(%w(-v --yjit-stats), '', true, true) refute_includes(stderr, "NoMethodError") @@ -320,10 +362,10 @@ class TestYJIT < Test::Unit::TestCase 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]) + assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset], frozen_string_literal: false) + assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset], frozen_string_literal: false) end def test_compile_attr_set @@ -505,7 +547,7 @@ class TestYJIT < Test::Unit::TestCase end def test_opt_getconstant_path_slowpath - assert_compiles(<<~RUBY, exits: { opt_getconstant_path: 1 }, result: [42, 42, 1, 1], call_threshold: 2) + assert_compiles(<<~RUBY, result: [42, 42, 1, 1], call_threshold: 2) class A FOO = 42 class << self @@ -602,6 +644,40 @@ class TestYJIT < Test::Unit::TestCase RUBY end + STRUCT_MAX_EMBEDDED_MEMBERS = ( + GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] - + GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] - + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] + ) / RbConfig::SIZEOF["void*"] + + def test_spilled_struct_aref + omit("FIXME: https://github.com/Shopify/ruby/issues/977") + assert_compiles(<<~RUBY) + LargeStruct = Struct.new(:foo, :bar, *(#{STRUCT_MAX_EMBEDDED_MEMBERS} - 2).times.map { :"m_\#{it}" }) + + def foo(obj) + foo = obj.foo + raise "Expected 1, got: \#{foo}" unless foo == 1 + bar = obj.bar + raise "Expected 2, got: \#{bar}" unless bar == 2 + end + + embedded_struct = LargeStruct.new(1, 2) + # Bump RCLASS_MAX_IV_COUNT for LargeStruct + embedded_struct.instance_variable_set(:@test, 1) + + # Next allocation reserves space for the imemo/fields reference. + heap_struct = LargeStruct.new(1, 2) + + RubyVM::YJIT.reset_stats! + + foo(embedded_struct) + foo(embedded_struct) + foo(heap_struct) + foo(heap_struct) + RUBY + end + def test_struct_aset assert_compiles(<<~RUBY) def foo(obj) @@ -615,6 +691,26 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_struct_aset_guards_recv_is_not_frozen + assert_compiles(<<~RUBY, result: :ok, exits: { opt_send_without_block: 1 }) + def foo(obj) + obj.foo = 123 + end + + Foo = Struct.new(:foo) + obj = Foo.new(123) + 100.times do + foo(obj) + end + obj.freeze + begin + foo(obj) + rescue FrozenError + :ok + end + RUBY + end + def test_getblockparam assert_compiles(<<~'RUBY', insns: [:getblockparam]) def foo &blk @@ -911,6 +1007,40 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_super_bmethod + # Bmethod defined at class scope + assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: true, exits: {}) + class SuperItself + define_method(:itself) { super() } + end + + obj = SuperItself.new + obj.itself + obj.itself == obj + RUBY + + # Bmethod defined inside a method (the block's local_iseq is ISEQ_TYPE_METHOD + # but the CME is at the bmethod frame, not the enclosing method's frame) + assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Base#foo via bmethod", exits: {}) + class Base + def foo = "Base#foo" + end + + class SetupHelper + def add_bmethod_to(klass) + klass.define_method(:foo) { super() + " via bmethod" } + end + end + + class Target < Base; end + + SetupHelper.new.add_bmethod_to(Target) + obj = Target.new + obj.foo + obj.foo + RUBY + end + # Tests calling a variadic cfunc with many args def test_build_large_struct assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], call_threshold: 2) @@ -1138,6 +1268,8 @@ class TestYJIT < Test::Unit::TestCase end def test_code_gc_with_auto_compact + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + assert_compiles((code_gc_helpers + <<~'RUBY'), exits: :any, result: :ok, mem_size: 1, code_gc: true) # Test ISEQ moves in the middle of code GC GC.auto_compact = true @@ -1266,6 +1398,8 @@ class TestYJIT < Test::Unit::TestCase end def test_gc_compact_cyclic_branch + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + assert_compiles(<<~'RUBY', result: 2) def foo i = 0 @@ -1300,7 +1434,7 @@ class TestYJIT < Test::Unit::TestCase end def test_tracing_str_uplus - assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1, definemethod: 1 }) + assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1 }) def str_uplus _ = 1 _ = 2 @@ -1458,7 +1592,7 @@ class TestYJIT < Test::Unit::TestCase end def test_str_concat_encoding_mismatch - assert_compiles(<<~'RUBY', result: "incompatible character encodings: ASCII-8BIT and EUC-JP") + assert_compiles(<<~'RUBY', result: "incompatible character encodings: BINARY (ASCII-8BIT) and EUC-JP") def bar(a, b) a << b rescue => e @@ -1471,8 +1605,8 @@ class TestYJIT < Test::Unit::TestCase end h = Hash.new { nil } - foo("\x80".b, "\xA1A1".force_encoding("EUC-JP"), h) - foo("\x80".b, "\xA1A1".force_encoding("EUC-JP"), h) + foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h) + foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h) RUBY end @@ -1488,14 +1622,6 @@ class TestYJIT < Test::Unit::TestCase RUBY end - def test_opt_aref_with - assert_compiles(<<~RUBY, insns: %i[opt_aref_with], result: "bar") - h = {"foo" => "bar"} - - h["foo"] - RUBY - end - def test_proc_block_arg assert_compiles(<<~RUBY, result: [:proc, :no_block]) def yield_if_given = block_given? ? yield : :no_block @@ -1574,26 +1700,222 @@ class TestYJIT < Test::Unit::TestCase end def test_kw_splat_nil - assert_compiles(<<~'RUBY', result: %i[ok ok ok], no_send_fallbacks: true) + assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true) def id(x) = x def kw_fw(arg, **) = id(arg, **) - def fw(...) = id(...) - def use = [fw(:ok), kw_fw(:ok), :ok.itself(**nil)] + def use = [kw_fw(:ok), :ok.itself(**nil)] use RUBY end def test_empty_splat - assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true) + assert_compiles(<<~'RUBY', result: :ok, no_send_fallbacks: true) def foo = :ok - def fw(...) = foo(...) - def use(empty) = [foo(*empty), fw] + def use(empty) = foo(*empty) use([]) RUBY end + def test_byteslice_sp_invalidation + assert_compiles(<<~'RUBY', result: 'ok', no_send_fallbacks: true) + "okng".itself.byteslice(0, 2) + RUBY + end + + def test_leaf_builtin + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: 1) + before = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf] + return 1 if before.nil? + + def entry = self.class + entry + + after = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf] + after - before + RUBY + end + + def test_runtime_stats_types + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + stats = RubyVM::YJIT.runtime_stats + return true unless stats[:all_stats] + + [ + stats[:object_shape_count].is_a?(Integer), + stats[:ratio_in_yjit].nil? || stats[:ratio_in_yjit].is_a?(Float), + ].all? + RUBY + end + + def test_runtime_stats_key_arg + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + # Collect single stat. + stat = RubyVM::YJIT.runtime_stats(:yjit_alloc_size) + + # Ensure this invocation had stats. + return true unless RubyVM::YJIT.runtime_stats[:all_stats] + + stat > 0.0 + RUBY + end + + def test_runtime_stats_arg_error + assert_compiles(<<~'RUBY', exits: :any, result: true) + begin + RubyVM::YJIT.runtime_stats(Object.new) + :no_error + rescue TypeError => e + e.message == "non-symbol given" + end + RUBY + end + + def test_runtime_stats_unknown_key + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + RubyVM::YJIT.runtime_stats(:some_key_unlikely_to_exist).nil? + RUBY + end + + def test_yjit_option_uses_array_each_in_ruby + assert_separately(["--yjit"], <<~'RUBY') + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # The backtrace, however, should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_replaces_array_each + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C for the interpreter + assert_nil Array.instance_method(:each).source_location + + # The backtrace should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + + RubyVM::YJIT.enable + + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # However, the backtrace should still not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:23:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_preserves_array_each_monkey_patch + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C initially + assert_nil Array.instance_method(:each).source_location + + # Override Array#each + $called = false + Array.prepend(Module.new { + def each + $called = true + super + end + }) + + RubyVM::YJIT.enable + + # The monkey-patch should still be alive + [].each {} + assert_true $called + + # YJIT should not replace Array#each with the "<internal:array>" one + assert_equal "-", Array.instance_method(:each).source_location.first + RUBY + end + + def test_yield_kwargs + assert_compiles(<<~RUBY, result: 3, no_send_fallbacks: true) + def req2kws = yield a: 1, b: 2 + + req2kws { |a:, b:| a + b } + RUBY + end + + def test_proc_block_with_kwrest + # When the bug was present this required --yjit-stats to trigger. + assert_compiles(<<~RUBY, result: {extra: 5}) + def foo = bar(w: 1, x: 2, y: 3, z: 4, extra: 5, &proc { _1 }) + def bar(w:, x:, y:, z:, **kwrest) = yield kwrest + + GC.stress = true + foo + foo + RUBY + end + + def test_yjit_dump_insns + # Testing that this undocumented debugging feature doesn't crash + args = [ + '--yjit-call-threshold=1', + '--yjit-dump-insns', + '-e def foo(case:) = {case:}[:case]', + '-e foo(case:0)', + ] + _out, _err, status = invoke_ruby(args, '', true, true) + assert_not_predicate(status, :signaled?) + end + + def test_yjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--yjit"], "", ignore_stderr: true) + end + end + + def test_exceptional_entry_into_env_escaped_before_yjit_enablement + threshold = 2 + assert_separately(["--disable-all", "--yjit-disable", "--yjit-call-threshold=#{threshold}"], <<~RUBY) + def run + @captured_env = ->{} + RubyVM::YJIT.enable + + i = 0 + while i < #{threshold} + next_i = i + 1 + from_break = tap { break i + 1 } # break from the block generates an exceptional entry + assert_equal(from_break, next_i, '[Bug #21941]') + i = next_i + end + end + + run + assert_equal(#{threshold}, @captured_env.binding.local_variable_get(:i)) + RUBY + end + private def code_gc_helpers @@ -1652,7 +1974,7 @@ class TestYJIT < Test::Unit::TestCase RUBY script = <<~RUBY - #{"# frozen_string_literal: true" if frozen_string_literal} + #{"# frozen_string_literal: " + frozen_string_literal.to_s unless frozen_string_literal.nil?} _test_proc = -> { #{test_script} } |
