summaryrefslogtreecommitdiff
path: root/bootstraptest
diff options
context:
space:
mode:
Diffstat (limited to 'bootstraptest')
-rwxr-xr-xbootstraptest/runner.rb33
-rw-r--r--bootstraptest/test_flow.rb4
-rw-r--r--bootstraptest/test_insns.rb7
-rw-r--r--bootstraptest/test_io.rb2
-rw-r--r--bootstraptest/test_method.rb13
-rw-r--r--bootstraptest/test_ractor.rb596
-rw-r--r--bootstraptest/test_syntax.rb8
-rw-r--r--bootstraptest/test_yjit.rb188
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"
+}