summaryrefslogtreecommitdiff
path: root/test/ruby/test_yjit.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_yjit.rb')
-rw-r--r--test/ruby/test_yjit.rb382
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}
}