diff options
Diffstat (limited to 'test/ruby/test_optimization.rb')
| -rw-r--r-- | test/ruby/test_optimization.rb | 468 |
1 files changed, 430 insertions, 38 deletions
diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index eebbc45e57..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 @@ -214,7 +330,7 @@ class TestRubyOptimization < Test::Unit::TestCase 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, @@ -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 @@ -303,7 +423,8 @@ class TestRubyOptimization < Test::Unit::TestCase def test_tailcall_symbol_block_arg bug12565 = '[ruby-core:46065]' - tailcall(<<-EOF) + tailcall("#{<<-"begin;"}\n#{<<~"end;"}") + begin; def apply_one_and_two(&block) yield(1, 2) end @@ -311,29 +432,31 @@ class TestRubyOptimization < Test::Unit::TestCase def add_one_and_two apply_one_and_two(&:+) end - EOF + 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 = <<EOS -RubyVM::InstructionSequence.compile_option = { - :tailcall_optimization => true, - :trace_instruction => false -} - -eval <<EOF -def foo - foo -end -puts("start") -STDOUT.flush -foo -EOF -EOS - status, err = EnvUtil.invoke_ruby([], "", true, true, {}) { + 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 @@ -360,7 +483,7 @@ EOS def test_tailcall_condition_block bug = '[ruby-core:78015] [Bug #12905]' - src = "#{<<-"begin;"}\n#{<<-"end;"}" + src = "#{<<-"begin;"}\n#{<<~"end;"}", __FILE__, nil, __LINE__+1 begin; def run(current, final) if current < final @@ -372,13 +495,13 @@ EOS end; obj = Object.new - self.class.tailcall(obj.singleton_class, src, tailcall: false) + 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) + 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) { @@ -386,6 +509,22 @@ EOS } 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? @@ -409,7 +548,8 @@ EOS end def test_string_freeze_block - assert_separately([], <<-"end;")# do + assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}") + begin; class String undef freeze def freeze @@ -422,7 +562,8 @@ EOS end def test_opt_case_dispatch - code = <<-EOF + code = "#{<<-"begin;"}\n#{<<~"end;"}" + begin; case foo when "foo" then :foo when true then true @@ -435,7 +576,7 @@ EOS else :nomatch end - EOF + end; check = { 'foo' => :foo, true => true, @@ -454,7 +595,8 @@ EOS 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) @@ -491,6 +633,40 @@ EOS 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 @@ -541,4 +717,220 @@ EOS 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 |
