diff options
Diffstat (limited to 'test/ruby/test_optimization.rb')
| -rw-r--r-- | test/ruby/test_optimization.rb | 566 |
1 files changed, 547 insertions, 19 deletions
diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index bbd468db6e..43795d150c 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -4,7 +4,8 @@ require 'objspace' class TestRubyOptimization < Test::Unit::TestCase def assert_redefine_method(klass, method, code, msg = nil) - assert_separately([], <<-"end;")# do + assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}") + begin; class #{klass} undef #{method} def #{method}(*args) @@ -44,6 +45,26 @@ class TestRubyOptimization < Test::Unit::TestCase 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') @@ -64,6 +85,26 @@ class TestRubyOptimization < Test::Unit::TestCase 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') @@ -104,6 +145,69 @@ class TestRubyOptimization < Test::Unit::TestCase 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_array_min + assert_equal 1, [1, 2, 4].min + assert_redefine_method('Array', 'min', 'assert_nil([1, 2, 4].min)') + assert_redefine_method('Array', 'min', 'assert_nil([1 + 0, 2, 4].min)') + end + + def test_array_max + assert_equal 4, [1, 2, 4].max + assert_redefine_method('Array', 'max', 'assert_nil([1, 2, 4].max)') + assert_redefine_method('Array', 'max', 'assert_nil([1 + 0, 2, 4].max)') + end + + def test_trace_optimized_methods + bug14870 = "[ruby-core:87638]" + expected = [:-@, :max, :min, :+, :-, :*, :/, :%, :==, :<, :<=, :>, :>=, :<<, + :&, :|, :[], :[]=, :length, :empty?, :nil?, :succ, :!, :=~] + [:c_call, :c_return].each do |type| + methods = [] + tp = TracePoint.new(type) { |tp| methods << tp.method_id } + tp.enable do + x = "a"; x = -x + [1].max + [1].min + x = 42 + 2 + x = 42 - 2 + x = 42 * 2 + x = 42 / 2 + x = 42 % 2 + y = x == 42 + y = x < 42 + y = x <= 42 + y = x > 42 + y = x >= 42 + x = x << 1 + x = x & 1 + x = x | 1 + x = []; x[1] + x[1] = 2 + x.length + x.empty? + x.nil? + x = 1; x.succ + !x + x = 'a'; x =~ /a/ + x = y + end + assert_equal(expected, methods, bug14870) + end + + methods = [] + tp = TracePoint.new(:c_call, :c_return) { |tp| methods << tp.method_id } + tp.enable do + x = 1 + x != 42 + end + assert_equal([:!=, :==, :==, :!=], methods, bug14870) + end + def test_string_freeze_saves_memory n = 16384 data = '.'.freeze @@ -141,6 +245,16 @@ class TestRubyOptimization < Test::Unit::TestCase 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]') @@ -178,20 +292,22 @@ class TestRubyOptimization < Test::Unit::TestCase def test_hash_aref_with h = { "foo" => 1 } assert_equal 1, h["foo"] - assert_redefine_method('Hash', '[]', <<-end) + assert_redefine_method('Hash', '[]', "#{<<-"begin;"}\n#{<<~"end;"}") + begin; h = { "foo" => 1 } assert_equal "foo", h["foo"] - end + end; end def test_hash_aset_with h = {} assert_equal 1, h["foo"] = 1 - assert_redefine_method('Hash', '[]=', <<-end) + 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; end class MyObj @@ -210,15 +326,15 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal true, MyObj.new == nil end - def self.tailcall(klass, src, file = nil, path = nil, line = nil) + 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 + line ||= loc.lineno + 1 end RubyVM::InstructionSequence.new("proc {|_|_.class_eval {#{src}}}", file, (path || file), line, - tailcall_optimization: true, + tailcall_optimization: tailcall, trace_instruction: false) .eval[klass] end @@ -230,7 +346,8 @@ class TestRubyOptimization < Test::Unit::TestCase def test_tailcall bug4082 = '[ruby-core:33289]' - tailcall(<<-EOF) + tailcall("#{<<-"begin;"}\n#{<<~"end;"}") + begin; def fact_helper(n, res) if n == 1 res @@ -241,14 +358,15 @@ class TestRubyOptimization < Test::Unit::TestCase def fact(n) fact_helper(n, 1) end - EOF + 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(<<-EOF) + tailcall("#{<<-"begin;"}\n#{<<~"end;"}") + begin; def identity(val) val end @@ -258,7 +376,7 @@ class TestRubyOptimization < Test::Unit::TestCase identity(yield) } end - EOF + end; assert_equal(123, delay { 123 }.call, message(bug6901) {disasm(:delay)}) end @@ -267,11 +385,12 @@ class TestRubyOptimization < Test::Unit::TestCase end def test_tailcall_inhibited_by_block - tailcall(<<-EOF) + tailcall("#{<<-"begin;"}\n#{<<~"end;"}") + begin; def yield_result just_yield {:ok} end - EOF + end; assert_equal(:ok, yield_result, message {disasm(:yield_result)}) end @@ -286,7 +405,8 @@ class TestRubyOptimization < Test::Unit::TestCase def test_tailcall_inhibited_by_rescue bug12082 = '[ruby-core:73871] [Bug #12082]' - tailcall(<<-'end;') + EnvUtil.suppress_warning {tailcall("#{<<-"begin;"}\n#{<<~"end;"}")} + begin; def to_be_rescued return do_raise 1 + 2 @@ -301,6 +421,110 @@ class TestRubyOptimization < Test::Unit::TestCase 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 defined?(RubyVM::MJIT) && 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? @@ -324,7 +548,8 @@ class TestRubyOptimization < Test::Unit::TestCase end def test_string_freeze_block - assert_separately([], <<-"end;")# do + assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}") + begin; class String undef freeze def freeze @@ -337,7 +562,8 @@ class TestRubyOptimization < Test::Unit::TestCase end def test_opt_case_dispatch - code = <<-EOF + code = "#{<<-"begin;"}\n#{<<~"end;"}" + begin; case foo when "foo" then :foo when true then true @@ -350,7 +576,7 @@ class TestRubyOptimization < Test::Unit::TestCase else :nomatch end - EOF + end; check = { 'foo' => :foo, true => true, @@ -369,7 +595,8 @@ class TestRubyOptimization < Test::Unit::TestCase assert_equal :nomatch, eval("foo = :blah\n#{code}") check.each do |foo, _| klass = foo.class.to_s - assert_separately([], <<-"end;") # do + assert_separately([], "#{<<~"begin;"}\n#{<<~"end;"}") + begin; class #{klass} undef === def ===(*args) @@ -405,4 +632,305 @@ class TestRubyOptimization < Test::Unit::TestCase 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 + GC.start; GC.disable # to disable GC while foo{} + ObjectSpace.count_objects(h1) + foo{} + ObjectSpace.count_objects(h2) + + assert_equal 0, h2[:T_DATA] - h1[:T_DATA] # Proc is T_DATA + 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 + + class Objtostring + end + + def test_objtostring + assert_raise(NoMethodError){"#{BasicObject.new}"} + assert_redefine_method('Symbol', 'to_s', <<-'end') + assert_match %r{\A#<Symbol:0x[0-9a-f]+>\z}, "#{:foo}" + end + assert_redefine_method('NilClass', 'to_s', <<-'end') + assert_match %r{\A#<NilClass:0x[0-9a-f]+>\z}, "#{nil}" + end + assert_redefine_method('TrueClass', 'to_s', <<-'end') + assert_match %r{\A#<TrueClass:0x[0-9a-f]+>\z}, "#{true}" + end + assert_redefine_method('FalseClass', 'to_s', <<-'end') + assert_match %r{\A#<FalseClass:0x[0-9a-f]+>\z}, "#{false}" + end + assert_redefine_method('Integer', 'to_s', <<-'end') + (-1..10).each { |i| + assert_match %r{\A#<Integer:0x[0-9a-f]+>\z}, "#{i}" + } + end + assert_equal "TestRubyOptimization::Objtostring", "#{Objtostring}" + assert_match %r{\A#<Class:0x[0-9a-f]+>\z}, "#{Class.new}" + assert_match %r{\A#<Module:0x[0-9a-f]+>\z}, "#{Module.new}" + o = Object.new + def o.to_s; 1; end + assert_match %r{\A#<Object:0x[0-9a-f]+>\z}, "#{o}" + end end |
