diff options
Diffstat (limited to 'bootstraptest')
| -rwxr-xr-x | bootstraptest/runner.rb | 65 | ||||
| -rw-r--r-- | bootstraptest/test_eval.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_fiber.rb | 5 | ||||
| -rw-r--r-- | bootstraptest/test_flow.rb | 4 | ||||
| -rw-r--r-- | bootstraptest/test_fork.rb | 27 | ||||
| -rw-r--r-- | bootstraptest/test_gc.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_insns.rb | 20 | ||||
| -rw-r--r-- | bootstraptest/test_io.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_literal.rb | 8 | ||||
| -rw-r--r-- | bootstraptest/test_load.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_method.rb | 66 | ||||
| -rw-r--r-- | bootstraptest/test_ractor.rb | 1824 | ||||
| -rw-r--r-- | bootstraptest/test_rjit.rb | 80 | ||||
| -rw-r--r-- | bootstraptest/test_syntax.rb | 12 | ||||
| -rw-r--r-- | bootstraptest/test_thread.rb | 5 | ||||
| -rw-r--r-- | bootstraptest/test_yjit.rb | 674 | ||||
| -rw-r--r-- | bootstraptest/test_yjit_rust_port.rb | 8 |
17 files changed, 2003 insertions, 803 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index 24324fa51f..04de0c93b9 100755 --- a/bootstraptest/runner.rb +++ b/bootstraptest/runner.rb @@ -16,6 +16,7 @@ rescue LoadError $:.unshift File.join(File.dirname(__FILE__), '../lib') retry end +require_relative '../tool/lib/test/jobserver' if !Dir.respond_to?(:mktmpdir) # copied from lib/tmpdir.rb @@ -110,35 +111,7 @@ BT = Class.new(bt) do def wn=(wn) unless wn == 1 - if /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ ENV.delete("MAKEFLAGS") - begin - if fifo = $3 - fifo.gsub!(/\\(?=.)/, '') - r = File.open(fifo, IO::RDONLY|IO::NONBLOCK|IO::BINARY) - w = File.open(fifo, IO::WRONLY|IO::NONBLOCK|IO::BINARY) - else - r = IO.for_fd($1.to_i(10), "rb", autoclose: false) - w = IO.for_fd($2.to_i(10), "wb", autoclose: false) - end - rescue - r.close if r - else - r.close_on_exec = true - w.close_on_exec = true - tokens = r.read_nonblock(wn > 0 ? wn : 1024, exception: false) - r.close - if String === tokens - tokens.freeze - auth = w - w = nil - at_exit {auth << tokens; auth.close} - wn = tokens.size + 1 - else - w.close - wn = 1 - end - end - end + wn = Test::JobServer.max_jobs(wn > 0 ? wn : 1024, ENV.delete("MAKEFLAGS")) || wn if wn <= 0 require 'etc' wn = [Etc.nprocessors / 2, 1].max @@ -163,7 +136,7 @@ def main BT.tty = nil BT.quiet = false BT.timeout = 180 - BT.timeout_scale = (defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? ? 3 : 1) # for --jit-wait + BT.timeout_scale = 1 if (ts = (ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"]).to_i) > 1 BT.timeout_scale *= ts end @@ -236,7 +209,19 @@ End return true end - require_relative '../tool/lib/launchable' + begin + require_relative '../tool/lib/launchable' + rescue LoadError + # The following error sometimes happens, so we're going to skip writing Launchable report files in this case. + # + # ``` + # /tmp/tmp.bISss9CtXZ/.ext/common/json/ext.rb:15:in 'Kernel#require': + # /tmp/tmp.bISss9CtXZ/.ext/x86_64-linux/json/ext/parser.so: + # undefined symbol: ruby_abi_version - ruby_abi_version (LoadError) + # ``` + # + return true + end BT.launchable_test_reports = writer = Launchable::JsonStreamWriter.new($1) writer.write_array('testCases') at_exit { @@ -286,7 +271,7 @@ End if defined?(RUBY_DESCRIPTION) puts "Driver is #{RUBY_DESCRIPTION}" elsif defined?(RUBY_PATCHLEVEL) - puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PLATFORM}) [#{RUBY_PLATFORM}]" + puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]" else puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" end @@ -342,6 +327,7 @@ def concurrent_exec_test begin while BT.wn != term_wn if r = rq.pop + BT_STATE.count += 1 case when BT.quiet when BT.tty @@ -399,7 +385,7 @@ module Launchable } ) @@duration = 0 - @@failure_log.clear + @@failure_log = '' end @@last_test_name = relative_path @@duration += t @@ -612,6 +598,8 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc) end end + class Timeout < StandardError; end + def get_result_string(opt = '', timeout: BT.timeout, **argh) if BT.ruby timeout = BT.apply_timeout_scale(timeout) @@ -621,7 +609,11 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc) out = IO.popen("#{BT.ruby} -W0 #{opt} #{filename}", **kw) pid = out.pid th = Thread.new {out.read.tap {Process.waitpid(pid); out.close}} - th.value if th.join(timeout) + if th.join(timeout) + th.value + else + Timeout.new("timed out after #{timeout} seconds") + end ensure raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] @@ -878,9 +870,8 @@ def yjit_enabled? ENV.key?('RUBY_YJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('yjit') || BT.ruby.include?('yjit') end -def rjit_enabled? - # Don't check `RubyVM::RJIT.enabled?`. On btest-bruby, target Ruby != runner Ruby. - ENV.fetch('RUN_OPTS', '').include?('rjit') +def zjit_enabled? + ENV.key?('RUBY_ZJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('zjit') || BT.ruby.include?('zjit') end exit main diff --git a/bootstraptest/test_eval.rb b/bootstraptest/test_eval.rb index d923a957bc..20bd9615f4 100644 --- a/bootstraptest/test_eval.rb +++ b/bootstraptest/test_eval.rb @@ -217,7 +217,7 @@ assert_equal %q{[10, main]}, %q{ } %w[break next redo].each do |keyword| - assert_match %r"Can't escape from eval with #{keyword}\b", %{ + assert_match %r"Invalid #{keyword}\b", %{ $stderr = STDOUT begin eval "0 rescue #{keyword}" diff --git a/bootstraptest/test_fiber.rb b/bootstraptest/test_fiber.rb index 2614dd13bf..ae809a5936 100644 --- a/bootstraptest/test_fiber.rb +++ b/bootstraptest/test_fiber.rb @@ -37,3 +37,8 @@ assert_normal_exit %q{ assert_normal_exit %q{ Fiber.new(&Object.method(:class_eval)).resume("foo") }, '[ruby-dev:34128]' + +# [Bug #21400] +assert_normal_exit %q{ + Thread.new { Fiber.current.kill }.join +} diff --git a/bootstraptest/test_flow.rb b/bootstraptest/test_flow.rb index 15528a4213..7a95def1e6 100644 --- a/bootstraptest/test_flow.rb +++ b/bootstraptest/test_flow.rb @@ -376,7 +376,7 @@ assert_equal %q{[1, 4, 7, 5, 8, 9]}, %q{$a = []; begin; ; $a << 1 ; $a << 3 end; $a << 4 def m2; $a << 5 - m1(:a, :b, (return 1; :c)); $a << 6 + m1(:a, :b, (return 1 if true; :c)); $a << 6 end; $a << 7 m2; $a << 8 ; $a << 9 @@ -399,7 +399,7 @@ assert_equal %q{[1, 3, 11, 4, 5, 6, 7, 12, 13]}, %q{$a = []; begin; ; $a << 1 m2(begin; $a << 5 2; $a << 6 ensure; $a << 7 - return 3; $a << 8 + return 3 if true; $a << 8 end); $a << 9 4; $a << 10 end; $a << 11 diff --git a/bootstraptest/test_fork.rb b/bootstraptest/test_fork.rb index 83923dad97..860ef285d0 100644 --- a/bootstraptest/test_fork.rb +++ b/bootstraptest/test_fork.rb @@ -75,3 +75,30 @@ assert_equal '[1, 2]', %q{ end }, '[ruby-dev:44005] [Ruby 1.9 - Bug #4950]' +assert_equal 'ok', %q{ + def now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + Thread.new do + loop { sleep 0.0001 } + end + + 10.times do + pid = fork{ exit!(0) } + deadline = now + 10 + while true + _, status = Process.waitpid2(pid, Process::WNOHANG) + break if status + if now > deadline + Process.kill(:KILL, pid) + raise "failed" + end + sleep 0.001 + end + unless status.success? + raise "child exited with status #{status}" + end + rescue NotImplementedError + end + :ok +}, '[Bug #20670]' + diff --git a/bootstraptest/test_gc.rb b/bootstraptest/test_gc.rb index 17bc497822..eb68c9845e 100644 --- a/bootstraptest/test_gc.rb +++ b/bootstraptest/test_gc.rb @@ -14,7 +14,7 @@ ms = "a".."k" o.send(meth) end end -}, '[ruby-dev:39453]' unless rjit_enabled? # speed up RJIT CI +}, '[ruby-dev:39453]' assert_normal_exit %q{ a = [] diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb index 18ab1800bd..1f70c8075c 100644 --- a/bootstraptest/test_insns.rb +++ b/bootstraptest/test_insns.rb @@ -86,7 +86,7 @@ tests = [ [ 'putobject', %q{ /(?<x>x)/ =~ "x"; x == "x" }, ], [ 'putspecialobject', %q{ {//=>true}[//] }, ], - [ 'putstring', %q{ "true" }, ], + [ 'dupstring', %q{ "true" }, ], [ 'tostring / concatstrings', %q{ "#{true}" }, ], [ 'toregexp', %q{ /#{true}/ =~ "true" && $~ }, ], [ 'intern', %q{ :"#{true}" }, ], @@ -214,8 +214,21 @@ tests = [ 'true'.freeze }, + [ 'opt_duparray_send', %q{ x = :a; [:a, :b].include?(x) }, ], + [ 'opt_duparray_send', <<-'},', ], # { + class Array + def include?(i) + i == 1 + end + end + x = 1 + [:a, :b].include?(x) + }, + [ 'opt_newarray_send', %q{ ![ ].hash.nil? }, ], + [ 'opt_newarray_send', %q{ v=2; [1, Object.new, 2].include?(v) }, ], + [ 'opt_newarray_send', %q{ [ ].max.nil? }, ], [ 'opt_newarray_send', %q{ [1, x = 2, 3].max == 3 }, ], [ 'opt_newarray_send', <<-'},', ], # { @@ -413,11 +426,6 @@ tests = [ x&.x[true] ||= true # here }, - [ 'opt_aref_with', %q{ { 'true' => true }['true'] }, ], - [ 'opt_aref_with', %q{ Struct.new(:nil).new['nil'].nil? }, ], - [ 'opt_aset_with', %q{ {}['true'] = true }, ], - [ 'opt_aset_with', %q{ Struct.new(:true).new['true'] = true }, ], - [ 'opt_length', %q{ 'true' .length == 4 }, ], [ 'opt_length', %q{ :true .length == 4 }, ], [ 'opt_length', %q{ [ 'true' ] .length == 1 }, ], diff --git a/bootstraptest/test_io.rb b/bootstraptest/test_io.rb index 4e5d6d59c9..4081769a8c 100644 --- a/bootstraptest/test_io.rb +++ b/bootstraptest/test_io.rb @@ -85,7 +85,7 @@ assert_normal_exit %q{ ARGF.set_encoding "foo" } -/freebsd/ =~ RUBY_PLATFORM or +/(freebsd|mswin)/ =~ RUBY_PLATFORM or 10.times do assert_normal_exit %q{ at_exit { p :foo } diff --git a/bootstraptest/test_literal.rb b/bootstraptest/test_literal.rb index a30661a796..39e6527027 100644 --- a/bootstraptest/test_literal.rb +++ b/bootstraptest/test_literal.rb @@ -117,16 +117,16 @@ assert_equal '1', 'a = [obj = Object.new]; a.size' assert_equal 'true', 'a = [obj = Object.new]; a[0] == obj' assert_equal '5', 'a = [1,2,3]; a[1] = 5; a[1]' assert_equal 'bar', '[*:foo];:bar' -assert_equal '[1, 2]', 'def nil.to_a; [2]; end; [1, *nil]' -assert_equal '[1, 2]', 'def nil.to_a; [1, 2]; end; [*nil]' -assert_equal '[0, 1, {2=>3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]" +assert_equal '[]', 'def nil.to_a; [1, 2]; end; [*nil]' +assert_equal '[1]', 'def nil.to_a; [2]; end; [1, *nil]' +assert_equal '[0, 1, {2 => 3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]" # hash assert_equal 'Hash', '{}.class' assert_equal '{}', '{}.inspect' assert_equal 'Hash', '{1=>2}.class' -assert_equal '{1=>2}', '{1=>2}.inspect' +assert_equal '{1 => 2}', '{1=>2}.inspect' assert_equal '2', 'h = {1 => 2}; h[1]' assert_equal '0', 'h = {1 => 2}; h.delete(1); h.size' assert_equal '', 'h = {1 => 2}; h.delete(1); h[1]' diff --git a/bootstraptest/test_load.rb b/bootstraptest/test_load.rb index 5046d4fcea..fa8d31c098 100644 --- a/bootstraptest/test_load.rb +++ b/bootstraptest/test_load.rb @@ -12,7 +12,7 @@ assert_equal 'ok', %q{ } }.map {|t| t.value } vs[0] == M && vs[1] == M ? :ok : :ng -}, '[ruby-dev:32048]' unless ENV.fetch('RUN_OPTS', '').include?('rjit') # Thread seems to be switching during JIT. To be fixed later. +}, '[ruby-dev:32048]' assert_equal 'ok', %q{ %w[a a/foo b].each {|d| Dir.mkdir(d)} diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb index af9443b8c6..e894f6f601 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -1374,3 +1374,69 @@ assert_equal 'ok', %q{ foo(:foo, b: :ok) foo(*["foo"], b: :ok) } + +assert_equal 'ok', %q{ + Thing = Struct.new(:value) + + Obj = Thing.new("ok") + + def delegate(...) + Obj.value(...) + end + + def no_args + delegate + end + + def splat_args(*args) + delegate(*args) + end + + no_args + splat_args +} + +assert_equal 'ok', %q{ + class A + private + def foo = "ng" + end + + class B + def initialize(o) + @o = o + end + + def foo(...) = @o.foo(...) + def internal_foo = foo + end + + b = B.new(A.new) + + begin + b.internal_foo + rescue NoMethodError + "ok" + end +} + +assert_equal 'ok', <<~RUBY + def test(*, kw: false) + "ok" + end + + test +RUBY + +assert_equal '[1, 2, 3]', %q{ + def target(*args) = args + def x = [1] + def forwarder(...) = target(*x, 2, ...) + forwarder(3).inspect +}, '[Bug #21832] post-splat args before forwarding' + +assert_equal '[nil, nil]', %q{ + def self_reading(a = a, kw:) = a + def through_binding(a = binding.local_variable_get(:a), kw:) = a + [self_reading(kw: 1), through_binding(kw: 1)] +}, 'nil initialization of optional parameters' diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index b6a4ef605e..4fe90703fc 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -67,7 +67,7 @@ assert_equal "#<Ractor:#1 running>", %q{ # Return id, loc, and status for no-name ractor assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{ r = Ractor.new { '' } - r.take + r.join sleep 0.1 until r.inspect =~ /terminated/ r.inspect } @@ -75,7 +75,7 @@ assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{ # Return id, name, loc, and status for named ractor assert_match /^#<Ractor:#([^ ]*?) Test Ractor .+:[0-9]+ terminated>$/, %q{ r = Ractor.new(name: 'Test Ractor') { '' } - r.take + r.join sleep 0.1 until r.inspect =~ /terminated/ r.inspect } @@ -86,7 +86,7 @@ assert_equal 'ok', %q{ r = Ractor.new do 'ok' end - r.take + r.value } # Passed arguments to Ractor.new will be a block parameter @@ -96,7 +96,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ok' do |msg| msg end - r.take + r.value } # Pass multiple arguments to Ractor.new @@ -105,7 +105,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ping', 'pong' do |msg, msg2| [msg, msg2] end - 'ok' if r.take == ['ping', 'pong'] + 'ok' if r.value == ['ping', 'pong'] } # Ractor#send passes an object with copy to a Ractor @@ -115,65 +115,23 @@ assert_equal 'ok', %q{ msg = Ractor.receive end r.send 'ok' - r.take + r.value } # Ractor#receive_if can filter the message -assert_equal '[2, 3, 1]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 - end - a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| msg == 3} - a << Ractor.receive -} - -# Ractor#receive_if with break -assert_equal '[2, [1, :break], 3]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 - end - - a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| break [msg, :break]} - a << Ractor.receive -} +assert_equal '[1, 2, 3]', %q{ + ports = 3.times.map{Ractor::Port.new} -# Ractor#receive_if can't be called recursively -assert_equal '[[:e1, 1], [:e2, 2]]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 + r = Ractor.new ports do |ports| + ports[0] << 3 + ports[1] << 1 + ports[2] << 2 end - a = [] - - Ractor.receive_if do |msg| - begin - Ractor.receive - rescue Ractor::Error - a << [:e1, msg] - end - true # delete 1 from queue - end - - Ractor.receive_if do |msg| - begin - Ractor.receive_if{} - rescue Ractor::Error - a << [:e2, msg] - end - true # delete 2 from queue - end - - a # + a << ports[1].receive # 1 + a << ports[2].receive # 2 + a << ports[0].receive # 3 + a } # dtoa race condition @@ -184,73 +142,145 @@ assert_equal '[:ok, :ok, :ok]', %q{ 10_000.times{ rand.to_s } :ok } - }.map(&:take) + }.map(&:value) +} + +assert_equal "42", %q{ + a = 42 + Ractor.shareable_lambda{ a }.call } -# Ractor.make_shareable issue for locals in proc [Bug #18023] +# Ractor.shareable_proc issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e]', %q{ v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e - closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } } + closure = Proc.new { [v1, v2, v3, v4, v5] } + Ractor.shareable_proc(&closure).call +} + +# Ractor.shareable_proc makes a copy of given Proc +assert_equal '[true, true]', %q{ + pr1 = Proc.new do + self + end + pr2 = Ractor.shareable_proc(&pr1) - Ractor.make_shareable(closure).call + [pr1.call == self, pr2.call == nil] } -# Ractor.make_shareable issue for locals in proc [Bug #18023] -assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{ - a = :a - closure = Ractor.current.instance_eval do - -> { - b, c, d = :b, :c, :d - -> { - e, f, g = :e, :f, :g - -> { [a, b, c, d, e, f, g] } - }.call - }.call +# Ractor.shareable_proc keeps the original Proc intact +assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{ + a = 42 + pr1 = Proc.new do + [self.class, eval("a"), binding.local_variable_get(:a)] end + a += 1 + pr2 = Ractor.shareable_proc(&pr1) - Ractor.make_shareable(closure).call + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end + + r << pr1.call << pr1.binding.class } -# Now autoload in non-main Ractor is not supported -assert_equal 'ok', %q{ - autoload :Foo, 'foo.rb' - r = Ractor.new do - p Foo - rescue Ractor::UnsafeError - :ok +# Ractor.make_shareable mutates the original Proc +# This is the current behavior, it's currently considered safe enough +# because in most cases it would raise anyway due to not-shared self or not-shared captured variable value +assert_equal '[[42, 42], Binding, true, SyntaxError, "Can\'t create Binding from isolated Proc"]', %q{ + a = 42 + pr1 = nil.instance_exec do + Proc.new do + [eval("a"), binding.local_variable_get(:a)] + end + end + + r = [pr1.call, pr1.binding.class] + + pr2 = Ractor.make_shareable(pr1) + r << pr1.equal?(pr2) + + begin + pr1.call + rescue SyntaxError + r << SyntaxError + end + + begin + r << pr1.binding + rescue ArgumentError + r << $!.message + end + + r +} + +# Ractor::IsolationError cases +assert_equal '3', %q{ + ok = 0 + + begin + a = 1 + Ractor.shareable_proc{a} + a = 2 + rescue Ractor::IsolationError => e + ok += 1 + end + + begin + cond = false + b = 1 + b = 2 if cond + Ractor.shareable_proc{b} + rescue Ractor::IsolationError => e + ok += 1 + end + + begin + 1.times{|i| + i = 2 + Ractor.shareable_proc{i} + } + rescue Ractor::IsolationError => e + ok += 1 end - r.take } ### ### # Ractor still has several memory corruption so skip huge number of tests -if ENV['GITHUB_WORKFLOW'] && - ENV['GITHUB_WORKFLOW'] == 'Compilations' +if ENV['GITHUB_WORKFLOW'] == 'Compilations' # ignore the follow else -# Ractor.select(*ractors) receives a values from a ractors. -# It is similar to select(2) and Go's select syntax. -# The return value is [ch, received_value] +# Ractor.select with a Ractor argument assert_equal 'ok', %q{ # select 1 r1 = Ractor.new{'r1'} - r, obj = Ractor.select(r1) - 'ok' if r == r1 and obj == 'r1' + port, obj = Ractor.select(r1) + if port == r1 and obj == 'r1' + 'ok' + else + # failed + [port, obj].inspect + end } # Ractor.select from two ractors. assert_equal '["r1", "r2"]', %q{ # select 2 - r1 = Ractor.new{'r1'} - r2 = Ractor.new{'r2'} - rs = [r1, r2] + p1 = Ractor::Port.new + p2 = Ractor::Port.new + r1 = Ractor.new(p1){|p1| p1 << 'r1'} + r2 = Ractor.new(p2){|p2| p2 << 'r2'} + ps = [p1, p2] as = [] - r, obj = Ractor.select(*rs) - rs.delete(r) + port, obj = Ractor.select(*ps) + ps.delete(port) as << obj - r, obj = Ractor.select(*rs) + port, obj = Ractor.select(*ps) as << obj as.sort #=> ["r1", "r2"] } @@ -286,7 +316,7 @@ assert_equal 30.times.map { 'ok' }.to_s, %q{ } unless (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878 # Exception for empty select -assert_match /specify at least one ractor/, %q{ +assert_match /specify at least one Ractor::Port or Ractor/, %q{ begin Ractor.select rescue ArgumentError => e @@ -294,30 +324,12 @@ assert_match /specify at least one ractor/, %q{ end } -# Outgoing port of a ractor will be closed when the Ractor is terminated. -assert_equal 'ok', %q{ - r = Ractor.new do - 'finish' - end - - r.take - sleep 0.1 until r.inspect =~ /terminated/ - - begin - o = r.take - rescue Ractor::ClosedError - 'ok' - else - "ng: #{o}" - end -} - # Raise Ractor::ClosedError when try to send into a terminated ractor assert_equal 'ok', %q{ r = Ractor.new do end - r.take # closed + r.join # closed sleep 0.1 until r.inspect =~ /terminated/ begin @@ -329,47 +341,16 @@ assert_equal 'ok', %q{ end } -# Raise Ractor::ClosedError when try to send into a closed actor -assert_equal 'ok', %q{ - r = Ractor.new { Ractor.receive } - r.close_incoming - - begin - r.send(1) - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# Raise Ractor::ClosedError when try to take from closed actor -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.yield 1 - Ractor.receive - end - - r.close_outgoing - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# Can mix with Thread#interrupt and Ractor#take [Bug #17366] +# Can mix with Thread#interrupt and Ractor#join [Bug #17366] assert_equal 'err', %q{ - Ractor.new{ + Ractor.new do t = Thread.current begin Thread.new{ t.raise "err" }.join rescue => e e.message end - }.take + end.value } # Killed Ractor's thread yields nil @@ -377,34 +358,18 @@ assert_equal 'nil', %q{ Ractor.new{ t = Thread.current Thread.new{ t.kill }.join - }.take.inspect #=> nil + }.value.inspect #=> nil } -# Ractor.yield raises Ractor::ClosedError when outgoing port is closed. +# Raise Ractor::ClosedError when try to send into a ractor with closed default port assert_equal 'ok', %q{ - r = Ractor.new Ractor.current do |main| + r = Ractor.new { + Ractor.current.close + Ractor.main << :ok Ractor.receive - main << true - Ractor.yield 1 - end - - r.close_outgoing - r << true - Ractor.receive - - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} + } -# Raise Ractor::ClosedError when try to send into a ractor with closed incoming port -assert_equal 'ok', %q{ - r = Ractor.new { Ractor.receive } - r.close_incoming + Ractor.receive # wait for ok begin r.send(1) @@ -415,154 +380,82 @@ assert_equal 'ok', %q{ end } -# A ractor with closed incoming port still can send messages out -assert_equal '[1, 2]', %q{ - r = Ractor.new do - Ractor.yield 1 - 2 - end - r.close_incoming - - [r.take, r.take] -} - -# Raise Ractor::ClosedError when try to take from a ractor with closed outgoing port -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.yield 1 - Ractor.receive - end - - sleep 0.01 # wait for Ractor.yield in r - r.close_outgoing - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# A ractor with closed outgoing port still can receive messages from incoming port -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.receive - end - - r.close_outgoing - begin - r.send(1) - rescue Ractor::ClosedError - 'ng' - else - 'ok' - end -} - # Ractor.main returns main ractor assert_equal 'true', %q{ Ractor.new{ Ractor.main - }.take == Ractor.current + }.value == Ractor.current } # a ractor with closed outgoing port should terminate assert_equal 'ok', %q{ Ractor.new do - close_outgoing + Ractor.current.close end true until Ractor.count == 1 :ok } -# multiple Ractors can receive (wait) from one Ractor -assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ - pipe = Ractor.new do - loop do - Ractor.yield Ractor.receive - end +# an exception in a Ractor main thread will be re-raised at Ractor#receive +assert_equal '[RuntimeError, "ok", true]', %q{ + r = Ractor.new do + raise 'ok' # exception will be transferred receiver end - - RN = 10 - rs = RN.times.map{|i| - Ractor.new pipe, i do |pipe, i| - msg = pipe.take - msg # ping-pong - end - } - RN.times{|i| - pipe << i - } - RN.times.map{ - r, n = Ractor.select(*rs) - rs.delete r - n - }.sort -} unless /mswin/ =~ RUBY_PLATFORM # randomly hangs on mswin https://github.com/ruby/ruby/actions/runs/3753871445/jobs/6377551069#step:20:131 - -# Ractor.select also support multiple take, receive and yield -assert_equal '[true, true, true]', %q{ - RN = 10 - CR = Ractor.current - - rs = (1..RN).map{ - Ractor.new do - CR.send 'send' + CR.take #=> 'sendyield' - 'take' - end - } - received = [] - taken = [] - yielded = [] - until received.size == RN && taken.size == RN && yielded.size == RN - r, v = Ractor.select(CR, *rs, yield_value: 'yield') - case r - when :receive - received << v - when :yield - yielded << v - else - taken << v - rs.delete r - end + begin + r.join + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true end - r = [received == ['sendyield'] * RN, - yielded == [nil] * RN, - taken == ['take'] * RN, - ] - - STDERR.puts [received, yielded, taken].inspect - r } -# multiple Ractors can send to one Ractor -assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ - pipe = Ractor.new do - loop do - Ractor.yield Ractor.receive - end +# an exception in a Ractor will be re-raised at Ractor#value +assert_equal '[RuntimeError, "ok", true]', %q{ + r = Ractor.new do + raise 'ok' # exception will be transferred receiver end + begin + r.value + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true + end +} - RN = 10 - RN.times.map{|i| - Ractor.new pipe, i do |pipe, i| - pipe << i +# an exception in a Ractor non-main thread will not be re-raised at Ractor#receive +assert_equal 'ok', %q{ + r = Ractor.new do + Thread.new do + raise 'ng' end - } - RN.times.map{ - pipe.take - }.sort + sleep 0.1 + 'ok' + end + r.value } -# an exception in a Ractor will be re-raised at Ractor#receive -assert_equal '[RuntimeError, "ok", true]', %q{ - r = Ractor.new do - raise 'ok' # exception will be transferred receiver +# SystemExit from a Ractor is re-raised +# [Bug #21505] +assert_equal '[SystemExit, "exit", true]', %q{ + r = Ractor.new { exit } + begin + r.value + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true end +} + +# SystemExit from a Thread inside a Ractor is re-raised +# [Bug #21505] +assert_equal '[SystemExit, "exit", true]', %q{ + r = Ractor.new { Thread.new { exit }.join } begin - r.take + r.value rescue Ractor::RemoteError => e [e.cause.class, #=> RuntimeError e.cause.message, #=> 'ok' @@ -571,7 +464,7 @@ assert_equal '[RuntimeError, "ok", true]', %q{ } # threads in a ractor will killed -assert_equal '{:ok=>3}', %q{ +assert_equal '{ok: 3}', %q{ Ractor.new Ractor.current do |main| q = Thread::Queue.new Thread.new do @@ -601,7 +494,7 @@ assert_equal '{:ok=>3}', %q{ end 3.times.map{Ractor.receive}.tally -} unless yjit_enabled? # `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() +} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() # unshareable object are copied assert_equal 'false', %q{ @@ -610,18 +503,18 @@ assert_equal 'false', %q{ msg.object_id end - obj.object_id == r.take + obj.object_id == r.value } # To copy the object, now Marshal#dump is used -assert_equal "allocator undefined for Thread", %q{ +assert_match /can't clone unshareable instance of Thread/, %q{ obj = Thread.new{} begin r = Ractor.new obj do |msg| msg end - rescue TypeError => e - e.message #=> no _dump_data is defined for class Thread + rescue Ractor::Error => e + e.message else 'ng' end @@ -629,10 +522,11 @@ assert_equal "allocator undefined for Thread", %q{ # send shareable and unshareable objects assert_equal "ok", <<~'RUBY', frozen_string_literal: false - echo_ractor = Ractor.new do + port = Ractor::Port.new + echo_ractor = Ractor.new port do |port| loop do v = Ractor.receive - Ractor.yield v + port << v end end @@ -680,13 +574,13 @@ assert_equal "ok", <<~'RUBY', frozen_string_literal: false shareable_objects.map{|o| echo_ractor << o - o2 = echo_ractor.take + o2 = port.receive results << "#{o} is copied" unless o.object_id == o2.object_id } unshareable_objects.map{|o| echo_ractor << o - o2 = echo_ractor.take + o2 = port.receive results << "#{o.inspect} is not copied" if o.object_id == o2.object_id } @@ -712,7 +606,7 @@ assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: fal def check obj1 obj2 = Ractor.new obj1 do |obj| obj - end.take + end.value obj1.object_id == obj2.object_id end @@ -734,7 +628,7 @@ assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false str = 'hello' r.send str, move: true - modified = r.take + modified = r.value begin str << ' exception' # raise Ractor::MovedError @@ -754,7 +648,7 @@ assert_equal '[0, 1]', %q{ a1 = [0] r.send a1, move: true - a2 = r.take + a2 = r.value begin a1 << 2 # raise Ractor::MovedError rescue Ractor::MovedError @@ -762,70 +656,39 @@ assert_equal '[0, 1]', %q{ end } -# move with yield -assert_equal 'hello', %q{ - r = Ractor.new do - Thread.current.report_on_exception = false - obj = 'hello' - Ractor.yield obj, move: true - obj << 'world' - end - - str = r.take - begin - r.take - rescue Ractor::RemoteError - str #=> "hello" - end -} - -# yield/move should not make moved object when the yield is not succeeded -assert_equal '"str"', %q{ - R = Ractor.new{} - M = Ractor.current - r = Ractor.new do - s = 'str' - selected_r, v = Ractor.select R, yield_value: s, move: true - raise if selected_r != R # taken from R - M.send s.inspect # s should not be a moved object - end - - Ractor.receive -} - -# yield/move can fail -assert_equal "allocator undefined for Thread", %q{ +# unshareable frozen objects should still be frozen in new ractor after move +assert_equal 'true', %q{ r = Ractor.new do - obj = Thread.new{} - Ractor.yield obj - rescue => e - e.message + obj = receive + { frozen: obj.frozen? } end - r.take + obj = [Object.new].freeze + r.send(obj, move: true) + r.value[:frozen] } -# Access to global-variables are prohibited -assert_equal 'can not access global variables $gv from non-main Ractors', %q{ +# Access to global-variables are prohibited (read) +assert_equal 'can not access global variable $gv from non-main Ractor', %q{ $gv = 1 r = Ractor.new do $gv end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } -# Access to global-variables are prohibited -assert_equal 'can not access global variables $gv from non-main Ractors', %q{ +# Access to global-variables are prohibited (write) +assert_equal 'can not access global variable $gv from non-main Ractor', %q{ r = Ractor.new do $gv = 1 end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end @@ -839,7 +702,7 @@ assert_equal 'ok', %q{ } end - [$stdin, $stdout, $stderr].zip(r.take){|io, (oid, fno)| + [$stdin, $stdout, $stderr].zip(r.value){|io, (oid, fno)| raise "should not be different object" if io.object_id == oid raise "fd should be same" unless io.fileno == fno } @@ -855,7 +718,7 @@ assert_equal 'ok', %q{ 'ok' end - r.take + r.value } # $DEBUG, $VERBOSE are Ractor local @@ -913,7 +776,7 @@ assert_equal 'true', %q{ h = Ractor.new do ractor_local_globals - end.take + end.value ractor_local_globals == h #=> true } @@ -922,7 +785,8 @@ assert_equal 'false', %q{ r = Ractor.new do self.object_id end - r.take == self.object_id #=> false + ret = r.value + ret == self.object_id } # self is a Ractor instance @@ -930,11 +794,16 @@ assert_equal 'true', %q{ r = Ractor.new do self.object_id end - r.object_id == r.take #=> true + ret = r.value + if r.object_id == ret #=> true + true + else + raise [ret, r.object_id].inspect + end } # given block Proc will be isolated, so can not access outer variables. -assert_equal 'ArgumentError', %q{ +assert_equal 'Ractor::IsolationError', %q{ begin a = true r = Ractor.new do @@ -945,8 +814,39 @@ assert_equal 'ArgumentError', %q{ end } +# eval with outer locals in a Ractor raises SyntaxError +# [Bug #21522] +assert_equal 'SyntaxError', %q{ + outer = 42 + r = Ractor.new do + eval("outer") + end + begin + r.value + rescue Ractor::RemoteError => e + e.cause.class + end +} + +# eval of an undefined name in a Ractor raises NameError +assert_equal 'NameError', %q{ + r = Ractor.new do + eval("totally_undefined_name") + end + begin + r.value + rescue Ractor::RemoteError => e + e.cause.class + end +} + +# eval of a local defined inside the Ractor works +assert_equal '99', %q{ + Ractor.new { inner = 99; eval("inner").to_s }.value +} + # ivar in shareable-objects are not allowed to access from non-main Ractor -assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors", <<~'RUBY', frozen_string_literal: false +assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors (@iv from C)", <<~'RUBY', frozen_string_literal: false class C @iv = 'str' end @@ -958,7 +858,7 @@ assert_equal "can not get unshareable values from instance variables of classes/ end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -974,7 +874,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -1000,7 +900,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -1021,7 +921,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -1035,7 +935,7 @@ assert_equal '11', %q{ Ractor.new obj do |obj| obj.instance_variable_get('@a') - end.take.to_s + end.value.to_s }.join } @@ -1061,25 +961,25 @@ assert_equal '333', %q{ def self.fstr = @fstr end - a = Ractor.new{ C.int }.take + a = Ractor.new{ C.int }.value b = Ractor.new do C.str.to_i rescue Ractor::IsolationError 10 - end.take + end.value c = Ractor.new do C.fstr.to_i - end.take + end.value - d = Ractor.new{ M.int }.take + d = Ractor.new{ M.int }.value e = Ractor.new do M.str.to_i rescue Ractor::IsolationError 20 - end.take + end.value f = Ractor.new do M.fstr.to_i - end.take + end.value # 1 + 10 + 100 + 2 + 20 + 200 @@ -1097,32 +997,32 @@ assert_equal '["instance-variable", "instance-variable", nil]', %q{ Ractor.new{ [C.iv1, C.iv2, C.iv3] - }.take + }.value } # moved objects have their shape properly set to original object's shape assert_equal '1234', %q{ -class Obj - attr_accessor :a, :b, :c, :d - def initialize - @a = 1 - @b = 2 - @c = 3 + class Obj + attr_accessor :a, :b, :c, :d + def initialize + @a = 1 + @b = 2 + @c = 3 + end end -end -r = Ractor.new do - obj = receive - obj.d = 4 - [obj.a, obj.b, obj.c, obj.d] -end -obj = Obj.new -r.send(obj, move: true) -values = r.take -values.join + r = Ractor.new do + obj = receive + obj.d = 4 + [obj.a, obj.b, obj.c, obj.d] + end + obj = Obj.new + r.send(obj, move: true) + values = r.value + values.join } -# cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors', %q{ +# Reading non-shareable cvar from non-main Ractor is not allowed +assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{ class C @@cv = 'str' end @@ -1134,14 +1034,14 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } -# also cached cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors', %q{ +# also cached non-shareable cvar read from non-main Ractor is not allowed +assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{ class C @@cv = 'str' def self.cv @@ -1156,12 +1056,101 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } +# Reading shareable cvar from non-main Ractor is allowed +assert_equal 'shareable', %q{ + class C + @@cv = 'shareable'.freeze + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# Reading shareable cvar (integer) from non-main Ractor is allowed +assert_equal '42', %q{ + class C + @@cv = 42 + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value.to_s +} + +# Reading shareable cvar via module include from non-main Ractor is allowed +assert_equal 'hello', %q{ + module M + @@cv = 'hello'.freeze + def self.cv + @@cv + end + end + + class C + include M + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# Writing cvar from non-main Ractor is not allowed +assert_equal 'can not set class variables from non-main Ractors (@@cv from C)', %q{ + class C + @@cv = 'str' + def self.cv=(v) + @@cv = v + end + end + + r = Ractor.new do + C.cv = 'new' + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Reading cvar that was made shareable after initial assignment +assert_equal 'made shareable', %q{ + class C + @@cv = +'made shareable' + Ractor.make_shareable(@@cv) + def self.cv + @@cv + end + end + + Ractor.new { C.cv }.value +} + +# cvar_defined? works from non-main Ractor +assert_equal 'true', %q{ + class C + @@cv = 42 + def self.cv? + defined?(@@cv) + end + end + + r = Ractor.new { C.cv? ? 'true' : 'false' } + r.value +} + # Getting non-shareable objects via constants by other Ractors is not allowed assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', <<~'RUBY', frozen_string_literal: false class C @@ -1171,19 +1160,33 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m C::CONST end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end RUBY -# Constant cache should care about non-sharable constants +# Constant cache should care about non-shareable constants assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" def str; STR; end s = str() # fill const cache begin - Ractor.new{ str() }.take + Ractor.new{ str() }.join + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY + +# The correct constant path shall be reported +assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false + STR = "hello" + module M + def self.str; STR; end + end + + begin + Ractor.new{ M.str }.join rescue Ractor::RemoteError => e e.cause.message end @@ -1197,7 +1200,7 @@ assert_equal 'can not set constants with non-shareable objects by non-main Racto C::CONST = 'str' end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end @@ -1208,7 +1211,7 @@ assert_equal "defined with an un-shareable Proc in a different Ractor", %q{ str = "foo" define_method(:buggy){|i| str << "#{i}"} begin - Ractor.new{buggy(10)}.take + Ractor.new{buggy(10)}.join rescue => e e.cause.message end @@ -1219,7 +1222,7 @@ assert_equal '[1000, 3]', %q{ A = Array.new(1000).freeze # [nil, ...] H = {a: 1, b: 2, c: 3}.freeze - Ractor.new{ [A.size, H.size] }.take + Ractor.new{ [A.size, H.size] }.value } # Ractor.count @@ -1229,15 +1232,15 @@ assert_equal '[1, 4, 3, 2, 1]', %q{ ractors = (1..3).map { Ractor.new { Ractor.receive } } counts << Ractor.count - ractors[0].send('End 0').take + ractors[0].send('End 0').join sleep 0.1 until ractors[0].inspect =~ /terminated/ counts << Ractor.count - ractors[1].send('End 1').take + ractors[1].send('End 1').join sleep 0.1 until ractors[1].inspect =~ /terminated/ counts << Ractor.count - ractors[2].send('End 2').take + ractors[2].send('End 2').join sleep 0.1 until ractors[2].inspect =~ /terminated/ counts << Ractor.count @@ -1250,7 +1253,7 @@ assert_equal '0', %q{ n = 0 ObjectSpace.each_object{|o| n += 1 unless Ractor.shareable?(o)} n - }.take + }.value } # ObjectSpace._id2ref can not handle unshareable objects with Ractors @@ -1263,7 +1266,25 @@ assert_equal 'ok', <<~'RUBY', frozen_string_literal: false rescue => e :ok end - end.take + end.value +RUBY + +# Inserting into the id2ref table should be Ractor-safe +assert_equal 'ok', <<~'RUBY' + # Force all calls to Kernel#object_id to insert into the id2ref table + obj = Object.new + ObjectSpace._id2ref(obj.object_id) rescue nil + + 10.times.map do + Ractor.new do + 10_000.times do + a = Object.new + a.object_id + end + end + end.map(&:value) + + :ok RUBY # Ractor.make_shareable(obj) @@ -1347,19 +1368,42 @@ assert_equal 'true', %q{ [a.frozen?, a[0].frozen?] == [true, false] } -# Ractor.make_shareable(a_proc) makes a proc shareable. -assert_equal 'true', %q{ - a = [1, [2, 3], {a: "4"}] +# Ractor.make_shareable(a_proc) requires a shareable receiver +assert_equal '[:ok, "Proc\'s self is not shareable:"]', %q{ + pr1 = nil.instance_exec { Proc.new{} } + pr2 = Proc.new{} - pr = Ractor.current.instance_eval do - Proc.new do - a + [pr1, pr2].map do |pr| + begin + Ractor.make_shareable(pr) + rescue Ractor::Error => e + e.message[/^.+?:/] + else + :ok end end +} - Ractor.make_shareable(a) # referred value should be shareable - Ractor.make_shareable(pr) - Ractor.shareable?(pr) +# Ractor.make_shareable(Method/UnboundMethod) +assert_equal 'true', %q{ + # raise because receiver is unshareable + begin + _m0 = Ractor.make_shareable(self.method(:__id__)) + rescue => e + raise e unless e.message =~ /can not make shareable object/ + else + raise "no error" + end + + # Method with shareable receiver + M1 = Ractor.make_shareable(Object.method(:__id__)) + + # UnboundMethod + M2 = Ractor.make_shareable(Object.instance_method(:__id__)) + + Ractor.new do + Object.__id__ == M1.call && M1.call == M2.bind_call(Object) + end.value } # Ractor.shareable?(recursive_objects) @@ -1394,29 +1438,10 @@ assert_equal '[C, M]', %q{ assert_equal '1', %q{ class C a = 1 - define_method "foo", Ractor.make_shareable(Proc.new{ a }) - a = 2 - end - - Ractor.new{ C.new.foo }.take -} - -# Ractor.make_shareable(a_proc) makes a proc shareable. -assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{ - a = b = nil - pr = Ractor.current.instance_eval do - Proc.new do - c = b # assign to a is okay because c is block local variable - # reading b is okay - a = b # assign to a is not allowed #=> Ractor::Error - end + define_method "foo", Ractor.shareable_proc{ a } end - begin - Ractor.make_shareable(pr) - rescue => e - e.message - end + Ractor.new{ C.new.foo }.value } # Ractor.make_shareable(obj, copy: true) makes copied shareable object. @@ -1441,7 +1466,7 @@ assert_equal '[6, 10]', %q{ Ractor.new{ # line 5 a = 1 b = 2 - }.take + }.value c = 3 # line 9 end rs @@ -1451,7 +1476,7 @@ assert_equal '[6, 10]', %q{ assert_equal '[true, false]', %q{ Ractor.new([[]].freeze) { |ary| [ary.frozen?, ary.first.frozen? ] - }.take + }.value } # Ractor deep copies frozen objects (str) @@ -1459,7 +1484,7 @@ assert_equal '[true, false]', %q{ s = String.new.instance_eval { @x = []; freeze} Ractor.new(s) { |s| [s.frozen?, s.instance_variable_get(:@x).frozen?] - }.take + }.value } # Can not trap with not isolated Proc on non-main ractor @@ -1467,15 +1492,15 @@ assert_equal '[:ok, :ok]', %q{ a = [] Ractor.new{ trap(:INT){p :ok} - }.take + }.join a << :ok begin Ractor.new{ s = 'str' trap(:INT){p s} - }.take - rescue => Ractor::RemoteError + }.join + rescue Ractor::RemoteError a << :ok end } @@ -1504,15 +1529,48 @@ assert_equal '[nil, "b", "a"]', %q{ ans = [] Ractor.current[:key] = 'a' r = Ractor.new{ - Ractor.yield self[:key] + Ractor.main << self[:key] self[:key] = 'b' self[:key] } - ans << r.take - ans << r.take + ans << Ractor.receive + ans << r.value ans << Ractor.current[:key] } +assert_equal '1', %q{ + N = 1_000 + Ractor.new{ + a = [] + 1_000.times.map{|i| + Thread.new(i){|i| + Thread.pass if i < N + a << Ractor.store_if_absent(:i){ i } + a << Ractor.current[:i] + } + }.each(&:join) + a.uniq.size + }.value +} + +# Ractor-local storage +assert_equal '2', %q{ + Ractor.new { + fails = 0 + begin + Ractor.main[:key] # cannot get ractor local storage from non-main ractor + rescue => e + fails += 1 if e.message =~ /Cannot get ractor local/ + end + begin + Ractor.main[:key] = 'val' + rescue => e + fails += 1 if e.message =~ /Cannot set ractor local/ + end + fails + }.value +} + ### ### Synchronization tests ### @@ -1526,10 +1584,28 @@ assert_equal "#{N}#{N}", %Q{ Ractor.new{ N.times{|i| -(i.to_s)} } - }.map{|r| r.take}.join + }.map{|r| r.value}.join } -# Generic ivtbl +assert_equal "ok", %Q{ + N = #{N} + a, b = 2.times.map{ + Ractor.new{ + N.times.map{|i| -(i.to_s)} + } + }.map{|r| r.value} + N.times do |i| + unless a[i].equal?(b[i]) + raise [a[i], b[i]].inspect + end + unless a[i] == i.to_s + raise [i, a[i], b[i]].inspect + end + end + :ok +} + +# Generic fields_tbl n = N/2 assert_equal "#{n}#{n}", %Q{ 2.times.map{ @@ -1542,21 +1618,20 @@ assert_equal "#{n}#{n}", %Q{ obj.instance_variable_defined?("@a") end end - }.map{|r| r.take}.join + }.map{|r| r.value}.join } -# NameError -assert_equal "ok", %q{ +# Now NoMethodError is copyable +assert_equal "NoMethodError", %q{ obj = "".freeze # NameError refers the receiver indirectly begin obj.bar rescue => err end - begin - Ractor.new{} << err - rescue TypeError - 'ok' - end + + r = Ractor.new{ Ractor.receive } + r << err + r.value.class } assert_equal "ok", %q{ @@ -1574,54 +1649,27 @@ assert_equal "ok", %q{ # Can yield back values while GC is sweeping [Bug #18117] assert_equal "ok", %q{ + port = Ractor::Port.new workers = (0...8).map do - Ractor.new do + Ractor.new port do |port| loop do 10_000.times.map { Object.new } - Ractor.yield Time.now + port << Time.now + Ractor.receive end end end - 1_000.times { idle_worker, tmp_reporter = Ractor.select(*workers) } + 100.times { + workers.each do + port.receive + end + workers.each do |w| + w.send(nil) + end + } "ok" -} unless yjit_enabled? || rjit_enabled? # flaky - -assert_equal "ok", %q{ - def foo(*); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo(**); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo(...); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo((x), (y)); ->{ super }; end - begin - Ractor.make_shareable(foo([], [])) - rescue Ractor::IsolationError - "ok" - end -} +} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky # check method cache invalidation assert_equal "ok", %q{ @@ -1686,14 +1734,41 @@ assert_equal 'true', %q{ } n = CS.inject(1){|r, c| r * c.foo} * LN - rs.map{|r| r.take} == Array.new(RN){n} + rs.map{|r| r.value} == Array.new(RN){n} +} + +# check method cache invalidation +assert_equal 'true', %q{ + class Foo + def hello = nil + end + + r1 = Ractor.new do + 1000.times do + class Foo + def hello = nil + end + end + end + + r2 = Ractor.new do + 1000.times do + o = Foo.new + o.hello + end + end + + r1.value + r2.value + + true } # check experimental warning -assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor is experimental/, %q{ +assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor API is experimental/, %q{ Warning[:experimental] = $VERBOSE = true STDERR.reopen(STDOUT) - eval("Ractor.new{}.take", nil, "test_ractor.rb", 1) + eval("Ractor.new{}.value", nil, "test_ractor.rb", 1) }, frozen_string_literal: false # check moved object @@ -1711,7 +1786,7 @@ assert_equal 'ok', %q{ end r.send obj, move: true - r.take + r.value } ## Ractor::Selector @@ -1787,10 +1862,11 @@ assert_equal '600', %q{ RN = 100 s = Ractor::Selector.new + port = Ractor::Port.new rs = RN.times.map{ Ractor.new{ - Ractor.main << Ractor.new{ Ractor.yield :v3; :v4 } - Ractor.main << Ractor.new{ Ractor.yield :v5; :v6 } + Ractor.main << Ractor.new(port){|port| port << :v3; :v4 } + Ractor.main << Ractor.new(port){|port| port << :v5; :v6 } Ractor.yield :v1 :v2 } @@ -1836,3 +1912,755 @@ assert_equal 'true', %q{ shareable = Ractor.make_shareable("chilled") shareable == "chilled" && Ractor.shareable?(shareable) } + +# require in Ractor +assert_equal 'true', %q{ + Module.new do + def require feature + return Ractor._require(feature) unless Ractor.main? + super + end + Object.prepend self + set_temporary_name 'Ractor#require' + end + + Ractor.new{ + begin + require 'tempfile' + Tempfile.new + rescue SystemStackError + # prism parser with -O0 build consumes a lot of machine stack + Data.define(:fileno).new(1) + end + }.value.fileno > 0 +} + +# require_relative in Ractor +assert_equal 'true', %q{ + dummyfile = File.join(__dir__, "dummy#{rand}.rb") + return true if File.exist?(dummyfile) + + begin + File.write dummyfile, '' + rescue Exception + # skip on any errors + return true + end + + begin + Ractor.new dummyfile do |f| + require_relative File.basename(f) + end.value + ensure + File.unlink dummyfile + end +} + +# require_relative in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + return true if File.exist?(dummyfile) + + Ractor.new dummyfile do |f| + begin + require_relative File.basename(f) + rescue LoadError => e + e.class + end + end.value +} + +# autolaod in Ractor +assert_equal 'true', %q{ + autoload :Tempfile, 'tempfile' + + r = Ractor.new do + begin + Tempfile.new + rescue SystemStackError + # prism parser with -O0 build consumes a lot of machine stack + Data.define(:fileno).new(1) + end + end + r.value.fileno > 0 +} + +# failed in autolaod in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + autoload :Tempfile, dummyfile + + r = Ractor.new do + begin + Tempfile.new + rescue LoadError => e + e.class + end + end + r.value +} + +# bind_call in Ractor [Bug #20934] +assert_equal 'ok', %q{ + 2.times.map do + Ractor.new do + 1000.times do + Object.instance_method(:itself).bind_call(self) + end + end + end.each(&:join) + GC.start + :ok.itself +} + +# moved objects being corrupted if embeded (String) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "foobarbazfoobarbazfoobarbazfoobarbaz" + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Array) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Hash) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = { foo: 1, bar: 2 } + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (MatchData) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "foo".match(/o/) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Struct) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Struct.new(:a, :b, :c, :d, :e, :f).new(1, 2, 3, 4, 5, 6) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Object) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + class SomeObject + attr_reader :a, :b, :c, :d, :e, :f + def initialize + @a = @b = @c = @d = @e = @f = 1 + end + + def ==(o) + @a == o.a && + @b == o.b && + @c == o.c && + @d == o.d && + @e == o.e && + @f == o.f + end + end + + SomeObject.new # initial non-embeded + + obj = SomeObject.new + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved arrays can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = [1] + ractor.send(obj, move: true) + begin + [].concat(obj) + rescue TypeError + :ok + else + :fail + end +} + +# moved strings can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "hello" + ractor.send(obj, move: true) + begin + "".replace(obj) + rescue TypeError + :ok + else + :fail + end +} + +# moved hashes can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = { a: 1 } + ractor.send(obj, move: true) + begin + {}.merge(obj) + rescue TypeError + :ok + else + :fail + end +} + +# move objects inside frozen containers +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + original = obj.dup + ractor.send([obj].freeze, move: true) + roundtripped_obj = ractor.value[0] + roundtripped_obj == original ? :ok : roundtripped_obj +} + +# move object with generic ivar +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array, [1]) + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array) == [1] ? :ok : roundtripped_obj +} + +# move object with many generic ivars +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + 0.upto(300) do |i| + obj.instance_variable_set(:"@array#{i}", [i]) + end + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# move object with complex generic ivars +assert_equal 'ok', %q{ + # Make Array complex + 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } + + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array1, [1]) + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# move object with generic ivars and existing id2ref table +# [Bug #21664] +assert_equal 'ok', %q{ + obj = [1] + obj.instance_variable_set("@field", :ok) + ObjectSpace._id2ref(obj.object_id) # build id2ref table + + ractor = Ractor.new { Ractor.receive } + ractor.send(obj, move: true) + obj = ractor.value + obj.instance_variable_get("@field") +} + +# copy object with complex generic ivars +assert_equal 'ok', %q{ + # Make Array complex + 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } + + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array1, [1]) + + ractor.send(obj) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# copy object with many generic ivars +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + 0.upto(300) do |i| + obj.instance_variable_set(:"@array#{i}", [i]) + end + + ractor.send(obj) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# moved composite types move their non-shareable parts properly +assert_equal 'ok', %q{ + k, v = String.new("key"), String.new("value") + h = { k => v } + h.instance_variable_set("@b", String.new("b")) + a = [k,v] + o_singleton = Object.new + def o_singleton.a + @a + end + o_singleton.instance_variable_set("@a", String.new("a")) + class MyObject + attr_reader :a + def initialize(a) + @a = a + end + end + struct_class = Struct.new(:a) + struct = struct_class.new(String.new('a')) + o = MyObject.new(String.new('a')) + port = Ractor::Port.new + + r = Ractor.new port do |port| + loop do + obj = Ractor.receive + val = case obj + when Hash + obj['key'] == 'value' && obj.instance_variable_get("@b") == 'b' + when Array + obj[0] == 'key' + when Struct + obj.a == 'a' + when Object + obj.a == 'a' + end + port << val + end + end + + objs = [h, a, o_singleton, o, struct] + objs.each_with_index do |obj, i| + klass = obj.class + parts_moved = {} + case obj + when Hash + parts_moved[klass] = [obj['key'], obj.instance_variable_get("@b")] + when Array + parts_moved[klass] = obj.dup # the contents + when Struct, Object + parts_moved[klass] = [obj.a] + end + r.send(obj, move: true) + val = port.receive + if val != true + raise "bad val in ractor for obj at i:#{i}" + end + begin + p obj + rescue + else + raise "should be moved" + end + parts_moved.each do |klass, parts| + parts.each_with_index do |part, j| + case part + when Ractor::MovedObject + else + raise "part for class #{klass} at i:#{j} should be moved" + end + end + end + end + 'ok' +} + +# fork after creating Ractor +assert_equal 'ok', %q{ +begin + Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { } + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Ractors should be terminated after fork +assert_equal 'ok', %q{ +begin + r = Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { + begin + raise if r.value != nil + end + } + r.send(123) + raise unless r.value == 123 + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Ractors should be terminated after fork +assert_equal 'ok', %q{ +begin + r = Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { + begin + r.send(123) + rescue Ractor::ClosedError + end + } + r.send(123) + raise unless r.value == 123 + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Creating classes inside of Ractors +# [Bug #18119] +assert_equal 'ok', %q{ + port = Ractor::Port.new + workers = (0...8).map do + Ractor.new port do |port| + loop do + 100.times.map { Class.new } + port << nil + end + end + end + + 100.times { port.receive } + + 'ok' +} + +# Using Symbol#to_proc inside ractors +# [Bug #21354] +assert_equal 'ok', %q{ + :inspect.to_proc + Ractor.new do + # It should not use this cached proc, it should create a new one. If it used + # the cached proc, we would get a ractor_confirm_belonging error here. + :inspect.to_proc + end.join + 'ok' +} + +# take vm lock when deleting generic ivars from the global table +assert_equal 'ok', %q{ + Ractor.new do + a = [1, 2, 3] + a.object_id + a.dup # this deletes generic ivar on dupped object + 'ok' + end.value +} + +## Ractor#monitor + +# monitor port returns `:exited` when the monitering Ractor terminated. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.main << :ok1 + :ok2 + end + + r.monitor port = Ractor::Port.new + Ractor.receive # :ok1 + port.receive == :exited +} + +# monitor port returns `:exited` even if the monitoring Ractor was terminated. +assert_equal 'true', %q{ + r = Ractor.new do + :ok + end + + r.join # wait for r's terminateion + + r.monitor port = Ractor::Port.new + port.receive == :exited +} + +# monitor returns false if the monitoring Ractor was terminated. +assert_equal 'false', %q{ + r = Ractor.new do + :ok + end + + r.join # wait for r's terminateion + + r.monitor Ractor::Port.new +} + +# monitor port returns `:aborted` when the monitering Ractor is aborted. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.main << :ok1 + raise 'ok' + end + + r.monitor port = Ractor::Port.new + Ractor.receive # :ok1 + port.receive == :aborted +} + +# monitor port returns `:aborted` even if the monitoring Ractor was aborted. +assert_equal 'true', %q{ + r = Ractor.new do + raise 'ok' + end + + begin + r.join # wait for r's terminateion + rescue Ractor::RemoteError + # ignore + end + + r.monitor port = Ractor::Port.new + port.receive == :aborted +} + +## Ractor#join + +# Ractor#join returns self when the Ractor is terminated. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.receive + end + + r << :ok + r.join + r.inspect in /terminated/ +} if false # TODO + +# Ractor#join raises RemoteError when the remote Ractor aborted with an exception +assert_equal 'err', %q{ + r = Ractor.new do + raise 'err' + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +## Ractor#value + +# Ractor#value returns the last expression even if it is unshareable +assert_equal 'true', %q{ + r = Ractor.new do + obj = [1, 2] + obj << obj.object_id + end + + ret = r.value + ret == [1, 2, ret.object_id] +} + +# Only one Ractor can call Ractor#value +assert_equal '[["Only the successor ractor can take a value", 9], ["ok", 2]]', %q{ + r = Ractor.new do + 'ok' + end + + RN = 10 + + rs = RN.times.map do + Ractor.new r do |r| + begin + Ractor.main << r.value + Ractor.main << r.value # this ractor can get same result + rescue Ractor::Error => e + Ractor.main << e.message + end + end + end + + (RN+1).times.map{ + Ractor.receive + }.tally.sort +} + +# Cause lots of inline CC misses. +assert_equal 'ok', <<~'RUBY' + class A; def test; 1 + 1; end; end + class B; def test; 1 + 1; end; end + class C; def test; 1 + 1; end; end + class D; def test; 1 + 1; end; end + class E; def test; 1 + 1; end; end + class F; def test; 1 + 1; end; end + class G; def test; 1 + 1; end; end + + objs = [A.new, B.new, C.new, D.new, E.new, F.new, G.new].freeze + + def call_test(obj) + obj.test + end + + ractors = 7.times.map do + Ractor.new(objs) do |objs| + objs = objs.shuffle + 100_000.times do + objs.each do |o| + call_test(o) + end + end + end + end + ractors.each(&:join) + :ok +RUBY + +# This test checks that we do not trigger a GC when we have malloc with Ractor +# locks. We cannot trigger a GC with Ractor locks because GC requires VM lock +# and Ractor barrier. If another Ractor is waiting on this Ractor lock, then it +# will deadlock because the other Ractor will never join the barrier. +# +# Creating Ractor::Port requires locking the Ractor and inserting into an +# st_table, which can call malloc. +assert_equal 'ok', <<~'RUBY' + r = Ractor.new do + loop do + Ractor::Port.new + end + end + + 10.times do + 10_000.times do + r.send(nil) + end + sleep(0.01) + end + :ok +RUBY + +assert_equal 'ok', <<~'RUBY' + begin + 100.times do |i| + Ractor.new(i) do |j| + 1000.times do |i| + "#{j}-#{i}" + end + Ractor.receive + end + pid = fork { } + _, status = Process.waitpid2 pid + raise unless status.success? + end + + :ok + rescue NotImplementedError + :ok + end +RUBY + +assert_equal 'ok', <<~'RUBY' + begin + 100.times do |i| + Ractor.new(i) do |j| + 1000.times do |i| + "#{j}-#{i}" + end + end + pid = fork do + GC.verify_internal_consistency + end + _, status = Process.waitpid2 pid + raise unless status.success? + end + + :ok + rescue NotImplementedError + :ok + end +RUBY + +# When creating bmethods in Ractors, they should only be usable from their +# defining ractor, even if it is GC'd +assert_equal 'ok', <<~'RUBY' + +begin + CLASSES = 1000.times.map { Class.new }.freeze + + # This would be better to run in parallel, but there's a bug with lambda + # creation and YJIT causing crashes in dev mode + ractors = CLASSES.map do |klass| + Ractor.new(klass) do |klass| + Ractor.receive + klass.define_method(:foo) {} + end + end + + ractors.each do |ractor| + ractor << nil + ractor.join + end + + ractors.clear + GC.start + + any = 1000.times.map do + Ractor.new do + CLASSES.any? do |klass| + begin + klass.new.foo + true + rescue RuntimeError + false + end + end + end + end.map(&:value).none? && :ok +rescue ThreadError => e + # ignore limited memory machine + if /can\'t create Thread/ =~ e.message + :ok + else + raise + end +end +RUBY + +# Concurrent super calls with keyword arguments must not race on the +# callinfo kwarg reference count. [Bug #22075] +assert_equal 'ok', %q{ + class Base + def foo(a:, b:, c:) = a + end + + class Sub < Base + def foo(a:, b:, c:) = super(a: a, b: b, c: c) + end + + 4.times.map do + Ractor.new do + obj = Sub.new + 100_000.times { obj.foo(a: 1, b: 2, c: 3) } + end + end.each(&:join) + + :ok +} diff --git a/bootstraptest/test_rjit.rb b/bootstraptest/test_rjit.rb deleted file mode 100644 index 4c8cd26dff..0000000000 --- a/bootstraptest/test_rjit.rb +++ /dev/null @@ -1,80 +0,0 @@ -# VM_CALL_OPT_SEND + VM_METHOD_TYPE_ATTRSET -assert_equal '1', %q{ - class Foo - attr_writer :foo - - def bar - send(:foo=, 1) - end - end - - Foo.new.bar -} - -# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_CALL -assert_equal 'foo', %q{ - def bar(&foo) - foo.send(:call) - end - - bar { :foo } -} - -# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_STRUCT_AREF -assert_equal 'bar', %q{ - def bar(foo) - foo.send(:bar) - end - - bar(Struct.new(:bar).new(:bar)) -} - -# AND with offset DISP32 -assert_equal '2', %q{ - def foo - a = 6; - b = {a: 1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1, h: a&3} - b[:h] - end - - foo -} - -# OR with offset DISP32 -assert_equal '6', %q{ - def foo - a = 4; - b = {a: 1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1, h: a|2} - b[:h] - end - - foo -} - -# kwargs default w/ checkkeyword + locals (which shouldn't overwrite unspecified_bits) -assert_equal '1', %q{ - def foo(bar: 1.to_s) - _ = 1 - bar - end - - def entry - foo - end - - entry -} - -# Updating local type in Context -assert_normal_exit %q{ - def foo(flag, object) - klass = if flag - object - end - klass ||= object - return klass.new - end - - foo(false, Object) - foo(true, Object) -} diff --git a/bootstraptest/test_syntax.rb b/bootstraptest/test_syntax.rb index 8301b344c6..29bf93cb8f 100644 --- a/bootstraptest/test_syntax.rb +++ b/bootstraptest/test_syntax.rb @@ -571,7 +571,7 @@ assert_equal 'ok', %q{ assert_equal 'ok', %q{ 1.times{ - p(1, (next; 2)) + p(1, (next if true; 2)) }; :ok } assert_equal '3', %q{ @@ -585,7 +585,7 @@ assert_equal '3', %q{ i = 0 1 + (while true break 2 if (i+=1) > 1 - p(1, (next; 2)) + p(1, (next if true; 2)) end) } # redo @@ -594,7 +594,7 @@ assert_equal 'ok', %q{ 1.times{ break if i>1 i+=1 - p(1, (redo; 2)) + p(1, (redo if true; 2)) }; :ok } assert_equal '3', %q{ @@ -608,7 +608,7 @@ assert_equal '3', %q{ i = 0 1 + (while true break 2 if (i+=1) > 1 - p(1, (redo; 2)) + p(1, (redo if true; 2)) end) } assert_equal '1', %q{ @@ -848,7 +848,7 @@ assert_normal_exit %q{ def x(a=1, b, *rest); nil end end end -}, bug2415 unless rjit_enabled? # flaky +}, bug2415 assert_normal_exit %q{ 0.times do @@ -880,7 +880,7 @@ assert_normal_exit %q{ end end end -}, bug2415 unless rjit_enabled? # flaky +}, bug2415 assert_normal_exit %q{ a { diff --git a/bootstraptest/test_thread.rb b/bootstraptest/test_thread.rb index 4040b68a27..7ff5bb4a38 100644 --- a/bootstraptest/test_thread.rb +++ b/bootstraptest/test_thread.rb @@ -301,7 +301,7 @@ assert_normal_exit %q{ }.each {|t| t.join } -} unless rjit_enabled? # flaky +} assert_equal 'ok', %q{ def m @@ -493,7 +493,8 @@ assert_equal 'foo', %q{ [th1, th2].each {|t| t.join } GC.start f.call.source -} unless rjit_enabled? # flaky +} + assert_normal_exit %q{ class C def inspect diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index eccaa542c8..e9ce905e2c 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -37,27 +37,35 @@ assert_equal "ok", %q{ } # test discarding extra yield arguments -assert_equal "2210150001501015", %q{ +assert_equal "22131300500015901015", %q{ def splat_kw(ary) = yield *ary, a: 1 def splat(ary) = yield *ary - def kw = yield 1, 2, a: 0 + def kw = yield 1, 2, a: 3 + + def kw_only = yield a: 0 def simple = yield 0, 1 + def none = yield + def calls [ splat([1, 1, 2]) { |x, y| x + y }, splat([1, 1, 2]) { |y, opt = raise| opt + y}, splat_kw([0, 1]) { |a:| a }, kw { |a:| a }, - kw { |a| a }, + kw { |one| one }, + kw { |one, a:| a }, + kw_only { |a:| a }, + kw_only { |a: 1| a }, simple { 5.itself }, simple { |a| a }, simple { |opt = raise| opt }, simple { |*rest| rest }, simple { |opt_kw: 5| opt_kw }, + none { |a: 9| a }, # autosplat ineractions [0, 1, 2].yield_self { |a, b| [a, b] }, [0, 1, 2].yield_self { |a, opt = raise| [a, opt] }, @@ -106,7 +114,7 @@ assert_equal '[:ae, :ae]', %q{ end [test(Array.new 5), test([])] -} unless rjit_enabled? # Not yet working on RJIT +} # regression test for arity check with splat and send assert_equal '[:ae, :ae]', %q{ @@ -123,7 +131,7 @@ assert_equal '[:ae, :ae]', %q{ # regression test for GC marking stubs in invalidated code assert_normal_exit %q{ - skip true unless defined?(GC.compact) + skip true unless GC.respond_to?(:compact) garbage = Array.new(10_000) { [] } # create garbage to cause iseq movement eval(<<~RUBY) def foo(n, garbage) @@ -158,7 +166,7 @@ assert_equal '0', "0.abs(&nil)" # regression test for invokeblock iseq guard assert_equal 'ok', %q{ - skip :ok unless defined?(GC.compact) + skip :ok unless GC.respond_to?(:compact) def foo = yield 10.times do |i| ret = eval("foo { #{i} }") @@ -166,7 +174,7 @@ assert_equal 'ok', %q{ GC.compact end :ok -} unless rjit_enabled? # Not yet working on RJIT +} # regression test for overly generous guard elision assert_equal '[0, :sum, 0, :sum]', %q{ @@ -212,6 +220,15 @@ assert_equal 'Sub', %q{ call(Sub.new('o')).class } +# String#dup with generic ivars +assert_equal '["str", "ivar"]', %q{ + def str_dup(str) = str.dup + str = "str" + str.instance_variable_set(:@ivar, "ivar") + str = str_dup(str) + [str, str.instance_variable_get(:@ivar)] +} + # test splat filling required and feeding rest assert_equal '[0, 1, 2, [3, 4]]', %q{ public def lead_rest(a, b, *rest) @@ -294,7 +311,7 @@ assert_equal '[:ok]', %q{ # Used to crash due to GC run in rb_ensure_iv_list_size() # not marking the newly allocated [:ok]. RegressionTest.new.extender.itself -} unless rjit_enabled? # Skip on RJIT since this uncovers a crash +} assert_equal 'true', %q{ # regression test for tracking type of locals for too long @@ -451,91 +468,6 @@ assert_normal_exit %q{ end } -assert_equal '0', %q{ - # This is a regression test for incomplete invalidation from - # opt_setinlinecache. This test might be brittle, so - # feel free to remove it in the future if it's too annoying. - # This test assumes --yjit-call-threshold=2. - module M - Foo = 1 - def foo - Foo - end - - def pin_self_type_then_foo - _ = @foo - foo - end - - def only_ints - 1 + self - foo - end - end - - class Integer - include M - end - - class Sub - include M - end - - foo_method = M.instance_method(:foo) - - dbg = ->(message) do - return # comment this out to get printouts - - $stderr.puts RubyVM::YJIT.disasm(foo_method) - $stderr.puts message - end - - 2.times { 42.only_ints } - - dbg["There should be two versions of getinlineache"] - - module M - remove_const(:Foo) - end - - dbg["There should be no getinlinecaches"] - - 2.times do - 42.only_ints - rescue NameError => err - _ = "caught name error #{err}" - end - - dbg["There should be one version of getinlineache"] - - 2.times do - Sub.new.pin_self_type_then_foo - rescue NameError - _ = 'second specialization' - end - - dbg["There should be two versions of getinlineache"] - - module M - Foo = 1 - end - - dbg["There should still be two versions of getinlineache"] - - 42.only_ints - - dbg["There should be no getinlinecaches"] - - # Find name of the first VM instruction in M#foo. - insns = RubyVM::InstructionSequence.of(foo_method).to_a - if defined?(RubyVM::YJIT.blocks_for) && (insns.last.find { Array === _1 }&.first == :opt_getinlinecache) - RubyVM::YJIT.blocks_for(RubyVM::InstructionSequence.of(foo_method)) - .filter { _1.iseq_start_index == 0 }.count - else - 0 # skip the test - end -} - # Check that frozen objects are respected assert_equal 'great', %q{ class Foo @@ -1230,7 +1162,7 @@ assert_equal 'special', %q{ # Test that object references in generated code get marked and moved assert_equal "good", %q{ - skip :good unless defined?(GC.compact) + skip :good unless GC.respond_to?(:compact) def bar "good" end @@ -1398,7 +1330,7 @@ assert_equal '[42, :default]', %q{ ] } -# Test default value block for Hash with opt_aref_with +# Test default value block for Hash assert_equal "false", <<~RUBY, frozen_string_literal: false def index_with_string(h) h["foo"] @@ -1798,7 +1730,7 @@ assert_equal '{}', %q{ } # test building hash with values -assert_equal '{:foo=>:bar}', %q{ +assert_equal '{foo: :bar}', %q{ def build_hash(val) { foo: val } end @@ -2048,7 +1980,7 @@ assert_equal '[97, :nil, 97, :nil, :raised]', %q{ getbyte("a", 0) [getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")] -} unless rjit_enabled? # Not yet working on RJIT +} # Basic test for String#setbyte assert_equal 'AoZ', %q{ @@ -2256,6 +2188,34 @@ assert_equal '7', %q{ foo(5,2) } +# regression test for argument registers with invalidation +assert_equal '[0, 1, 2]', %q{ + def test(n) + ret = n + binding + ret + end + + [0, 1, 2].map do |n| + test(n) + end +} + +# regression test for argument registers +assert_equal 'true', %q{ + class Foo + def ==(other) + other == nil + end + end + + def test + [Foo.new].include?(Foo.new) + end + + test +} + # test pattern matching assert_equal '[:ok, :ok]', %q{ class C @@ -2323,7 +2283,7 @@ assert_equal '123', %q{ # Test EP == BP invalidation with moving ISEQs assert_equal 'ok', %q{ - skip :ok unless defined?(GC.compact) + skip :ok unless GC.respond_to?(:compact) def entry ok = proc { :ok } # set #entry as an EP-escaping ISEQ [nil].reverse_each do # avoid exiting the JIT frame on the constant @@ -2523,6 +2483,32 @@ assert_equal '[0, 2]', %q{ B.new.foo } +# invokesuper in a weird block +assert_equal '["block->A#itself", "block->singleton#itself"]', %q{ + # This test runs the same block as first as a block and then as a method, + # testing the routine that finds the currently running method, which is + # relevant for `super`. + class BlockIseqDuality + prepend(Module.new do + def itself + nested = -> { "block->" + super() } + @singleton_itself.define_singleton_method(:itself, &nested) + nested + end + end) + + attr_reader :singleton_itself + def initialize = (@singleton_itself = "singleton#itself") + + def itself = "A#itself" + end + + tester = BlockIseqDuality.new + super_lambda = tester.itself + super_lambda.call # warmup + [super_lambda.call, tester.singleton_itself.itself] +} + # invokesuper zsuper in a bmethod assert_equal 'ok', %q{ class Foo @@ -2718,7 +2704,23 @@ assert_equal '[1, 2]', %q{ expandarray_redefined_nilclass expandarray_redefined_nilclass -} unless rjit_enabled? +} + +assert_equal 'not_array', %q{ + def expandarray_not_array(obj) + a, = obj + a + end + + obj = Object.new + def obj.method_missing(m, *args, &block) + return [:not_array] if m == :to_ary + super + end + + expandarray_not_array(obj) + expandarray_not_array(obj) +} assert_equal '[1, 2, nil]', %q{ def expandarray_rhs_too_small @@ -2830,7 +2832,7 @@ assert_equal '[[:c_return, :String, :string_alias, "events_to_str"]]', %q{ events.compiled(events) events -} unless rjit_enabled? # RJIT calls extra Ruby methods +} # test enabling a TracePoint that targets a particular line in a C method call assert_equal '[true]', %q{ @@ -2912,7 +2914,7 @@ assert_equal '[[:c_call, :itself]]', %q{ tp.enable { shouldnt_compile } events -} unless rjit_enabled? # RJIT calls extra Ruby methods +} # test enabling c_return tracing before compiling assert_equal '[[:c_return, :itself, main]]', %q{ @@ -2927,7 +2929,7 @@ assert_equal '[[:c_return, :itself, main]]', %q{ tp.enable { shouldnt_compile } events -} unless rjit_enabled? # RJIT calls extra Ruby methods +} # test c_call invalidation assert_equal '[[:c_call, :itself]]', %q{ @@ -2973,15 +2975,16 @@ assert_equal '[:itself]', %q{ itself end - tracing_ractor = Ractor.new do + port = Ractor::Port.new + tracing_ractor = Ractor.new port do |port| # 1: start tracing events = [] tp = TracePoint.new(:c_call) { events << _1.method_id } tp.enable - Ractor.yield(nil) + port << nil # 3: run compiled method on tracing ractor - Ractor.yield(nil) + port << nil traced_method events @@ -2989,13 +2992,13 @@ assert_equal '[:itself]', %q{ tp&.disable end - tracing_ractor.take + port.receive # 2: compile on non tracing ractor traced_method - tracing_ractor.take - tracing_ractor.take + port.receive + tracing_ractor.value } # Try to hit a lazy branch stub while another ractor enables tracing @@ -3009,17 +3012,18 @@ assert_equal '42', %q{ end end - ractor = Ractor.new do + port = Ractor::Port.new + ractor = Ractor.new port do |port| compiled(false) - Ractor.yield(nil) + port << nil compiled(41) end tp = TracePoint.new(:line) { itself } - ractor.take + port.receive tp.enable - ractor.take + ractor.value } # Test equality with changing types @@ -3095,7 +3099,7 @@ assert_equal '42', %q{ A.foo A.foo - Ractor.new { A.foo }.take + Ractor.new { A.foo }.value } assert_equal '["plain", "special", "sub", "plain"]', %q{ @@ -3347,7 +3351,7 @@ assert_equal '[[1, 2, 3, 4]]', %q{ } # cfunc kwargs -assert_equal '{:foo=>123}', %q{ +assert_equal '{foo: 123}', %q{ def foo(bar) bar.store(:value, foo: 123) bar[:value] @@ -3358,7 +3362,7 @@ assert_equal '{:foo=>123}', %q{ } # cfunc kwargs -assert_equal '{:foo=>123}', %q{ +assert_equal '{foo: 123}', %q{ def foo(bar) bar.replace(foo: 123) end @@ -3368,7 +3372,7 @@ assert_equal '{:foo=>123}', %q{ } # cfunc kwargs -assert_equal '{:foo=>123, :bar=>456}', %q{ +assert_equal '{foo: 123, bar: 456}', %q{ def foo(bar) bar.replace(foo: 123, bar: 456) end @@ -3378,7 +3382,7 @@ assert_equal '{:foo=>123, :bar=>456}', %q{ } # variadic cfunc kwargs -assert_equal '{:foo=>123}', %q{ +assert_equal '{foo: 123}', %q{ def foo(bar) bar.merge(foo: 123) end @@ -3502,7 +3506,7 @@ assert_equal "true", %q{ } # duphash -assert_equal '{:foo=>123}', %q{ +assert_equal '{foo: 123}', %q{ def foo {foo: 123} end @@ -3512,7 +3516,7 @@ assert_equal '{:foo=>123}', %q{ } # newhash -assert_equal '{:foo=>2}', %q{ +assert_equal '{foo: 2}', %q{ def foo {foo: 1+1} end @@ -3622,6 +3626,74 @@ assert_equal 'new', %q{ test } +# Bug #21257 (infinite jmp) +assert_equal 'ok', %q{ + Good = :ok + + def first + second + end + + def second + ::Good + end + + # Make `second` side exit on its first instruction + trace = TracePoint.new(:line) { } + trace.enable(target: method(:second)) + + first + # Recompile now that the constant cache is populated, so we get a fallthrough from `first` to `second` + # (this is need to reproduce with --yjit-call-threshold=1) + RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT) + first + + # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called + module InvalidateConstantCache + Good = nil + end + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + first + first +} + +assert_equal 'ok', %q{ + # Multiple incoming branches into second + Good = :ok + + def incoming_one + second + end + + def incoming_two + second + end + + def second + ::Good + end + + # Make `second` side exit on its first instruction + trace = TracePoint.new(:line) { } + trace.enable(target: method(:second)) + + incoming_one + # Recompile now that the constant cache is populated, so we get a fallthrough from `incoming_one` to `second` + # (this is need to reproduce with --yjit-call-threshold=1) + RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT) + incoming_one + incoming_two + + # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called + module InvalidateConstantCache + Good = nil + end + + incoming_one +} + assert_equal 'ok', %q{ # Try to compile new method while OOM def foo @@ -3746,36 +3818,6 @@ assert_equal '3,12', %q{ pt_inspect(p) } -# Regression test for deadlock between branch_stub_hit and ractor_receive_if -assert_equal '10', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 - main << 4 - main << 5 - main << 6 - main << 7 - main << 8 - main << 9 - main << 10 - end - - a = [] - a << Ractor.receive_if{|msg| msg == 10} - a << Ractor.receive_if{|msg| msg == 9} - a << Ractor.receive_if{|msg| msg == 8} - a << Ractor.receive_if{|msg| msg == 7} - a << Ractor.receive_if{|msg| msg == 6} - a << Ractor.receive_if{|msg| msg == 5} - a << Ractor.receive_if{|msg| msg == 4} - a << Ractor.receive_if{|msg| msg == 3} - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| msg == 1} - - a.length -} - # checktype assert_equal 'false', %q{ def function() @@ -4081,6 +4123,26 @@ assert_equal '1', %q{ bar { } } +# unshareable bmethod call through Method#to_proc#call +assert_equal '1000', %q{ + define_method(:bmethod) do + self + end + + Ractor.new do + errors = 0 + 1000.times do + p = method(:bmethod).to_proc + begin + p.call + rescue RuntimeError + errors += 1 + end + end + errors + end.value +} + # test for return stub lifetime issue assert_equal '1', %q{ def foo(n) @@ -4321,7 +4383,7 @@ assert_equal "ArgumentError", %q{ # Rest with block # Simplified code from railsbench -assert_equal '[{"/a"=>"b", :as=>:c, :via=>:post}, [], nil]', %q{ +assert_equal '[{"/a" => "b", as: :c, via: :post}, [], nil]', %q{ def match(path, *rest, &block) [path, rest, block] end @@ -4449,7 +4511,7 @@ assert_equal 'true', %q{ rescue ArgumentError true end -} unless rjit_enabled? # Not yet working on RJIT +} # Regression test: register allocator on expandarray assert_equal '[]', %q{ @@ -4566,6 +4628,11 @@ assert_equal '[2, 4611686018427387904]', %q{ [1.succ, 4611686018427387903.succ] } +# Integer pred and overflow +assert_equal '[0, -4611686018427387905]', %q{ + [1.pred, -4611686018427387904.pred] +} + # Integer right shift assert_equal '[0, 1, -4]', %q{ [0 >> 1, 2 >> 1, -7 >> 1] @@ -4582,7 +4649,7 @@ assert_equal '[nil, "yield"]', %q{ } # splat with ruby2_keywords into rest parameter -assert_equal '[[{:a=>1}], {}]', %q{ +assert_equal '[[{a: 1}], {}]', %q{ ruby2_keywords def foo(*args) = args def bar(*args, **kw) = [args, kw] @@ -4623,7 +4690,7 @@ assert_normal_exit %q{ } # a kwrest case -assert_equal '[1, 2, {:complete=>false}]', %q{ +assert_equal '[1, 2, {complete: false}]', %q{ def rest(foo: 1, bar: 2, **kwrest) [foo, bar, kwrest] end @@ -4682,7 +4749,7 @@ assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{ } # Class#new (arity=-1), splat, and ruby2_keywords -assert_equal '[0, {1=>1}]', %q{ +assert_equal '[0, {1 => 1}]', %q{ class KwInit attr_reader :init_args def initialize(x = 0, **kw) @@ -4698,7 +4765,7 @@ assert_equal '[0, {1=>1}]', %q{ } # Chilled string setivar trigger warning -assert_equal 'literal string will be frozen in the future', %q{ +assert_match(/literal string will be frozen in the future/, %q{ Warning[:deprecated] = true $VERBOSE = true $warning = "no-warning" @@ -4726,7 +4793,7 @@ assert_equal 'literal string will be frozen in the future', %q{ setivar!("chilled") # Emit warning $warning -} +}) # arity=-2 cfuncs assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{ @@ -4848,6 +4915,16 @@ assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{ tests } +# regression test for splat with &proc{} when the target has rest (Bug #21266) +assert_equal '[]', %q{ + def foo(args) = bar(*args, &proc { _1 }) + def bar(_, _, _, _, *rest) = yield rest + + GC.stress = true + foo([1,2,3,4]) + foo([1,2,3,4]) +} + # regression test for invalidating an empty block assert_equal '0', %q{ def foo = (* = 1).pred @@ -4859,7 +4936,7 @@ assert_equal '0', %q{ end foo # try again -} unless rjit_enabled? # doesn't work on RJIT +} # test integer left shift with constant rhs assert_equal [0x80000000000, 'a+', :ok].inspect, %q{ @@ -4967,7 +5044,7 @@ assert_equal '[[true, false, false], [true, true, false], [true, :error, :error] end results << test -} unless rjit_enabled? # Not yet working on RJIT +} # test FalseClass#=== before and after redefining FalseClass#== assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ @@ -5002,7 +5079,7 @@ assert_equal '[[true, false, false], [true, false, true], [true, :error, :error] end results << test -} unless rjit_enabled? # Not yet working on RJIT +} # test NilClass#=== before and after redefining NilClass#== assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ @@ -5037,7 +5114,7 @@ assert_equal '[[true, false, false], [true, false, true], [true, :error, :error] end results << test -} unless rjit_enabled? # Not yet working on RJIT +} # test struct accessors fire c_call events assert_equal '[[:c_call, :x=], [:c_call, :x]]', %q{ @@ -5186,6 +5263,7 @@ end test RUBY +# opt_newarray_send pack/buffer assert_equal '[true, true]', <<~'RUBY' def pack v = 1.23 @@ -5201,3 +5279,279 @@ assert_equal '[true, true]', <<~'RUBY' [pack, with_buffer] RUBY + +# String#[] / String#slice +assert_equal 'ok', <<~'RUBY' + def error(klass) + yield + rescue klass + true + end + + def test + str = "こんにちは" + substr = "にち" + failures = [] + + # Use many small statements to keep context for each slice call smaller than MAX_CTX_TEMPS + + str[1] == "ん" && str.slice(4) == "は" || failures << :index + str[5].nil? && str.slice(5).nil? || failures << :index_end + + str[1, 2] == "んに" && str.slice(2, 1) == "に" || failures << :beg_len + str[5, 1] == "" && str.slice(5, 1) == "" || failures << :beg_len_end + + str[1..2] == "んに" && str.slice(2..2) == "に" || failures << :range + + str[/に./] == "にち" && str.slice(/に./) == "にち" || failures << :regexp + + str[/に./, 0] == "にち" && str.slice(/に./, 0) == "にち" || failures << :regexp_cap0 + + str[/に(.)/, 1] == "ち" && str.slice(/に(.)/, 1) == "ち" || failures << :regexp_cap1 + + str[substr] == substr && str.slice(substr) == substr || failures << :substr + + error(TypeError) { str[Object.new] } && error(TypeError) { str.slice(Object.new, 1) } || failures << :type_error + error(RangeError) { str[Float::INFINITY] } && error(RangeError) { str.slice(Float::INFINITY) } || failures << :range_error + + return "ok" if failures.empty? + {failures: failures} + end + + test +RUBY + +# opt_duparray_send :include? +assert_equal '[true, false]', <<~'RUBY' + def test(x) + [:a, :b].include?(x) + end + + [ + test(:b), + test(:c), + ] +RUBY + +# opt_newarray_send :include? +assert_equal '[true, false]', <<~'RUBY' + def test(x) + [Object.new, :a, :b].include?(x.to_sym) + end + + [ + test("b"), + test("c"), + ] +RUBY + +# YARV: swap and opt_reverse +assert_equal '["x", "Y", "c", "A", "t", "A", "b", "C", "d"]', <<~'RUBY' + class Swap + def initialize(s) + @a, @b, @c, @d = s.split("") + end + + def swap + a, b = @a, @b + b = b.upcase + @a, @b = a, b + end + + def reverse_odd + a, b, c = @a, @b, @c + b = b.upcase + @a, @b, @c = a, b, c + end + + def reverse_even + a, b, c, d = @a, @b, @c, @d + a = a.upcase + c = c.upcase + @a, @b, @c, @d = a, b, c, d + end + end + + Swap.new("xy").swap + Swap.new("cat").reverse_odd + Swap.new("abcd").reverse_even +RUBY + +assert_normal_exit %{ + class Bug20997 + def foo(&) = self.class.name(&) + + new.foo + end +} + +# This used to trigger a "try to mark T_NONE" +# due to an uninitialized local in foo. +assert_normal_exit %{ + def foo(...) + _local_that_should_nil_on_call = GC.start + end + + def test_bug21021 + puts [], [], [], [], [], [] + foo [] + end + + GC.stress = true + test_bug21021 +} + +assert_equal 'nil', %{ + def foo(...) + _a = _b = _c = binding.local_variable_get(:_c) + + _c + end + + # [Bug #21021] + def test_local_fill_in_forwardable + puts [], [], [], [], [] + foo [] + end + + test_local_fill_in_forwardable.inspect +} + +# Test defined?(yield) and block_given? in non-method context. +# It's good that the body of this runs at true top level and isn't wrapped in a block. +assert_equal 'false', %{ + RESULT = [] + RESULT << defined?(yield) + RESULT << block_given? + + 1.times do + RESULT << defined?(yield) + RESULT << block_given? + end + + module ModuleContext + 1.times do + RESULT << defined?(yield) + RESULT << block_given? + end + end + + class << self + RESULT << defined?(yield) + RESULT << block_given? + end + + RESULT.any? +} + +# throw and String#dup with GC stress +assert_equal 'foo', %{ + GC.stress = true + + def foo + 1.times { return "foo".dup } + end + + 10.times.map { foo.dup }.last +} + +# regression test for [Bug #21772] +# local variable type tracking desync +assert_normal_exit %q{ + def some_method = 0 + + def test_body(key) + some_method + key = key.to_s # setting of local relevant + + key == "symbol" + end + + def jit_caller = test_body("session_id") + + jit_caller # first iteration, non-escaped environment + alias some_method binding # induce environment escape + test_body(:symbol) +} + +# regression test for missing check in identity method inlining +assert_normal_exit %q{ + # Use dead code (if false) to create a local + # without initialization instructions. + def foo(a) + if false + x = nil + end + x + end + def test = foo(1) + test + test +} + +# regression test for tracing invalidation with on-stack compiled methods +# Exercises the on_stack_iseqs path in rb_yjit_tracing_invalidate_all +# where delayed deallocation must not create aliasing &mut references +# to IseqPayload (use-after-free of version_map backing storage). +assert_normal_exit %q{ + def deep = 42 + def mid = deep + def outer = mid + + # Compile all three methods with YJIT + 10.times { outer } + + # Enable tracing from within a call chain so that outer/mid/deep + # are on the stack when rb_yjit_tracing_invalidate_all runs. + # This triggers the on_stack_iseqs (delayed deallocation) path. + def deep + TracePoint.new(:line) {}.enable + 42 + end + + outer + + # After invalidation, verify YJIT can recompile and run correctly + def deep = 42 + 10.times { outer } +} + +# regression test for tracing invalidation with on-stack fibers +# Suspended fibers have iseqs on their stack that must survive invalidation. +assert_equal '42', %q{ + def compiled_method + Fiber.yield + 42 + end + + # Compile the method + 10.times { compiled_method rescue nil } + + fiber = Fiber.new { compiled_method } + fiber.resume # suspends inside compiled_method — it's now on the fiber's stack + + # Enable tracing while compiled_method is on the fiber's stack. + # This triggers rb_yjit_tracing_invalidate_all with on-stack iseqs. + TracePoint.new(:call) {}.enable + + # Resume the fiber — compiled_method's iseq must still be valid + fiber.resume.to_s +} + +# regression test for register mapping of methods with over 256 locals +# [Bug #22074] +assert_equal "ok", %q{ + source = +"def many_locals\n" + source << " total = 0\n" + + 128.times do |i| + source << " y#{i} = 1\n" + source << " x#{i} = Object.new\n" + end + + source << " total += 1\n" + source << " raise total.inspect unless total == 1\n" + source << "end\n" + + eval(source) + many_locals + "ok" +} diff --git a/bootstraptest/test_yjit_rust_port.rb b/bootstraptest/test_yjit_rust_port.rb index e399e0e49e..2dbcebc03a 100644 --- a/bootstraptest/test_yjit_rust_port.rb +++ b/bootstraptest/test_yjit_rust_port.rb @@ -374,7 +374,7 @@ assert_equal 'ok', %q{ r = Ractor.new do 'ok' end - r.take + r.value } # Passed arguments to Ractor.new will be a block parameter @@ -384,7 +384,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ok' do |msg| msg end - r.take + r.value } # Pass multiple arguments to Ractor.new @@ -393,7 +393,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ping', 'pong' do |msg, msg2| [msg, msg2] end - 'ok' if r.take == ['ping', 'pong'] + 'ok' if r.value == ['ping', 'pong'] } # Ractor#send passes an object with copy to a Ractor @@ -403,7 +403,7 @@ assert_equal 'ok', %q{ msg = Ractor.receive end r.send 'ok' - r.take + r.value } assert_equal '[1, 2, 3]', %q{ |
