diff options
Diffstat (limited to 'bootstraptest')
| -rwxr-xr-x | bootstraptest/runner.rb | 45 | ||||
| -rw-r--r-- | bootstraptest/test_fiber.rb | 5 | ||||
| -rw-r--r-- | bootstraptest/test_flow.rb | 4 | ||||
| -rw-r--r-- | bootstraptest/test_fork.rb | 7 | ||||
| -rw-r--r-- | bootstraptest/test_insns.rb | 7 | ||||
| -rw-r--r-- | bootstraptest/test_io.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_literal.rb | 4 | ||||
| -rw-r--r-- | bootstraptest/test_method.rb | 45 | ||||
| -rw-r--r-- | bootstraptest/test_ractor.rb | 1729 | ||||
| -rw-r--r-- | bootstraptest/test_syntax.rb | 8 | ||||
| -rw-r--r-- | bootstraptest/test_yjit.rb | 424 | ||||
| -rw-r--r-- | bootstraptest/test_yjit_rust_port.rb | 8 |
12 files changed, 1608 insertions, 680 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index 16bfdd9ea2..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 @@ -298,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 @@ -625,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) @@ -634,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"] @@ -891,4 +870,8 @@ def yjit_enabled? ENV.key?('RUBY_YJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('yjit') || BT.ruby.include?('yjit') end +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_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 a54316dbca..860ef285d0 100644 --- a/bootstraptest/test_fork.rb +++ b/bootstraptest/test_fork.rb @@ -85,13 +85,18 @@ assert_equal 'ok', %q{ 10.times do pid = fork{ exit!(0) } deadline = now + 10 - until Process.waitpid(pid, Process::WNOHANG) + 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 diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb index 8a6efae089..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}" }, ], @@ -426,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 7295f7a148..39e6527027 100644 --- a/bootstraptest/test_literal.rb +++ b/bootstraptest/test_literal.rb @@ -117,8 +117,8 @@ 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 '[]', '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]" diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb index f6f04541d6..e894f6f601 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -1395,3 +1395,48 @@ assert_equal 'ok', %q{ 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 55dae42111..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,100 +115,137 @@ 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 +assert_equal '[1, 2, 3]', %q{ + ports = 3.times.map{Ractor::Port.new} + + r = Ractor.new ports do |ports| + ports[0] << 3 + ports[1] << 1 + ports[2] << 2 end a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| msg == 3} - a << Ractor.receive + a << ports[1].receive # 1 + a << ports[2].receive # 2 + a << ports[0].receive # 3 + a +} + +# dtoa race condition +assert_equal '[:ok, :ok, :ok]', %q{ + n = 3 + n.times.map{ + Ractor.new{ + 10_000.times{ rand.to_s } + :ok + } + }.map(&:value) } -# 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 +assert_equal "42", %q{ + a = 42 + Ractor.shareable_lambda{ a }.call +} + +# 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 = 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) - a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| break [msg, :break]} - a << Ractor.receive + [pr1.call == self, pr2.call == nil] } -# 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 +# 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) - a = [] + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end - Ractor.receive_if do |msg| - begin - Ractor.receive - rescue Ractor::Error - a << [:e1, msg] + r << pr1.call << pr1.binding.class +} + +# 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 - 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 + r = [pr1.call, pr1.binding.class] + + pr2 = Ractor.make_shareable(pr1) + r << pr1.equal?(pr2) + + begin + pr1.call + rescue SyntaxError + r << SyntaxError end - a # -} + begin + r << pr1.binding + rescue ArgumentError + r << $!.message + end -# dtoa race condition -assert_equal '[:ok, :ok, :ok]', %q{ - n = 3 - n.times.map{ - Ractor.new{ - 10_000.times{ rand.to_s } - :ok - } - }.map(&:take) + r } -# Ractor.make_shareable 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] } } +# Ractor::IsolationError cases +assert_equal '3', %q{ + ok = 0 - Ractor.make_shareable(closure).call -} + begin + a = 1 + Ractor.shareable_proc{a} + a = 2 + rescue Ractor::IsolationError => e + ok += 1 + end -# 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 + begin + cond = false + b = 1 + b = 2 if cond + Ractor.shareable_proc{b} + rescue Ractor::IsolationError => e + ok += 1 end - Ractor.make_shareable(closure).call + begin + 1.times{|i| + i = 2 + Ractor.shareable_proc{i} + } + rescue Ractor::IsolationError => e + ok += 1 + end } ### @@ -218,27 +255,32 @@ 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"] } @@ -274,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 @@ -282,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 @@ -317,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 @@ -365,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) @@ -403,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' @@ -589,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{ @@ -598,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 @@ -617,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 @@ -668,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 } @@ -700,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 @@ -722,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 @@ -742,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 @@ -752,79 +658,37 @@ assert_equal '[0, 1]', %q{ # unshareable frozen objects should still be frozen in new ractor after move assert_equal 'true', %q{ -r = Ractor.new do - obj = receive - { frozen: obj.frozen? } -end -obj = [Object.new].freeze -r.send(obj, move: true) -r.take[:frozen] -} - -# 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 + obj = receive + { frozen: obj.frozen? } end - - Ractor.receive + obj = [Object.new].freeze + r.send(obj, move: true) + r.value[:frozen] } -# yield/move can fail -assert_equal "allocator undefined for Thread", %q{ - r = Ractor.new do - obj = Thread.new{} - Ractor.yield obj - rescue => e - e.message - end - r.take -} - -# 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 @@ -838,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 } @@ -854,7 +718,7 @@ assert_equal 'ok', %q{ 'ok' end - r.take + r.value } # $DEBUG, $VERBOSE are Ractor local @@ -912,7 +776,7 @@ assert_equal 'true', %q{ h = Ractor.new do ractor_local_globals - end.take + end.value ractor_local_globals == h #=> true } @@ -921,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 @@ -929,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 @@ -944,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 @@ -957,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 @@ -973,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 @@ -999,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 @@ -1020,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 @@ -1034,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 } @@ -1060,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 @@ -1096,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 @@ -1133,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 @@ -1155,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 @@ -1170,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 @@ -1196,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 @@ -1207,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 @@ -1218,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 @@ -1228,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 @@ -1249,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 @@ -1262,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) @@ -1346,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) @@ -1393,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. @@ -1440,7 +1466,7 @@ assert_equal '[6, 10]', %q{ Ractor.new{ # line 5 a = 1 b = 2 - }.take + }.value c = 3 # line 9 end rs @@ -1450,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) @@ -1458,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 @@ -1466,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 } @@ -1503,12 +1529,12 @@ 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] } @@ -1524,7 +1550,25 @@ assert_equal '1', %q{ } }.each(&:join) a.uniq.size - }.take + }.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 } ### @@ -1540,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{ @@ -1556,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{ @@ -1588,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? # 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{ @@ -1700,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 @@ -1725,7 +1786,7 @@ assert_equal 'ok', %q{ end r.send obj, move: true - r.take + r.value } ## Ractor::Selector @@ -1801,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 } @@ -1870,7 +1932,7 @@ assert_equal 'true', %q{ # prism parser with -O0 build consumes a lot of machine stack Data.define(:fileno).new(1) end - }.take.fileno > 0 + }.value.fileno > 0 } # require_relative in Ractor @@ -1888,7 +1950,7 @@ assert_equal 'true', %q{ begin Ractor.new dummyfile do |f| require_relative File.basename(f) - end.take + end.value ensure File.unlink dummyfile end @@ -1905,7 +1967,7 @@ assert_equal 'LoadError', %q{ rescue LoadError => e e.class end - end.take + end.value } # autolaod in Ractor @@ -1920,7 +1982,7 @@ assert_equal 'true', %q{ Data.define(:fileno).new(1) end end - r.take.fileno > 0 + r.value.fileno > 0 } # failed in autolaod in Ractor @@ -1935,5 +1997,670 @@ assert_equal 'LoadError', %q{ e.class end end - r.take + 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_syntax.rb b/bootstraptest/test_syntax.rb index fbc9c6f62e..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{ diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 5a5e08f095..e9ce905e2c 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -220,7 +220,7 @@ assert_equal 'Sub', %q{ call(Sub.new('o')).class } -# String#dup with FL_EXIVAR +# String#dup with generic ivars assert_equal '["str", "ivar"]', %q{ def str_dup(str) = str.dup str = "str" @@ -468,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 @@ -1415,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"] @@ -2568,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 @@ -2765,6 +2706,22 @@ assert_equal '[1, 2]', %q{ expandarray_redefined_nilclass } +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 a, b, c = [1, 2] @@ -3018,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 @@ -3034,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 @@ -3054,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 @@ -3140,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{ @@ -3667,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 @@ -3791,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() @@ -4126,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) @@ -4898,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 @@ -5387,3 +5414,144 @@ assert_equal 'nil', %{ 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{ |
