diff options
Diffstat (limited to 'bootstraptest')
| -rwxr-xr-x | bootstraptest/runner.rb | 270 | ||||
| -rw-r--r-- | bootstraptest/test_attr.rb | 16 | ||||
| -rw-r--r-- | bootstraptest/test_autoload.rb | 12 | ||||
| -rw-r--r-- | bootstraptest/test_eval.rb | 43 | ||||
| -rw-r--r-- | bootstraptest/test_exception.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_fiber.rb | 5 | ||||
| -rw-r--r-- | bootstraptest/test_finalizer.rb | 8 | ||||
| -rw-r--r-- | bootstraptest/test_flow.rb | 6 | ||||
| -rw-r--r-- | bootstraptest/test_fork.rb | 27 | ||||
| -rw-r--r-- | bootstraptest/test_insns.rb | 82 | ||||
| -rw-r--r-- | bootstraptest/test_io.rb | 5 | ||||
| -rw-r--r-- | bootstraptest/test_jump.rb | 2 | ||||
| -rw-r--r-- | bootstraptest/test_literal.rb | 9 | ||||
| -rw-r--r-- | bootstraptest/test_literal_suffix.rb | 12 | ||||
| -rw-r--r-- | bootstraptest/test_load.rb | 12 | ||||
| -rw-r--r-- | bootstraptest/test_method.rb | 296 | ||||
| -rw-r--r-- | bootstraptest/test_ractor.rb | 2060 | ||||
| -rw-r--r-- | bootstraptest/test_syntax.rb | 58 | ||||
| -rw-r--r-- | bootstraptest/test_thread.rb | 23 | ||||
| -rw-r--r-- | bootstraptest/test_yjit.rb | 2532 | ||||
| -rw-r--r-- | bootstraptest/test_yjit_rust_port.rb | 8 |
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{ |
