summaryrefslogtreecommitdiff
path: root/bootstraptest
diff options
context:
space:
mode:
Diffstat (limited to 'bootstraptest')
-rwxr-xr-xbootstraptest/runner.rb270
-rw-r--r--bootstraptest/test_attr.rb16
-rw-r--r--bootstraptest/test_autoload.rb12
-rw-r--r--bootstraptest/test_eval.rb43
-rw-r--r--bootstraptest/test_exception.rb2
-rw-r--r--bootstraptest/test_fiber.rb5
-rw-r--r--bootstraptest/test_finalizer.rb8
-rw-r--r--bootstraptest/test_flow.rb6
-rw-r--r--bootstraptest/test_fork.rb27
-rw-r--r--bootstraptest/test_insns.rb82
-rw-r--r--bootstraptest/test_io.rb5
-rw-r--r--bootstraptest/test_jump.rb2
-rw-r--r--bootstraptest/test_literal.rb9
-rw-r--r--bootstraptest/test_literal_suffix.rb12
-rw-r--r--bootstraptest/test_load.rb12
-rw-r--r--bootstraptest/test_method.rb296
-rw-r--r--bootstraptest/test_ractor.rb2060
-rw-r--r--bootstraptest/test_syntax.rb58
-rw-r--r--bootstraptest/test_thread.rb23
-rw-r--r--bootstraptest/test_yjit.rb2532
-rw-r--r--bootstraptest/test_yjit_rust_port.rb8
21 files changed, 4735 insertions, 753 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb
index 3d42390254..04de0c93b9 100755
--- a/bootstraptest/runner.rb
+++ b/bootstraptest/runner.rb
@@ -6,7 +6,6 @@
# Never use optparse in this file.
# Never use test/unit in this file.
# Never use Ruby extensions in this file.
-# Maintain Ruby 1.8 compatibility for now
$start_time = Time.now
@@ -17,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
@@ -61,7 +61,7 @@ if !Dir.respond_to?(:mktmpdir)
end
# Configuration
-BT = Struct.new(:ruby,
+bt = Struct.new(:ruby,
:verbose,
:color,
:tty,
@@ -73,9 +73,57 @@ BT = Struct.new(:ruby,
:failed,
:reset,
:columns,
+ :window_width,
:width,
+ :indent,
:platform,
- ).new
+ :timeout,
+ :timeout_scale,
+ :launchable_test_reports
+ )
+BT = Class.new(bt) do
+ def indent=(n)
+ super
+ if (self.columns ||= 0) < n
+ $stderr.print(' ' * (n - self.columns))
+ end
+ self.columns = indent
+ end
+
+ def putc(c)
+ unless self.quiet
+ if self.window_width == nil
+ unless w = ENV["COLUMNS"] and (w = w.to_i) > 0
+ w = 80
+ end
+ w -= 1
+ self.window_width = w
+ end
+ if self.window_width and self.columns >= self.window_width
+ $stderr.print "\n", " " * (self.indent ||= 0)
+ self.columns = indent
+ end
+ $stderr.print c
+ $stderr.flush
+ self.columns += 1
+ end
+ end
+
+ def wn=(wn)
+ unless wn == 1
+ wn = Test::JobServer.max_jobs(wn > 0 ? wn : 1024, ENV.delete("MAKEFLAGS")) || wn
+ if wn <= 0
+ require 'etc'
+ wn = [Etc.nprocessors / 2, 1].max
+ end
+ end
+ super wn
+ end
+
+ def apply_timeout_scale(timeout)
+ timeout&.*(timeout_scale)
+ end
+end.new
BT_STATE = Struct.new(:count, :error).new
@@ -87,7 +135,13 @@ def main
BT.color = nil
BT.tty = nil
BT.quiet = false
- BT.wn = 1
+ BT.timeout = 180
+ BT.timeout_scale = 1
+ if (ts = (ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"]).to_i) > 1
+ BT.timeout_scale *= ts
+ end
+
+ # BT.wn = 1
dir = nil
quiet = false
tests = nil
@@ -117,19 +171,18 @@ def main
warn "unknown --tty argument: #$3" if $3
BT.tty = !$1 || !$2
true
- when /\A(-q|--q(uiet))\z/
+ when /\A(-q|--q(uiet)?)\z/
quiet = true
BT.quiet = true
true
when /\A-j(\d+)?/
- wn = $1.to_i
- if wn <= 0
- require 'etc'
- wn = [Etc.nprocessors / 2, 1].max
- end
- BT.wn = wn
+ BT.wn = $1.to_i
true
- when /\A(-v|--v(erbose))\z/
+ when /\A--timeout=(\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?)(?::(\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?))?/
+ BT.timeout = $1.to_f
+ BT.timeout_scale = $2.to_f if defined?($2)
+ true
+ when /\A(-v|--v(erbose)?)\z/
BT.verbose = true
BT.quiet = false
true
@@ -141,6 +194,7 @@ Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...]
default: /tmp/bootstraptestXXXXX.tmpwd
--color[=WHEN] Colorize the output. WHEN defaults to 'always'
or can be 'never' or 'auto'.
+ --timeout=TIMEOUT Default timeout in seconds.
-s, --stress stress test.
-v, --verbose Output test name before exec.
-q, --quiet Don\'t print header message.
@@ -149,21 +203,46 @@ End
exit true
when /\A-j/
true
+ when /--launchable-test-reports=(.*)/
+ if File.exist?($1)
+ # To protect files from overwritten, do nothing when the file exists.
+ return true
+ end
+
+ begin
+ require_relative '../tool/lib/launchable'
+ rescue LoadError
+ # The following error sometimes happens, so we're going to skip writing Launchable report files in this case.
+ #
+ # ```
+ # /tmp/tmp.bISss9CtXZ/.ext/common/json/ext.rb:15:in 'Kernel#require':
+ # /tmp/tmp.bISss9CtXZ/.ext/x86_64-linux/json/ext/parser.so:
+ # undefined symbol: ruby_abi_version - ruby_abi_version (LoadError)
+ # ```
+ #
+ return true
+ end
+ BT.launchable_test_reports = writer = Launchable::JsonStreamWriter.new($1)
+ writer.write_array('testCases')
+ at_exit {
+ writer.close
+ }
+ true
else
false
end
}
if tests and not ARGV.empty?
- $stderr.puts "--tests and arguments are exclusive"
- exit false
+ abort "--sets and arguments are exclusive"
end
tests ||= ARGV
tests = Dir.glob("#{File.dirname($0)}/test_*.rb").sort if tests.empty?
- pathes = tests.map {|path| File.expand_path(path) }
+ paths = tests.map {|path| File.expand_path(path) }
BT.progress = %w[- \\ | /]
BT.progress_bs = "\b" * BT.progress[0].size
BT.tty = $stderr.tty? if BT.tty.nil?
+ BT.wn ||= /-j(\d+)?/ =~ (ENV["MAKEFLAGS"] || ENV["MFLAGS"]) ? $1.to_i : 1
case BT.color
when nil
@@ -192,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
@@ -202,7 +281,7 @@ End
end
in_temporary_working_directory(dir) do
- exec_test pathes
+ exec_test paths
end
end
@@ -214,8 +293,8 @@ def erase(e = true)
end
end
-def load_test pathes
- pathes.each do |path|
+def load_test paths
+ paths.each do |path|
load File.expand_path(path)
end
end
@@ -241,19 +320,20 @@ def concurrent_exec_test
end
end
- $stderr.print ' ' unless BT.quiet
+ BT.indent = 1
aq.close
i = 1
term_wn = 0
begin
while BT.wn != term_wn
if r = rq.pop
+ BT_STATE.count += 1
case
when BT.quiet
when BT.tty
$stderr.print "#{BT.progress_bs}#{BT.progress[(i+=1) % BT.progress.size]}"
else
- $stderr.print '.'
+ BT.putc '.'
end
else
term_wn += 1
@@ -265,17 +345,70 @@ def concurrent_exec_test
end
end
-def exec_test(pathes)
+##
+# Module for writing a test file for uploading test results into Launchable.
+# In bootstraptest, we aggregate the test results based on file level.
+module Launchable
+ @@last_test_name = nil
+ @@failure_log = ''
+ @@duration = 0
+
+ def show_progress(message = '')
+ faildesc, t = super
+
+ if writer = BT.launchable_test_reports
+ if faildesc
+ @@failure_log += faildesc
+ end
+ repo_path = File.expand_path("#{__dir__}/../")
+ relative_path = File.join(__dir__, self.path).delete_prefix("#{repo_path}/")
+ if @@last_test_name != nil && @@last_test_name != relative_path
+ # The test path is a URL-encoded representation.
+ # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18
+ test_path = "#{encode_test_path_component("file")}=#{encode_test_path_component(@@last_test_name)}"
+ if @@failure_log.size > 0
+ status = 'TEST_FAILED'
+ else
+ status = 'TEST_PASSED'
+ end
+ writer.write_object(
+ {
+ testPath: test_path,
+ status: status,
+ duration: t,
+ createdAt: Time.now.to_s,
+ stderr: @@failure_log,
+ stdout: nil,
+ data: {
+ lineNumber: self.lineno
+ }
+ }
+ )
+ @@duration = 0
+ @@failure_log = ''
+ end
+ @@last_test_name = relative_path
+ @@duration += t
+ end
+ end
+
+ private
+ def encode_test_path_component component
+ component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26')
+ end
+end
+
+def exec_test(paths)
# setup
- load_test pathes
+ load_test paths
BT_STATE.count = 0
BT_STATE.error = 0
BT.columns = 0
- BT.width = pathes.map {|path| File.basename(path).size}.max + 2
+ BT.width = paths.map {|path| File.basename(path).size}.max + 2
# execute tests
if BT.wn > 1
- concurrent_exec_test if BT.wn > 1
+ concurrent_exec_test
else
prev_basename = nil
Assertion.all.each do |basename, assertions|
@@ -341,6 +474,7 @@ def target_platform
end
class Assertion < Struct.new(:src, :path, :lineno, :proc)
+ prepend Launchable
@count = 0
@all = Hash.new{|h, k| h[k] = []}
@errbuf = []
@@ -364,7 +498,7 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
def initialize(*args)
super
self.class.add self
- @category = self.path.match(/test_(.+)\.rb/)[1]
+ @category = self.path[/\Atest_(.+)\.rb\z/, 1]
end
def call
@@ -415,9 +549,9 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
$stderr.print "#{BT.progress_bs}#{BT.progress[BT_STATE.count % BT.progress.size]}"
end
- t = Time.now if BT.verbose
+ t = Time.now if BT.verbose || BT.launchable_test_reports
faildesc, errout = with_stderr {yield}
- t = Time.now - t if BT.verbose
+ t = Time.now - t if BT.verbose || BT.launchable_test_reports
if !faildesc
# success
@@ -428,7 +562,7 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
elsif BT.verbose
$stderr.printf(". %.3f\n", t)
else
- $stderr.print '.'
+ BT.putc '.'
end
else
$stderr.print "#{BT.failed}F"
@@ -444,6 +578,8 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
$stderr.printf("%-*s%s", BT.width, path, BT.progress[BT_STATE.count % BT.progress.size])
end
end
+
+ [faildesc, t]
rescue Interrupt
$stderr.puts "\##{@id} #{path}:#{lineno}"
raise
@@ -462,14 +598,22 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
end
end
- def get_result_string(opt = '', **argh)
+ class Timeout < StandardError; end
+
+ def get_result_string(opt = '', timeout: BT.timeout, **argh)
if BT.ruby
+ timeout = BT.apply_timeout_scale(timeout)
filename = make_srcfile(**argh)
begin
kw = self.err ? {err: self.err} : {}
out = IO.popen("#{BT.ruby} -W0 #{opt} #{filename}", **kw)
pid = out.pid
- out.read.tap{ Process.waitpid(pid); out.close }
+ th = Thread.new {out.read.tap {Process.waitpid(pid); out.close}}
+ 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"]
@@ -487,9 +631,14 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
def make_srcfile(frozen_string_literal: nil)
filename = "bootstraptest.#{self.path}_#{self.lineno}_#{self.id}.rb"
File.open(filename, 'w') {|f|
- f.puts "#frozen_string_literal:true" if frozen_string_literal
- f.puts "GC.stress = true" if $stress
- f.puts "print(begin; #{self.src}; end)"
+ f.puts "#frozen_string_literal:#{frozen_string_literal}" unless frozen_string_literal.nil?
+ if $stress
+ f.puts "GC.stress = true" if $stress
+ else
+ f.puts ""
+ end
+ f.puts "class BT_Skip < Exception; end; def skip(msg) = raise(BT_Skip, msg.to_s)"
+ f.puts "print(begin; #{self.src}; rescue BT_Skip; $!.message; end)"
}
filename
end
@@ -503,9 +652,9 @@ def add_assertion src, pr
Assertion.new(src, path, lineno, pr)
end
-def assert_equal(expected, testsrc, message = '', opt = '', **argh)
+def assert_equal(expected, testsrc, message = '', opt = '', **kwargs)
add_assertion testsrc, -> as do
- as.assert_check(message, opt, **argh) {|result|
+ as.assert_check(message, opt, **kwargs) {|result|
if expected == result
nil
else
@@ -516,9 +665,9 @@ def assert_equal(expected, testsrc, message = '', opt = '', **argh)
end
end
-def assert_match(expected_pattern, testsrc, message = '')
+def assert_match(expected_pattern, testsrc, message = '', **argh)
add_assertion testsrc, -> as do
- as.assert_check(message) {|result|
+ as.assert_check(message, **argh) {|result|
if expected_pattern =~ result
nil
else
@@ -550,8 +699,9 @@ def assert_valid_syntax(testsrc, message = '')
end
end
-def assert_normal_exit(testsrc, *rest, timeout: nil, **opt)
+def assert_normal_exit(testsrc, *rest, timeout: BT.timeout, **opt)
add_assertion testsrc, -> as do
+ timeout = BT.apply_timeout_scale(timeout)
message, ignore_signals = rest
message ||= ''
as.show_progress(message) {
@@ -560,23 +710,19 @@ def assert_normal_exit(testsrc, *rest, timeout: nil, **opt)
timeout_signaled = false
logfile = "assert_normal_exit.#{as.path}.#{as.lineno}.log"
- begin
- err = open(logfile, "w")
- io = IO.popen("#{BT.ruby} -W0 #{filename}", err: err)
- pid = io.pid
- th = Thread.new {
- io.read
- io.close
- $?
- }
- if !th.join(timeout)
- Process.kill :KILL, pid
- timeout_signaled = true
- end
- status = th.value
- ensure
- err.close
+ io = IO.popen("#{BT.ruby} -W0 #{filename}", err: logfile)
+ pid = io.pid
+ th = Thread.new {
+ io.read
+ io.close
+ $?
+ }
+ if !th.join(timeout)
+ Process.kill :KILL, pid
+ timeout_signaled = true
end
+ status = th.value
+
if status && status.signaled?
signo = status.termsig
signame = Signal.list.invert[signo]
@@ -605,9 +751,7 @@ end
def assert_finish(timeout_seconds, testsrc, message = '')
add_assertion testsrc, -> as do
- if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
- timeout_seconds *= 3
- end
+ timeout_seconds = BT.apply_timeout_scale(timeout_seconds)
as.show_progress(message) {
faildesc = nil
@@ -663,6 +807,8 @@ end
def pretty(src, desc, result)
src = src.sub(/\A\s*\n/, '')
+ lines = src.lines
+ src = lines[0..20].join + "(...snip)\n" if lines.size > 20
(/\n/ =~ src ? "\n#{adjust_indent(src)}" : src) + " #=> #{desc}"
end
@@ -720,4 +866,12 @@ def check_coredump
end
end
+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_attr.rb b/bootstraptest/test_attr.rb
index 721a847145..3cb9d3eb39 100644
--- a/bootstraptest/test_attr.rb
+++ b/bootstraptest/test_attr.rb
@@ -34,3 +34,19 @@ assert_equal %{ok}, %{
print "ok"
end
}, '[ruby-core:15120]'
+
+assert_equal %{ok}, %{
+ class Big
+ attr_reader :foo
+ def initialize
+ @foo = "ok"
+ end
+ end
+
+ obj = Big.new
+ 100.times do |i|
+ obj.instance_variable_set(:"@ivar_\#{i}", i)
+ end
+
+ Big.new.foo
+}
diff --git a/bootstraptest/test_autoload.rb b/bootstraptest/test_autoload.rb
index 9e0850bc52..de66f1f3ee 100644
--- a/bootstraptest/test_autoload.rb
+++ b/bootstraptest/test_autoload.rb
@@ -11,7 +11,7 @@ assert_equal 'ok', %q{
}, '[ruby-dev:43816]'
assert_equal 'ok', %q{
- open('zzz2.rb', 'w') {|f| f.puts '' }
+ File.write('zzz2.rb', '')
instance_eval do
autoload :ZZZ, './zzz2.rb'
begin
@@ -23,7 +23,7 @@ assert_equal 'ok', %q{
}, '[ruby-dev:43816]'
assert_equal 'ok', %q{
- open('zzz3.rb', 'w') {|f| f.puts 'class ZZZ; def self.ok;:ok;end;end'}
+ File.write('zzz3.rb', "class ZZZ; def self.ok;:ok;end;end\n")
instance_eval do
autoload :ZZZ, './zzz3.rb'
ZZZ.ok
@@ -31,20 +31,20 @@ assert_equal 'ok', %q{
}, '[ruby-dev:43816]'
assert_equal 'ok', %q{
- open("zzz4.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"}
+ File.write("zzz4.rb", "class ZZZ; def self.ok;:ok;end;end\n")
autoload :ZZZ, "./zzz4.rb"
ZZZ.ok
}
assert_equal 'ok', %q{
- open("zzz5.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"}
+ File.write("zzz5.rb", "class ZZZ; def self.ok;:ok;end;end\n")
autoload :ZZZ, "./zzz5.rb"
require "./zzz5.rb"
ZZZ.ok
}
assert_equal 'okok', %q{
- open("zzz6.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"}
+ File.write("zzz6.rb", "class ZZZ; def self.ok;:ok;end;end\n")
autoload :ZZZ, "./zzz6.rb"
t1 = Thread.new {ZZZ.ok}
t2 = Thread.new {ZZZ.ok}
@@ -60,7 +60,7 @@ assert_finish 5, %q{
}, '[ruby-core:21696]'
assert_equal 'A::C', %q{
- open("zzz7.rb", "w") {}
+ File.write("zzz7.rb", "")
class A
autoload :C, "./zzz7"
class C
diff --git a/bootstraptest/test_eval.rb b/bootstraptest/test_eval.rb
index a9f389c673..20bd9615f4 100644
--- a/bootstraptest/test_eval.rb
+++ b/bootstraptest/test_eval.rb
@@ -217,7 +217,7 @@ assert_equal %q{[10, main]}, %q{
}
%w[break next redo].each do |keyword|
- assert_match %r"Can't escape from eval with #{keyword}\b", %{
+ assert_match %r"Invalid #{keyword}\b", %{
$stderr = STDOUT
begin
eval "0 rescue #{keyword}"
@@ -227,6 +227,16 @@ assert_equal %q{[10, main]}, %q{
}, '[ruby-dev:31372]'
end
+assert_normal_exit %{
+ $stderr = STDOUT
+ 5000.times do
+ begin
+ eval "0 rescue break"
+ rescue SyntaxError
+ end
+ end
+}
+
assert_normal_exit %q{
$stderr = STDOUT
class Foo
@@ -354,3 +364,34 @@ assert_normal_exit %q{
end
}, 'check escaping the internal value th->base_block'
+assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal"
+ eval("'test'").frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal", frozen_string_literal: true
+ eval("'test'").frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal"
+ eval("'test'").frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal", frozen_string_literal: false
+ eval("'test'").frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal"
+ eval("__FILE__").frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal", frozen_string_literal: true
+ eval("__FILE__").frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal"
+ eval("__FILE__").frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal", frozen_string_literal: false
+ eval("__FILE__").frozen?
+RUBY
diff --git a/bootstraptest/test_exception.rb b/bootstraptest/test_exception.rb
index 0fb6f552b8..decfdc08a3 100644
--- a/bootstraptest/test_exception.rb
+++ b/bootstraptest/test_exception.rb
@@ -370,7 +370,7 @@ assert_equal %q{}, %q{
}
##
-assert_match /undefined method `foo\'/, %q{#`
+assert_match /undefined method 'foo\'/, %q{#`
STDERR.reopen(STDOUT)
class C
def inspect
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_finalizer.rb b/bootstraptest/test_finalizer.rb
index 22a16b1220..ccfa0b55d6 100644
--- a/bootstraptest/test_finalizer.rb
+++ b/bootstraptest/test_finalizer.rb
@@ -6,3 +6,11 @@ ObjectSpace.define_finalizer(b1,proc{b1.inspect})
ObjectSpace.define_finalizer(a2,proc{a1.inspect})
ObjectSpace.define_finalizer(a1,proc{})
}, '[ruby-dev:35778]'
+
+assert_equal 'true', %q{
+ obj = Object.new
+ id = obj.object_id
+
+ ObjectSpace.define_finalizer(obj, proc { |i| print(id == i) })
+ nil
+}
diff --git a/bootstraptest/test_flow.rb b/bootstraptest/test_flow.rb
index 35f19db588..7a95def1e6 100644
--- a/bootstraptest/test_flow.rb
+++ b/bootstraptest/test_flow.rb
@@ -363,7 +363,7 @@ assert_equal %q{[1, 2, 3, 5, 2, 3, 5, 7, 8]}, %q{$a = []; begin; ; $a << 1
; $a << 8
; rescue Exception; $a << 99; end; $a}
assert_equal %q{[1, 2, 6, 3, 5, 7, 8]}, %q{$a = []; begin; ; $a << 1
- o = "test"; $a << 2
+ o = "test".dup; $a << 2
def o.test(a); $a << 3
return a; $a << 4
ensure; $a << 5
@@ -376,7 +376,7 @@ assert_equal %q{[1, 4, 7, 5, 8, 9]}, %q{$a = []; begin; ; $a << 1
; $a << 3
end; $a << 4
def m2; $a << 5
- m1(:a, :b, (return 1; :c)); $a << 6
+ m1(:a, :b, (return 1 if true; :c)); $a << 6
end; $a << 7
m2; $a << 8
; $a << 9
@@ -399,7 +399,7 @@ assert_equal %q{[1, 3, 11, 4, 5, 6, 7, 12, 13]}, %q{$a = []; begin; ; $a << 1
m2(begin; $a << 5
2; $a << 6
ensure; $a << 7
- return 3; $a << 8
+ return 3 if true; $a << 8
end); $a << 9
4; $a << 10
end; $a << 11
diff --git a/bootstraptest/test_fork.rb b/bootstraptest/test_fork.rb
index 83923dad97..860ef285d0 100644
--- a/bootstraptest/test_fork.rb
+++ b/bootstraptest/test_fork.rb
@@ -75,3 +75,30 @@ assert_equal '[1, 2]', %q{
end
}, '[ruby-dev:44005] [Ruby 1.9 - Bug #4950]'
+assert_equal 'ok', %q{
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ Thread.new do
+ loop { sleep 0.0001 }
+ end
+
+ 10.times do
+ pid = fork{ exit!(0) }
+ deadline = now + 10
+ while true
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
+ break if status
+ if now > deadline
+ Process.kill(:KILL, pid)
+ raise "failed"
+ end
+ sleep 0.001
+ end
+ unless status.success?
+ raise "child exited with status #{status}"
+ end
+ rescue NotImplementedError
+ end
+ :ok
+}, '[Bug #20670]'
+
diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb
index 91fba9b011..1f70c8075c 100644
--- a/bootstraptest/test_insns.rb
+++ b/bootstraptest/test_insns.rb
@@ -86,13 +86,13 @@ 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}" }, ],
[ 'newarray', %q{ ["true"][0] }, ],
- [ 'newarraykwsplat', %q{ [**{x:'true'}][0][:x] }, ],
+ [ 'pushtoarraykwsplat', %q{ [**{x:'true'}][0][:x] }, ],
[ 'duparray', %q{ [ true ][0] }, ],
[ 'expandarray', %q{ y = [ true, false, nil ]; x, = y; x }, ],
[ 'expandarray', %q{ y = [ true, false, nil ]; x, *z = y; x }, ],
@@ -214,9 +214,24 @@ tests = [
'true'.freeze
},
- [ 'opt_newarray_max', %q{ [ ].max.nil? }, ],
- [ 'opt_newarray_max', %q{ [1, x = 2, 3].max == 3 }, ],
- [ 'opt_newarray_max', <<-'},', ], # {
+ [ 'opt_duparray_send', %q{ x = :a; [:a, :b].include?(x) }, ],
+ [ 'opt_duparray_send', <<-'},', ], # {
+ class Array
+ def include?(i)
+ i == 1
+ end
+ end
+ x = 1
+ [:a, :b].include?(x)
+ },
+
+ [ 'opt_newarray_send', %q{ ![ ].hash.nil? }, ],
+
+ [ 'opt_newarray_send', %q{ v=2; [1, Object.new, 2].include?(v) }, ],
+
+ [ 'opt_newarray_send', %q{ [ ].max.nil? }, ],
+ [ 'opt_newarray_send', %q{ [1, x = 2, 3].max == 3 }, ],
+ [ 'opt_newarray_send', <<-'},', ], # {
class Array
def max
true
@@ -224,9 +239,9 @@ tests = [
end
[1, x = 2, 3].max
},
- [ 'opt_newarray_min', %q{ [ ].min.nil? }, ],
- [ 'opt_newarray_min', %q{ [3, x = 2, 1].min == 1 }, ],
- [ 'opt_newarray_min', <<-'},', ], # {
+ [ 'opt_newarray_send', %q{ [ ].min.nil? }, ],
+ [ 'opt_newarray_send', %q{ [3, x = 2, 1].min == 1 }, ],
+ [ 'opt_newarray_send', <<-'},', ], # {
class Array
def min
true
@@ -234,6 +249,48 @@ tests = [
end
[3, x = 2, 1].min
},
+ [ 'opt_newarray_send', %q{ v = 1.23; [v, v*2].pack("E*").unpack("E*") == [v, v*2] }, ],
+ [ 'opt_newarray_send', %q{ v = 4.56; b = +"x"; [v, v*2].pack("E*", buffer: b); b[1..].unpack("E*") == [v, v*2] }, ],
+ [ 'opt_newarray_send', <<-'},', ], # {
+ v = 7.89;
+ b = +"x";
+ class Array
+ alias _pack pack
+ def pack(s, buffer: nil, prefix: "y")
+ buffer ||= +"b"
+ buffer << prefix
+ _pack(s, buffer: buffer)
+ end
+ end
+ tests = []
+
+ ret = [v].pack("E*", prefix: "z")
+ tests << (ret[0..1] == "bz")
+ tests << (ret[2..].unpack("E*") == [v])
+
+ ret = [v].pack("E*")
+ tests << (ret[0..1] == "by")
+ tests << (ret[2..].unpack("E*") == [v])
+
+ [v, v*2, v*3].pack("E*", buffer: b)
+ tests << (b[0..1] == "xy")
+ tests << (b[2..].unpack("E*") == [v, v*2, v*3])
+
+ class Array
+ def pack(_fmt, buffer:) = buffer
+ end
+
+ b = nil
+ tests << [v].pack("E*", buffer: b).nil?
+
+ class Array
+ def pack(_fmt, **kw) = kw.empty?
+ end
+
+ tests << [v].pack("E*") == true
+
+ tests.all? or puts tests
+ },
[ 'throw', %q{ false.tap { break true } }, ],
[ 'branchif', %q{ x = nil; x ||= true }, ],
@@ -352,7 +409,7 @@ tests = [
[ 'opt_ge', %q{ +0.0.next_float >= 0.0 }, ],
[ 'opt_ge', %q{ ?z >= ?a }, ],
- [ 'opt_ltlt', %q{ '' << 'true' }, ],
+ [ 'opt_ltlt', %q{ +'' << 'true' }, ],
[ 'opt_ltlt', %q{ ([] << 'true').join }, ],
[ 'opt_ltlt', %q{ (1 << 31) == 2147483648 }, ],
@@ -361,7 +418,7 @@ tests = [
[ 'opt_aref', %q{ 'true'[0] == ?t }, ],
[ 'opt_aset', %q{ [][0] = true }, ],
[ 'opt_aset', %q{ {}[0] = true }, ],
- [ 'opt_aset', %q{ x = 'frue'; x[0] = 't'; x }, ],
+ [ 'opt_aset', %q{ x = +'frue'; x[0] = 't'; x }, ],
[ 'opt_aset', <<-'},', ], # {
# opt_aref / opt_aset mixup situation
class X; def x; {}; end; end
@@ -369,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 ff26497696..4081769a8c 100644
--- a/bootstraptest/test_io.rb
+++ b/bootstraptest/test_io.rb
@@ -31,6 +31,7 @@ assert_finish 10, %q{
end
}, '[ruby-dev:32566]'
+/freebsd/ =~ RUBY_PLATFORM or
assert_finish 5, %q{
r, w = IO.pipe
Thread.new {
@@ -84,13 +85,13 @@ 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 }
megacontent = "abc" * 12345678
- #File.open("megasrc", "w") {|f| f << megacontent }
+ #File.write("megasrc", megacontent)
t0 = Thread.main
Thread.new { sleep 0.001 until t0.stop?; Process.kill(:INT, $$) }
diff --git a/bootstraptest/test_jump.rb b/bootstraptest/test_jump.rb
index d07c47a56d..8751343b1f 100644
--- a/bootstraptest/test_jump.rb
+++ b/bootstraptest/test_jump.rb
@@ -292,7 +292,7 @@ assert_equal "true", %q{
end
end
end
- s = "foo"
+ s = +"foo"
s.return_eigenclass == class << s; self; end
}, '[ruby-core:21379]'
diff --git a/bootstraptest/test_literal.rb b/bootstraptest/test_literal.rb
index a0d4ee08c6..39e6527027 100644
--- a/bootstraptest/test_literal.rb
+++ b/bootstraptest/test_literal.rb
@@ -70,6 +70,7 @@ if /wasi/ !~ target_platform
assert_equal "foo\n", %q(`echo foo`)
assert_equal "foo\n", %q(s = "foo"; `echo #{s}`)
end
+assert_equal "ECHO FOO", %q(def `(s) s.upcase; end; `echo foo`)
# regexp
assert_equal '', '//.source'
@@ -116,16 +117,16 @@ assert_equal '1', 'a = [obj = Object.new]; a.size'
assert_equal 'true', 'a = [obj = Object.new]; a[0] == obj'
assert_equal '5', 'a = [1,2,3]; a[1] = 5; a[1]'
assert_equal 'bar', '[*:foo];:bar'
-assert_equal '[1, 2]', 'def nil.to_a; [2]; end; [1, *nil]'
-assert_equal '[1, 2]', 'def nil.to_a; [1, 2]; end; [*nil]'
-assert_equal '[0, 1, {2=>3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]"
+assert_equal '[]', 'def nil.to_a; [1, 2]; end; [*nil]'
+assert_equal '[1]', 'def nil.to_a; [2]; end; [1, *nil]'
+assert_equal '[0, 1, {2 => 3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]"
# hash
assert_equal 'Hash', '{}.class'
assert_equal '{}', '{}.inspect'
assert_equal 'Hash', '{1=>2}.class'
-assert_equal '{1=>2}', '{1=>2}.inspect'
+assert_equal '{1 => 2}', '{1=>2}.inspect'
assert_equal '2', 'h = {1 => 2}; h[1]'
assert_equal '0', 'h = {1 => 2}; h.delete(1); h.size'
assert_equal '', 'h = {1 => 2}; h.delete(1); h[1]'
diff --git a/bootstraptest/test_literal_suffix.rb b/bootstraptest/test_literal_suffix.rb
index c36fa7078f..7a4d67d0fa 100644
--- a/bootstraptest/test_literal_suffix.rb
+++ b/bootstraptest/test_literal_suffix.rb
@@ -46,9 +46,9 @@ assert_equal '1', '1rescue nil'
assert_equal '10000000000000000001/10000000000000000000',
'1.0000000000000000001r'
-assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input',
- %q{begin eval('1ir', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end}
-assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input',
- %q{begin eval('1.2ir', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end}
-assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input',
- %q{begin eval('1e1r', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end}
+assert_equal 'unexpected local variable or method, expecting end-of-input',
+ %q{begin eval('1ir', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end}
+assert_equal 'unexpected local variable or method, expecting end-of-input',
+ %q{begin eval('1.2ir', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end}
+assert_equal 'unexpected local variable or method, expecting end-of-input',
+ %q{begin eval('1e1r', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end}
diff --git a/bootstraptest/test_load.rb b/bootstraptest/test_load.rb
index e63c93a8f4..fa8d31c098 100644
--- a/bootstraptest/test_load.rb
+++ b/bootstraptest/test_load.rb
@@ -1,9 +1,9 @@
assert_equal 'ok', %q{
- open("require-lock-test.rb", "w") {|f|
- f.puts "sleep 0.1"
- f.puts "module M"
- f.puts "end"
- }
+ File.write("require-lock-test.rb", <<-END)
+ sleep 0.1
+ module M
+ end
+ END
$:.unshift Dir.pwd
vs = (1..2).map {|i|
Thread.start {
@@ -16,7 +16,7 @@ assert_equal 'ok', %q{
assert_equal 'ok', %q{
%w[a a/foo b].each {|d| Dir.mkdir(d)}
- open("b/foo", "w") {|f| f.puts "$ok = :ok"}
+ File.write("b/foo", "$ok = :ok\n")
$:.replace(%w[a b])
begin
load "foo"
diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb
index 04c9eb2d11..e894f6f601 100644
--- a/bootstraptest/test_method.rb
+++ b/bootstraptest/test_method.rb
@@ -340,24 +340,6 @@ assert_equal '1', %q( class C; def m() 7 end; private :m end
assert_equal '1', %q( class C; def m() 1 end; private :m end
C.new.send(:m) )
-# with block
-assert_equal '[[:ok1, :foo], [:ok2, :foo, :bar]]',
-%q{
- class C
- def [](a)
- $ary << [yield, a]
- end
- def []=(a, b)
- $ary << [yield, a, b]
- end
- end
-
- $ary = []
- C.new[:foo, &lambda{:ok1}]
- C.new[:foo, &lambda{:ok2}] = :bar
- $ary
-}
-
# with
assert_equal '[:ok1, [:ok2, 11]]', %q{
class C
@@ -404,7 +386,6 @@ $result
# aset and splat
assert_equal '4', %q{class Foo;def []=(a,b,c,d);end;end;Foo.new[1,*a=[2,3]]=4}
-assert_equal '4', %q{class Foo;def []=(a,b,c,d);end;end;def m(&blk)Foo.new[1,*a=[2,3],&blk]=4;end;m{}}
# post test
assert_equal %q{[1, 2, :o1, :o2, [], 3, 4, NilClass, nil, nil]}, %q{
@@ -1107,10 +1088,6 @@ assert_equal 'ok', %q{
'ok'
end
}
-assert_equal 'ok', %q{
- [0][0, &proc{}] += 21
- 'ok'
-}, '[ruby-core:30534]'
# should not cache when splat
assert_equal 'ok', %q{
@@ -1190,3 +1167,276 @@ assert_equal 'DC', %q{
test2 o1, [], block
$result.join
}
+
+assert_equal 'ok', %q{
+ def foo
+ binding
+ ["ok"].first
+ end
+ foo
+ foo
+}, '[Bug #20178]'
+
+assert_equal 'ok', %q{
+ def bar(x); x; end
+ def foo(...); bar(...); end
+ foo('ok')
+}
+
+assert_equal 'ok', %q{
+ def bar(x); x; end
+ def foo(z, ...); bar(...); end
+ foo(1, 'ok')
+}
+
+assert_equal 'ok', %q{
+ def bar(x, y); x; end
+ def foo(...); bar("ok", ...); end
+ foo(1)
+}
+
+assert_equal 'ok', %q{
+ def bar(x); x; end
+ def foo(...); 1.times { return bar(...) }; end
+ foo("ok")
+}
+
+assert_equal 'ok', %q{
+ def bar(x); x; end
+ def foo(...); x = nil; 1.times { x = bar(...) }; x; end
+ foo("ok")
+}
+
+assert_equal 'ok', %q{
+ def bar(x); yield; end
+ def foo(...); bar(...); end
+ foo(1) { "ok" }
+}
+
+assert_equal 'ok', %q{
+ def baz(x); x; end
+ def bar(...); baz(...); end
+ def foo(...); bar(...); end
+ foo("ok")
+}
+
+assert_equal '[1, 2, 3, 4]', %q{
+ def baz(a, b, c, d); [a, b, c, d]; end
+ def bar(...); baz(1, ...); end
+ def foo(...); bar(2, ...); end
+ foo(3, 4)
+}
+
+assert_equal 'ok', %q{
+ class Foo; def self.foo(x); x; end; end
+ class Bar < Foo; def self.foo(...); super; end; end
+ Bar.foo('ok')
+}
+
+assert_equal 'ok', %q{
+ class Foo; def self.foo(x); x; end; end
+ class Bar < Foo; def self.foo(...); super(...); end; end
+ Bar.foo('ok')
+}
+
+assert_equal 'ok', %q{
+ class Foo; def self.foo(x, y); x + y; end; end
+ class Bar < Foo; def self.foo(...); super("o", ...); end; end
+ Bar.foo('k')
+}
+
+assert_equal 'ok', %q{
+ def bar(a); a; end
+ def foo(...); lambda { bar(...) }; end
+ foo("ok").call
+}
+
+assert_equal 'ok', %q{
+ class Foo; def self.foo(x, y); x + y; end; end
+ class Bar < Foo; def self.y(&b); b; end; def self.foo(...); y { super("o", ...) }; end; end
+ Bar.foo('k').call
+}
+
+assert_equal 'ok', %q{
+ def baz(n); n; end
+ def foo(...); bar = baz(...); lambda { lambda { bar } }; end
+ foo("ok").call.call
+}
+
+assert_equal 'ok', %q{
+ class A; def self.foo(...); new(...); end; attr_reader :b; def initialize(a, b:"ng"); @a = a; @b = b; end end
+ A.foo(1).b
+ A.foo(1, b: "ok").b
+}
+
+assert_equal 'ok', %q{
+ class A; def initialize; @a = ["ok"]; end; def first(...); @a.first(...); end; end
+ def call x; x.first; end
+ def call1 x; x.first(1); end
+ call(A.new)
+ call1(A.new).first
+}
+
+assert_equal 'ok', %q{
+ class A; def foo; yield("o"); end; end
+ class B < A; def foo(...); super { |x| yield(x + "k") }; end; end
+ B.new.foo { |x| x }
+}
+
+assert_equal "[1, 2, 3, 4]", %q{
+ def foo(*b) = b
+
+ def forward(...)
+ splat = [1,2,3]
+ foo(*splat, ...)
+ end
+
+ forward(4)
+}
+
+assert_equal "[1, 2, 3, 4]", %q{
+class A
+ def foo(*b) = b
+end
+
+class B < A
+ def foo(...)
+ splat = [1,2,3]
+ super(*splat, ...)
+ end
+end
+
+B.new.foo(4)
+}
+
+assert_equal 'ok', %q{
+ class A; attr_reader :iv; def initialize(...) = @iv = "ok"; end
+ A.new("foo", bar: []).iv
+}
+
+assert_equal 'ok', %q{
+ def foo(a, b) = a + b
+ def bar(...) = foo(...)
+ bar(1, 2)
+ bar(1, 2)
+ begin
+ bar(1, 2, 3)
+ "ng"
+ rescue ArgumentError
+ "ok"
+ end
+}
+
+assert_equal 'ok', %q{
+ class C
+ def foo(...) = :ok
+ def bar(...) = __send__(:foo, ...)
+ end
+
+ C.new.bar
+}
+
+assert_equal 'ok', %q{
+ class C
+ def method_missing(...) = :ok
+ def foo(...) = xyzzy(...)
+ end
+
+ C.new.foo
+}
+
+assert_equal 'ok', %q{
+ class C
+ def initialize(a)
+ end
+ end
+
+ def foo(...)
+ C.new(...)
+ :ok
+ end
+
+ foo(*["bar"])
+ foo("baz")
+}
+
+assert_equal 'ok', %q{
+ class C
+ def foo(b:)
+ b
+ end
+ end
+
+ def foo(...)
+ C.new.send(...)
+ end
+
+ foo(:foo, b: :ok)
+ foo(*["foo"], b: :ok)
+}
+
+assert_equal 'ok', %q{
+ Thing = Struct.new(:value)
+
+ Obj = Thing.new("ok")
+
+ def delegate(...)
+ Obj.value(...)
+ end
+
+ def no_args
+ delegate
+ end
+
+ def splat_args(*args)
+ delegate(*args)
+ end
+
+ no_args
+ splat_args
+}
+
+assert_equal 'ok', %q{
+ class A
+ private
+ def foo = "ng"
+ end
+
+ class B
+ def initialize(o)
+ @o = o
+ end
+
+ def foo(...) = @o.foo(...)
+ def internal_foo = foo
+ end
+
+ b = B.new(A.new)
+
+ begin
+ b.internal_foo
+ rescue NoMethodError
+ "ok"
+ end
+}
+
+assert_equal 'ok', <<~RUBY
+ def test(*, kw: false)
+ "ok"
+ end
+
+ test
+RUBY
+
+assert_equal '[1, 2, 3]', %q{
+ def target(*args) = args
+ def x = [1]
+ def forwarder(...) = target(*x, 2, ...)
+ forwarder(3).inspect
+}, '[Bug #21832] post-splat args before forwarding'
+
+assert_equal '[nil, nil]', %q{
+ def self_reading(a = a, kw:) = a
+ def through_binding(a = binding.local_variable_get(:a), kw:) = a
+ [self_reading(kw: 1), through_binding(kw: 1)]
+}, 'nil initialization of optional parameters'
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb
index b29db7ab0e..4fe90703fc 100644
--- a/bootstraptest/test_ractor.rb
+++ b/bootstraptest/test_ractor.rb
@@ -67,7 +67,7 @@ assert_equal "#<Ractor:#1 running>", %q{
# Return id, loc, and status for no-name ractor
assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{
r = Ractor.new { '' }
- r.take
+ r.join
sleep 0.1 until r.inspect =~ /terminated/
r.inspect
}
@@ -75,7 +75,7 @@ assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{
# Return id, name, loc, and status for named ractor
assert_match /^#<Ractor:#([^ ]*?) Test Ractor .+:[0-9]+ terminated>$/, %q{
r = Ractor.new(name: 'Test Ractor') { '' }
- r.take
+ r.join
sleep 0.1 until r.inspect =~ /terminated/
r.inspect
}
@@ -86,7 +86,7 @@ assert_equal 'ok', %q{
r = Ractor.new do
'ok'
end
- r.take
+ r.value
}
# Passed arguments to Ractor.new will be a block parameter
@@ -96,7 +96,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ok' do |msg|
msg
end
- r.take
+ r.value
}
# Pass multiple arguments to Ractor.new
@@ -105,7 +105,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ping', 'pong' do |msg, msg2|
[msg, msg2]
end
- 'ok' if r.take == ['ping', 'pong']
+ 'ok' if r.value == ['ping', 'pong']
}
# Ractor#send passes an object with copy to a Ractor
@@ -115,65 +115,23 @@ assert_equal 'ok', %q{
msg = Ractor.receive
end
r.send 'ok'
- r.take
+ r.value
}
# Ractor#receive_if can filter the message
-assert_equal '[2, 3, 1]', %q{
- r = Ractor.new Ractor.current do |main|
- main << 1
- main << 2
- main << 3
- end
- a = []
- a << Ractor.receive_if{|msg| msg == 2}
- a << Ractor.receive_if{|msg| msg == 3}
- a << Ractor.receive
-}
+assert_equal '[1, 2, 3]', %q{
+ ports = 3.times.map{Ractor::Port.new}
-# 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
+ 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| break [msg, :break]}
- a << Ractor.receive
-}
-
-# 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
- end
-
- a = []
-
- Ractor.receive_if do |msg|
- begin
- Ractor.receive
- rescue Ractor::Error
- a << [:e1, msg]
- end
- true # delete 1 from queue
- end
-
- Ractor.receive_if do |msg|
- begin
- Ractor.receive_if{}
- rescue Ractor::Error
- a << [:e2, msg]
- end
- true # delete 2 from queue
- end
-
- a #
+ a << ports[1].receive # 1
+ a << ports[2].receive # 2
+ a << ports[0].receive # 3
+ a
}
# dtoa race condition
@@ -184,73 +142,145 @@ assert_equal '[:ok, :ok, :ok]', %q{
10_000.times{ rand.to_s }
:ok
}
- }.map(&:take)
+ }.map(&:value)
}
-# 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.make_shareable(closure).call
+# Ractor.shareable_proc keeps the original Proc intact
+assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{
+ a = 42
+ pr1 = Proc.new do
+ [self.class, eval("a"), binding.local_variable_get(:a)]
+ end
+ a += 1
+ pr2 = Ractor.shareable_proc(&pr1)
+
+ r = []
+ begin
+ pr2.call
+ rescue SyntaxError
+ r << SyntaxError
+ end
+
+ r << pr1.call << pr1.binding.class
}
-# 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.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
}
-# Now autoload in non-main Ractor is not supported
-assert_equal 'ok', %q{
- autoload :Foo, 'foo.rb'
- r = Ractor.new do
- p Foo
- rescue Ractor::UnsafeError
- :ok
+# Ractor::IsolationError cases
+assert_equal '3', %q{
+ ok = 0
+
+ begin
+ a = 1
+ Ractor.shareable_proc{a}
+ a = 2
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
+
+ begin
+ cond = false
+ b = 1
+ b = 2 if cond
+ Ractor.shareable_proc{b}
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
+
+ begin
+ 1.times{|i|
+ i = 2
+ Ractor.shareable_proc{i}
+ }
+ rescue Ractor::IsolationError => e
+ ok += 1
end
- r.take
}
###
###
# Ractor still has several memory corruption so skip huge number of tests
-if ENV['GITHUB_WORKFLOW'] &&
- ENV['GITHUB_WORKFLOW'] == 'Compilations'
+if ENV['GITHUB_WORKFLOW'] == 'Compilations'
# ignore the follow
else
-# Ractor.select(*ractors) receives a values from a ractors.
-# It is similar to select(2) and Go's select syntax.
-# The return value is [ch, received_value]
+# Ractor.select with a Ractor argument
assert_equal 'ok', %q{
# select 1
r1 = Ractor.new{'r1'}
- r, obj = Ractor.select(r1)
- 'ok' if r == r1 and obj == 'r1'
+ port, obj = Ractor.select(r1)
+ if port == r1 and obj == 'r1'
+ 'ok'
+ else
+ # failed
+ [port, obj].inspect
+ end
}
# Ractor.select from two ractors.
assert_equal '["r1", "r2"]', %q{
# select 2
- r1 = Ractor.new{'r1'}
- r2 = Ractor.new{'r2'}
- rs = [r1, r2]
+ p1 = Ractor::Port.new
+ p2 = Ractor::Port.new
+ r1 = Ractor.new(p1){|p1| p1 << 'r1'}
+ r2 = Ractor.new(p2){|p2| p2 << 'r2'}
+ ps = [p1, p2]
as = []
- r, obj = Ractor.select(*rs)
- rs.delete(r)
+ port, obj = Ractor.select(*ps)
+ ps.delete(port)
as << obj
- r, obj = Ractor.select(*rs)
+ port, obj = Ractor.select(*ps)
as << obj
as.sort #=> ["r1", "r2"]
}
@@ -283,11 +313,10 @@ assert_equal 30.times.map { 'ok' }.to_s, %q{
30.times.map{|i|
test i
}
-} unless ENV['RUN_OPTS'] =~ /--jit-min-calls=5/ || # This always fails with --jit-wait --jit-min-calls=5
- (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878
+} 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
@@ -295,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
@@ -330,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
@@ -378,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)
@@ -416,148 +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
+ begin
+ r.join
+ rescue Ractor::RemoteError => e
+ [e.cause.class, #=> RuntimeError
+ e.cause.message, #=> 'ok'
+ e.ractor == r] #=> true
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
}
-# 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 = []
- take = []
- yielded = []
- until rs.empty?
- r, v = Ractor.select(CR, *rs, yield_value: 'yield')
- case r
- when :receive
- received << v
- when :yield
- yielded << v
- else
- take << v
- rs.delete r
- 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
- [received.all?('sendyield'), yielded.all?(nil), take.all?('take')]
}
-# 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
+# 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
+ sleep 0.1
+ 'ok'
end
-
- RN = 10
- RN.times.map{|i|
- Ractor.new pipe, i do |pipe, i|
- pipe << i
- end
- }
- RN.times.map{
- pipe.take
- }.sort
+ 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'
@@ -566,7 +464,7 @@ assert_equal '[RuntimeError, "ok", true]', %q{
}
# threads in a ractor will killed
-assert_equal '{:ok=>3}', %q{
+assert_equal '{ok: 3}', %q{
Ractor.new Ractor.current do |main|
q = Thread::Queue.new
Thread.new do
@@ -596,7 +494,7 @@ assert_equal '{:ok=>3}', %q{
end
3.times.map{Ractor.receive}.tally
-}
+} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec()
# unshareable object are copied
assert_equal 'false', %q{
@@ -605,29 +503,30 @@ 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
}
# send shareable and unshareable objects
-assert_equal "ok", %q{
- echo_ractor = Ractor.new do
+assert_equal "ok", <<~'RUBY', frozen_string_literal: false
+ port = Ractor::Port.new
+ echo_ractor = Ractor.new port do |port|
loop do
v = Ractor.receive
- Ractor.yield v
+ port << v
end
end
@@ -675,13 +574,13 @@ assert_equal "ok", %q{
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
}
@@ -690,10 +589,10 @@ assert_equal "ok", %q{
else
results.inspect
end
-}
+RUBY
# frozen Objects are shareable
-assert_equal [false, true, false].inspect, %q{
+assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: false
class C
def initialize freeze
@a = 1
@@ -707,7 +606,7 @@ assert_equal [false, true, false].inspect, %q{
def check obj1
obj2 = Ractor.new obj1 do |obj|
obj
- end.take
+ end.value
obj1.object_id == obj2.object_id
end
@@ -716,11 +615,11 @@ assert_equal [false, true, false].inspect, %q{
results << check(C.new(true)) # false
results << check(C.new(true).freeze) # true
results << check(C.new(false).freeze) # false
-}
+RUBY
# move example2: String
# touching moved object causes an error
-assert_equal 'hello world', %q{
+assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false
# move
r = Ractor.new do
obj = Ractor.receive
@@ -729,7 +628,7 @@ assert_equal 'hello world', %q{
str = 'hello'
r.send str, move: true
- modified = r.take
+ modified = r.value
begin
str << ' exception' # raise Ractor::MovedError
@@ -738,7 +637,7 @@ assert_equal 'hello world', %q{
else
raise 'unreachable'
end
-}
+RUBY
# move example2: Array
assert_equal '[0, 1]', %q{
@@ -749,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
@@ -757,70 +656,39 @@ assert_equal '[0, 1]', %q{
end
}
-# move with yield
-assert_equal 'hello', %q{
- r = Ractor.new do
- Thread.current.report_on_exception = false
- obj = 'hello'
- Ractor.yield obj, move: true
- obj << 'world'
- end
-
- str = r.take
- begin
- r.take
- rescue Ractor::RemoteError
- str #=> "hello"
- end
-}
-
-# yield/move should not make moved object when the yield is not succeeded
-assert_equal '"str"', %q{
- R = Ractor.new{}
- M = Ractor.current
- r = Ractor.new do
- s = 'str'
- selected_r, v = Ractor.select R, yield_value: s, move: true
- raise if selected_r != R # taken from R
- M.send s.inspect # s should not be a moved object
- end
-
- Ractor.receive
-}
-
-# yield/move can fail
-assert_equal "allocator undefined for Thread", %q{
+# unshareable frozen objects should still be frozen in new ractor after move
+assert_equal 'true', %q{
r = Ractor.new do
- obj = Thread.new{}
- Ractor.yield obj
- rescue => e
- e.message
+ obj = receive
+ { frozen: obj.frozen? }
end
- r.take
+ obj = [Object.new].freeze
+ r.send(obj, move: true)
+ r.value[:frozen]
}
-# Access to global-variables are prohibited
-assert_equal 'can not access global variables $gv from non-main Ractors', %q{
+# Access to global-variables are prohibited (read)
+assert_equal 'can not access global variable $gv from non-main Ractor', %q{
$gv = 1
r = Ractor.new do
$gv
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
}
-# Access to global-variables are prohibited
-assert_equal 'can not access global variables $gv from non-main Ractors', %q{
+# Access to global-variables are prohibited (write)
+assert_equal 'can not access global variable $gv from non-main Ractor', %q{
r = Ractor.new do
$gv = 1
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -834,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
}
@@ -850,7 +718,7 @@ assert_equal 'ok', %q{
'ok'
end
- r.take
+ r.value
}
# $DEBUG, $VERBOSE are Ractor local
@@ -908,7 +776,7 @@ assert_equal 'true', %q{
h = Ractor.new do
ractor_local_globals
- end.take
+ end.value
ractor_local_globals == h #=> true
}
@@ -917,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
@@ -925,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
@@ -940,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", %q{
+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
@@ -952,13 +857,12 @@ assert_equal "can not get unshareable values from instance variables of classes/
end
end
-
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
e.cause.message
end
-}
+RUBY
# ivar in shareable-objects are not allowed to access from non-main Ractor
assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{
@@ -970,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
@@ -996,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
@@ -1017,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
@@ -1031,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
}
@@ -1057,33 +961,68 @@ 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
a + b + c + d + e + f
}
-# 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{
+assert_equal '["instance-variable", "instance-variable", nil]', %q{
+ class C
+ @iv1 = ""
+ @iv2 = 42
+ def self.iv1 = defined?(@iv1) # "instance-variable"
+ def self.iv2 = defined?(@iv2) # "instance-variable"
+ def self.iv3 = defined?(@iv3) # nil
+ end
+
+ Ractor.new{
+ [C.iv1, C.iv2, C.iv3]
+ }.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
+ 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.value
+ values.join
+}
+
+# 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
@@ -1095,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
@@ -1117,14 +1056,103 @@ 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.', %q{
+assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', <<~'RUBY', frozen_string_literal: false
class C
CONST = 'str'
end
@@ -1132,44 +1160,58 @@ 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
-assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", %q{
+# 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
+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', %q{
+assert_equal 'can not set constants with non-shareable objects by non-main Ractors', <<~'RUBY', frozen_string_literal: false
class C
end
r = Ractor.new do
C::CONST = 'str'
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
-}
+RUBY
# define_method is not allowed
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
@@ -1180,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
@@ -1190,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
@@ -1211,11 +1253,11 @@ 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
-assert_equal 'ok', %q{
+assert_equal 'ok', <<~'RUBY', frozen_string_literal: false
s = 'hello'
Ractor.new s.object_id do |id ;s|
@@ -1224,11 +1266,29 @@ assert_equal 'ok', %q{
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)
-assert_equal 'true', %q{
+assert_equal 'true', <<~'RUBY', frozen_string_literal: false
class C
def initialize
@a = 'foo'
@@ -1299,7 +1359,7 @@ assert_equal 'true', %q{
}
Ractor.shareable?(a)
-}
+RUBY
# Ractor.make_shareable(obj) doesn't freeze shareable objects
assert_equal 'true', %q{
@@ -1308,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(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.make_shareable(a) # referred value should be shareable
- Ractor.make_shareable(pr)
- Ractor.shareable?(pr)
+ Ractor.new do
+ Object.__id__ == M1.call && M1.call == M2.bind_call(Object)
+ end.value
}
# Ractor.shareable?(recursive_objects)
@@ -1355,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
+ define_method "foo", Ractor.shareable_proc{ a }
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
- 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.
@@ -1396,14 +1460,14 @@ assert_equal '[false, false, true, true]', %q{
}
# TracePoint with normal Proc should be Ractor local
-assert_equal '[4, 8]', %q{
+assert_equal '[6, 10]', %q{
rs = []
TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do
- Ractor.new{ # line 4
+ Ractor.new{ # line 5
a = 1
b = 2
- }.take
- c = 3 # line 8
+ }.value
+ c = 3 # line 9
end
rs
}
@@ -1412,7 +1476,7 @@ assert_equal '[4, 8]', %q{
assert_equal '[true, false]', %q{
Ractor.new([[]].freeze) { |ary|
[ary.frozen?, ary.first.frozen? ]
- }.take
+ }.value
}
# Ractor deep copies frozen objects (str)
@@ -1420,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
@@ -1428,33 +1492,85 @@ 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
}
+# Ractor.select is interruptible
+assert_normal_exit %q{
+ trap(:INT) do
+ exit
+ end
+
+ r = Ractor.new do
+ loop do
+ sleep 1
+ end
+ end
+
+ Thread.new do
+ sleep 0.5
+ Process.kill(:INT, Process.pid)
+ end
+ Ractor.select(r)
+}
+
# Ractor-local storage
assert_equal '[nil, "b", "a"]', %q{
ans = []
Ractor.current[:key] = 'a'
r = Ractor.new{
- Ractor.yield self[:key]
+ Ractor.main << self[:key]
self[:key] = 'b'
self[:key]
}
- ans << r.take
- ans << r.take
+ ans << Ractor.receive
+ ans << r.value
ans << Ractor.current[:key]
}
+assert_equal '1', %q{
+ N = 1_000
+ Ractor.new{
+ a = []
+ 1_000.times.map{|i|
+ Thread.new(i){|i|
+ Thread.pass if i < N
+ a << Ractor.store_if_absent(:i){ i }
+ a << Ractor.current[:i]
+ }
+ }.each(&:join)
+ a.uniq.size
+ }.value
+}
+
+# Ractor-local storage
+assert_equal '2', %q{
+ Ractor.new {
+ fails = 0
+ begin
+ Ractor.main[:key] # cannot get ractor local storage from non-main ractor
+ rescue => e
+ fails += 1 if e.message =~ /Cannot get ractor local/
+ end
+ begin
+ Ractor.main[:key] = 'val'
+ rescue => e
+ fails += 1 if e.message =~ /Cannot set ractor local/
+ end
+ fails
+ }.value
+}
+
###
### Synchronization tests
###
@@ -1468,51 +1584,54 @@ assert_equal "#{N}#{N}", %Q{
Ractor.new{
N.times{|i| -(i.to_s)}
}
- }.map{|r| r.take}.join
+ }.map{|r| r.value}.join
}
-# enc_table
-assert_equal "#{N/10}", %Q{
- Ractor.new do
- loop do
- Encoding.find("test-enc-#{rand(5_000)}").inspect
- rescue ArgumentError => e
+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
-
- src = Encoding.find("UTF-8")
- #{N/10}.times{|i|
- src.replicate("test-enc-\#{i}")
- }
+ :ok
}
-# Generic ivtbl
+# Generic fields_tbl
n = N/2
assert_equal "#{n}#{n}", %Q{
2.times.map{
Ractor.new do
#{n}.times do
- obj = ''
+ obj = +''
obj.instance_variable_set("@a", 1)
obj.instance_variable_set("@b", 1)
obj.instance_variable_set("@c", 1)
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
- bar
+ 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{
@@ -1530,53 +1649,1018 @@ 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"
-}
+} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky
+# check method cache invalidation
assert_equal "ok", %q{
- def foo(*); ->{ super }; end
+ module M
+ def foo
+ @foo
+ end
+ end
+
+ class A
+ include M
+
+ def initialize
+ 100.times { |i| instance_variable_set(:"@var_#{i}", "bad: #{i}") }
+ @foo = 2
+ end
+ end
+
+ class B
+ include M
+
+ def initialize
+ @foo = 1
+ end
+ end
+
+ Ractor.new do
+ b = B.new
+ 100_000.times do
+ raise unless b.foo == 1
+ end
+ end
+
+ a = A.new
+ 100_000.times do
+ raise unless a.foo == 2
+ end
+
+ "ok"
+}
+
+# check method cache invalidation
+assert_equal 'true', %q{
+ class C1; def self.foo = 1; end
+ class C2; def self.foo = 2; end
+ class C3; def self.foo = 3; end
+ class C4; def self.foo = 5; end
+ class C5; def self.foo = 7; end
+ class C6; def self.foo = 11; end
+ class C7; def self.foo = 13; end
+ class C8; def self.foo = 17; end
+
+ LN = 10_000
+ RN = 10
+ CS = [C1, C2, C3, C4, C5, C6, C7, C8]
+ rs = RN.times.map{|i|
+ Ractor.new(CS.shuffle){|cs|
+ LN.times.sum{
+ cs.inject(1){|r, c| r * c.foo} # c.foo invalidates method cache entry
+ }
+ }
+ }
+
+ n = CS.inject(1){|r, c| r * c.foo} * LN
+ 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 API is experimental/, %q{
+ Warning[:experimental] = $VERBOSE = true
+ STDERR.reopen(STDOUT)
+ eval("Ractor.new{}.value", nil, "test_ractor.rb", 1)
+}, frozen_string_literal: false
+
+# check moved object
+assert_equal 'ok', %q{
+ r = Ractor.new do
+ Ractor.receive
+ GC.start
+ :ok
+ end
+
+ obj = begin
+ raise
+ rescue => e
+ e = Marshal.load(Marshal.dump(e))
+ end
+
+ r.send obj, move: true
+ r.value
+}
+
+## Ractor::Selector
+
+# Selector#empty? returns true
+assert_equal 'true', %q{
+ skip true unless defined? Ractor::Selector
+
+ s = Ractor::Selector.new
+ s.empty?
+}
+
+# Selector#empty? returns false if there is target ractors
+assert_equal 'false', %q{
+ skip false unless defined? Ractor::Selector
+
+ s = Ractor::Selector.new
+ s.add Ractor.new{}
+ s.empty?
+}
+
+# Selector#clear removes all ractors from the waiting list
+assert_equal 'true', %q{
+ skip true unless defined? Ractor::Selector
+
+ s = Ractor::Selector.new
+ s.add Ractor.new{10}
+ s.add Ractor.new{20}
+ s.clear
+ s.empty?
+}
+
+# Selector#wait can wait multiple ractors
+assert_equal '[10, 20, true]', %q{
+ skip [10, 20, true] unless defined? Ractor::Selector
+
+ s = Ractor::Selector.new
+ s.add Ractor.new{10}
+ s.add Ractor.new{20}
+ r, v = s.wait
+ vs = []
+ vs << v
+ r, v = s.wait
+ vs << v
+ [*vs.sort, s.empty?]
+} if defined? Ractor::Selector
+
+# Selector#wait can wait multiple ractors with receiving.
+assert_equal '30', %q{
+ skip 30 unless defined? Ractor::Selector
+
+ RN = 30
+ rs = RN.times.map{
+ Ractor.new{ :v }
+ }
+ s = Ractor::Selector.new(*rs)
+
+ results = []
+ until s.empty?
+ results << s.wait
+
+ # Note that s.wait can raise an exception because other Ractors/Threads
+ # can take from the same ractors in the waiting set.
+ # In this case there is no other takers so `s.wait` doesn't raise an error.
+ end
+
+ results.size
+} if defined? Ractor::Selector
+
+# Selector#wait can support dynamic addition
+assert_equal '600', %q{
+ skip 600 unless defined? Ractor::Selector
+
+ RN = 100
+ s = Ractor::Selector.new
+ port = Ractor::Port.new
+ rs = RN.times.map{
+ Ractor.new{
+ Ractor.main << Ractor.new(port){|port| port << :v3; :v4 }
+ Ractor.main << Ractor.new(port){|port| port << :v5; :v6 }
+ Ractor.yield :v1
+ :v2
+ }
+ }
+
+ rs.each{|r| s.add(r)}
+ h = {v1: 0, v2: 0, v3: 0, v4: 0, v5: 0, v6: 0}
+
+ loop do
+ case s.wait receive: true
+ in :receive, r
+ s.add r
+ in r, v
+ h[v] += 1
+ break if h.all?{|k, v| v == RN}
+ end
+ end
+
+ h.sum{|k, v| v}
+} unless yjit_enabled? # http://ci.rvm.jp/results/trunk-yjit@ruby-sp2-docker/4466770
+
+# Selector should be GCed (free'ed) without trouble
+assert_equal 'ok', %q{
+ skip :ok unless defined? Ractor::Selector
+
+ RN = 30
+ rs = RN.times.map{
+ Ractor.new{ :v }
+ }
+ s = Ractor::Selector.new(*rs)
+ :ok
+}
+
+end # if !ENV['GITHUB_WORKFLOW']
+
+# Chilled strings are not shareable
+assert_equal 'false', %q{
+ Ractor.shareable?("chilled")
+}
+
+# Chilled strings can be made shareable
+assert_equal 'true', %q{
+ shareable = Ractor.make_shareable("chilled")
+ shareable == "chilled" && Ractor.shareable?(shareable)
+}
+
+# require in Ractor
+assert_equal 'true', %q{
+ Module.new do
+ def require feature
+ return Ractor._require(feature) unless Ractor.main?
+ super
+ end
+ Object.prepend self
+ set_temporary_name 'Ractor#require'
+ end
+
+ Ractor.new{
+ begin
+ require 'tempfile'
+ Tempfile.new
+ rescue SystemStackError
+ # prism parser with -O0 build consumes a lot of machine stack
+ Data.define(:fileno).new(1)
+ end
+ }.value.fileno > 0
+}
+
+# require_relative in Ractor
+assert_equal 'true', %q{
+ dummyfile = File.join(__dir__, "dummy#{rand}.rb")
+ return true if File.exist?(dummyfile)
+
begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
+ File.write dummyfile, ''
+ rescue Exception
+ # skip on any errors
+ return true
+ end
+
+ begin
+ Ractor.new dummyfile do |f|
+ require_relative File.basename(f)
+ end.value
+ ensure
+ File.unlink dummyfile
end
}
-assert_equal "ok", %q{
- def foo(**); ->{ super }; end
+# require_relative in Ractor
+assert_equal 'LoadError', %q{
+ dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
+ return true if File.exist?(dummyfile)
+
+ Ractor.new dummyfile do |f|
+ begin
+ require_relative File.basename(f)
+ rescue LoadError => e
+ e.class
+ end
+ end.value
+}
+
+# autolaod in Ractor
+assert_equal 'true', %q{
+ autoload :Tempfile, 'tempfile'
+
+ r = Ractor.new do
+ begin
+ Tempfile.new
+ rescue SystemStackError
+ # prism parser with -O0 build consumes a lot of machine stack
+ Data.define(:fileno).new(1)
+ end
+ end
+ r.value.fileno > 0
+}
+
+# failed in autolaod in Ractor
+assert_equal 'LoadError', %q{
+ dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
+ autoload :Tempfile, dummyfile
+
+ r = Ractor.new do
+ begin
+ Tempfile.new
+ rescue LoadError => e
+ e.class
+ end
+ end
+ r.value
+}
+
+# bind_call in Ractor [Bug #20934]
+assert_equal 'ok', %q{
+ 2.times.map do
+ Ractor.new do
+ 1000.times do
+ Object.instance_method(:itself).bind_call(self)
+ end
+ end
+ end.each(&:join)
+ GC.start
+ :ok.itself
+}
+
+# moved objects being corrupted if embeded (String)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "foobarbazfoobarbazfoobarbazfoobarbaz"
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Array)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Hash)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = { foo: 1, bar: 2 }
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (MatchData)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "foo".match(/o/)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Struct)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Struct.new(:a, :b, :c, :d, :e, :f).new(1, 2, 3, 4, 5, 6)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Object)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ class SomeObject
+ attr_reader :a, :b, :c, :d, :e, :f
+ def initialize
+ @a = @b = @c = @d = @e = @f = 1
+ end
+
+ def ==(o)
+ @a == o.a &&
+ @b == o.b &&
+ @c == o.c &&
+ @d == o.d &&
+ @e == o.e &&
+ @f == o.f
+ end
+ end
+
+ SomeObject.new # initial non-embeded
+
+ obj = SomeObject.new
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved arrays can't be used
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = [1]
+ ractor.send(obj, move: true)
begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
+ [].concat(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
end
}
-assert_equal "ok", %q{
- def foo(...); ->{ super }; end
+# moved strings can't be used
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "hello"
+ ractor.send(obj, move: true)
begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
+ "".replace(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
end
}
-assert_equal "ok", %q{
- def foo((x), (y)); ->{ super }; 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
- Ractor.make_shareable(foo([], []))
- rescue Ractor::IsolationError
- "ok"
+ {}.merge(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
end
}
-end # if !ENV['GITHUB_WORKFLOW']
+# 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 948e2d7809..29bf93cb8f 100644
--- a/bootstraptest/test_syntax.rb
+++ b/bootstraptest/test_syntax.rb
@@ -528,24 +528,24 @@ assert_equal %q{1}, %q{
i
}
def assert_syntax_error expected, code, message = ''
- assert_equal "#{expected}",
- "begin eval(%q{#{code}}, nil, '', 0)"'; rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end', message
+ assert_match /^#{Regexp.escape(expected)}/,
+ "begin eval(%q{#{code}}, nil, '', 0)"'; rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)?(?! syntax errors? found)(?: syntax error,)?) (.*)/, 1] end', message
end
assert_syntax_error "unterminated string meets end of file", '().."', '[ruby-dev:29732]'
assert_equal %q{[]}, %q{$&;[]}, '[ruby-dev:31068]'
-assert_syntax_error "syntax error, unexpected *, expecting '}'", %q{{*0}}, '[ruby-dev:31072]'
-assert_syntax_error "`@0' is not allowed as an instance variable name", %q{@0..0}, '[ruby-dev:31095]'
-assert_syntax_error "identifier $00 is not valid to get", %q{$00..0}, '[ruby-dev:31100]'
-assert_syntax_error "identifier $00 is not valid to set", %q{0..$00=1}
+assert_syntax_error "unexpected *, expecting '}'", %q{{*0}}, '[ruby-dev:31072]'
+assert_syntax_error "'@0' is not allowed as an instance variable name", %q{@0..0}, '[ruby-dev:31095]'
+assert_syntax_error "'$00' is not allowed as a global variable name", %q{$00..0}, '[ruby-dev:31100]'
+assert_syntax_error "'$00' is not allowed as a global variable name", %q{0..$00=1}
assert_equal %q{0}, %q{[*0];0}, '[ruby-dev:31102]'
-assert_syntax_error "syntax error, unexpected ')'", %q{v0,(*,v1,) = 0}, '[ruby-dev:31104]'
+assert_syntax_error "unexpected ')'", %q{v0,(*,v1,) = 0}, '[ruby-dev:31104]'
assert_equal %q{1}, %q{
class << (ary=[]); def []; 0; end; def []=(x); super(0,x);end;end; ary[]+=1
}, '[ruby-dev:31110]'
assert_syntax_error "Can't set variable $1", %q{0..$1=1}, '[ruby-dev:31118]'
assert_valid_syntax %q{1.times{1+(1&&next)}}, '[ruby-dev:31119]'
assert_valid_syntax %q{x=-1;loop{x+=1&&redo if (x+=1).zero?}}, '[ruby-dev:31119]'
-assert_syntax_error %q{syntax error, unexpected end-of-input}, %q{!}, '[ruby-dev:31243]'
+assert_syntax_error %q{unexpected end-of-input}, %q{!}, '[ruby-dev:31243]'
assert_equal %q{[nil]}, %q{[()]}, '[ruby-dev:31252]'
assert_equal %q{true}, %q{!_=()}, '[ruby-dev:31263]'
assert_equal 'ok', %q{while true; redo; end if 1 == 2; :ok}, '[ruby-dev:31360]'
@@ -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{
@@ -629,7 +629,7 @@ assert_equal '2', %q{
assert_match /invalid multibyte char/, %q{
$stderr = STDOUT
- eval("\"\xf0".force_encoding("utf-8"))
+ eval("\"\xf0".dup.force_encoding("utf-8"))
}, '[ruby-dev:32429]'
# method ! and !=
@@ -904,3 +904,35 @@ assert_normal_exit %q{
Class
end
}, '[ruby-core:30293]'
+
+assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal"
+ 'test'.frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "literal strings are frozen", "--disable-frozen-string-literal", frozen_string_literal: true
+ 'test'.frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal"
+ 'test'.frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "literal strings are mutable", "--enable-frozen-string-literal", frozen_string_literal: false
+ 'test'.frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal"
+ __FILE__.frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--disable-frozen-string-literal", frozen_string_literal: true
+ __FILE__.frozen?
+RUBY
+
+assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal"
+ __FILE__.frozen?
+RUBY
+
+assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--enable-frozen-string-literal", frozen_string_literal: false
+ __FILE__.frozen?
+RUBY
diff --git a/bootstraptest/test_thread.rb b/bootstraptest/test_thread.rb
index 5361828403..7ff5bb4a38 100644
--- a/bootstraptest/test_thread.rb
+++ b/bootstraptest/test_thread.rb
@@ -242,9 +242,22 @@ assert_equal 'true', %{
end
}
+assert_equal 'true', %{
+ Thread.new{}.join
+ begin
+ Process.waitpid2 fork{
+ Thread.new{
+ sleep 0.1
+ }.join
+ }
+ true
+ rescue NotImplementedError
+ true
+ end
+}
+
assert_equal 'ok', %{
- open("zzz_t1.rb", "w") do |f|
- f.puts <<-END
+ File.write("zzz_t1.rb", <<-END)
begin
Thread.new { fork { GC.start } }.join
pid, status = Process.wait2
@@ -253,7 +266,6 @@ assert_equal 'ok', %{
$result = :ok
end
END
- end
require "./zzz_t1.rb"
$result
}
@@ -408,8 +420,7 @@ assert_equal 'ok', %q{
}
assert_equal 'ok', %{
- open("zzz_t2.rb", "w") do |f|
- f.puts <<-'end;' # do
+ File.write("zzz_t2.rb", <<-'end;') # do
begin
m = Thread::Mutex.new
parent = Thread.current
@@ -431,7 +442,6 @@ assert_equal 'ok', %{
$result = :ok
end
end;
- end
require "./zzz_t2.rb"
$result
}
@@ -484,6 +494,7 @@ assert_equal 'foo', %q{
GC.start
f.call.source
}
+
assert_normal_exit %q{
class C
def inspect
diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb
index 364ed7094b..e9ce905e2c 100644
--- a/bootstraptest/test_yjit.rb
+++ b/bootstraptest/test_yjit.rb
@@ -1,3 +1,318 @@
+# To run the tests in this file only, with YJIT enabled:
+# make btest BTESTS=bootstraptest/test_yjit.rb RUN_OPTS="--yjit-call-threshold=1"
+
+# regression test for popping before side exit
+assert_equal "ok", %q{
+ def foo(a, *) = a
+
+ def call(args, &)
+ foo(1) # spill at where the block arg will be
+ foo(*args, &)
+ end
+
+ call([1, 2])
+
+ begin
+ call([])
+ rescue ArgumentError
+ :ok
+ end
+}
+
+# regression test for send processing before side exit
+assert_equal "ok", %q{
+ def foo(a, *) = :foo
+
+ def call(args)
+ send(:foo, *args)
+ end
+
+ call([1, 2])
+
+ begin
+ call([])
+ rescue ArgumentError
+ :ok
+ end
+}
+
+# test discarding extra yield arguments
+assert_equal "22131300500015901015", %q{
+ def splat_kw(ary) = yield *ary, a: 1
+
+ def splat(ary) = yield *ary
+
+ def kw = yield 1, 2, a: 3
+
+ def kw_only = yield a: 0
+
+ def simple = yield 0, 1
+
+ def none = yield
+
+ def calls
+ [
+ splat([1, 1, 2]) { |x, y| x + y },
+ splat([1, 1, 2]) { |y, opt = raise| opt + y},
+ splat_kw([0, 1]) { |a:| a },
+ kw { |a:| a },
+ kw { |one| one },
+ kw { |one, a:| a },
+ kw_only { |a:| a },
+ kw_only { |a: 1| a },
+ simple { 5.itself },
+ simple { |a| a },
+ simple { |opt = raise| opt },
+ simple { |*rest| rest },
+ simple { |opt_kw: 5| opt_kw },
+ none { |a: 9| a },
+ # autosplat ineractions
+ [0, 1, 2].yield_self { |a, b| [a, b] },
+ [0, 1, 2].yield_self { |a, opt = raise| [a, opt] },
+ [1].yield_self { |a, opt = 4| a + opt },
+ ]
+ end
+
+ calls.join
+}
+
+# test autosplat with empty splat
+assert_equal "ok", %q{
+ def m(pos, splat) = yield pos, *splat
+
+ m([:ok], []) {|v0,| v0 }
+}
+
+# regression test for send stack shifting
+assert_normal_exit %q{
+ def foo(a, b)
+ a.singleton_methods(b)
+ end
+
+ def call_foo
+ [1, 1, 1, 1, 1, 1, send(:foo, 1, 1)]
+ end
+
+ call_foo
+}
+
+# regression test for keyword splat with yield
+assert_equal 'nil', %q{
+ def splat_kw(kwargs) = yield(**kwargs)
+
+ splat_kw({}) { _1 }.inspect
+}
+
+# regression test for arity check with splat
+assert_equal '[:ae, :ae]', %q{
+ def req_one(a_, b_ = 1) = raise
+
+ def test(args)
+ req_one *args
+ rescue ArgumentError
+ :ae
+ end
+
+ [test(Array.new 5), test([])]
+}
+
+# regression test for arity check with splat and send
+assert_equal '[:ae, :ae]', %q{
+ def two_reqs(a, b_, _ = 1) = a.gsub(a, a)
+
+ def test(name, args)
+ send(name, *args)
+ rescue ArgumentError
+ :ae
+ end
+
+ [test(:two_reqs, ["g", nil, nil, nil]), test(:two_reqs, ["g"])]
+}
+
+# regression test for GC marking stubs in invalidated code
+assert_normal_exit %q{
+ skip true unless GC.respond_to?(:compact)
+ garbage = Array.new(10_000) { [] } # create garbage to cause iseq movement
+ eval(<<~RUBY)
+ def foo(n, garbage)
+ if n == 2
+ # 1.times.each to create a cfunc frame to preserve the JIT frame
+ # which will return to a stub housed in an invalidated block
+ return 1.times.each do
+ Object.define_method(:foo) {}
+ garbage.clear
+ GC.verify_compaction_references(toward: :empty, expand_heap: true)
+ end
+ end
+
+ foo(n + 1, garbage)
+ end
+ RUBY
+
+ foo(1, garbage)
+}
+
+# regression test for callee block handler overlapping with arguments
+assert_equal '3', %q{
+ def foo(_req, *args) = args.last
+
+ def call_foo = foo(0, 1, 2, 3, &->{})
+
+ call_foo
+}
+
+# call leaf builtin with a block argument
+assert_equal '0', "0.abs(&nil)"
+
+# regression test for invokeblock iseq guard
+assert_equal 'ok', %q{
+ skip :ok unless GC.respond_to?(:compact)
+ def foo = yield
+ 10.times do |i|
+ ret = eval("foo { #{i} }")
+ raise "failed at #{i}" unless ret == i
+ GC.compact
+ end
+ :ok
+}
+
+# regression test for overly generous guard elision
+assert_equal '[0, :sum, 0, :sum]', %q{
+ # In faulty versions, the following happens:
+ # 1. YJIT puts object on the temp stack with type knowledge
+ # (CArray or CString) about RBASIC_CLASS(object).
+ # 2. In iter=0, due to the type knowledge, YJIT generates
+ # a call to sum() without any guard on RBASIC_CLASS(object).
+ # 3. In iter=1, a singleton class is added to the object,
+ # changing RBASIC_CLASS(object), falsifying the type knowledge.
+ # 4. Because the code from (1) has no class guard, it is incorrectly
+ # reused and the wrong method is invoked.
+ # Putting a literal is important for gaining type knowledge.
+ def carray(iter)
+ array = []
+ array.sum(iter.times { def array.sum(_) = :sum })
+ end
+
+ def cstring(iter)
+ string = "".dup
+ string.sum(iter.times { def string.sum(_) = :sum })
+ end
+
+ [carray(0), carray(1), cstring(0), cstring(1)]
+}
+
+# regression test for return type of Integer#/
+# It can return a T_BIGNUM when inputs are T_FIXNUM.
+assert_equal 0x3fffffffffffffff.to_s, %q{
+ def call(fixnum_min)
+ (fixnum_min / -1) - 1
+ end
+
+ call(-(2**62))
+}
+
+# regression test for return type of String#<<
+assert_equal 'Sub', %q{
+ def call(sub) = (sub << sub).itself
+
+ class Sub < String; end
+
+ call(Sub.new('o')).class
+}
+
+# String#dup with generic ivars
+assert_equal '["str", "ivar"]', %q{
+ def str_dup(str) = str.dup
+ str = "str"
+ str.instance_variable_set(:@ivar, "ivar")
+ str = str_dup(str)
+ [str, str.instance_variable_get(:@ivar)]
+}
+
+# test splat filling required and feeding rest
+assert_equal '[0, 1, 2, [3, 4]]', %q{
+ public def lead_rest(a, b, *rest)
+ [self, a, b, rest]
+ end
+
+ def call(args) = 0.lead_rest(*args)
+
+ call([1, 2, 3, 4])
+}
+
+# test missing opts are nil initialized
+assert_equal '[[0, 1, nil, 3], [0, 1, nil, 3], [0, 1, nil, 3, []], [0, 1, nil, 3, []]]', %q{
+ public def lead_opts(a, b=binding.local_variable_get(:c), c=3)
+ [self, a, b, c]
+ end
+
+ public def opts_rest(a=raise, b=binding.local_variable_get(:c), c=3, *rest)
+ [self, a, b, c, rest]
+ end
+
+ def call(args)
+ [
+ 0.lead_opts(1),
+ 0.lead_opts(*args),
+
+ 0.opts_rest(1),
+ 0.opts_rest(*args),
+ ]
+ end
+
+ call([1])
+}
+
+# test filled optionals with unspecified keyword param
+assert_equal 'ok', %q{
+ def opt_rest_opt_kw(_=1, *, k: :ok) = k
+
+ def call = opt_rest_opt_kw(0)
+
+ call
+}
+
+# test splat empty array with rest param
+assert_equal '[0, 1, 2, []]', %q{
+ public def foo(a=1, b=2, *rest)
+ [self, a, b, rest]
+ end
+
+ def call(args) = 0.foo(*args)
+
+ call([])
+}
+
+# Regression test for yielding with autosplat to block with
+# optional parameters. https://github.com/Shopify/yjit/issues/313
+assert_equal '[:a, :b, :a, :b]', %q{
+ def yielder(arg) = yield(arg) + yield(arg)
+
+ yielder([:a, :b]) do |c = :c, d = :d|
+ [c, d]
+ end
+}
+
+# Regression test for GC mishap while doing shape transition
+assert_equal '[:ok]', %q{
+ # [Bug #19601]
+ class RegressionTest
+ def initialize
+ @a = @b = @fourth_ivar_does_shape_transition = nil
+ end
+
+ def extender
+ @first_extended_ivar = [:ok]
+ end
+ end
+
+ GC.stress = true
+
+ # Used to crash due to GC run in rb_ensure_iv_list_size()
+ # not marking the newly allocated [:ok].
+ RegressionTest.new.extender.itself
+}
+
assert_equal 'true', %q{
# regression test for tracking type of locals for too long
def local_setting_cmp(five)
@@ -45,6 +360,29 @@ assert_normal_exit %q{
Object.send(:remove_const, :Foo)
}
+assert_normal_exit %q{
+ # Test to ensure send on overridden c functions
+ # doesn't corrupt the stack
+ class Bar
+ def bar(x)
+ x
+ end
+ end
+
+ class Foo
+ def bar
+ Bar.new
+ end
+ end
+
+ foo = Foo.new
+ # before this change, this line would error
+ # because "s" would still be on the stack
+ # String.to_s is the overridden method here
+ p foo.bar.bar("s".__send__(:to_s))
+}
+
+
assert_equal '[nil, nil, nil, nil, nil, nil]', %q{
[NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass|
klass.class_eval("def foo = @foo")
@@ -56,88 +394,77 @@ assert_equal '[nil, nil, nil, nil, nil, nil]', %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
+assert_equal '[nil, nil, nil, nil, nil, nil]', %q{
+ # Tests defined? on non-heap objects
+ [NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass|
+ klass.class_eval("def foo = defined?(@foo)")
end
- class Sub
- include M
+ [nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance|
+ instance.foo
+ instance.foo
end
+}
- foo_method = M.instance_method(:foo)
+assert_equal '[nil, "instance-variable", nil, "instance-variable"]', %q{
+ # defined? on object that changes shape between calls
+ class Foo
+ def foo
+ defined?(@foo)
+ end
- dbg = ->(message) do
- return # comment this out to get printouts
+ def add
+ @foo = 1
+ end
- $stderr.puts RubyVM::YJIT.disasm(foo_method)
- $stderr.puts message
+ def remove
+ self.remove_instance_variable(:@foo)
+ end
end
- 2.times { 42.only_ints }
-
- dbg["There should be two versions of getinlineache"]
-
- module M
- remove_const(:Foo)
- end
+ obj = Foo.new
+ [obj.foo, (obj.add; obj.foo), (obj.remove; obj.foo), (obj.add; obj.foo)]
+}
- dbg["There should be no getinlinecaches"]
+assert_equal '["instance-variable", 5]', %q{
+ # defined? on object too complex for shape information
+ class Foo
+ def initialize
+ 100.times { |i| instance_variable_set("@foo#{i}", i) }
+ end
- 2.times do
- 42.only_ints
- rescue NameError => err
- _ = "caught name error #{err}"
+ def foo
+ [defined?(@foo5), @foo5]
+ end
end
- dbg["There should be one version of getinlineache"]
-
- 2.times do
- Sub.new.pin_self_type_then_foo
- rescue NameError
- _ = 'second specialization'
- end
+ Foo.new.foo
+}
- dbg["There should be two versions of getinlineache"]
+# getinstancevariable with shape too complex
+assert_normal_exit %q{
+ class Foo
+ def initialize
+ @a = 1
+ end
- module M
- Foo = 1
+ def getter
+ @foobar
+ end
end
- dbg["There should still be two versions of getinlineache"]
-
- 42.only_ints
-
- dbg["There should be no getinlinecaches"]
+ # Initialize ivars in changing order, making the Foo
+ # class have shape too complex
+ 100.times do |x|
+ foo = Foo.new
+ foo.instance_variable_set(:"@a#{x}", 1)
+ foo.instance_variable_set(:"@foobar", 777)
- # 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
+ # The getter method eventually sees shape too complex
+ r = foo.getter
+ if r != 777
+ raise "error"
+ end
end
}
@@ -198,6 +525,8 @@ assert_equal 'string', %q{
# Check that exceptions work when getting global variable
assert_equal 'rescued', %q{
+ Warning[:deprecated] = true
+
module Warning
def warn(message)
raise
@@ -351,6 +680,45 @@ assert_equal 'false', %q{
less_than 2
}
+# BOP redefinition works on Integer#<=
+assert_equal 'false', %q{
+ def le(x, y) = x <= y
+
+ le(2, 2)
+
+ class Integer
+ def <=(_) = false
+ end
+
+ le(2, 2)
+}
+
+# BOP redefinition works on Integer#>
+assert_equal 'false', %q{
+ def gt(x, y) = x > y
+
+ gt(3, 2)
+
+ class Integer
+ def >(_) = false
+ end
+
+ gt(3, 2)
+}
+
+# BOP redefinition works on Integer#>=
+assert_equal 'false', %q{
+ def ge(x, y) = x >= y
+
+ ge(2, 2)
+
+ class Integer
+ def >=(_) = false
+ end
+
+ ge(2, 2)
+}
+
# Putobject, less-than operator, fixnums
assert_equal '2', %q{
def check_index(index)
@@ -794,6 +1162,7 @@ assert_equal 'special', %q{
# Test that object references in generated code get marked and moved
assert_equal "good", %q{
+ skip :good unless GC.respond_to?(:compact)
def bar
"good"
end
@@ -817,7 +1186,7 @@ assert_equal "good", %q{
# Test polymorphic getinstancevariable. T_OBJECT -> T_STRING
assert_equal 'ok', %q{
@hello = @h1 = @h2 = @h3 = @h4 = 'ok'
- str = ""
+ str = +""
str.instance_variable_set(:@hello, 'ok')
public def get
@@ -961,6 +1330,18 @@ assert_equal '[42, :default]', %q{
]
}
+# Test default value block for Hash
+assert_equal "false", <<~RUBY, frozen_string_literal: false
+ def index_with_string(h)
+ h["foo"]
+ end
+
+ h = Hash.new { |h, k| k.frozen? }
+
+ index_with_string(h)
+ index_with_string(h)
+RUBY
+
# A regression test for making sure cfp->sp is proper when
# hitting stubs. See :stub-sp-flush:
assert_equal 'ok', %q{
@@ -1118,6 +1499,38 @@ assert_equal '42', %q{
run
}
+# splatting an empty array on a specialized method
+assert_equal 'ok', %q{
+ def run
+ "ok".to_s(*[])
+ end
+
+ run
+ run
+}
+
+# splatting an single element array on a specialized method
+assert_equal '[1]', %q{
+ def run
+ [].<<(*[1])
+ end
+
+ run
+ run
+}
+
+# specialized method with wrong args
+assert_equal 'ok', %q{
+ def run(x)
+ "bad".to_s(123) if x
+ rescue
+ :ok
+ end
+
+ run(false)
+ run(true)
+}
+
# getinstancevariable on Symbol
assert_equal '[nil, nil]', %q{
# @foo to exercise the getinstancevariable instruction
@@ -1317,7 +1730,7 @@ assert_equal '{}', %q{
}
# test building hash with values
-assert_equal '{:foo=>:bar}', %q{
+assert_equal '{foo: :bar}', %q{
def build_hash(val)
{ foo: val }
end
@@ -1427,7 +1840,7 @@ assert_equal 'foo', %q{
}
# Test that String unary plus returns the same object ID for an unfrozen string.
-assert_equal 'true', %q{
+assert_equal 'true', <<~RUBY, frozen_string_literal: false
def jittable_method
str = "bar"
@@ -1437,7 +1850,7 @@ assert_equal 'true', %q{
uplus_str.object_id == old_obj_id
end
jittable_method
-}
+RUBY
# Test that String unary plus returns a different unfrozen string when given a frozen string
assert_equal 'false', %q{
@@ -1551,6 +1964,85 @@ assert_equal 'true', %q{
jittable_method
}
+# test getbyte on string class
+assert_equal '[97, :nil, 97, :nil, :raised]', %q{
+ def getbyte(s, i)
+ byte = begin
+ s.getbyte(i)
+ rescue TypeError
+ :raised
+ end
+
+ byte || :nil
+ end
+
+ getbyte("a", 0)
+ getbyte("a", 0)
+
+ [getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")]
+}
+
+# Basic test for String#setbyte
+assert_equal 'AoZ', %q{
+ s = +"foo"
+ s.setbyte(0, 65)
+ s.setbyte(-1, 90)
+ s
+}
+
+# String#setbyte IndexError
+assert_equal 'String#setbyte', %q{
+ def ccall = "".setbyte(1, 0)
+ begin
+ ccall
+ rescue => e
+ e.backtrace.first.split("'").last
+ end
+}
+
+# String#setbyte TypeError
+assert_equal 'String#setbyte', %q{
+ def ccall = "".setbyte(nil, 0)
+ begin
+ ccall
+ rescue => e
+ e.backtrace.first.split("'").last
+ end
+}
+
+# String#setbyte FrozenError
+assert_equal 'String#setbyte', %q{
+ def ccall = "a".freeze.setbyte(0, 0)
+ begin
+ ccall
+ rescue => e
+ e.backtrace.first.split("'").last
+ end
+}
+
+# non-leaf String#setbyte
+assert_equal 'String#setbyte', %q{
+ def to_int
+ @caller = caller
+ 0
+ end
+
+ def ccall = "a".dup.setbyte(self, 98)
+ ccall
+
+ @caller.first.split("'").last
+}
+
+# non-leaf String#byteslice
+assert_equal 'TypeError', %q{
+ def ccall = "".byteslice(nil, nil)
+ begin
+ ccall
+ rescue => e
+ e.class
+ end
+}
+
# Test << operator on string subclass
assert_equal 'abab', %q{
class MyString < String; end
@@ -1696,6 +2188,34 @@ assert_equal '7', %q{
foo(5,2)
}
+# regression test for argument registers with invalidation
+assert_equal '[0, 1, 2]', %q{
+ def test(n)
+ ret = n
+ binding
+ ret
+ end
+
+ [0, 1, 2].map do |n|
+ test(n)
+ end
+}
+
+# regression test for argument registers
+assert_equal 'true', %q{
+ class Foo
+ def ==(other)
+ other == nil
+ end
+ end
+
+ def test
+ [Foo.new].include?(Foo.new)
+ end
+
+ test
+}
+
# test pattern matching
assert_equal '[:ok, :ok]', %q{
class C
@@ -1761,6 +2281,20 @@ assert_equal '123', %q{
foo(Foo)
}
+# Test EP == BP invalidation with moving ISEQs
+assert_equal 'ok', %q{
+ skip :ok unless GC.respond_to?(:compact)
+ def entry
+ ok = proc { :ok } # set #entry as an EP-escaping ISEQ
+ [nil].reverse_each do # avoid exiting the JIT frame on the constant
+ GC.compact # move #entry ISEQ
+ end
+ ok # should be read off of escaped EP
+ end
+
+ entry.call
+}
+
# invokesuper edge case
assert_equal '[:A, [:A, :B]]', %q{
class B
@@ -1917,6 +2451,76 @@ assert_equal '[:A, :Btwo]', %q{
ins.foo
}
+# invokesuper with a block
+assert_equal 'true', %q{
+ class A
+ def foo = block_given?
+ end
+
+ class B < A
+ def foo = super()
+ end
+
+ B.new.foo { }
+ B.new.foo { }
+}
+
+# invokesuper in a block
+assert_equal '[0, 2]', %q{
+ class A
+ def foo(x) = x * 2
+ end
+
+ class B < A
+ def foo
+ 2.times.map do |x|
+ super(x)
+ end
+ end
+ end
+
+ B.new.foo
+ 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
+ define_method(:itself) { super }
+ end
+ begin
+ Foo.new.itself
+ rescue RuntimeError
+ :ok
+ end
+}
+
# Call to fixnum
assert_equal '[true, false]', %q{
def is_odd(obj)
@@ -1957,6 +2561,16 @@ assert_equal '[true, false, true, false]', %q{
[is_odd(123), is_odd(456), is_odd(bignum), is_odd(bignum+1)]
}
+# Flonum and Flonum
+assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{
+ [1.0 + 1.0, 1.0 - 1.0, 1.0 * 1.0, 8.0 / 2.0]
+}
+
+# Flonum and Fixnum
+assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{
+ [1.0 + 1, 1.0 - 1, 1.0 * 1, 8.0 / 2]
+}
+
# Call to static and dynamic symbol
assert_equal 'bar', %q{
def to_string(obj)
@@ -1997,6 +2611,30 @@ assert_equal '[1, 2, 3, 4, 5]', %q{
splatarray
}
+# splatkw
+assert_equal '[1, 2]', %q{
+ def foo(a:) = [a, yield]
+
+ def entry(&block)
+ a = { a: 1 }
+ foo(**a, &block)
+ end
+
+ entry { 2 }
+}
+assert_equal '[1, 2]', %q{
+ def foo(a:) = [a, yield]
+
+ def entry(obj, &block)
+ foo(**obj, &block)
+ end
+
+ entry({ a: 3 }) { 2 }
+ obj = Object.new
+ def obj.to_hash = { a: 1 }
+ entry(obj) { 2 }
+}
+
assert_equal '[1, 1, 2, 1, 2, 3]', %q{
def expandarray
arr = [1, 2, 3]
@@ -2051,6 +2689,39 @@ assert_equal '[:not_array, nil, nil]', %q{
expandarray_not_array(obj)
}
+assert_equal '[1, 2]', %q{
+ class NilClass
+ private
+ def to_ary
+ [1, 2]
+ end
+ end
+
+ def expandarray_redefined_nilclass
+ a, b = nil
+ [a, b]
+ end
+
+ expandarray_redefined_nilclass
+ 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]
@@ -2061,6 +2732,17 @@ assert_equal '[1, 2, nil]', %q{
expandarray_rhs_too_small
}
+assert_equal '[nil, 2, nil]', %q{
+ def foo(arr)
+ a, b, c = arr
+ end
+
+ a, b, c1 = foo([0, 1])
+ a, b, c2 = foo([0, 1, 2])
+ a, b, c3 = foo([0, 1])
+ [c1, c2, c3]
+}
+
assert_equal '[1, [2]]', %q{
def expandarray_splat
a, *b = [1, 2]
@@ -2249,6 +2931,26 @@ assert_equal '[[:c_return, :itself, main]]', %q{
events
}
+# test c_call invalidation
+assert_equal '[[:c_call, :itself]]', %q{
+ # enable the event once to make sure invalidation
+ # happens the second time we enable it
+ TracePoint.new(:c_call) {}.enable{}
+
+ def compiled
+ itself
+ end
+
+ # assume first call compiles
+ compiled
+
+ events = []
+ tp = TracePoint.new(:c_call) { |tp| events << [tp.event, tp.method_id] }
+ tp.enable { compiled }
+
+ events
+}
+
# test enabling tracing for a suspended fiber
assert_equal '[[:return, 42]]', %q{
def traced_method
@@ -2273,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
@@ -2289,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
@@ -2309,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
@@ -2395,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{
@@ -2647,7 +3351,7 @@ assert_equal '[[1, 2, 3, 4]]', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.store(:value, foo: 123)
bar[:value]
@@ -2658,7 +3362,7 @@ assert_equal '{:foo=>123}', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.replace(foo: 123)
end
@@ -2668,7 +3372,7 @@ assert_equal '{:foo=>123}', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123, :bar=>456}', %q{
+assert_equal '{foo: 123, bar: 456}', %q{
def foo(bar)
bar.replace(foo: 123, bar: 456)
end
@@ -2678,7 +3382,7 @@ assert_equal '{:foo=>123, :bar=>456}', %q{
}
# variadic cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.merge(foo: 123)
end
@@ -2802,7 +3506,7 @@ assert_equal "true", %q{
}
# duphash
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo
{foo: 123}
end
@@ -2812,7 +3516,7 @@ assert_equal '{:foo=>123}', %q{
}
# newhash
-assert_equal '{:foo=>2}', %q{
+assert_equal '{foo: 2}', %q{
def foo
{foo: 1+1}
end
@@ -2922,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
@@ -3046,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()
@@ -3255,3 +3997,1561 @@ assert_equal '[1, 2]', %q{
foo
foo
}
+
+# respond_to? with changing symbol
+assert_equal 'false', %q{
+ def foo(name)
+ :sym.respond_to?(name)
+ end
+ foo(:to_s)
+ foo(:to_s)
+ foo(:not_exist)
+}
+
+# respond_to? with method being defined
+assert_equal 'true', %q{
+ def foo
+ :sym.respond_to?(:not_yet_defined)
+ end
+ foo
+ foo
+ module Kernel
+ def not_yet_defined = true
+ end
+ foo
+}
+
+# respond_to? with undef method
+assert_equal 'false', %q{
+ module Kernel
+ def to_be_removed = true
+ end
+ def foo
+ :sym.respond_to?(:to_be_removed)
+ end
+ foo
+ foo
+ class Object
+ undef_method :to_be_removed
+ end
+ foo
+}
+
+# respond_to? with respond_to_missing?
+assert_equal 'true', %q{
+ class Foo
+ end
+ def foo(x)
+ x.respond_to?(:bar)
+ end
+ foo(Foo.new)
+ foo(Foo.new)
+ class Foo
+ def respond_to_missing?(*) = true
+ end
+ foo(Foo.new)
+}
+
+# bmethod
+assert_equal '[1, 2, 3]', %q{
+ one = 1
+ define_method(:foo) do
+ one
+ end
+
+ 3.times.map { |i| foo + i }
+}
+
+# return inside bmethod
+assert_equal 'ok', %q{
+ define_method(:foo) do
+ 1.tap { return :ok }
+ end
+
+ foo
+}
+
+# bmethod optional and keywords
+assert_equal '[[1, nil, 2]]', %q{
+ define_method(:opt_and_kwargs) do |a = {}, b: nil, c: nil|
+ [a, b, c]
+ end
+
+ 5.times.map { opt_and_kwargs(1, c: 2) }.uniq
+}
+
+# bmethod with forwarded block
+assert_equal '2', %q{
+ define_method(:foo) do |&block|
+ block.call
+ end
+
+ def bar(&block)
+ foo(&block)
+ end
+
+ bar { 1 }
+ bar { 2 }
+}
+
+# bmethod with forwarded block and arguments
+assert_equal '5', %q{
+ define_method(:foo) do |n, &block|
+ n + block.call
+ end
+
+ def bar(n, &block)
+ foo(n, &block)
+ end
+
+ bar(0) { 1 }
+ bar(3) { 2 }
+}
+
+# bmethod with forwarded unwanted block
+assert_equal '1', %q{
+ one = 1
+ define_method(:foo) do
+ one
+ end
+
+ def bar(&block)
+ foo(&block)
+ end
+
+ bar { }
+ 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)
+ if n == 2
+ return 1.times { Object.define_method(:foo) {} }
+ end
+
+ foo(n + 1)
+ end
+
+ foo(1)
+}
+
+# case-when with redefined ===
+assert_equal 'ok', %q{
+ class Symbol
+ def ===(a)
+ true
+ end
+ end
+
+ def cw(arg)
+ case arg
+ when :b
+ :ok
+ when 4
+ :ng
+ end
+ end
+
+ cw(4)
+}
+
+assert_equal 'threw', %q{
+ def foo(args)
+ wrap(*args)
+ rescue ArgumentError
+ 'threw'
+ end
+
+ def wrap(a)
+ [a]
+ end
+
+ foo([Hash.ruby2_keywords_hash({})])
+}
+
+assert_equal 'threw', %q{
+ # C call
+ def bar(args)
+ Array(*args)
+ rescue ArgumentError
+ 'threw'
+ end
+
+ bar([Hash.ruby2_keywords_hash({})])
+}
+
+# Test instance_of? and is_a?
+assert_equal 'true', %q{
+ 1.instance_of?(Integer) && 1.is_a?(Integer)
+}
+
+# Test instance_of? and is_a? for singleton classes
+assert_equal 'true', %q{
+ a = []
+ def a.test = :test
+ a.instance_of?(Array) && a.is_a?(Array)
+}
+
+# Test instance_of? for singleton_class
+# Yes this does really return false
+assert_equal 'false', %q{
+ a = []
+ def a.test = :test
+ a.instance_of?(a.singleton_class)
+}
+
+# Test is_a? for singleton_class
+assert_equal 'true', %q{
+ a = []
+ def a.test = :test
+ a.is_a?(a.singleton_class)
+}
+
+# Test send with splat to a cfunc
+assert_equal 'true', %q{
+ 1.send(:==, 1, *[])
+}
+
+# Test empty splat with cfunc
+assert_equal '2', %q{
+ def foo
+ Integer.sqrt(4, *[])
+ end
+ # call twice to deal with constant exiting
+ foo
+ foo
+}
+
+# Test non-empty splat with cfunc
+assert_equal 'Hello World', %q{
+ def bar
+ args = ["Hello "]
+ greeting = +"World"
+ greeting.insert(0, *args)
+ greeting
+ end
+ bar
+}
+
+# Regression: this creates a temp stack with > 127 elements
+assert_normal_exit %q{
+ def foo(a)
+ [
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a, a, a,
+ a, a, a, a, a, a, a, a,
+ ]
+ end
+
+ def entry
+ foo(1)
+ end
+
+ entry
+}
+
+# Test that splat and rest combined
+# properly dupe the array
+assert_equal "[]", %q{
+ def foo(*rest)
+ rest << 1
+ end
+
+ def test(splat)
+ foo(*splat)
+ end
+
+ EMPTY = []
+ custom = Object.new
+ def custom.to_a
+ EMPTY
+ end
+
+ test(custom)
+ test(custom)
+ EMPTY
+}
+
+# Rest with send
+assert_equal '[1, 2, 3]', %q{
+ def bar(x, *rest)
+ rest.insert(0, x)
+ end
+ send(:bar, 1, 2, 3)
+}
+
+# Fix splat block arg bad compilation
+assert_equal "foo", %q{
+ def literal(*args, &block)
+ s = ''.dup
+ literal_append(s, *args, &block)
+ s
+ end
+
+ def literal_append(sql, v)
+ sql << v
+ end
+
+ literal("foo")
+}
+
+# regression test for accidentally having a parameter truncated
+# due to Rust/C signature mismatch. Used to crash with
+# > [BUG] rb_vm_insn_addr2insn: invalid insn address ...
+# or
+# > ... `Err` value: TryFromIntError(())'
+assert_normal_exit %q{
+ n = 16384
+ eval(
+ "def foo(arg); " + "_=arg;" * n + '_=1;' + "Object; end"
+ )
+ foo 1
+}
+
+# Regression test for CantCompile not using starting_ctx
+assert_normal_exit %q{
+ class Integer
+ def ===(other)
+ false
+ end
+ end
+
+ def my_func(x)
+ case x
+ when 1
+ 1
+ when 2
+ 2
+ else
+ 3
+ end
+ end
+
+ my_func(1)
+}
+
+# Regression test for CantCompile not using starting_ctx
+assert_equal "ArgumentError", %q{
+ def literal(*args, &block)
+ s = ''.dup
+ args = [1, 2, 3]
+ literal_append(s, *args, &block)
+ s
+ end
+
+ def literal_append(sql, v)
+ [sql.inspect, v.inspect]
+ end
+
+ begin
+ literal("foo")
+ rescue ArgumentError
+ "ArgumentError"
+ end
+}
+
+# Rest with block
+# Simplified code from railsbench
+assert_equal '[{"/a" => "b", as: :c, via: :post}, [], nil]', %q{
+ def match(path, *rest, &block)
+ [path, rest, block]
+ end
+
+ def map_method(method, args, &block)
+ options = args.last
+ args.pop
+ options[:via] = method
+ match(*args, options, &block)
+ end
+
+ def post(*args, &block)
+ map_method(:post, args, &block)
+ end
+
+ post "/a" => "b", as: :c
+}
+
+# Test rest and kw_args
+assert_equal '[true, true, true, true]', %q{
+ def my_func(*args, base: nil, sort: true)
+ [args, base, sort]
+ end
+
+ def calling_my_func
+ results = []
+ results << (my_func("test") == [["test"], nil, true])
+ results << (my_func("test", base: :base) == [["test"], :base, true])
+ results << (my_func("test", sort: false) == [["test"], nil, false])
+ results << (my_func("test", "other", base: :base) == [["test", "other"], :base, true])
+ results
+ end
+ calling_my_func
+}
+
+# Test Integer#[] with 2 args
+assert_equal '0', %q{
+ 3[0, 0]
+}
+
+# unspecified_bits + checkkeyword
+assert_equal '2', %q{
+ def callee = 1
+
+ # checkkeyword should see unspecified_bits=0 (use bar), not Integer 1 (set bar = foo).
+ def foo(foo, bar: foo) = bar
+
+ def entry(&block)
+ # write 1 at stack[3]. Calling #callee spills stack[3].
+ 1 + (1 + (1 + (1 + callee)))
+ # &block is written to a register instead of stack[3]. When &block is popped and
+ # unspecified_bits is pushed, it must be written to stack[3], not to a register.
+ foo(1, bar: 2, &block)
+ end
+
+ entry # call branch_stub_hit (spill temps)
+ entry # doesn't call branch_stub_hit (not spill temps)
+}
+
+# Test rest and optional_params
+assert_equal '[true, true, true, true]', %q{
+ def my_func(stuff, base=nil, sort=true, *args)
+ [stuff, base, sort, args]
+ end
+
+ def calling_my_func
+ results = []
+ results << (my_func("test") == ["test", nil, true, []])
+ results << (my_func("test", :base) == ["test", :base, true, []])
+ results << (my_func("test", :base, false) == ["test", :base, false, []])
+ results << (my_func("test", :base, false, "other", "other") == ["test", :base, false, ["other", "other"]])
+ results
+ end
+ calling_my_func
+}
+
+# Test rest and optional_params and splat
+assert_equal '[true, true, true, true, true]', %q{
+ def my_func(stuff, base=nil, sort=true, *args)
+ [stuff, base, sort, args]
+ end
+
+ def calling_my_func
+ results = []
+ splat = ["test"]
+ results << (my_func(*splat) == ["test", nil, true, []])
+ splat = [:base]
+ results << (my_func("test", *splat) == ["test", :base, true, []])
+ splat = [:base, false]
+ results << (my_func("test", *splat) == ["test", :base, false, []])
+ splat = [:base, false, "other", "other"]
+ results << (my_func("test", *splat) == ["test", :base, false, ["other", "other"]])
+ splat = ["test", :base, false, "other", "other"]
+ results << (my_func(*splat) == ["test", :base, false, ["other", "other"]])
+ results
+ end
+ calling_my_func
+}
+
+# Regression test: rest and optional and splat
+assert_equal 'true', %q{
+ def my_func(base=nil, *args)
+ [base, args]
+ end
+
+ def calling_my_func
+ array = []
+ my_func(:base, :rest1, *array) == [:base, [:rest1]]
+ end
+
+ calling_my_func
+}
+
+# Fix failed case for large splat
+assert_equal 'true', %q{
+ def d(a, b=:b)
+ end
+
+ def calling_func
+ ary = 1380888.times;
+ d(*ary)
+ end
+ begin
+ calling_func
+ rescue ArgumentError
+ true
+ end
+}
+
+# Regression test: register allocator on expandarray
+assert_equal '[]', %q{
+ func = proc { [] }
+ proc do
+ _x, _y = func.call
+ end.call
+}
+
+# Catch TAG_BREAK in a non-FINISH frame with JIT code
+assert_equal '1', %q{
+ def entry
+ catch_break
+ end
+
+ def catch_break
+ while_true do
+ break
+ end
+ 1
+ end
+
+ def while_true
+ while true
+ yield
+ end
+ end
+
+ entry
+}
+
+assert_equal '6', %q{
+ class Base
+ def number = 1 + yield
+ end
+
+ class Sub < Base
+ def number = super + 2
+ end
+
+ Sub.new.number { 3 }
+}
+
+# Integer multiplication and overflow
+assert_equal '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{
+ def foo(a, b)
+ a * b
+ end
+
+ r1 = foo(2, 3)
+ r2 = foo(2, -3)
+ r3 = foo(2 << 40, 2 << 41)
+ r4 = foo(2 << 40, -2 << 41)
+ r5 = foo(1 << 62, 1 << 62)
+
+ [r1, r2, r3, r4, r5]
+}
+
+# Integer multiplication and overflow (minimized regression test from test-basic)
+assert_equal '8515157028618240000', %q{2128789257154560000 * 4}
+
+# Inlined method calls
+assert_equal 'nil', %q{
+ def putnil = nil
+ def entry = putnil
+ entry.inspect
+}
+assert_equal '1', %q{
+ def putobject_1 = 1
+ def entry = putobject_1
+ entry
+}
+assert_equal 'false', %q{
+ def putobject(_unused_arg1) = false
+ def entry = putobject(nil)
+ entry
+}
+assert_equal 'true', %q{
+ def entry = yield
+ entry { true }
+}
+assert_equal 'sym', %q{
+ def entry = :sym.to_sym
+ entry
+}
+
+assert_normal_exit %q{
+ ivars = 1024.times.map { |i| "@iv_#{i} = #{i}\n" }.join
+ Foo = Class.new
+ Foo.class_eval "def initialize() #{ivars} end"
+ Foo.new
+}
+
+assert_equal '0', %q{
+ def spill
+ 1.to_i # not inlined
+ end
+
+ def inline(_stack1, _stack2, _stack3, _stack4, _stack5)
+ 0 # inlined
+ end
+
+ def entry
+ # RegTemps is 00111110 prior to the #inline call.
+ # Its return value goes to stack_idx=0, which conflicts with stack_idx=5.
+ inline(spill, 2, 3, 4, 5)
+ end
+
+ entry
+}
+
+# Integer succ and overflow
+assert_equal '[2, 4611686018427387904]', %q{
+ [1.succ, 4611686018427387903.succ]
+}
+
+# Integer pred and overflow
+assert_equal '[0, -4611686018427387905]', %q{
+ [1.pred, -4611686018427387904.pred]
+}
+
+# Integer right shift
+assert_equal '[0, 1, -4]', %q{
+ [0 >> 1, 2 >> 1, -7 >> 1]
+}
+
+# Integer XOR
+assert_equal '[0, 0, 4]', %q{
+ [0 ^ 0, 1 ^ 1, 7 ^ 3]
+}
+
+assert_equal '[nil, "yield"]', %q{
+ def defined_yield = defined?(yield)
+ [defined_yield, defined_yield {}]
+}
+
+# splat with ruby2_keywords into rest parameter
+assert_equal '[[{a: 1}], {}]', %q{
+ ruby2_keywords def foo(*args) = args
+
+ def bar(*args, **kw) = [args, kw]
+
+ def pass_bar(*args) = bar(*args)
+
+ def body
+ args = foo(a: 1)
+ pass_bar(*args)
+ end
+
+ body
+}
+
+# concatarray
+assert_equal '[1, 2]', %q{
+ def foo(a, b) = [a, b]
+ arr = [2]
+ foo(*[1], *arr)
+}
+
+# pushtoarray
+assert_equal '[1, 2]', %q{
+ def foo(a, b) = [a, b]
+ arr = [1]
+ foo(*arr, 2)
+}
+
+# pop before fallback
+assert_normal_exit %q{
+ class Foo
+ attr_reader :foo
+
+ def try = foo(0, &nil)
+ end
+
+ Foo.new.try
+}
+
+# a kwrest case
+assert_equal '[1, 2, {complete: false}]', %q{
+ def rest(foo: 1, bar: 2, **kwrest)
+ [foo, bar, kwrest]
+ end
+
+ def callsite = rest(complete: false)
+
+ callsite
+}
+
+# splat+kw_splat+opt+rest
+assert_equal '[1, []]', %q{
+ def opt_rest(a = 0, *rest) = [a, rest]
+
+ def call_site(args) = opt_rest(*args, **nil)
+
+ call_site([1])
+}
+
+# splat and nil kw_splat
+assert_equal 'ok', %q{
+ def identity(x) = x
+
+ def splat_nil_kw_splat(args) = identity(*args, **nil)
+
+ splat_nil_kw_splat([:ok])
+}
+
+# empty splat and kwsplat into leaf builtins
+assert_equal '[1, 1, 1]', %q{
+ empty = []
+ [1.abs(*empty), 1.abs(**nil), 1.bit_length(*empty, **nil)]
+}
+
+# splat into C methods with -1 arity
+assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{
+ class Foo < Array
+ def push(args) = super(1, *args)
+ end
+
+ def test_cfunc_vargs_splat(sub_instance, array_class, empty_kw_hash)
+ splat = [2, 3]
+ kw_splat = [empty_kw_hash]
+ [
+ sub_instance.push(splat),
+ array_class[0, *splat, **nil],
+ array_class[1, *splat, &nil],
+ array_class[2, *splat, **nil, &nil],
+ array_class.send(:[], *kw_splat),
+ # kw_splat disables keywords hash handling
+ array_class[*kw_splat],
+ array_class[*kw_splat, **nil],
+ ]
+ end
+
+ test_cfunc_vargs_splat(Foo.new, Array, Hash.ruby2_keywords_hash({}))
+}
+
+# Class#new (arity=-1), splat, and ruby2_keywords
+assert_equal '[0, {1 => 1}]', %q{
+ class KwInit
+ attr_reader :init_args
+ def initialize(x = 0, **kw)
+ @init_args = [x, kw]
+ end
+ end
+
+ def test(klass, args)
+ klass.new(*args).init_args
+ end
+
+ test(KwInit, [Hash.ruby2_keywords_hash({1 => 1})])
+}
+
+# Chilled string setivar trigger warning
+assert_match(/literal string will be frozen in the future/, %q{
+ Warning[:deprecated] = true
+ $VERBOSE = true
+ $warning = "no-warning"
+ module ::Warning
+ def self.warn(message)
+ $warning = message.split("warning: ").last.strip
+ end
+ end
+
+ class String
+ def setivar!
+ @ivar = 42
+ end
+ end
+
+ def setivar!(str)
+ str.setivar!
+ end
+
+ 10.times { setivar!("mutable".dup) }
+ 10.times do
+ setivar!("frozen".freeze)
+ rescue FrozenError
+ end
+
+ setivar!("chilled") # Emit warning
+ $warning
+})
+
+# arity=-2 cfuncs
+assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{
+ def test_cases(file, chain)
+ new_chain = chain.allocate # to call initialize directly
+ new_chain.send(:initialize, [0], ok: 1)
+
+ [
+ file.join,
+ file.join("1", "2"),
+ new_chain.to_a,
+ ]
+ end
+
+ test_cases(File, Enumerator::Chain)
+}
+
+# singleton class should invalidate Type::CString assumption
+assert_equal 'foo', %q{
+ def define_singleton(str, define)
+ if define
+ # Wrap a C method frame to avoid exiting JIT code on defineclass
+ [nil].reverse_each do
+ class << str
+ def +(_)
+ "foo"
+ end
+ end
+ end
+ end
+ "bar"
+ end
+
+ def entry(define)
+ str = ""
+ # When `define` is false, #+ compiles to rb_str_plus() without a class guard.
+ # When the code is reused with `define` is true, the class of `str` is changed
+ # to a singleton class, so the block should be invalidated.
+ str + define_singleton(str, define)
+ end
+
+ entry(false)
+ entry(true)
+}
+
+assert_equal 'ok', %q{
+ def ok
+ :ok
+ end
+
+ def delegator(...)
+ ok(...)
+ end
+
+ def caller
+ send(:delegator)
+ end
+
+ caller
+}
+
+# test inlining of simple iseqs
+assert_equal '[:ok, :ok, :ok]', %q{
+ def identity(x) = x
+ def foo(x, _) = x
+ def bar(_, _, _, _, x) = x
+
+ def tests
+ [
+ identity(:ok),
+ foo(:ok, 2),
+ bar(1, 2, 3, 4, :ok),
+ ]
+ end
+
+ tests
+}
+
+# test inlining of simple iseqs with kwargs
+assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{
+ def optional_unused(x, opt: :not_ok) = x
+ def optional_used(x, opt: :ok) = opt
+ def required_unused(x, req:) = x
+ def required_used(x, req:) = req
+ def unknown(x) = x
+
+ def tests
+ [
+ optional_unused(:ok),
+ optional_used(:not_ok),
+ required_unused(:ok, req: :not_ok),
+ required_used(:not_ok, req: :ok),
+ begin unknown(:not_ok, unknown_kwarg: :not_ok) rescue ArgumentError; :ok end,
+ ]
+ end
+
+ tests
+}
+
+# test simple iseqs not eligible for inlining
+assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{
+ def identity(x) = x
+ def arg_splat(x, *args) = x
+ def kwarg_splat(x, **kwargs) = x
+ def block_arg(x, &blk) = x
+ def block_iseq(x) = x
+ def call_forwarding(...) = identity(...)
+
+ def tests
+ [
+ arg_splat(:ok),
+ kwarg_splat(:ok),
+ block_arg(:ok, &proc { :not_ok }),
+ block_iseq(:ok) { :not_ok },
+ call_forwarding(:ok),
+ ]
+ end
+
+ 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
+
+ foo # compile it
+
+ class Integer
+ def to_ary = [] # invalidate
+ end
+
+ foo # try again
+}
+
+# test integer left shift with constant rhs
+assert_equal [0x80000000000, 'a+', :ok].inspect, %q{
+ def shift(val) = val << 43
+
+ def tests
+ int = shift(1)
+ str = shift("a")
+
+ Integer.define_method(:<<) { |_| :ok }
+ redef = shift(1)
+
+ [int, str, redef]
+ end
+
+ tests
+}
+
+# test integer left shift fusion followed by opt_getconstant_path
+assert_equal '33', %q{
+ def test(a)
+ (a << 5) | (Object; a)
+ end
+
+ test(1)
+}
+
+# test String#stebyte with arguments that need conversion
+assert_equal "abc", %q{
+ str = +"a00"
+ def change_bytes(str, one, two)
+ str.setbyte(one, "b".ord)
+ str.setbyte(2, two)
+ end
+
+ to_int_1 = Object.new
+ to_int_99 = Object.new
+ def to_int_1.to_int = 1
+ def to_int_99.to_int = 99
+
+ change_bytes(str, to_int_1, to_int_99)
+ str
+}
+
+# test --yjit-verify-ctx for arrays with a singleton class
+assert_equal "ok", %q{
+ class Array
+ def foo
+ self.singleton_class.define_method(:first) { :ok }
+ first
+ end
+ end
+
+ def test = [].foo
+
+ test
+}
+
+assert_equal '["raised", "Module", "Object"]', %q{
+ def foo(obj)
+ obj.superclass.name
+ end
+
+ ret = []
+
+ begin
+ foo(Class.allocate)
+ rescue TypeError
+ ret << 'raised'
+ end
+
+ ret += [foo(Class), foo(Class.new)]
+}
+
+# test TrueClass#=== before and after redefining TrueClass#==
+assert_equal '[[true, false, false], [true, true, false], [true, :error, :error]]', %q{
+ def true_eqq(x)
+ true === x
+ rescue NoMethodError
+ :error
+ end
+
+ def test
+ [
+ # first one is always true because rb_equal does object comparison before calling #==
+ true_eqq(true),
+ # these will use TrueClass#==
+ true_eqq(false),
+ true_eqq(:truthy),
+ ]
+ end
+
+ results = [test]
+
+ class TrueClass
+ def ==(x)
+ !x
+ end
+ end
+
+ results << test
+
+ class TrueClass
+ undef_method :==
+ end
+
+ results << test
+}
+
+# test FalseClass#=== before and after redefining FalseClass#==
+assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{
+ def case_equal(x, y)
+ x === y
+ rescue NoMethodError
+ :error
+ end
+
+ def test
+ [
+ # first one is always true because rb_equal does object comparison before calling #==
+ case_equal(false, false),
+ # these will use #==
+ case_equal(false, true),
+ case_equal(false, nil),
+ ]
+ end
+
+ results = [test]
+
+ class FalseClass
+ def ==(x)
+ !x
+ end
+ end
+
+ results << test
+
+ class FalseClass
+ undef_method :==
+ end
+
+ results << test
+}
+
+# test NilClass#=== before and after redefining NilClass#==
+assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{
+ def case_equal(x, y)
+ x === y
+ rescue NoMethodError
+ :error
+ end
+
+ def test
+ [
+ # first one is always true because rb_equal does object comparison before calling #==
+ case_equal(nil, nil),
+ # these will use #==
+ case_equal(nil, true),
+ case_equal(nil, false),
+ ]
+ end
+
+ results = [test]
+
+ class NilClass
+ def ==(x)
+ !x
+ end
+ end
+
+ results << test
+
+ class NilClass
+ undef_method :==
+ end
+
+ results << test
+}
+
+# test struct accessors fire c_call events
+assert_equal '[[:c_call, :x=], [:c_call, :x]]', %q{
+ c = Struct.new(:x)
+ obj = c.new
+
+ events = []
+ TracePoint.new(:c_call) do
+ events << [_1.event, _1.method_id]
+ end.enable do
+ obj.x = 100
+ obj.x
+ end
+
+ events
+}
+
+# regression test for splatting empty array
+assert_equal '1', %q{
+ def callee(foo) = foo
+
+ def test_body(args) = callee(1, *args)
+
+ test_body([])
+ array = Array.new(100)
+ array.clear
+ test_body(array)
+}
+
+# regression test for splatting empty array to cfunc
+assert_normal_exit %q{
+ def test_body(args) = Array(1, *args)
+
+ test_body([])
+ 0x100.times do
+ array = Array.new(100)
+ array.clear
+ test_body(array)
+ end
+}
+
+# compiling code shouldn't emit warnings as it may call into more Ruby code
+assert_equal 'ok', <<~'RUBY'
+ # [Bug #20522]
+ $VERBOSE = true
+ Warning[:performance] = true
+
+ module StrictWarnings
+ def warn(msg, **)
+ raise msg
+ end
+ end
+ Warning.singleton_class.prepend(StrictWarnings)
+
+ class A
+ def compiled_method(is_private)
+ @some_ivar = is_private
+ end
+ end
+
+ shape_max_variations = 8
+ if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) && RubyVM::Shape::SHAPE_MAX_VARIATIONS != shape_max_variations
+ raise "Expected SHAPE_MAX_VARIATIONS to be #{shape_max_variations}, got: #{RubyVM::Shape::SHAPE_MAX_VARIATIONS}"
+ end
+
+ 100.times do |i|
+ klass = Class.new(A)
+ (shape_max_variations - 1).times do |j|
+ obj = klass.new
+ obj.instance_variable_set("@base_#{i}", 42)
+ obj.instance_variable_set("@ivar_#{j}", 42)
+ end
+ obj = klass.new
+ obj.instance_variable_set("@base_#{i}", 42)
+ begin
+ obj.compiled_method(true)
+ rescue
+ # expected
+ end
+ end
+
+ :ok
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+ class MyRelation
+ def callee(...)
+ :ok
+ end
+
+ def uncached(...)
+ callee(...)
+ end
+
+ def takes_block(&block)
+ # push blockhandler
+ uncached(&block) # CI1
+ end
+ end
+
+ relation = MyRelation.new
+ relation.takes_block { }
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+ def _exec_scope(...)
+ instance_exec(...)
+ end
+
+ def ok args, body
+ _exec_scope(*args, &body)
+ end
+
+ ok([], -> { "ok" })
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+ def _exec_scope(...)
+ instance_exec(...)
+ end
+
+ def ok args, body
+ _exec_scope(*args, &body)
+ end
+
+ ok(["ok"], ->(x) { x })
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+def baz(a, b)
+ a + b
+end
+
+def bar(...)
+ baz(...)
+end
+
+def foo(a, ...)
+ bar(a, ...)
+end
+
+def test
+ foo("o", "k")
+end
+
+test
+RUBY
+
+# opt_newarray_send pack/buffer
+assert_equal '[true, true]', <<~'RUBY'
+ def pack
+ v = 1.23
+ [v, v*2, v*3].pack("E*").unpack("E*") == [v, v*2, v*3]
+ end
+
+ def with_buffer
+ v = 4.56
+ b = +"x"
+ [v, v*2, v*3].pack("E*", buffer: b)
+ b[1..].unpack("E*") == [v, v*2, v*3]
+ end
+
+ [pack, with_buffer]
+RUBY
+
+# String#[] / String#slice
+assert_equal 'ok', <<~'RUBY'
+ def error(klass)
+ yield
+ rescue klass
+ true
+ end
+
+ def test
+ str = "こんにちは"
+ substr = "にち"
+ failures = []
+
+ # Use many small statements to keep context for each slice call smaller than MAX_CTX_TEMPS
+
+ str[1] == "ん" && str.slice(4) == "は" || failures << :index
+ str[5].nil? && str.slice(5).nil? || failures << :index_end
+
+ str[1, 2] == "んに" && str.slice(2, 1) == "に" || failures << :beg_len
+ str[5, 1] == "" && str.slice(5, 1) == "" || failures << :beg_len_end
+
+ str[1..2] == "んに" && str.slice(2..2) == "に" || failures << :range
+
+ str[/に./] == "にち" && str.slice(/に./) == "にち" || failures << :regexp
+
+ str[/に./, 0] == "にち" && str.slice(/に./, 0) == "にち" || failures << :regexp_cap0
+
+ str[/に(.)/, 1] == "ち" && str.slice(/に(.)/, 1) == "ち" || failures << :regexp_cap1
+
+ str[substr] == substr && str.slice(substr) == substr || failures << :substr
+
+ error(TypeError) { str[Object.new] } && error(TypeError) { str.slice(Object.new, 1) } || failures << :type_error
+ error(RangeError) { str[Float::INFINITY] } && error(RangeError) { str.slice(Float::INFINITY) } || failures << :range_error
+
+ return "ok" if failures.empty?
+ {failures: failures}
+ end
+
+ test
+RUBY
+
+# opt_duparray_send :include?
+assert_equal '[true, false]', <<~'RUBY'
+ def test(x)
+ [:a, :b].include?(x)
+ end
+
+ [
+ test(:b),
+ test(:c),
+ ]
+RUBY
+
+# opt_newarray_send :include?
+assert_equal '[true, false]', <<~'RUBY'
+ def test(x)
+ [Object.new, :a, :b].include?(x.to_sym)
+ end
+
+ [
+ test("b"),
+ test("c"),
+ ]
+RUBY
+
+# YARV: swap and opt_reverse
+assert_equal '["x", "Y", "c", "A", "t", "A", "b", "C", "d"]', <<~'RUBY'
+ class Swap
+ def initialize(s)
+ @a, @b, @c, @d = s.split("")
+ end
+
+ def swap
+ a, b = @a, @b
+ b = b.upcase
+ @a, @b = a, b
+ end
+
+ def reverse_odd
+ a, b, c = @a, @b, @c
+ b = b.upcase
+ @a, @b, @c = a, b, c
+ end
+
+ def reverse_even
+ a, b, c, d = @a, @b, @c, @d
+ a = a.upcase
+ c = c.upcase
+ @a, @b, @c, @d = a, b, c, d
+ end
+ end
+
+ Swap.new("xy").swap + Swap.new("cat").reverse_odd + Swap.new("abcd").reverse_even
+RUBY
+
+assert_normal_exit %{
+ class Bug20997
+ def foo(&) = self.class.name(&)
+
+ new.foo
+ end
+}
+
+# This used to trigger a "try to mark T_NONE"
+# due to an uninitialized local in foo.
+assert_normal_exit %{
+ def foo(...)
+ _local_that_should_nil_on_call = GC.start
+ end
+
+ def test_bug21021
+ puts [], [], [], [], [], []
+ foo []
+ end
+
+ GC.stress = true
+ test_bug21021
+}
+
+assert_equal 'nil', %{
+ def foo(...)
+ _a = _b = _c = binding.local_variable_get(:_c)
+
+ _c
+ end
+
+ # [Bug #21021]
+ def test_local_fill_in_forwardable
+ puts [], [], [], [], []
+ foo []
+ end
+
+ test_local_fill_in_forwardable.inspect
+}
+
+# Test defined?(yield) and block_given? in non-method context.
+# It's good that the body of this runs at true top level and isn't wrapped in a block.
+assert_equal 'false', %{
+ RESULT = []
+ RESULT << defined?(yield)
+ RESULT << block_given?
+
+ 1.times do
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+
+ module ModuleContext
+ 1.times do
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+ end
+
+ class << self
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+
+ RESULT.any?
+}
+
+# throw and String#dup with GC stress
+assert_equal 'foo', %{
+ GC.stress = true
+
+ def foo
+ 1.times { return "foo".dup }
+ end
+
+ 10.times.map { foo.dup }.last
+}
+
+# regression test for [Bug #21772]
+# local variable type tracking desync
+assert_normal_exit %q{
+ def some_method = 0
+
+ def test_body(key)
+ some_method
+ key = key.to_s # setting of local relevant
+
+ key == "symbol"
+ end
+
+ def jit_caller = test_body("session_id")
+
+ jit_caller # first iteration, non-escaped environment
+ alias some_method binding # induce environment escape
+ test_body(:symbol)
+}
+
+# regression test for missing check in identity method inlining
+assert_normal_exit %q{
+ # Use dead code (if false) to create a local
+ # without initialization instructions.
+ def foo(a)
+ if false
+ x = nil
+ end
+ x
+ end
+ def test = foo(1)
+ test
+ test
+}
+
+# regression test for tracing invalidation with on-stack compiled methods
+# Exercises the on_stack_iseqs path in rb_yjit_tracing_invalidate_all
+# where delayed deallocation must not create aliasing &mut references
+# to IseqPayload (use-after-free of version_map backing storage).
+assert_normal_exit %q{
+ def deep = 42
+ def mid = deep
+ def outer = mid
+
+ # Compile all three methods with YJIT
+ 10.times { outer }
+
+ # Enable tracing from within a call chain so that outer/mid/deep
+ # are on the stack when rb_yjit_tracing_invalidate_all runs.
+ # This triggers the on_stack_iseqs (delayed deallocation) path.
+ def deep
+ TracePoint.new(:line) {}.enable
+ 42
+ end
+
+ outer
+
+ # After invalidation, verify YJIT can recompile and run correctly
+ def deep = 42
+ 10.times { outer }
+}
+
+# regression test for tracing invalidation with on-stack fibers
+# Suspended fibers have iseqs on their stack that must survive invalidation.
+assert_equal '42', %q{
+ def compiled_method
+ Fiber.yield
+ 42
+ end
+
+ # Compile the method
+ 10.times { compiled_method rescue nil }
+
+ fiber = Fiber.new { compiled_method }
+ fiber.resume # suspends inside compiled_method — it's now on the fiber's stack
+
+ # Enable tracing while compiled_method is on the fiber's stack.
+ # This triggers rb_yjit_tracing_invalidate_all with on-stack iseqs.
+ TracePoint.new(:call) {}.enable
+
+ # Resume the fiber — compiled_method's iseq must still be valid
+ fiber.resume.to_s
+}
+
+# regression test for register mapping of methods with over 256 locals
+# [Bug #22074]
+assert_equal "ok", %q{
+ source = +"def many_locals\n"
+ source << " total = 0\n"
+
+ 128.times do |i|
+ source << " y#{i} = 1\n"
+ source << " x#{i} = Object.new\n"
+ end
+
+ source << " total += 1\n"
+ source << " raise total.inspect unless total == 1\n"
+ source << "end\n"
+
+ eval(source)
+ many_locals
+ "ok"
+}
diff --git a/bootstraptest/test_yjit_rust_port.rb b/bootstraptest/test_yjit_rust_port.rb
index e399e0e49e..2dbcebc03a 100644
--- a/bootstraptest/test_yjit_rust_port.rb
+++ b/bootstraptest/test_yjit_rust_port.rb
@@ -374,7 +374,7 @@ assert_equal 'ok', %q{
r = Ractor.new do
'ok'
end
- r.take
+ r.value
}
# Passed arguments to Ractor.new will be a block parameter
@@ -384,7 +384,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ok' do |msg|
msg
end
- r.take
+ r.value
}
# Pass multiple arguments to Ractor.new
@@ -393,7 +393,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ping', 'pong' do |msg, msg2|
[msg, msg2]
end
- 'ok' if r.take == ['ping', 'pong']
+ 'ok' if r.value == ['ping', 'pong']
}
# Ractor#send passes an object with copy to a Ractor
@@ -403,7 +403,7 @@ assert_equal 'ok', %q{
msg = Ractor.receive
end
r.send 'ok'
- r.take
+ r.value
}
assert_equal '[1, 2, 3]', %q{