/test/ruby/test_optimization.rb
Ruby | 848 lines | 736 code | 109 blank | 3 comment | 36 complexity | 6fe82716a9dd400b0c1cfe2109db3d75 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, AGPL-3.0
- # frozen_string_literal: false
- require 'test/unit'
- require 'objspace'
- class TestRubyOptimization < Test::Unit::TestCase
- def assert_redefine_method(klass, method, code, msg = nil)
- assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- class #{klass}
- undef #{method}
- def #{method}(*args)
- args[0]
- end
- end
- #{code}
- end;
- end
- def disasm(name)
- RubyVM::InstructionSequence.of(method(name)).disasm
- end
- def test_fixnum_plus
- assert_equal 21, 10 + 11
- assert_redefine_method('Integer', '+', 'assert_equal 11, 10 + 11')
- end
- def test_fixnum_minus
- assert_equal 5, 8 - 3
- assert_redefine_method('Integer', '-', 'assert_equal 3, 8 - 3')
- end
- def test_fixnum_mul
- assert_equal 15, 3 * 5
- assert_redefine_method('Integer', '*', 'assert_equal 5, 3 * 5')
- end
- def test_fixnum_div
- assert_equal 3, 15 / 5
- assert_redefine_method('Integer', '/', 'assert_equal 5, 15 / 5')
- end
- def test_fixnum_mod
- assert_equal 1, 8 % 7
- assert_redefine_method('Integer', '%', 'assert_equal 7, 8 % 7')
- end
- def test_fixnum_lt
- assert_equal true, 1 < 2
- assert_redefine_method('Integer', '<', 'assert_equal 2, 1 < 2')
- end
- def test_fixnum_le
- assert_equal true, 1 <= 2
- assert_redefine_method('Integer', '<=', 'assert_equal 2, 1 <= 2')
- end
- def test_fixnum_gt
- assert_equal false, 1 > 2
- assert_redefine_method('Integer', '>', 'assert_equal 2, 1 > 2')
- end
- def test_fixnum_ge
- assert_equal false, 1 >= 2
- assert_redefine_method('Integer', '>=', 'assert_equal 2, 1 >= 2')
- end
- def test_float_plus
- assert_equal 4.0, 2.0 + 2.0
- assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0')
- end
- def test_float_minus
- assert_equal 4.0, 2.0 + 2.0
- assert_redefine_method('Float', '+', 'assert_equal 2.0, 2.0 + 2.0')
- end
- def test_float_mul
- assert_equal 29.25, 4.5 * 6.5
- assert_redefine_method('Float', '*', 'assert_equal 6.5, 4.5 * 6.5')
- end
- def test_float_div
- assert_in_delta 0.63063063063063063, 4.2 / 6.66
- assert_redefine_method('Float', '/', 'assert_equal 6.66, 4.2 / 6.66', "[Bug #9238]")
- end
- def test_float_lt
- assert_equal true, 1.1 < 2.2
- assert_redefine_method('Float', '<', 'assert_equal 2.2, 1.1 < 2.2')
- end
- def test_float_le
- assert_equal true, 1.1 <= 2.2
- assert_redefine_method('Float', '<=', 'assert_equal 2.2, 1.1 <= 2.2')
- end
- def test_float_gt
- assert_equal false, 1.1 > 2.2
- assert_redefine_method('Float', '>', 'assert_equal 2.2, 1.1 > 2.2')
- end
- def test_float_ge
- assert_equal false, 1.1 >= 2.2
- assert_redefine_method('Float', '>=', 'assert_equal 2.2, 1.1 >= 2.2')
- end
- def test_string_length
- assert_equal 6, "string".length
- assert_redefine_method('String', 'length', 'assert_nil "string".length')
- end
- def test_string_size
- assert_equal 6, "string".size
- assert_redefine_method('String', 'size', 'assert_nil "string".size')
- end
- def test_string_empty?
- assert_equal true, "".empty?
- assert_equal false, "string".empty?
- assert_redefine_method('String', 'empty?', 'assert_nil "string".empty?')
- end
- def test_string_plus
- assert_equal "", "" + ""
- assert_equal "x", "x" + ""
- assert_equal "x", "" + "x"
- assert_equal "ab", "a" + "b"
- assert_redefine_method('String', '+', 'assert_equal "b", "a" + "b"')
- end
- def test_string_succ
- assert_equal 'b', 'a'.succ
- assert_equal 'B', 'A'.succ
- end
- def test_string_format
- assert_equal '2', '%d' % 2
- assert_redefine_method('String', '%', 'assert_equal 2, "%d" % 2')
- end
- def test_string_freeze
- assert_equal "foo", "foo".freeze
- assert_equal "foo".freeze.object_id, "foo".freeze.object_id
- assert_redefine_method('String', 'freeze', 'assert_nil "foo".freeze')
- end
- def test_string_uminus
- assert_same "foo".freeze, -"foo"
- assert_redefine_method('String', '-@', 'assert_nil(-"foo")')
- end
- def test_string_freeze_saves_memory
- n = 16384
- data = '.'.freeze
- r, w = IO.pipe
- w.write data
- s = r.readpartial(n, '')
- assert_operator ObjectSpace.memsize_of(s), :>=, n,
- 'IO buffer NOT resized prematurely because will likely be reused'
- s.freeze
- assert_equal ObjectSpace.memsize_of(data), ObjectSpace.memsize_of(s),
- 'buffer resized on freeze since it cannot be written to again'
- ensure
- r.close if r
- w.close if w
- end
- def test_string_eq_neq
- %w(== !=).each do |m|
- assert_redefine_method('String', m, <<-end)
- assert_equal :b, ("a" #{m} "b").to_sym
- b = 'b'
- assert_equal :b, ("a" #{m} b).to_sym
- assert_equal :b, (b #{m} "b").to_sym
- end
- end
- end
- def test_string_ltlt
- assert_equal "", "" << ""
- assert_equal "x", "x" << ""
- assert_equal "x", "" << "x"
- assert_equal "ab", "a" << "b"
- assert_redefine_method('String', '<<', 'assert_equal "b", "a" << "b"')
- end
- def test_fixnum_and
- assert_equal 1, 1&3
- assert_redefine_method('Integer', '&', 'assert_equal 3, 1&3')
- end
- def test_fixnum_or
- assert_equal 3, 1|3
- assert_redefine_method('Integer', '|', 'assert_equal 1, 3|1')
- end
- def test_array_plus
- assert_equal [1,2], [1]+[2]
- assert_redefine_method('Array', '+', 'assert_equal [2], [1]+[2]')
- end
- def test_array_minus
- assert_equal [2], [1,2] - [1]
- assert_redefine_method('Array', '-', 'assert_equal [1], [1,2]-[1]')
- end
- def test_array_length
- assert_equal 0, [].length
- assert_equal 3, [1,2,3].length
- assert_redefine_method('Array', 'length', 'assert_nil([].length); assert_nil([1,2,3].length)')
- end
- def test_array_empty?
- assert_equal true, [].empty?
- assert_equal false, [1,2,3].empty?
- assert_redefine_method('Array', 'empty?', 'assert_nil([].empty?); assert_nil([1,2,3].empty?)')
- end
- def test_hash_length
- assert_equal 0, {}.length
- assert_equal 1, {1=>1}.length
- assert_redefine_method('Hash', 'length', 'assert_nil({}.length); assert_nil({1=>1}.length)')
- end
- def test_hash_empty?
- assert_equal true, {}.empty?
- assert_equal false, {1=>1}.empty?
- assert_redefine_method('Hash', 'empty?', 'assert_nil({}.empty?); assert_nil({1=>1}.empty?)')
- end
- def test_hash_aref_with
- h = { "foo" => 1 }
- assert_equal 1, h["foo"]
- assert_redefine_method('Hash', '[]', "#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- h = { "foo" => 1 }
- assert_equal "foo", h["foo"]
- end;
- end
- def test_hash_aset_with
- h = {}
- assert_equal 1, h["foo"] = 1
- assert_redefine_method('Hash', '[]=', "#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- h = {}
- assert_equal 1, h["foo"] = 1, "assignment always returns value set"
- assert_nil h["foo"]
- end;
- end
- class MyObj
- def ==(other)
- true
- end
- end
- def test_eq
- assert_equal true, nil == nil
- assert_equal true, 1 == 1
- assert_equal true, 'string' == 'string'
- assert_equal true, 1 == MyObj.new
- assert_equal false, nil == MyObj.new
- assert_equal true, MyObj.new == 1
- assert_equal true, MyObj.new == nil
- end
- def self.tailcall(klass, src, file = nil, path = nil, line = nil, tailcall: true)
- unless file
- loc, = caller_locations(1, 1)
- file = loc.path
- line ||= loc.lineno + 1
- end
- RubyVM::InstructionSequence.new("proc {|_|_.class_eval {#{src}}}",
- file, (path || file), line,
- tailcall_optimization: tailcall,
- trace_instruction: false)
- .eval[klass]
- end
- def tailcall(*args)
- self.class.tailcall(singleton_class, *args)
- end
- def test_tailcall
- bug4082 = '[ruby-core:33289]'
- tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- def fact_helper(n, res)
- if n == 1
- res
- else
- fact_helper(n - 1, n * res)
- end
- end
- def fact(n)
- fact_helper(n, 1)
- end
- end;
- assert_equal(9131, fact(3000).to_s.size, message(bug4082) {disasm(:fact_helper)})
- end
- def test_tailcall_with_block
- bug6901 = '[ruby-dev:46065]'
- tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- def identity(val)
- val
- end
- def delay
- -> {
- identity(yield)
- }
- end
- end;
- assert_equal(123, delay { 123 }.call, message(bug6901) {disasm(:delay)})
- end
- def just_yield
- yield
- end
- def test_tailcall_inhibited_by_block
- tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- def yield_result
- just_yield {:ok}
- end
- end;
- assert_equal(:ok, yield_result, message {disasm(:yield_result)})
- end
- def do_raise
- raise "should be rescued"
- end
- def errinfo
- $!
- end
- def test_tailcall_inhibited_by_rescue
- bug12082 = '[ruby-core:73871] [Bug #12082]'
- EnvUtil.suppress_warning {tailcall("#{<<-"begin;"}\n#{<<~"end;"}")}
- begin;
- def to_be_rescued
- return do_raise
- 1 + 2
- rescue
- errinfo
- end
- end;
- result = assert_nothing_raised(RuntimeError, message(bug12082) {disasm(:to_be_rescued)}) {
- to_be_rescued
- }
- assert_instance_of(RuntimeError, result, bug12082)
- assert_equal("should be rescued", result.message, bug12082)
- end
- def test_tailcall_symbol_block_arg
- bug12565 = '[ruby-core:46065]'
- tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- def apply_one_and_two(&block)
- yield(1, 2)
- end
- def add_one_and_two
- apply_one_and_two(&:+)
- end
- end;
- assert_equal(3, add_one_and_two,
- message(bug12565) {disasm(:add_one_and_two)})
- end
- def test_tailcall_interrupted_by_sigint
- bug12576 = 'ruby-core:76327'
- script = "#{<<-"begin;"}\n#{<<~'end;'}"
- begin;
- RubyVM::InstructionSequence.compile_option = {
- :tailcall_optimization => true,
- :trace_instruction => false
- }
- eval "#{<<~"begin;"}\n#{<<~'end;1'}"
- begin;
- def foo
- foo
- end
- puts("start")
- STDOUT.flush
- foo
- end;1
- end;
- status, _err = EnvUtil.invoke_ruby([], "", true, true, **{}) {
- |in_p, out_p, err_p, pid|
- in_p.write(script)
- in_p.close
- out_p.gets
- sig = :INT
- begin
- Process.kill(sig, pid)
- Timeout.timeout(1) do
- *, stat = Process.wait2(pid)
- [stat, err_p.read]
- end
- rescue Timeout::Error
- if sig == :INT
- sig = :KILL
- retry
- else
- raise
- end
- end
- }
- assert_not_equal("SEGV", Signal.signame(status.termsig || 0), bug12576)
- end unless /mswin|mingw/ =~ RUBY_PLATFORM
- def test_tailcall_condition_block
- bug = '[ruby-core:78015] [Bug #12905]'
- src = "#{<<-"begin;"}\n#{<<~"end;"}", __FILE__, nil, __LINE__+1
- begin;
- def run(current, final)
- if current < final
- run(current+1, final)
- else
- nil
- end
- end
- end;
- obj = Object.new
- self.class.tailcall(obj.singleton_class, *src, tailcall: false)
- e = assert_raise(SystemStackError) {
- obj.run(1, Float::INFINITY)
- }
- level = e.backtrace_locations.size
- obj = Object.new
- self.class.tailcall(obj.singleton_class, *src, tailcall: true)
- level *= 2
- mesg = message {"#{bug}: #{$!.backtrace_locations.size} / #{level} stack levels"}
- assert_nothing_raised(SystemStackError, mesg) {
- obj.run(1, level)
- }
- end
- def test_tailcall_not_to_grow_stack
- skip 'currently JIT-ed code always creates a new stack frame' if RubyVM::MJIT.enabled?
- bug16161 = '[ruby-core:94881]'
- tailcall("#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- def foo(n)
- return :ok if n < 1
- foo(n - 1)
- end
- end;
- assert_nothing_raised(SystemStackError, bug16161) do
- assert_equal(:ok, foo(1_000_000), bug16161)
- end
- end
- class Bug10557
- def [](_)
- block_given?
- end
- def []=(_, _)
- block_given?
- end
- end
- def test_block_given_aset_aref
- bug10557 = '[ruby-core:66595]'
- assert_equal(true, Bug10557.new.[](nil){}, bug10557)
- assert_equal(true, Bug10557.new.[](0){}, bug10557)
- assert_equal(true, Bug10557.new.[](false){}, bug10557)
- assert_equal(true, Bug10557.new.[](''){}, bug10557)
- assert_equal(true, Bug10557.new.[]=(nil, 1){}, bug10557)
- assert_equal(true, Bug10557.new.[]=(0, 1){}, bug10557)
- assert_equal(true, Bug10557.new.[]=(false, 1){}, bug10557)
- assert_equal(true, Bug10557.new.[]=('', 1){}, bug10557)
- end
- def test_string_freeze_block
- assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}")
- begin;
- class String
- undef freeze
- def freeze
- block_given?
- end
- end
- assert_equal(true, "block".freeze {})
- assert_equal(false, "block".freeze)
- end;
- end
- def test_opt_case_dispatch
- code = "#{<<-"begin;"}\n#{<<~"end;"}"
- begin;
- case foo
- when "foo" then :foo
- when true then true
- when false then false
- when :sym then :sym
- when 6 then :fix
- when nil then nil
- when 0.1 then :float
- when 0xffffffffffffffff then :big
- else
- :nomatch
- end
- end;
- check = {
- 'foo' => :foo,
- true => true,
- false => false,
- :sym => :sym,
- 6 => :fix,
- nil => nil,
- 0.1 => :float,
- 0xffffffffffffffff => :big,
- }
- iseq = RubyVM::InstructionSequence.compile(code)
- assert_match %r{\bopt_case_dispatch\b}, iseq.disasm
- check.each do |foo, expect|
- assert_equal expect, eval("foo = #{foo.inspect}\n#{code}")
- end
- assert_equal :nomatch, eval("foo = :blah\n#{code}")
- check.each do |foo, _|
- klass = foo.class.to_s
- assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}")
- begin;
- class #{klass}
- undef ===
- def ===(*args)
- false
- end
- end
- foo = #{foo.inspect}
- ret = #{code}
- assert_equal :nomatch, ret, foo.inspect
- end;
- end
- end
- def test_eqq
- [ nil, true, false, 0.1, :sym, 'str', 0xffffffffffffffff ].each do |v|
- k = v.class.to_s
- assert_redefine_method(k, '===', "assert_equal(#{v.inspect} === 0, 0)")
- end
- end
- def test_opt_case_dispatch_inf
- inf = 1.0/0.0
- result = case inf
- when 1 then 1
- when 0 then 0
- else
- inf.to_i rescue nil
- end
- assert_nil result, '[ruby-dev:49423] [Bug #11804]'
- end
- def test_nil_safe_conditional_assign
- bug11816 = '[ruby-core:74993] [Bug #11816]'
- assert_ruby_status([], 'nil&.foo &&= false', bug11816)
- end
- def test_peephole_string_literal_range
- code = "#{<<~"begin;"}\n#{<<~"end;"}"
- begin;
- case ver
- when "2.0.0".."2.3.2" then :foo
- when "1.8.0"..."1.8.8" then :bar
- end
- end;
- [ true, false ].each do |opt|
- iseq = RubyVM::InstructionSequence.compile(code,
- frozen_string_literal: opt)
- insn = iseq.disasm
- assert_match %r{putobject\s+#{Regexp.quote('"1.8.0"..."1.8.8"')}}, insn
- assert_match %r{putobject\s+#{Regexp.quote('"2.0.0".."2.3.2"')}}, insn
- assert_no_match(/putstring/, insn)
- assert_no_match(/newrange/, insn)
- end
- end
- def test_peephole_dstr
- code = "#{<<~'begin;'}\n#{<<~'end;'}"
- begin;
- exp = -'a'
- z = 'a'
- [exp, -"#{z}"]
- end;
- [ false, true ].each do |fsl|
- iseq = RubyVM::InstructionSequence.compile(code,
- frozen_string_literal: fsl)
- assert_same(*iseq.eval,
- "[ruby-core:85542] [Bug #14475] fsl: #{fsl}")
- end
- end
- def test_branch_condition_backquote
- bug = '[ruby-core:80740] [Bug #13444] redefined backquote should be called'
- class << self
- def `(s)
- @q = s
- @r
- end
- end
- @q = nil
- @r = nil
- assert_equal("bar", ("bar" unless `foo`), bug)
- assert_equal("foo", @q, bug)
- @q = nil
- @r = true
- assert_equal("bar", ("bar" if `foo`), bug)
- assert_equal("foo", @q, bug)
- @q = nil
- @r = "z"
- assert_equal("bar", ("bar" if `foo#{@r}`))
- assert_equal("fooz", @q, bug)
- end
- def test_branch_condition_def
- bug = '[ruby-core:80740] [Bug #13444] method should be defined'
- c = Class.new do
- raise "bug" unless def t;:ok;end
- end
- assert_nothing_raised(NoMethodError, bug) do
- assert_equal(:ok, c.new.t)
- end
- end
- def test_branch_condition_defs
- bug = '[ruby-core:80740] [Bug #13444] singleton method should be defined'
- raise "bug" unless def self.t;:ok;end
- assert_nothing_raised(NameError, bug) do
- assert_equal(:ok, t)
- end
- end
- def test_retry_label_in_unreachable_chunk
- bug = '[ruby-core:81272] [Bug #13578]'
- assert_valid_syntax("#{<<-"begin;"}\n#{<<-"end;"}", bug)
- begin;
- def t; if false; case 42; when s {}; end; end; end
- end;
- end
- def bptest_yield &b
- yield
- end
- def bptest_yield_pass &b
- bptest_yield(&b)
- end
- def bptest_bp_value &b
- b
- end
- def bptest_bp_pass_bp_value &b
- bptest_bp_value(&b)
- end
- def bptest_binding &b
- binding
- end
- def bptest_set &b
- b = Proc.new{2}
- end
- def test_block_parameter
- assert_equal(1, bptest_yield{1})
- assert_equal(1, bptest_yield_pass{1})
- assert_equal(1, send(:bptest_yield){1})
- assert_equal(Proc, bptest_bp_value{}.class)
- assert_equal nil, bptest_bp_value
- assert_equal(Proc, bptest_bp_pass_bp_value{}.class)
- assert_equal nil, bptest_bp_pass_bp_value
- assert_equal Proc, bptest_binding{}.local_variable_get(:b).class
- assert_equal 2, bptest_set{1}.call
- end
- def test_block_parameter_should_not_create_objects
- assert_separately [], <<-END
- #
- def foo &b
- end
- h1 = {}; h2 = {}
- ObjectSpace.count_objects(h1) # rehearsal
- ObjectSpace.count_objects(h1)
- foo{}
- ObjectSpace.count_objects(h2)
- assert_equal 0, h2[:TOTAL] - h1[:TOTAL]
- END
- end
- def test_peephole_optimization_without_trace
- assert_separately [], <<-END
- RubyVM::InstructionSequence.compile_option = {trace_instruction: false}
- eval "def foo; 1.times{|(a), &b| nil && a}; end"
- END
- end
- def test_clear_unreachable_keyword_args
- assert_separately [], <<-END, timeout: 60
- script = <<-EOS
- if true
- else
- foo(k1:1)
- end
- EOS
- GC.stress = true
- 30.times{
- RubyVM::InstructionSequence.compile(script)
- }
- END
- end
- def test_callinfo_unreachable_path
- assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
- begin;
- iseq = RubyVM::InstructionSequence.compile("if false; foo(bar: :baz); else :ok end")
- bin = iseq.to_binary
- iseq = RubyVM::InstructionSequence.load_from_binary(bin)
- assert_instance_of(RubyVM::InstructionSequence, iseq)
- assert_equal(:ok, iseq.eval)
- end;
- end
- def test_side_effect_in_popped_splat
- bug = '[ruby-core:84340] [Bug #14201]'
- eval("{**(bug = nil; {})};42")
- assert_nil(bug)
- bug = '[ruby-core:85486] [Bug #14459]'
- h = {}
- assert_equal(bug, eval('{ok: 42, **h}; bug'))
- assert_equal(:ok, eval('{ok: bug = :ok, **h}; bug'))
- assert_empty(h)
- end
- def test_overwritten_blockparam
- obj = Object.new
- def obj.a(&block)
- block = 1
- return :ok if block
- :ng
- end
- assert_equal(:ok, obj.a())
- end
- def test_blockparam_in_rescue
- obj = Object.new
- def obj.foo(&b)
- raise
- rescue
- b.call
- end
- result = nil
- assert_equal(42, obj.foo {result = 42})
- assert_equal(42, result)
- end
- def test_unconditional_branch_to_leave_block
- assert_valid_syntax("#{<<~"begin;"}\n#{<<~'end;'}")
- begin;
- tap {true || tap {}}
- end;
- end
- def test_jump_elimination_with_optimized_out_block
- x = Object.new
- def x.bug(obj)
- if obj || obj
- obj = obj
- else
- raise "[ruby-core:87830] [Bug #14897]"
- end
- obj
- end
- assert_equal(:ok, x.bug(:ok))
- end
- def test_jump_elimination_with_optimized_out_block_2
- x = Object.new
- def x.bug
- a = "aaa"
- ok = :NG
- if a == "bbb" || a == "ccc" then
- a = a
- else
- ok = :ok
- end
- ok
- end
- assert_equal(:ok, x.bug)
- end
- def test_peephole_jump_after_newarray
- i = 0
- %w(1) || 2 while (i += 1) < 100
- assert_equal(100, i)
- end
- def test_optimized_empty_ensure
- assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 10)
- begin;
- assert_raise(RuntimeError) {
- begin raise ensure nil if nil end
- }
- end;
- end
- def test_optimized_rescue
- assert_in_out_err("", "#{<<~"begin;"}\n#{<<~'end;'}", [], /END \(RuntimeError\)/)
- begin;
- if false
- begin
- require "some_mad_stuff"
- rescue LoadError
- puts "no mad stuff loaded"
- end
- end
- raise "END"
- end;
- end
- end