diff options
Diffstat (limited to 'bootstraptest')
| -rwxr-xr-x | bootstraptest/runner.rb | 33 | ||||
| -rw-r--r-- | bootstraptest/test_flow.rb | 4 | ||||
| -rw-r--r-- | bootstraptest/test_insns.rb | 7 | ||||
| -rw-r--r-- | bootstraptest/test_io.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_method.rb | 13 | ||||
| -rw-r--r-- | bootstraptest/test_ractor.rb | 596 | ||||
| -rw-r--r-- | bootstraptest/test_syntax.rb | 8 | ||||
| -rw-r--r-- | bootstraptest/test_yjit.rb | 188 |
8 files changed, 655 insertions, 196 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index a8e67f3496..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 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_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_method.rb b/bootstraptest/test_method.rb index 997675200e..e894f6f601 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -1427,3 +1427,16 @@ assert_equal 'ok', <<~RUBY 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 4a58ece8ac..4fe90703fc 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -145,28 +145,107 @@ assert_equal '[:ok, :ok, :ok]', %q{ }.map(&:value) } -# Ractor.make_shareable issue for locals in proc [Bug #18023] +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 = 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) + + [pr1.call == self, pr2.call == nil] +} + +# 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) + + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end + + 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 + 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 - Ractor.make_shareable(closure).call + r } -# 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::IsolationError cases +assert_equal '3', %q{ + ok = 0 + + begin + a = 1 + Ractor.shareable_proc{a} + a = 2 + rescue Ractor::IsolationError => e + ok += 1 end - Ractor.make_shareable(closure).call + 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 } ### @@ -237,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 @@ -415,7 +494,7 @@ assert_equal '{ok: 3}', %q{ end 3.times.map{Ractor.receive}.tally -} unless yjit_enabled? || zjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec(), ZJIT hangs +} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() # unshareable object are copied assert_equal 'false', %q{ @@ -428,14 +507,14 @@ assert_equal 'false', %q{ } # 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 @@ -724,7 +803,7 @@ assert_equal 'true', %q{ } # 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 @@ -735,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 @@ -911,8 +1021,8 @@ assert_equal '1234', %q{ 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 @@ -930,8 +1040,8 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ 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 @@ -952,6 +1062,95 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ 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 @@ -967,7 +1166,7 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m 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 @@ -979,6 +1178,20 @@ assert_equal "can not access non-shareable objects in constant Object::STR by no 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 +RUBY + # Setting non-shareable objects into constants by other Ractors is not allowed assert_equal 'can not set constants with non-shareable objects by non-main Ractors', <<~'RUBY', frozen_string_literal: false class C @@ -1056,6 +1269,24 @@ assert_equal 'ok', <<~'RUBY', frozen_string_literal: false 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) assert_equal 'true', <<~'RUBY', frozen_string_literal: false class C @@ -1137,41 +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(a_proc) makes inner structure shareable and freezes it -assert_equal 'true,true,true,true', %q{ - class Proc - attr_reader :obj - def initialize - @obj = Object.new - end +# 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 - pr = Ractor.current.instance_eval do - Proc.new {} - end + # Method with shareable receiver + M1 = Ractor.make_shareable(Object.method(:__id__)) - results = [] - Ractor.make_shareable(pr) - results << Ractor.shareable?(pr) - results << pr.frozen? - results << Ractor.shareable?(pr.obj) - results << pr.obj.frozen? - results.map(&:to_s).join(',') + # 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) @@ -1202,50 +1434,16 @@ assert_equal '[C, M]', %q{ Ractor.make_shareable(ary = [C, M]) } -# Ractor.make_shareable with curried proc checks isolation of original proc -assert_equal 'isolation error', %q{ - a = Object.new - orig = proc { a } - curried = orig.curry - - begin - Ractor.make_shareable(curried) - rescue Ractor::IsolationError - 'isolation error' - else - 'no error' - end -} - # define_method() can invoke different Ractor's proc if the proc is shareable. assert_equal '1', %q{ class C a = 1 - define_method "foo", Ractor.make_shareable(Proc.new{ a }) - a = 2 + define_method "foo", Ractor.shareable_proc{ a } end Ractor.new{ C.new.foo }.value } -# 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 - end - - begin - Ractor.make_shareable(pr) - rescue => e - e.message - end -} - # Ractor.make_shareable(obj, copy: true) makes copied shareable object. assert_equal '[false, false, true, true]', %q{ r = [] @@ -1302,7 +1500,7 @@ assert_equal '[:ok, :ok]', %q{ s = 'str' trap(:INT){p s} }.join - rescue => Ractor::RemoteError + rescue Ractor::RemoteError a << :ok end } @@ -1400,6 +1598,9 @@ assert_equal "ok", %Q{ 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 } @@ -1420,18 +1621,17 @@ assert_equal "#{n}#{n}", %Q{ }.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{ @@ -1471,42 +1671,6 @@ assert_equal "ok", %q{ "ok" } if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # 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 -} - # check method cache invalidation assert_equal "ok", %q{ module M @@ -1601,7 +1765,7 @@ assert_equal 'true', %q{ } # 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{}.value", nil, "test_ractor.rb", 1) @@ -1999,7 +2163,7 @@ assert_equal 'ok', %q{ # move object with complex generic ivars assert_equal 'ok', %q{ - # Make Array too_complex + # Make Array complex 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } ractor = Ractor.new { Ractor.receive } @@ -2011,9 +2175,22 @@ assert_equal 'ok', %q{ 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 too_complex + # Make Array complex 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } ractor = Ractor.new { Ractor.receive } @@ -2327,22 +2504,6 @@ assert_equal '[["Only the successor ractor can take a value", 9], ["ok", 2]]', % }.tally.sort } -# Ractor#take will warn for compatibility. -# This method will be removed after 2025/09/01 -assert_equal "2", %q{ - raise "remove Ractor#take and this test" if Time.now > Time.new(2025, 9, 2) - $VERBOSE = true - r = Ractor.new{42} - $msg = [] - def Warning.warn(msg) - $msg << msg - end - r.take - r.take - raise unless $msg.all?{/Ractor#take/ =~ it} - $msg.size -} - # Cause lots of inline CC misses. assert_equal 'ok', <<~'RUBY' class A; def test; 1 + 1; end; end @@ -2372,3 +2533,134 @@ assert_equal 'ok', <<~'RUBY' 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 f76af3633d..e9ce905e2c 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -1330,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"] @@ -2483,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 @@ -2680,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] @@ -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) @@ -4853,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 @@ -5369,3 +5441,117 @@ assert_equal 'false', %{ 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" +} |
