diff options
Diffstat (limited to 'bootstraptest')
-rwxr-xr-x | bootstraptest/runner.rb | 842 | ||||
-rw-r--r-- | bootstraptest/test_attr.rb | 16 | ||||
-rw-r--r-- | bootstraptest/test_autoload.rb | 30 | ||||
-rw-r--r-- | bootstraptest/test_constant_cache.rb | 187 | ||||
-rw-r--r-- | bootstraptest/test_eval.rb | 72 | ||||
-rw-r--r-- | bootstraptest/test_exception.rb | 2 | ||||
-rw-r--r-- | bootstraptest/test_finalizer.rb | 8 | ||||
-rw-r--r-- | bootstraptest/test_flow.rb | 2 | ||||
-rw-r--r-- | bootstraptest/test_gc.rb | 2 | ||||
-rw-r--r-- | bootstraptest/test_insns.rb | 21 | ||||
-rw-r--r-- | bootstraptest/test_io.rb | 5 | ||||
-rw-r--r-- | bootstraptest/test_jump.rb | 6 | ||||
-rw-r--r-- | bootstraptest/test_literal.rb | 8 | ||||
-rw-r--r-- | bootstraptest/test_literal_suffix.rb | 12 | ||||
-rw-r--r-- | bootstraptest/test_load.rb | 2 | ||||
-rw-r--r-- | bootstraptest/test_method.rb | 34 | ||||
-rw-r--r-- | bootstraptest/test_ractor.rb | 418 | ||||
-rw-r--r-- | bootstraptest/test_rjit.rb | 58 | ||||
-rw-r--r-- | bootstraptest/test_syntax.rb | 52 | ||||
-rw-r--r-- | bootstraptest/test_thread.rb | 26 | ||||
-rw-r--r-- | bootstraptest/test_yjit.rb | 2581 | ||||
-rw-r--r-- | bootstraptest/test_yjit_rust_port.rb | 422 |
22 files changed, 4364 insertions, 442 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index 11d5a6c17e..20f121cdf4 100755 --- a/bootstraptest/runner.rb +++ b/bootstraptest/runner.rb @@ -6,7 +6,8 @@ # 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 begin require 'fileutils' @@ -58,24 +59,114 @@ if !Dir.respond_to?(:mktmpdir) end end +# Configuration +bt = Struct.new(:ruby, + :verbose, + :color, + :tty, + :quiet, + :wn, + :progress, + :progress_bs, + :passed, + :failed, + :reset, + :columns, + :window_width, + :width, + :indent, + :platform, + ) +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 + if /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ ENV.delete("MAKEFLAGS") + begin + if fifo = $3 + fifo.gsub!(/\\(?=.)/, '') + r = File.open(fifo, IO::RDONLY|IO::NONBLOCK|IO::BINARY) + w = File.open(fifo, IO::WRONLY|IO::NONBLOCK|IO::BINARY) + else + r = IO.for_fd($1.to_i(10), "rb", autoclose: false) + w = IO.for_fd($2.to_i(10), "wb", autoclose: false) + end + rescue + r.close if r + else + r.close_on_exec = true + w.close_on_exec = true + tokens = r.read_nonblock(wn > 0 ? wn : 1024, exception: false) + r.close + if String === tokens + tokens.freeze + auth = w + w = nil + at_exit {auth << tokens; auth.close} + wn = tokens.size + 1 + else + w.close + wn = 1 + end + end + end + if wn <= 0 + require 'etc' + wn = [Etc.nprocessors / 2, 1].max + end + end + super wn + end +end.new + +BT_STATE = Struct.new(:count, :error).new + def main - @ruby = File.expand_path('miniruby') - @verbose = false + BT.ruby = File.expand_path('miniruby') + BT.verbose = false $VERBOSE = false $stress = false - @color = nil - @tty = nil - @quiet = false + BT.color = nil + BT.tty = nil + BT.quiet = false + # BT.wn = 1 dir = nil quiet = false tests = nil ARGV.delete_if {|arg| case arg when /\A--ruby=(.*)/ - @ruby = $1 - @ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} - @ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} - @ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + ruby = $1 + ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} + ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} + ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + BT.ruby = ruby true when /\A--sets=(.*)/ tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort @@ -88,18 +179,23 @@ def main $stress = true when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/ warn "unknown --color argument: #$3" if $3 - @color = $1 ? nil : !$2 + BT.color = color = $1 ? nil : !$2 true when /\A--tty(=(?:yes|(no)|(.*)))?\z/ warn "unknown --tty argument: #$3" if $3 - @tty = !$1 || !$2 + BT.tty = !$1 || !$2 true when /\A(-q|--q(uiet))\z/ quiet = true - @quiet = true + BT.quiet = true + true + when /\A-j(\d+)?/ + BT.wn = $1.to_i true when /\A(-v|--v(erbose))\z/ - @verbose = true + BT.verbose = true + BT.quiet = false + true when /\A(-h|--h(elp)?)\z/ puts(<<-End) Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...] @@ -121,22 +217,23 @@ End 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) } - @progress = %w[- \\ | /] - @progress_bs = "\b" * @progress[0].size - @tty = $stderr.tty? if @tty.nil? - case @color + 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 - @color = @tty && /dumb/ !~ ENV["TERM"] + BT.color = BT.tty && /dumb/ !~ ENV["TERM"] end - @tty &&= !@verbose - if @color + BT.tty &&= !BT.verbose + if BT.color # dircolors-like style colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} begin @@ -145,14 +242,16 @@ End end rescue end - @passed = "\e[;#{colors["pass"] || "32"}m" - @failed = "\e[;#{colors["fail"] || "31"}m" - @reset = "\e[m" + BT.passed = "\e[;#{colors["pass"] || "32"}m" + BT.failed = "\e[;#{colors["fail"] || "31"}m" + BT.reset = "\e[m" else - @passed = @failed = @reset = "" + BT.passed = BT.failed = BT.reset = "" end + target_version = `#{BT.ruby} -v`.chomp + BT.platform = target_version[/\[(.*)\]\z/, 1] unless quiet - puts Time.now + puts $start_time if defined?(RUBY_DESCRIPTION) puts "Driver is #{RUBY_DESCRIPTION}" elsif defined?(RUBY_PATCHLEVEL) @@ -160,272 +259,474 @@ End else puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" end - puts "Target is #{`#{@ruby} -v`.chomp}" + puts "Target is #{target_version}" puts $stdout.flush end - in_temporary_working_directory(dir) { + in_temporary_working_directory(dir) do exec_test pathes - } + end end def erase(e = true) - if e and @columns > 0 and @tty and !@verbose + if e and BT.columns > 0 and BT.tty and !BT.verbose "\e[1K\r" else "" end end -def exec_test(pathes) - @count = 0 - @error = 0 - @errbuf = [] - @location = nil - @columns = 0 - @width = pathes.map {|path| File.basename(path).size}.max + 2 +def load_test pathes pathes.each do |path| - @basename = File.basename(path) - $stderr.printf("%s%-*s ", erase(@quiet), @width, @basename) - $stderr.flush - @columns = @width + 1 - $stderr.puts if @verbose - count = @count - error = @error load File.expand_path(path) - if @tty - if @error == error - msg = "PASS #{@count-count}" - @columns += msg.size - 1 - $stderr.print "#{@progress_bs}#{@passed}#{msg}#{@reset}" + end +end + +def concurrent_exec_test + aq = Queue.new + rq = Queue.new + + ts = BT.wn.times.map do + Thread.new do + while as = aq.pop + as.call + rq << as + end + ensure + rq << nil + end + end + + Assertion.all.to_a.shuffle.each do |path, assertions| + assertions.each do |as| + aq << as + end + end + + BT.indent = 1 + aq.close + i = 1 + term_wn = 0 + begin + while BT.wn != term_wn + if r = rq.pop + case + when BT.quiet + when BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[(i+=1) % BT.progress.size]}" + else + BT.putc '.' + end else - msg = "FAIL #{@error-error}/#{@count-count}" - $stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}" - @columns = 0 + term_wn += 1 end end - $stderr.puts unless @quiet and @tty and @error == error + ensure + ts.each(&:kill) + ts.each(&:join) end - $stderr.print(erase) if @quiet - @errbuf.each do |msg| +end + +def exec_test(pathes) + # setup + load_test pathes + BT_STATE.count = 0 + BT_STATE.error = 0 + BT.columns = 0 + BT.width = pathes.map {|path| File.basename(path).size}.max + 2 + + # execute tests + if BT.wn > 1 + concurrent_exec_test + else + prev_basename = nil + Assertion.all.each do |basename, assertions| + if !BT.quiet && basename != prev_basename + prev_basename = basename + $stderr.printf("%s%-*s ", erase(BT.quiet), BT.width, basename) + $stderr.flush + end + BT.columns = BT.width + 1 + $stderr.puts if BT.verbose + count = BT_STATE.count + error = BT_STATE.error + + assertions.each do |assertion| + BT_STATE.count += 1 + assertion.call + end + + if BT.tty + if BT_STATE.error == error + msg = "PASS #{BT_STATE.count-count}" + BT.columns += msg.size - 1 + $stderr.print "#{BT.progress_bs}#{BT.passed}#{msg}#{BT.reset}" unless BT.quiet + else + msg = "FAIL #{BT_STATE.error-error}/#{BT_STATE.count-count}" + $stderr.print "#{BT.progress_bs}#{BT.failed}#{msg}#{BT.reset}" + BT.columns = 0 + end + end + $stderr.puts if !BT.quiet and (BT.tty or BT_STATE.error == error) + end + end + + # show results + unless BT.quiet + $stderr.puts(erase) + + sec = Time.now - $start_time + $stderr.puts "Finished in #{'%.2f' % sec} sec\n\n" if Assertion.count > 0 + end + + Assertion.errbuf.each do |msg| $stderr.puts msg end - if @error == 0 - if @count == 0 - $stderr.puts "No tests, no problem" + + out = BT.quiet ? $stdout : $stderr + + if BT_STATE.error == 0 + if Assertion.count == 0 + out.puts "No tests, no problem" unless BT.quiet else - $stderr.puts "#{@passed}PASS#{@reset} all #{@count} tests" + out.puts "#{BT.passed}PASS#{BT.reset} all #{Assertion.count} tests" end - exit true + true else - $stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed" - exit false + $stderr.puts "#{BT.failed}FAIL#{BT.reset} #{BT_STATE.error}/#{BT_STATE.count} tests failed" + false end end -def show_progress(message = '') - if @verbose - $stderr.print "\##{@count} #{@location} " - elsif @tty - $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" - end - t = Time.now if @verbose - faildesc, errout = with_stderr {yield} - t = Time.now - t if @verbose - if !faildesc - if @tty - $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" - elsif @verbose - $stderr.printf(". %.3f\n", t) - else - $stderr.print '.' +def target_platform + BT.platform or RUBY_PLATFORM +end + +class Assertion < Struct.new(:src, :path, :lineno, :proc) + @count = 0 + @all = Hash.new{|h, k| h[k] = []} + @errbuf = [] + + class << self + attr_reader :count, :errbuf + + def all + @all end - else - $stderr.print "#{@failed}F" - $stderr.printf(" %.3f", t) if @verbose - $stderr.print @reset - $stderr.puts if @verbose - error faildesc, message - unless errout.empty? - $stderr.print "#{@failed}stderr output is not empty#{@reset}\n", adjust_indent(errout) + + def add as + @all[as.path] << as + as.id = (@count += 1) end - if @tty and !@verbose - $stderr.printf("%-*s%s", @width, @basename, @progress[@count % @progress.size]) + end + + attr_accessor :id + attr_reader :err, :category + + def initialize(*args) + super + self.class.add self + @category = self.path[/\Atest_(.+)\.rb\z/, 1] + end + + def call + self.proc.call self + end + + def assert_check(message = '', opt = '', **argh) + show_progress(message) { + result = get_result_string(opt, **argh) + yield(result) + } + end + + def with_stderr + out = err = nil + r, w = IO.pipe + @err = w + err_reader = Thread.new{ r.read } + + begin + out = yield + ensure + w.close + err = err_reader.value + r.close rescue nil end + + return out, err end -rescue Interrupt - $stderr.puts "\##{@count} #{@location}" - raise -rescue Exception => err - $stderr.print 'E' - $stderr.puts if @verbose - error err.message, message -ensure - begin - check_coredump - rescue CoreDumpError => err + + def show_error(msg, additional_message) + msg = "#{BT.failed}\##{self.id} #{self.path}:#{self.lineno}#{BT.reset}: #{msg} #{additional_message}" + if BT.tty + $stderr.puts "#{erase}#{msg}" + else + Assertion.errbuf << msg + end + BT_STATE.error += 1 + end + + + def show_progress(message = '') + if BT.quiet || BT.wn > 1 + # do nothing + elsif BT.verbose + $stderr.print "\##{@id} #{self.path}:#{self.lineno} " + elsif BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[BT_STATE.count % BT.progress.size]}" + end + + t = Time.now if BT.verbose + faildesc, errout = with_stderr {yield} + t = Time.now - t if BT.verbose + + if !faildesc + # success + if BT.quiet || BT.wn > 1 + # do nothing + elsif BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[BT_STATE.count % BT.progress.size]}" + elsif BT.verbose + $stderr.printf(". %.3f\n", t) + else + BT.putc '.' + end + else + $stderr.print "#{BT.failed}F" + $stderr.printf(" %.3f", t) if BT.verbose + $stderr.print BT.reset + $stderr.puts if BT.verbose + show_error faildesc, message + unless errout.empty? + $stderr.print "#{BT.failed}stderr output is not empty#{BT.reset}\n", adjust_indent(errout) + end + + if BT.tty and !BT.verbose and BT.wn == 1 + $stderr.printf("%-*s%s", BT.width, path, BT.progress[BT_STATE.count % BT.progress.size]) + end + end + rescue Interrupt + $stderr.puts "\##{@id} #{path}:#{lineno}" + raise + rescue Exception => err $stderr.print 'E' - $stderr.puts if @verbose - error err.message, message + $stderr.puts if BT.verbose + show_error err.message, message + ensure + begin + check_coredump + rescue CoreDumpError => err + $stderr.print 'E' + $stderr.puts if BT.verbose + show_error err.message, message + cleanup_coredump + end end -end -def show_limit(testsrc, opt = '', **argh) - result = get_result_string(testsrc, opt, **argh) - if @tty and @verbose - $stderr.puts ".{#@reset}\n#{erase}#{result}" - else - @errbuf.push result + def get_result_string(opt = '', **argh) + if BT.ruby + 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 } + ensure + raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] + + begin + Process.kill :KILL, pid + rescue Errno::ESRCH + # OK + end + end + else + eval(src).to_s + end + end + + 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:#{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 end -def assert_check(testsrc, message = '', opt = '', **argh) - show_progress(message) { - result = get_result_string(testsrc, opt, **argh) - yield(result) - } +def add_assertion src, pr + loc = caller_locations(2, 1).first + lineno = loc.lineno + path = File.basename(loc.path) + + Assertion.new(src, path, lineno, pr) end -def assert_equal(expected, testsrc, message = '', opt = '', **argh) - newtest - assert_check(testsrc, message, opt, **argh) {|result| - if expected == result - nil - else - desc = "#{result.inspect} (expected #{expected.inspect})" - pretty(testsrc, desc, result) - end - } +def assert_equal(expected, testsrc, message = '', opt = '', **kwargs) + add_assertion testsrc, -> as do + as.assert_check(message, opt, **kwargs) {|result| + if expected == result + nil + else + desc = "#{result.inspect} (expected #{expected.inspect})" + pretty(testsrc, desc, result) + end + } + end end -def assert_match(expected_pattern, testsrc, message = '') - newtest - assert_check(testsrc, message) {|result| - if expected_pattern =~ result - nil - else - desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}" - pretty(testsrc, desc, result) - end - } +def assert_match(expected_pattern, testsrc, message = '', **argh) + add_assertion testsrc, -> as do + as.assert_check(message, **argh) {|result| + if expected_pattern =~ result + nil + else + desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}" + pretty(testsrc, desc, result) + end + } + end end def assert_not_match(unexpected_pattern, testsrc, message = '') - newtest - assert_check(testsrc, message) {|result| - if unexpected_pattern !~ result - nil - else - desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}" - pretty(testsrc, desc, result) - end - } + add_assertion testsrc, -> as do + as.assert_check(message) {|result| + if unexpected_pattern !~ result + nil + else + desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}" + pretty(testsrc, desc, result) + end + } + end end def assert_valid_syntax(testsrc, message = '') - newtest - assert_check(testsrc, message, '-c') {|result| - result if /Syntax OK/ !~ result - } + add_assertion testsrc, -> as do + as.assert_check(message, '-c') {|result| + result if /Syntax OK/ !~ result + } + end end def assert_normal_exit(testsrc, *rest, timeout: nil, **opt) - newtest - message, ignore_signals = rest - message ||= '' - show_progress(message) { - faildesc = nil - filename = make_srcfile(testsrc) - old_stderr = $stderr.dup - timeout_signaled = false - begin - $stderr.reopen("assert_normal_exit.log", "w") - io = IO.popen("#{@ruby} -W0 #{filename}") - 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 - $stderr.reopen(old_stderr) - old_stderr.close - end - if status && status.signaled? - signo = status.termsig - signame = Signal.list.invert[signo] - unless ignore_signals and ignore_signals.include?(signame) - sigdesc = "signal #{signo}" - if signame - sigdesc = "SIG#{signame} (#{sigdesc})" - end - if timeout_signaled - sigdesc << " (timeout)" + add_assertion testsrc, -> as do + message, ignore_signals = rest + message ||= '' + as.show_progress(message) { + faildesc = nil + filename = as.make_srcfile + 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 - faildesc = pretty(testsrc, "killed by #{sigdesc}", nil) - stderr_log = File.read("assert_normal_exit.log") - if !stderr_log.empty? - faildesc << "\n" if /\n\z/ !~ faildesc - stderr_log << "\n" if /\n\z/ !~ stderr_log - stderr_log.gsub!(/^.*\n/) { '| ' + $& } - faildesc << stderr_log + status = th.value + ensure + err.close + end + if status && status.signaled? + signo = status.termsig + signame = Signal.list.invert[signo] + unless ignore_signals and ignore_signals.include?(signame) + sigdesc = "signal #{signo}" + if signame + sigdesc = "SIG#{signame} (#{sigdesc})" + end + if timeout_signaled + sigdesc << " (timeout)" + end + faildesc = pretty(testsrc, "killed by #{sigdesc}", nil) + stderr_log = File.read(logfile) + if !stderr_log.empty? + faildesc << "\n" if /\n\z/ !~ faildesc + stderr_log << "\n" if /\n\z/ !~ stderr_log + stderr_log.gsub!(/^.*\n/) { '| ' + $& } + faildesc << stderr_log + end end end - end - faildesc - } + faildesc + } + end end def assert_finish(timeout_seconds, testsrc, message = '') - if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? # for --jit-wait - timeout_seconds *= 3 - end - newtest - show_progress(message) { - faildesc = nil - filename = make_srcfile(testsrc) - io = IO.popen("#{@ruby} -W0 #{filename}") - pid = io.pid - waited = false - tlimit = Time.now + timeout_seconds - diff = timeout_seconds - while diff > 0 - if Process.waitpid pid, Process::WNOHANG - waited = true - break - end - if io.respond_to?(:read_nonblock) - if IO.select([io], nil, nil, diff) - begin - io.read_nonblock(1024) - rescue Errno::EAGAIN, IO::WaitReadable, EOFError - break - end while true + add_assertion testsrc, -> as do + if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # for --jit-wait + timeout_seconds *= 3 + end + + as.show_progress(message) { + faildesc = nil + filename = as.make_srcfile + io = IO.popen("#{BT.ruby} -W0 #{filename}", err: as.err) + pid = io.pid + waited = false + tlimit = Time.now + timeout_seconds + diff = timeout_seconds + while diff > 0 + if Process.waitpid pid, Process::WNOHANG + waited = true + break end - else - sleep 0.1 + if io.respond_to?(:read_nonblock) + if IO.select([io], nil, nil, diff) + begin + io.read_nonblock(1024) + rescue Errno::EAGAIN, IO::WaitReadable, EOFError + break + end while true + end + else + sleep 0.1 + end + diff = tlimit - Time.now end - diff = tlimit - Time.now - end - if !waited - Process.kill(:KILL, pid) - Process.waitpid pid - faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil) - end - io.close - faildesc - } + if !waited + Process.kill(:KILL, pid) + Process.waitpid pid + faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil) + end + io.close + faildesc + } + end end def flunk(message = '') - newtest - show_progress('') { message } + add_assertion '', -> as do + as.show_progress('') { message } + end +end + +def show_limit(testsrc, opt = '', **argh) + return if BT.quiet + + add_assertion testsrc, -> as do + result = as.get_result_string(opt, **argh) + Assertion.errbuf << result + end end def pretty(src, desc, result) @@ -443,66 +744,6 @@ def untabify(str) str.gsub(/^\t+/) {' ' * (8 * $&.size) } end -def make_srcfile(src, frozen_string_literal: nil) - filename = 'bootstraptest.tmp.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; #{src}; end)" - } - filename -end - -def get_result_string(src, opt = '', **argh) - if @ruby - filename = make_srcfile(src, **argh) - begin - `#{@ruby} -W0 #{opt} #{filename}` - ensure - raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] - end - else - eval(src).to_s - end -end - -def with_stderr - out = err = nil - begin - r, w = IO.pipe - stderr = $stderr.dup - $stderr.reopen(w) - w.close - reader = Thread.start {r.read} - begin - out = yield - ensure - $stderr.reopen(stderr) - err = reader.value - end - ensure - w.close rescue nil - r.close rescue nil - end - return out, err -end - -def newtest - @location = File.basename(caller(2).first) - @count += 1 - cleanup_coredump -end - -def error(msg, additional_message) - msg = "#{@failed}\##{@count} #{@location}#{@reset}: #{msg} #{additional_message}" - if @tty - $stderr.puts "#{erase}#{msg}" - else - @errbuf.push msg - end - @error += 1 -end - def in_temporary_working_directory(dir) if dir Dir.mkdir dir @@ -530,21 +771,30 @@ def cleanup_coredump core_path = "/tmp/bootstraptest-core.#{Time.now.utc.iso8601}" warn "A core file is found. Saving it at: #{core_path.dump}" FileUtils.mv('core', core_path) - cmd = ['gdb', @ruby, '-c', core_path, '-ex', 'bt', '-batch'] + cmd = ['gdb', BT.ruby, '-c', core_path, '-ex', 'bt', '-batch'] p cmd # debugging why it's not working system(*cmd) end FileUtils.rm_f Dir.glob('core.*') - FileUtils.rm_f @ruby+'.stackdump' if @ruby + FileUtils.rm_f BT.ruby+'.stackdump' if BT.ruby end class CoreDumpError < StandardError; end def check_coredump if File.file?('core') or not Dir.glob('core.*').empty? or - (@ruby and File.exist?(@ruby+'.stackdump')) + (BT.ruby and File.exist?(BT.ruby+'.stackdump')) raise CoreDumpError, "core dumped" end end -main +def yjit_enabled? + ENV.key?('RUBY_YJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('yjit') || BT.ruby.include?('yjit') +end + +def rjit_enabled? + # Don't check `RubyVM::RJIT.enabled?`. On btest-bruby, target Ruby != runner Ruby. + ENV.fetch('RUN_OPTS', '').include?('rjit') +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 a9f8e6dacd..9e0850bc52 100644 --- a/bootstraptest/test_autoload.rb +++ b/bootstraptest/test_autoload.rb @@ -1,7 +1,7 @@ assert_equal 'ok', %q{ - File.unlink('zzz.rb') if File.file?('zzz.rb') + File.unlink('zzz1.rb') if File.file?('zzz1.rb') instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz1.rb' begin ZZZ rescue LoadError @@ -11,9 +11,9 @@ assert_equal 'ok', %q{ }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open('zzz.rb', 'w') {|f| f.puts '' } + open('zzz2.rb', 'w') {|f| f.puts '' } instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz2.rb' begin ZZZ rescue NameError @@ -23,29 +23,29 @@ assert_equal 'ok', %q{ }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open('zzz.rb', 'w') {|f| f.puts 'class ZZZ; def self.ok;:ok;end;end'} + open('zzz3.rb', 'w') {|f| f.puts 'class ZZZ; def self.ok;:ok;end;end'} instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz3.rb' ZZZ.ok end }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" + open("zzz4.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} + autoload :ZZZ, "./zzz4.rb" ZZZ.ok } assert_equal 'ok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" - require "./zzz.rb" + open("zzz5.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} + autoload :ZZZ, "./zzz5.rb" + require "./zzz5.rb" ZZZ.ok } assert_equal 'okok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" + open("zzz6.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} + autoload :ZZZ, "./zzz6.rb" t1 = Thread.new {ZZZ.ok} t2 = Thread.new {ZZZ.ok} [t1.value, t2.value].join @@ -60,9 +60,9 @@ assert_finish 5, %q{ }, '[ruby-core:21696]' assert_equal 'A::C', %q{ - open("zzz.rb", "w") {} + open("zzz7.rb", "w") {} class A - autoload :C, "./zzz" + autoload :C, "./zzz7" class C end C diff --git a/bootstraptest/test_constant_cache.rb b/bootstraptest/test_constant_cache.rb new file mode 100644 index 0000000000..1fa83256ed --- /dev/null +++ b/bootstraptest/test_constant_cache.rb @@ -0,0 +1,187 @@ +# Constant lookup is cached. +assert_equal '1', %q{ + CONST = 1 + + def const + CONST + end + + const + const +} + +# Invalidate when a constant is set. +assert_equal '2', %q{ + CONST = 1 + + def const + CONST + end + + const + + CONST = 2 + + const +} + +# Invalidate when a constant of the same name is set. +assert_equal '1', %q{ + CONST = 1 + + def const + CONST + end + + const + + class Container + CONST = 2 + end + + const +} + +# Invalidate when a constant is removed. +assert_equal 'missing', %q{ + class Container + CONST = 1 + + def const + CONST + end + + def self.const_missing(name) + 'missing' + end + + new.const + remove_const :CONST + end + + Container.new.const +} + +# Invalidate when a constant's visibility changes. +assert_equal 'missing', %q{ + class Container + CONST = 1 + + def self.const_missing(name) + 'missing' + end + end + + def const + Container::CONST + end + + const + + Container.private_constant :CONST + + const +} + +# Invalidate when a constant's visibility changes even if the call to the +# visibility change method fails. +assert_equal 'missing', %q{ + class Container + CONST1 = 1 + + def self.const_missing(name) + 'missing' + end + end + + def const1 + Container::CONST1 + end + + const1 + + begin + Container.private_constant :CONST1, :CONST2 + rescue NameError + end + + const1 +} + +# Invalidate when a module is included. +assert_equal 'INCLUDE', %q{ + module Include + CONST = :INCLUDE + end + + class Parent + CONST = :PARENT + end + + class Child < Parent + def const + CONST + end + + new.const + + include Include + end + + Child.new.const +} + +# Invalidate when const_missing is hit. +assert_equal '2', %q{ + module Container + Foo = 1 + Bar = 2 + + class << self + attr_accessor :count + + def const_missing(name) + @count += 1 + @count == 1 ? Foo : Bar + end + end + + @count = 0 + end + + def const + Container::Baz + end + + const + const +} + +# Invalidate when the iseq gets cleaned up. +assert_equal '2', %q{ + CONSTANT = 1 + + iseq = RubyVM::InstructionSequence.compile(<<~RUBY) + CONSTANT + RUBY + + iseq.eval + iseq = nil + + GC.start + CONSTANT = 2 +} + +# Invalidate when the iseq gets cleaned up even if it was never in the cache. +assert_equal '2', %q{ + CONSTANT = 1 + + iseq = RubyVM::InstructionSequence.compile(<<~RUBY) + CONSTANT + RUBY + + iseq = nil + + GC.start + CONSTANT = 2 +} diff --git a/bootstraptest/test_eval.rb b/bootstraptest/test_eval.rb index 5d2593c306..d923a957bc 100644 --- a/bootstraptest/test_eval.rb +++ b/bootstraptest/test_eval.rb @@ -116,6 +116,33 @@ assert_equal %q{1}, %q{ Const } } +assert_equal %q{1}, %q{ + class TrueClass + Const = 1 + end + true.instance_eval %{ + Const + } +} +assert_equal %q{[:Const]}, %q{ + mod = Module.new + mod.instance_eval %{ + Const = 1 + } + raise if defined?(Module::Const) + mod.singleton_class.constants +} +assert_equal %q{can't define singleton}, %q{ + begin + 123.instance_eval %{ + Const = 1 + } + "bad" + rescue TypeError => e + raise "bad" if defined?(Integer::Const) + e.message + end +} assert_equal %q{top}, %q{ Const = :top class C @@ -191,7 +218,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", %{ - STDERR.reopen(STDOUT) + $stderr = STDOUT begin eval "0 rescue #{keyword}" rescue SyntaxError => e @@ -200,8 +227,18 @@ 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.reopen(STDOUT) + $stderr = STDOUT class Foo def self.add_method class_eval("def some-bad-name; puts 'hello' unless @some_variable.some_function(''); end") @@ -327,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_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..15528a4213 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 diff --git a/bootstraptest/test_gc.rb b/bootstraptest/test_gc.rb index eb68c9845e..17bc497822 100644 --- a/bootstraptest/test_gc.rb +++ b/bootstraptest/test_gc.rb @@ -14,7 +14,7 @@ ms = "a".."k" o.send(meth) end end -}, '[ruby-dev:39453]' +}, '[ruby-dev:39453]' unless rjit_enabled? # speed up RJIT CI assert_normal_exit %q{ a = [] diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb index 31fdc29d02..06828a7f7a 100644 --- a/bootstraptest/test_insns.rb +++ b/bootstraptest/test_insns.rb @@ -92,7 +92,7 @@ tests = [ [ '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 }, ], @@ -120,6 +120,7 @@ tests = [ [ 'dup', %q{ x = y = true; x }, ], [ 'dupn', %q{ Object::X ||= true }, ], [ 'reverse', %q{ q, (w, e), r = 1, [2, 3], 4; e == 3 }, ], + [ 'swap', %q{ !!defined?([[]]) }, ], [ 'swap', <<-'},', ], # { x = [[false, true]] for i, j in x # here @@ -213,9 +214,11 @@ tests = [ 'true'.freeze }, - [ 'opt_newarray_max', %q{ [ ].max.nil? }, ], - [ 'opt_newarray_max', %q{ [1, x = 2, 3].max == 3 }, ], - [ 'opt_newarray_max', <<-'},', ], # { + [ 'opt_newarray_send', %q{ ![ ].hash.nil? }, ], + + [ 'opt_newarray_send', %q{ [ ].max.nil? }, ], + [ 'opt_newarray_send', %q{ [1, x = 2, 3].max == 3 }, ], + [ 'opt_newarray_send', <<-'},', ], # { class Array def max true @@ -223,9 +226,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 @@ -351,7 +354,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 }, ], @@ -360,7 +363,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 diff --git a/bootstraptest/test_io.rb b/bootstraptest/test_io.rb index 89c00d0b88..666e5a011b 100644 --- a/bootstraptest/test_io.rb +++ b/bootstraptest/test_io.rb @@ -1,3 +1,4 @@ +/freebsd/ =~ RUBY_PLATFORM or assert_finish 5, %q{ r, w = IO.pipe t1 = Thread.new { r.sysread(1) } @@ -30,7 +31,8 @@ assert_finish 10, %q{ end }, '[ruby-dev:32566]' -assert_finish 1, %q{ +/freebsd/ =~ RUBY_PLATFORM or +assert_finish 5, %q{ r, w = IO.pipe Thread.new { w << "ab" @@ -83,6 +85,7 @@ assert_normal_exit %q{ ARGF.set_encoding "foo" } +/freebsd/ =~ RUBY_PLATFORM or 10.times do assert_normal_exit %q{ at_exit { p :foo } diff --git a/bootstraptest/test_jump.rb b/bootstraptest/test_jump.rb index 18a2737ea3..8751343b1f 100644 --- a/bootstraptest/test_jump.rb +++ b/bootstraptest/test_jump.rb @@ -147,7 +147,7 @@ assert_equal %q{131}, %q{ } } assert_match %r{Invalid retry}, %q{ -STDERR.reopen(STDOUT) +$stderr = STDOUT begin eval %q{ 1.times{ @@ -292,12 +292,12 @@ assert_equal "true", %q{ end end end - s = "foo" + s = +"foo" s.return_eigenclass == class << s; self; end }, '[ruby-core:21379]' assert_match %r{Invalid yield}, %q{ -STDERR.reopen(STDOUT) +$stderr = STDOUT begin eval %q{ class Object diff --git a/bootstraptest/test_literal.rb b/bootstraptest/test_literal.rb index 9b3c10d519..a30661a796 100644 --- a/bootstraptest/test_literal.rb +++ b/bootstraptest/test_literal.rb @@ -65,8 +65,12 @@ assert_equal ':a3c', ':"a#{1+2}c".inspect' assert_equal 'Symbol', ':"a#{1+2}c".class' # xstring -assert_equal "foo\n", %q(`echo foo`) -assert_equal "foo\n", %q(s = "foo"; `echo #{s}`) +# WASI doesn't support spawning a new process for now. +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' 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..3253582a32 100644 --- a/bootstraptest/test_load.rb +++ b/bootstraptest/test_load.rb @@ -12,7 +12,7 @@ assert_equal 'ok', %q{ } }.map {|t| t.value } vs[0] == M && vs[1] == M ? :ok : :ng -}, '[ruby-dev:32048]' +}, '[ruby-dev:32048]' unless ENV.fetch('RUN_OPTS', '').include?('rjit') # Thread seems to be switching during JIT. To be fixed later. assert_equal 'ok', %q{ %w[a a/foo b].each {|d| Dir.mkdir(d)} diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb index 3462aa9434..d1d1f57d55 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -22,7 +22,7 @@ assert_match /\Awrong number of arguments \(.*\b0\b.* 1\)\z/, %q{ } # default argument -assert_equal '1', 'def m(x=1) x end; m()' +assert_equal '1', 'def m(x=1) x end; m();' assert_equal '1', 'def m(x=7) x end; m(1)' assert_equal '1', 'def m(a,x=1) x end; m(7)' assert_equal '1', 'def m(a,x=7) x end; m(7,1)' @@ -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,12 @@ assert_equal 'DC', %q{ test2 o1, [], block $result.join } + +assert_equal 'ok', %q{ + def foo + binding + ["ok"].first + end + foo + foo +}, '[Bug #20178]' diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index bf80b94cc7..451b58e793 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -190,7 +190,7 @@ assert_equal '[:ok, :ok, :ok]', %q{ # Ractor.make_shareable issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e]', %q{ v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e - closure = Proc.new { [v1, v2, v3, v4, v5] } + closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } } Ractor.make_shareable(closure).call } @@ -198,17 +198,30 @@ assert_equal '[:a, :b, :c, :d, :e]', %q{ # Ractor.make_shareable issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{ a = :a - closure = -> { - b, c, d = :b, :c, :d + closure = Ractor.current.instance_eval do -> { - e, f, g = :e, :f, :g - -> { [a, b, c, d, e, f, g] } + b, c, d = :b, :c, :d + -> { + e, f, g = :e, :f, :g + -> { [a, b, c, d, e, f, g] } + }.call }.call - }.call + end Ractor.make_shareable(closure).call } +# 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 + end + r.take +} + ### ### # Ractor still has several memory corruption so skip huge number of tests @@ -270,8 +283,7 @@ 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{ @@ -488,7 +500,7 @@ assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ rs.delete r n }.sort -} +} unless /mswin/ =~ RUBY_PLATFORM # randomly hangs on mswin https://github.com/ruby/ruby/actions/runs/3753871445/jobs/6377551069#step:20:131 # Ractor.select also support multiple take, receive and yield assert_equal '[true, true, true]', %q{ @@ -502,9 +514,9 @@ assert_equal '[true, true, true]', %q{ end } received = [] - take = [] + taken = [] yielded = [] - until rs.empty? + until received.size == RN && taken.size == RN && yielded.size == RN r, v = Ractor.select(CR, *rs, yield_value: 'yield') case r when :receive @@ -512,11 +524,17 @@ assert_equal '[true, true, true]', %q{ when :yield yielded << v else - take << v + taken << v rs.delete r end end - [received.all?('sendyield'), yielded.all?(nil), take.all?('take')] + r = [received == ['sendyield'] * RN, + yielded == [nil] * RN, + taken == ['take'] * RN, + ] + + STDERR.puts [received, yielded, taken].inspect + r } # multiple Ractors can send to one Ractor @@ -583,7 +601,7 @@ assert_equal '{:ok=>3}', %q{ end 3.times.map{Ractor.receive}.tally -} +} unless yjit_enabled? # `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() # unshareable object are copied assert_equal 'false', %q{ @@ -610,7 +628,7 @@ assert_equal "allocator undefined for Thread", %q{ } # send shareable and unshareable objects -assert_equal "ok", %q{ +assert_equal "ok", <<~'RUBY', frozen_string_literal: false echo_ractor = Ractor.new do loop do v = Ractor.receive @@ -677,10 +695,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 @@ -703,11 +721,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 @@ -725,7 +743,7 @@ assert_equal 'hello world', %q{ else raise 'unreachable' end -} +RUBY # move example2: Array assert_equal '[0, 1]', %q{ @@ -928,7 +946,7 @@ assert_equal 'ArgumentError', %q{ } # 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", <<~'RUBY', frozen_string_literal: false class C @iv = 'str' end @@ -939,13 +957,12 @@ assert_equal "can not get unshareable values from instance variables of classes/ end end - begin r.take 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{ @@ -1069,6 +1086,27 @@ assert_equal '333', %q{ a + b + c + d + e + f } +# 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.take +values.join +} + # cvar in shareable-objects are not allowed to access from non-main Ractor assert_equal 'can not access class variables from non-main Ractors', %q{ class C @@ -1088,8 +1126,30 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ end } +# also cached cvar in shareable-objects are not allowed to access from non-main Ractor +assert_equal 'can not access class variables from non-main Ractors', %q{ + class C + @@cv = 'str' + def self.cv + @@cv + end + end + + C.cv # cache + + r = Ractor.new do + C.cv + end + + begin + r.take + rescue Ractor::RemoteError => e + e.cause.message + end +} + # 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 @@ -1101,10 +1161,10 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m 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{ +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 @@ -1113,10 +1173,10 @@ assert_equal "can not access non-shareable objects in constant Object::STR by no 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 @@ -1127,7 +1187,7 @@ assert_equal 'can not set constants with non-shareable objects by non-main Racto 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{ @@ -1180,7 +1240,7 @@ assert_equal '0', %q{ } # 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| @@ -1190,10 +1250,10 @@ assert_equal 'ok', %q{ :ok end end.take -} +RUBY # Ractor.make_shareable(obj) -assert_equal 'true', %q{ +assert_equal 'true', <<~'RUBY', frozen_string_literal: false class C def initialize @a = 'foo' @@ -1264,7 +1324,7 @@ assert_equal 'true', %q{ } Ractor.shareable?(a) -} +RUBY # Ractor.make_shareable(obj) doesn't freeze shareable objects assert_equal 'true', %q{ @@ -1276,9 +1336,13 @@ assert_equal 'true', %q{ # Ractor.make_shareable(a_proc) makes a proc shareable. assert_equal 'true', %q{ a = [1, [2, 3], {a: "4"}] - pr = Proc.new do - a + + pr = Ractor.current.instance_eval do + Proc.new do + a + end end + Ractor.make_shareable(a) # referred value should be shareable Ractor.make_shareable(pr) Ractor.shareable?(pr) @@ -1326,10 +1390,12 @@ assert_equal '1', %q{ # 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 = 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 + 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 @@ -1355,14 +1421,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 + c = 3 # line 9 end rs } @@ -1430,28 +1496,13 @@ assert_equal "#{N}#{N}", %Q{ }.map{|r| r.take}.join } -# enc_table -assert_equal "#{N/10}", %Q{ - Ractor.new do - loop do - Encoding.find("test-enc-#{rand(5_000)}").inspect - rescue ArgumentError => e - end - end - - src = Encoding.find("UTF-8") - #{N/10}.times{|i| - src.replicate("test-enc-\#{i}") - } -} - # Generic ivtbl 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) @@ -1463,8 +1514,9 @@ assert_equal "#{n}#{n}", %Q{ # NameError assert_equal "ok", %q{ + obj = "".freeze # NameError refers the receiver indirectly begin - bar + obj.bar rescue => err end begin @@ -1500,6 +1552,254 @@ assert_equal "ok", %q{ 1_000.times { idle_worker, tmp_reporter = Ractor.select(*workers) } "ok" +} unless yjit_enabled? || rjit_enabled? # flaky + +assert_equal "ok", %q{ + def foo(*); ->{ super }; end + begin + Ractor.make_shareable(foo) + rescue Ractor::IsolationError + "ok" + end +} + +assert_equal "ok", %q{ + def foo(**); ->{ super }; end + begin + Ractor.make_shareable(foo) + rescue Ractor::IsolationError + "ok" + end +} + +assert_equal "ok", %q{ + def foo(...); ->{ super }; end + begin + Ractor.make_shareable(foo) + rescue Ractor::IsolationError + "ok" + end +} + +assert_equal "ok", %q{ + def foo((x), (y)); ->{ super }; end + begin + Ractor.make_shareable(foo([], [])) + rescue Ractor::IsolationError + "ok" + end +} + +# check method cache invalidation +assert_equal "ok", %q{ + 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.take} == Array.new(RN){n} +} + +# check experimental warning +assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor is experimental/, %q{ + Warning[:experimental] = $VERBOSE = true + STDERR.reopen(STDOUT) + eval("Ractor.new{}.take", 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.take +} + +## 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 + rs = RN.times.map{ + Ractor.new{ + Ractor.main << Ractor.new{ Ractor.yield :v3; :v4 } + Ractor.main << Ractor.new{ Ractor.yield :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) +} diff --git a/bootstraptest/test_rjit.rb b/bootstraptest/test_rjit.rb new file mode 100644 index 0000000000..e123f35160 --- /dev/null +++ b/bootstraptest/test_rjit.rb @@ -0,0 +1,58 @@ +# VM_CALL_OPT_SEND + VM_METHOD_TYPE_ATTRSET +assert_equal '1', %q{ + class Foo + attr_writer :foo + + def bar + send(:foo=, 1) + end + end + + Foo.new.bar +} + +# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_CALL +assert_equal 'foo', %q{ + def bar(&foo) + foo.send(:call) + end + + bar { :foo } +} + +# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_STRUCT_AREF +assert_equal 'bar', %q{ + def bar(foo) + foo.send(:bar) + end + + bar(Struct.new(:bar).new(:bar)) +} + +# kwargs default w/ checkkeyword + locals (which shouldn't overwrite unspecified_bits) +assert_equal '1', %q{ + def foo(bar: 1.to_s) + _ = 1 + bar + end + + def entry + foo + end + + entry +} + +# Updating local type in Context +assert_normal_exit %q{ + def foo(flag, object) + klass = if flag + object + end + klass ||= object + return klass.new + end + + foo(false, Object) + foo(true, Object) +} diff --git a/bootstraptest/test_syntax.rb b/bootstraptest/test_syntax.rb index fa27bf2aeb..fbc9c6f62e 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]' @@ -628,8 +628,8 @@ assert_equal '2', %q{ } assert_match /invalid multibyte char/, %q{ - STDERR.reopen(STDOUT) - eval("\"\xf0".force_encoding("utf-8")) + $stderr = STDOUT + 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 38a55ff229..a4d46e2f10 100644 --- a/bootstraptest/test_thread.rb +++ b/bootstraptest/test_thread.rb @@ -242,8 +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.rb", "w") do |f| + open("zzz_t1.rb", "w") do |f| f.puts <<-END begin Thread.new { fork { GC.start } }.join @@ -254,7 +268,7 @@ assert_equal 'ok', %{ end END end - require "./zzz.rb" + require "./zzz_t1.rb" $result } @@ -289,7 +303,7 @@ assert_normal_exit %q{ }.each {|t| t.join } -} +} unless rjit_enabled? # flaky assert_equal 'ok', %q{ def m @@ -408,7 +422,7 @@ assert_equal 'ok', %q{ } assert_equal 'ok', %{ - open("zzz.rb", "w") do |f| + open("zzz_t2.rb", "w") do |f| f.puts <<-'end;' # do begin m = Thread::Mutex.new @@ -432,7 +446,7 @@ assert_equal 'ok', %{ end end; end - require "./zzz.rb" + require "./zzz_t2.rb" $result } @@ -483,7 +497,7 @@ assert_equal 'foo', %q{ [th1, th2].each {|t| t.join } GC.start f.call.source -} +} unless rjit_enabled? # flaky assert_normal_exit %q{ class C def inspect diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 0a3aa81860..31bb626690 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -1,3 +1,540 @@ +# 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 "2210150001501015", %q{ + def splat_kw(ary) = yield *ary, a: 1 + + def splat(ary) = yield *ary + + def kw = yield 1, 2, a: 0 + + def simple = yield 0, 1 + + 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 { |a| a }, + simple { 5.itself }, + simple { |a| a }, + simple { |opt = raise| opt }, + simple { |*rest| rest }, + simple { |opt_kw: 5| opt_kw }, + # 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([])] +} unless rjit_enabled? # Not yet working on RJIT + +# 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{ + 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{ + return :ok unless defined?(GC.compact) + def foo = yield + 10.times do |i| + ret = eval("foo { #{i} }") + raise "failed at #{i}" unless ret == i + GC.compact + end + :ok +} unless rjit_enabled? # Not yet working on RJIT + +# 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 +} + +# 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 +} unless rjit_enabled? # Skip on RJIT since this uncovers a crash + +assert_equal 'true', %q{ + # regression test for tracking type of locals for too long + def local_setting_cmp(five) + victim = 5 + five.define_singleton_method(:respond_to?) do |_, _| + victim = nil + end + + # +1 makes YJIT track that victim is a number and + # defined? calls respond_to? from above indirectly + unless (victim + 1) && defined?(five.something) + # Would return wrong result if we still think `five` is a number + victim.nil? + end + end + + local_setting_cmp(Object.new) + local_setting_cmp(Object.new) +} + +assert_equal '18374962167983112447', %q{ + # regression test for incorrectly discarding 32 bits of a pointer when it + # comes to default values. + def large_literal_default(n: 0xff00_fabcafe0_00ff) + n + end + + def call_graph_root + large_literal_default + end + + call_graph_root + call_graph_root +} + +assert_normal_exit %q{ + # regression test for a leak caught by an assert on --yjit-call-threshold=2 + Foo = 1 + + eval("def foo = [#{(['Foo,']*256).join}]") + + foo + foo + + 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") + end + + [nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance| + instance.foo + instance.foo + end +} + +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 + + [nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance| + instance.foo + instance.foo + end +} + +assert_equal '[nil, "instance-variable", nil, "instance-variable"]', %q{ + # defined? on object that changes shape between calls + class Foo + def foo + defined?(@foo) + end + + def add + @foo = 1 + end + + def remove + self.remove_instance_variable(:@foo) + end + end + + obj = Foo.new + [obj.foo, (obj.add; obj.foo), (obj.remove; obj.foo), (obj.add; obj.foo)] +} + +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 + + def foo + [defined?(@foo5), @foo5] + end + end + + Foo.new.foo +} + +# getinstancevariable with shape too complex +assert_normal_exit %q{ + class Foo + def initialize + @a = 1 + end + + def getter + @foobar + end + end + + # 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) + + # The getter method eventually sees shape too complex + r = foo.getter + if r != 777 + raise "error" + end + end +} + +assert_equal '0', %q{ + # This is a regression test for incomplete invalidation from + # opt_setinlinecache. This test might be brittle, so + # feel free to remove it in the future if it's too annoying. + # This test assumes --yjit-call-threshold=2. + module M + Foo = 1 + def foo + Foo + end + + def pin_self_type_then_foo + _ = @foo + foo + end + + def only_ints + 1 + self + foo + end + end + + class Integer + include M + end + + class Sub + include M + end + + foo_method = M.instance_method(:foo) + + dbg = ->(message) do + return # comment this out to get printouts + + $stderr.puts RubyVM::YJIT.disasm(foo_method) + $stderr.puts message + end + + 2.times { 42.only_ints } + + dbg["There should be two versions of getinlineache"] + + module M + remove_const(:Foo) + end + + dbg["There should be no getinlinecaches"] + + 2.times do + 42.only_ints + rescue NameError => err + _ = "caught name error #{err}" + end + + dbg["There should be one version of getinlineache"] + + 2.times do + Sub.new.pin_self_type_then_foo + rescue NameError + _ = 'second specialization' + end + + dbg["There should be two versions of getinlineache"] + + module M + Foo = 1 + end + + dbg["There should still be two versions of getinlineache"] + + 42.only_ints + + dbg["There should be no getinlinecaches"] + + # Find name of the first VM instruction in M#foo. + insns = RubyVM::InstructionSequence.of(foo_method).to_a + if defined?(RubyVM::YJIT.blocks_for) && (insns.last.find { Array === _1 }&.first == :opt_getinlinecache) + RubyVM::YJIT.blocks_for(RubyVM::InstructionSequence.of(foo_method)) + .filter { _1.iseq_start_index == 0 }.count + else + 0 # skip the test + end +} + # Check that frozen objects are respected assert_equal 'great', %q{ class Foo @@ -208,6 +745,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) @@ -663,7 +1239,7 @@ assert_equal "good", %q{ foo begin - GC.verify_compaction_references(double_heap: true, toward: :empty) + GC.verify_compaction_references(expand_heap: true, toward: :empty) rescue NotImplementedError # in case compaction isn't supported end @@ -674,7 +1250,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 @@ -818,6 +1394,18 @@ assert_equal '[42, :default]', %q{ ] } +# Test default value block for Hash with opt_aref_with +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{ @@ -975,6 +1563,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 @@ -1081,6 +1701,19 @@ assert_equal '[1, 2, 42]', %q{ [foo {1}, foo {2}, foo {42}] } +# test calling without block param +assert_equal '[1, false, 2, false]', %q{ + def bar + block_given? && yield + end + + def foo(&block) + bar(&block) + end + + [foo { 1 }, foo, foo { 2 }, foo] +} + # test calling block param failing assert_equal '42', %q{ def foo(&block) @@ -1214,6 +1847,285 @@ assert_equal 'foo123', %q{ make_str("foo", 123) } +# test that invalidation of String#to_s doesn't crash +assert_equal 'meh', %q{ + def inval_method + "".to_s + end + + inval_method + + class String + def to_s + "meh" + end + end + + inval_method +} + +# test that overriding to_s on a String subclass works consistently +assert_equal 'meh', %q{ + class MyString < String + def to_s + "meh" + end + end + + def test_to_s(obj) + obj.to_s + end + + OBJ = MyString.new + + # Should return '' both times + test_to_s("") + test_to_s("") + + # Can return '' if YJIT optimises String#to_s too aggressively + test_to_s(OBJ) + test_to_s(OBJ) +} + +# test string interpolation with overridden to_s +assert_equal 'foo', %q{ + class String + def to_s + "bad" + end + end + + def make_str(foo) + "#{foo}" + end + + make_str("foo") + make_str("foo") +} + +# Test that String unary plus returns the same object ID for an unfrozen string. +assert_equal 'true', <<~RUBY, frozen_string_literal: false + def jittable_method + str = "bar" + + old_obj_id = str.object_id + uplus_str = +str + + 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{ + # Logic needs to be inside an ISEQ, such as a method, for YJIT to compile it + def jittable_method + frozen_str = "foo".freeze + + old_obj_id = frozen_str.object_id + uplus_str = +frozen_str + + uplus_str.object_id == old_obj_id || uplus_str.frozen? + end + + jittable_method +} + +# String-subclass objects should behave as expected inside string-interpolation via concatstrings +assert_equal 'monkeys / monkeys, yo!', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def to_s + super + ", yo!" + end + end + + def jittable_method + m = MyString.new('monkeys') + "#{m} / #{m.to_s}" + end + + jittable_method +} + +# String-subclass objects should behave as expected for string equality +assert_equal 'false', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def ==(b) + "#{self}_" == b + end + end + + def jittable_method + ma = MyString.new("a") + + # Check equality with string-subclass receiver + ma == "a" || ma != "a_" || + # Check equality with string receiver + "a_" == ma || "a" != ma || + # Check equality between string subclasses + ma != MyString.new("a_") || + # Make sure "string always equals itself" check isn't used with overridden equality + ma == ma + end + jittable_method +} + +# Test to_s duplicates a string subclass object but not a string +assert_equal 'false', %q{ + class MyString < String; end + + def jittable_method + a = "a" + ma = MyString.new("a") + + a.object_id != a.to_s.object_id || + ma.object_id == ma.to_s.object_id + end + jittable_method +} + +# Test freeze on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Freezing a string subclass should not duplicate it + fma.object_id == fma.freeze.object_id + end + jittable_method +} + +# Test unary minus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + ma = MyString.new("a") + fma = MyString.new("a").freeze + + # Unary minus on frozen string subclass should not duplicate it + fma.object_id == (-fma).object_id && + # Unary minus on unfrozen string subclass should duplicate it + ma.object_id != (-ma).object_id + end + jittable_method +} + +# Test unary plus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Unary plus on frozen string subclass should not duplicate it + fma.object_id != (+fma).object_id + end + 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")] +} unless rjit_enabled? # Not yet working on RJIT + +# 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 + + def jittable_method + a = -"a" + mb = MyString.new("b") + + buf = String.new + mbuf = MyString.new + + buf << a << mb + mbuf << a << mb + + buf + mbuf + end + jittable_method +} + # test invokebuiltin as used in struct assignment assert_equal '123', %q{ def foo(obj) @@ -1405,6 +2317,19 @@ assert_equal '123', %q{ foo(Foo) } +# Test EP == BP invalidation with moving ISEQs +assert_equal 'ok', %q{ + 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 @@ -1561,6 +2486,50 @@ 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 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) @@ -1601,6 +2570,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) @@ -1641,6 +2620,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] @@ -1695,6 +2698,23 @@ 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 +} unless rjit_enabled? + assert_equal '[1, 2, nil]', %q{ def expandarray_rhs_too_small a, b, c = [1, 2] @@ -1705,6 +2725,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] @@ -1794,7 +2825,7 @@ assert_equal '[[:c_return, :String, :string_alias, "events_to_str"]]', %q{ events.compiled(events) events -} +} unless rjit_enabled? # RJIT calls extra Ruby methods # test enabling a TracePoint that targets a particular line in a C method call assert_equal '[true]', %q{ @@ -1876,7 +2907,7 @@ assert_equal '[[:c_call, :itself]]', %q{ tp.enable { shouldnt_compile } events -} +} unless rjit_enabled? # RJIT calls extra Ruby methods # test enabling c_return tracing before compiling assert_equal '[[:c_return, :itself, main]]', %q{ @@ -1891,6 +2922,26 @@ assert_equal '[[:c_return, :itself, main]]', %q{ tp.enable { shouldnt_compile } events +} unless rjit_enabled? # RJIT calls extra Ruby methods + +# 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 @@ -1917,7 +2968,6 @@ assert_equal '[:itself]', %q{ itself end - tracing_ractor = Ractor.new do # 1: start tracing events = [] @@ -2086,6 +3136,22 @@ assert_equal '[1]', %q{ 5.times.map { kwargs(value: 1) }.uniq } +assert_equal '[:ok]', %q{ + def kwargs(value:) + value + end + + 5.times.map { kwargs() rescue :ok }.uniq +} + +assert_equal '[:ok]', %q{ + def kwargs(a:, b: nil) + value + end + + 5.times.map { kwargs(b: 123) rescue :ok }.uniq +} + assert_equal '[[1, 2]]', %q{ def kwargs(left:, right:) [left, right] @@ -2107,6 +3173,40 @@ assert_equal '[[1, 2]]', %q{ 5.times.map { kwargs(1, kwarg: 2) }.uniq } +# optional and keyword args +assert_equal '[[1, 2, 3]]', %q{ + def opt_and_kwargs(a, b=2, c: nil) + [a,b,c] + end + + 5.times.map { opt_and_kwargs(1, c: 3) }.uniq +} + +assert_equal '[[1, 2, 3]]', %q{ + def opt_and_kwargs(a, b=nil, c: nil) + [a,b,c] + end + + 5.times.map { opt_and_kwargs(1, 2, c: 3) }.uniq +} + +# Bug #18453 +assert_equal '[[1, nil, 2]]', %q{ + def opt_and_kwargs(a = {}, b: nil, c: nil) + [a, b, c] + end + + 5.times.map { opt_and_kwargs(1, c: 2) }.uniq +} + +assert_equal '[[{}, nil, 1]]', %q{ + def opt_and_kwargs(a = {}, b: nil, c: nil) + [a, b, c] + end + + 5.times.map { opt_and_kwargs(c: 1) }.uniq +} + # leading and keyword arguments are swapped into the right order assert_equal '[[1, 2, 3, 4, 5, 6]]', %q{ def kwargs(five, six, a:, b:, c:, d:) @@ -2165,6 +3265,133 @@ assert_equal '[2]', %q{ 5.times.map { default_expression(value: 2) }.uniq } +# constant default values on keywords +assert_equal '[3]', %q{ + def default_expression(value: 3) + value + end + + 5.times.map { default_expression }.uniq +} + +# non-constant default values on keywords +assert_equal '[3]', %q{ + def default_value + 3 + end + + def default_expression(value: default_value) + value + end + + 5.times.map { default_expression }.uniq +} + +# reordered optional kwargs +assert_equal '[[100, 1]]', %q{ + def foo(capacity: 100, max: nil) + [capacity, max] + end + + 5.times.map { foo(max: 1) }.uniq +} + +# invalid lead param +assert_equal 'ok', %q{ + def bar(baz: 2) + baz + end + + def foo + bar(1, baz: 123) + end + + begin + foo + foo + rescue ArgumentError => e + print "ok" + end +} + +# reordered required kwargs +assert_equal '[[1, 2, 3, 4]]', %q{ + def foo(default1: 1, required1:, default2: 3, required2:) + [default1, required1, default2, required2] + end + + 5.times.map { foo(required1: 2, required2: 4) }.uniq +} + +# reordered default expression kwargs +assert_equal '[[:one, :two, 3]]', %q{ + def foo(arg1: (1+0), arg2: (2+0), arg3: (3+0)) + [arg1, arg2, arg3] + end + + 5.times.map { foo(arg2: :two, arg1: :one) }.uniq +} + +# complex kwargs +assert_equal '[[1, 2, 3, 4]]', %q{ + def foo(required:, specified: 999, simple_default: 3, complex_default: "4".to_i) + [required, specified, simple_default, complex_default] + end + + 5.times.map { foo(specified: 2, required: 1) }.uniq +} + +# cfunc kwargs +assert_equal '{:foo=>123}', %q{ + def foo(bar) + bar.store(:value, foo: 123) + bar[:value] + end + + foo({}) + foo({}) +} + +# cfunc kwargs +assert_equal '{:foo=>123}', %q{ + def foo(bar) + bar.replace(foo: 123) + end + + foo({}) + foo({}) +} + +# cfunc kwargs +assert_equal '{:foo=>123, :bar=>456}', %q{ + def foo(bar) + bar.replace(foo: 123, bar: 456) + end + + foo({}) + foo({}) +} + +# variadic cfunc kwargs +assert_equal '{:foo=>123}', %q{ + def foo(bar) + bar.merge(foo: 123) + end + + foo({}) + foo({}) +} + +# optimized cfunc kwargs +assert_equal 'false', %q{ + def foo + :foo.eql?(foo: :foo) + end + + foo + foo +} + # attr_reader on frozen object assert_equal 'false', %q{ class Foo @@ -2288,3 +3515,1347 @@ assert_equal '{:foo=>2}', %q{ foo foo } + +# block invalidation edge case +assert_equal 'undef', %q{ + class A + def foo(arg) + arg.times { A.remove_method(:bar) } + self + end + + def bar + 4 + end + + def use(arg) + # two consecutive sends. When bar is removed, the return address + # for calling it is already on foo's control frame + foo(arg).bar + rescue NoMethodError + :undef + end + end + + A.new.use 0 + A.new.use 0 + A.new.use 1 +} + +# block invalidation edge case +assert_equal 'ok', %q{ + class A + Good = :ng + def foo(arg) + arg.times { A.const_set(:Good, :ok) } + self + end + + def id(arg) + arg + end + + def use(arg) + # send followed by an opt_getinlinecache. + # The return address remains on the control frame + # when opt_getinlinecache is invalidated. + foo(arg).id(Good) + end + end + + A.new.use 0 + A.new.use 0 + A.new.use 1 +} + +assert_equal 'ok', %q{ + # test hitting a branch stub when out of memory + def nimai(jita) + if jita + :ng + else + :ok + end + end + + nimai(true) + nimai(true) + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + nimai(false) +} + +assert_equal 'new', %q{ + # test block invalidation while out of memory + def foo + :old + end + + def test + foo + end + + def bar + :bar + end + + + test + test + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + # Old simulat_omm! leaves one byte of space and this fills it up + bar + bar + + def foo + :new + end + + test +} + +assert_equal 'ok', %q{ + # Try to compile new method while OOM + def foo + :ok + end + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + foo + foo +} + +# struct aref embedded +assert_equal '2', %q{ + def foo(s) + s.foo + end + + S = Struct.new(:foo) + foo(S.new(1)) + foo(S.new(2)) +} + +# struct aref non-embedded +assert_equal '4', %q{ + def foo(s) + s.d + end + + S = Struct.new(:a, :b, :c, :d, :e) + foo(S.new(1,2,3,4,5)) + foo(S.new(1,2,3,4,5)) +} + +# struct aset embedded +assert_equal '123', %q{ + def foo(s) + s.foo = 123 + end + + s = Struct.new(:foo).new + foo(s) + s = Struct.new(:foo).new + foo(s) + s.foo +} + +# struct aset non-embedded +assert_equal '[1, 2, 3, 4, 5]', %q{ + def foo(s) + s.a = 1 + s.b = 2 + s.c = 3 + s.d = 4 + s.e = 5 + end + + S = Struct.new(:a, :b, :c, :d, :e) + s = S.new + foo(s) + s = S.new + foo(s) + [s.a, s.b, s.c, s.d, s.e] +} + +# struct aref too many args +assert_equal 'ok', %q{ + def foo(s) + s.foo(:bad) + end + + s = Struct.new(:foo).new + foo(s) rescue :ok + foo(s) rescue :ok +} + +# struct aset too many args +assert_equal 'ok', %q{ + def foo(s) + s.set_foo(123, :bad) + end + + s = Struct.new(:foo) do + alias :set_foo :foo= + end + foo(s) rescue :ok + foo(s) rescue :ok +} + +# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2) +assert_equal 'foo/bar', %q{ + def foo + File.join("foo", "bar") + end + + foo + foo +} + +# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2) +assert_equal '', %q{ + def foo + File.join() + end + + foo + foo +} + +# Make sure we're correctly reading RStruct's as.ary union for embedded RStructs +assert_equal '3,12', %q{ + pt_struct = Struct.new(:x, :y) + p = pt_struct.new(3, 12) + def pt_inspect(pt) + "#{pt.x},#{pt.y}" + end + + # Make sure pt_inspect is JITted + 10.times { pt_inspect(p) } + + # Make sure it's returning '3,12' instead of e.g. '3,false' + 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() + [1, 2] in [Integer, String] + end + function() +} + +# opt_send_without_block (VM_METHOD_TYPE_ATTRSET) +assert_equal 'foo', %q{ + class Foo + attr_writer :foo + + def foo() + self.foo = "foo" + end + end + foo = Foo.new + foo.foo +} + +# anytostring, intern +assert_equal 'true', %q{ + def foo() + :"#{true}" + end + foo() +} + +# toregexp, objtostring +assert_equal '/true/', %q{ + def foo() + /#{true}/ + end + foo().inspect +} + +# concatstrings, objtostring +assert_equal '9001', %q{ + def foo() + "#{9001}" + end + foo() +} + +# opt_send_without_block (VM_METHOD_TYPE_CFUNC) +assert_equal 'nil', %q{ + def foo + nil.inspect # argc: 0 + end + foo +} +assert_equal '4', %q{ + def foo + 2.pow(2) # argc: 1 + end + foo +} +assert_equal 'aba', %q{ + def foo + "abc".tr("c", "a") # argc: 2 + end + foo +} +assert_equal 'true', %q{ + def foo + respond_to?(:inspect) # argc: -1 + end + foo +} +assert_equal '["a", "b"]', %q{ + def foo + "a\nb".lines(chomp: true) # kwargs + end + foo +} + +# invokebuiltin +assert_equal '123', %q{ + def foo(obj) + obj.foo = 123 + end + + struct = Struct.new(:foo) + obj = struct.new + foo(obj) +} + +# invokebuiltin_delegate +assert_equal '.', %q{ + def foo(path) + Dir.open(path).path + end + foo(".") +} + +# opt_invokebuiltin_delegate_leave +assert_equal '[0]', %q{"\x00".unpack("c")} + +# opt_send_without_block (VM_METHOD_TYPE_ISEQ) +assert_equal '1', %q{ + def foo = 1 + def bar = foo + bar +} +assert_equal '[1, 2, 3]', %q{ + def foo(a, b) = [1, a, b] + def bar = foo(2, 3) + bar +} +assert_equal '[1, 2, 3, 4, 5, 6]', %q{ + def foo(a, b, c:, d:, e: 0, f: 6) = [a, b, c, d, e, f] + def bar = foo(1, 2, c: 3, d: 4, e: 5) + bar +} +assert_equal '[1, 2, 3, 4]', %q{ + def foo(a, b = 2) = [a, b] + def bar = foo(1) + foo(3, 4) + bar +} + +assert_equal '1', %q{ + def foo(a) = a + def bar = foo(1) { 2 } + bar +} +assert_equal '[1, 2]', %q{ + def foo(a, &block) = [a, block.call] + def bar = foo(1) { 2 } + bar +} + +# opt_send_without_block (VM_METHOD_TYPE_IVAR) +assert_equal 'foo', %q{ + class Foo + attr_reader :foo + + def initialize + @foo = "foo" + end + end + Foo.new.foo +} + +# opt_send_without_block (VM_METHOD_TYPE_OPTIMIZED) +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("bar").bar = "foo" +} +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("foo").bar +} + +# getblockparamproxy +assert_equal 'foo', %q{ + def foo(&block) + block.call + end + foo { "foo" } +} + +# getblockparam +assert_equal 'foo', %q{ + def foo(&block) + block + end + foo { "foo" }.call +} + +assert_equal '[1, 2]', %q{ + def foo + x = [2] + [1, *x] + end + + 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 { } +} + +# 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 +} unless rjit_enabled? # Not yet working on RJIT + +# 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 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_equal '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, :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 +} + +# 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 +} unless rjit_enabled? # doesn't work on RJIT + +# 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 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)] +} diff --git a/bootstraptest/test_yjit_rust_port.rb b/bootstraptest/test_yjit_rust_port.rb new file mode 100644 index 0000000000..e399e0e49e --- /dev/null +++ b/bootstraptest/test_yjit_rust_port.rb @@ -0,0 +1,422 @@ +# Simple tests that we know we can pass +# To keep track of what we got working during the Rust port +# And avoid breaking/losing functionality +# +# Say "Thread" here to dodge WASM CI check. We use ractors here +# which WASM doesn't support and it only greps for "Thread". + +# Test for opt_mod +assert_equal '2', %q{ + def mod(a, b) + a % b + end + + mod(7, 5) + mod(7, 5) +} + +# Test for opt_mult +assert_equal '12', %q{ + def mult(a, b) + a * b + end + + mult(6, 2) + mult(6, 2) +} + +# Test for opt_div +assert_equal '3', %q{ + def div(a, b) + a / b + end + + div(6, 2) + div(6, 2) +} + +assert_equal '5', %q{ + def plus(a, b) + a + b + end + + plus(3, 2) +} + +assert_equal '1', %q{ + def foo(a, b) + a - b + end + + foo(3, 2) +} + +assert_equal 'true', %q{ + def foo(a, b) + a < b + end + + foo(2, 3) +} + +# Bitwise left shift +assert_equal '4', %q{ + def foo(a, b) + 1 << 2 + end + + foo(1, 2) +} + +assert_equal '-7', %q{ + def foo(a, b) + -7 + end + + foo(1, 2) +} + +# Putstring +assert_equal 'foo', %q{ + def foo(a, b) + "foo" + end + + foo(1, 2) +} + +assert_equal '-6', %q{ + def foo(a, b) + a + -7 + end + + foo(1, 2) +} + +assert_equal 'true', %q{ + def foo(a, b) + a == b + end + + foo(3, 3) +} + +assert_equal 'true', %q{ + def foo(a, b) + a < b + end + + foo(3, 5) +} + +assert_equal '777', %q{ + def foo(a) + if a + 777 + else + 333 + end + end + + foo(true) +} + +assert_equal '5', %q{ + def foo(a, b) + while a < b + a += 1 + end + a + end + + foo(1, 5) +} + +# opt_aref +assert_equal '2', %q{ + def foo(a, b) + a[b] + end + + foo([0, 1, 2], 2) +} + +# Simple function calls with 0, 1, 2 arguments +assert_equal '-2', %q{ + def bar() + -2 + end + + def foo(a, b) + bar() + end + + foo(3, 2) +} +assert_equal '2', %q{ + def bar(a) + a + end + + def foo(a, b) + bar(b) + end + + foo(3, 2) +} +assert_equal '1', %q{ + def bar(a, b) + a - b + end + + def foo(a, b) + bar(a, b) + end + + foo(3, 2) +} + +# Regression test for assembler bug +assert_equal '1', %q{ + def check_index(index) + if 0x40000000 < index + return -1 + end + 1 + end + + check_index 2 +} + +# Setivar test +assert_equal '2', %q{ + class Klass + attr_accessor :a + + def set() + @a = 2 + end + + def get() + @a + end + end + + o = Klass.new + o.set() + o.a +} + +# Regression for putobject bug +assert_equal '1.5', %q{ + def foo(x) + x + end + + def bar + foo(1.5) + end + + bar() +} + +# Getivar with an extended ivar table +assert_equal '3', %q{ + class Foo + def initialize + @x1 = 1 + @x2 = 1 + @x3 = 1 + @x4 = 3 + end + + def bar + @x4 + end + end + + f = Foo.new + f.bar +} + +assert_equal 'true', %q{ + x = [[false, true]] + for i, j in x + ; + end + j +} + +# Regression for getivar +assert_equal '[nil]', %q{ + [TrueClass].each do |klass| + klass.class_eval("def foo = @foo") + end + + [true].map do |instance| + instance.foo + end +} + +# Regression for send +assert_equal 'ok', %q{ + def bar(baz: 2) + baz + end + + def foo + bar(1, baz: 123) + end + + begin + foo + foo + rescue ArgumentError => e + print "ok" + end +} + +# Array access regression test +assert_equal '[0, 1, 2, 3, 4, 5]', %q{ + def expandarray_useless_splat + arr = [0, 1, 2, 3, 4, 5] + a, * = arr + end + + expandarray_useless_splat +} + +# Make sure we're correctly reading RStruct's as.ary union for embedded RStructs +assert_equal '3,12', %q{ + pt_struct = Struct.new(:x, :y) + p = pt_struct.new(3, 12) + def pt_inspect(pt) + "#{pt.x},#{pt.y}" + end + + # Make sure pt_inspect is JITted + 10.times { pt_inspect(p) } + + # Make sure it's returning '3,12' instead of e.g. '3,false' + pt_inspect(p) +} + +assert_equal '2', %q{ + def foo(s) + s.foo + end + + S = Struct.new(:foo) + foo(S.new(1)) + foo(S.new(2)) +} + +# Try to compile new method while OOM +assert_equal 'ok', %q{ + def foo + :ok + end + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + foo +} + +# test hitting a branch stub when out of memory +assert_equal 'ok', %q{ + def nimai(jita) + if jita + :ng + else + :ok + end + end + + nimai(true) + nimai(true) + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + nimai(false) +} + +# Ractor.current returns a current ractor +assert_equal 'Ractor', %q{ + Ractor.current.class +} + +# Ractor.new returns new Ractor +assert_equal 'Ractor', %q{ + Ractor.new{}.class +} + +# Ractor.allocate is not supported +assert_equal "[:ok, :ok]", %q{ + rs = [] + begin + Ractor.allocate + rescue => e + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + begin + Ractor.new{}.dup + rescue + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + rs +} + +# A return value of a Ractor block will be a message from the Ractor. +assert_equal 'ok', %q{ + # join + r = Ractor.new do + 'ok' + end + r.take +} + +# Passed arguments to Ractor.new will be a block parameter +# The values are passed with Ractor-communication pass. +assert_equal 'ok', %q{ + # ping-pong with arg + r = Ractor.new 'ok' do |msg| + msg + end + r.take +} + +# Pass multiple arguments to Ractor.new +assert_equal 'ok', %q{ + # ping-pong with two args + r = Ractor.new 'ping', 'pong' do |msg, msg2| + [msg, msg2] + end + 'ok' if r.take == ['ping', 'pong'] +} + +# Ractor#send passes an object with copy to a Ractor +# and Ractor.receive in the Ractor block can receive the passed value. +assert_equal 'ok', %q{ + r = Ractor.new do + msg = Ractor.receive + end + r.send 'ok' + r.take +} + +assert_equal '[1, 2, 3]', %q{ + def foo(arr) + arr << 1 + arr << 2 + arr << 3 + arr + end + + def bar() + foo([]) + end + + bar() +} |