mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
68260018df
argument optimization when tail call optimization is enabled, in order to avoid SEGV. [ruby-core:76288] [Bug #12565] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55605 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
423 lines
10 KiB
Ruby
423 lines
10 KiB
Ruby
# 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([], <<-"end;")# do
|
|
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_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_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_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_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', '[]', <<-end)
|
|
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', '[]=', <<-end)
|
|
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)
|
|
unless file
|
|
loc, = caller_locations(1, 1)
|
|
file = loc.path
|
|
line ||= loc.lineno
|
|
end
|
|
RubyVM::InstructionSequence.new("proc {|_|_.class_eval {#{src}}}",
|
|
file, (path || file), line,
|
|
tailcall_optimization: true,
|
|
trace_instruction: false)
|
|
.eval[klass]
|
|
end
|
|
|
|
def tailcall(*args)
|
|
self.class.tailcall(singleton_class, *args)
|
|
end
|
|
|
|
def test_tailcall
|
|
bug4082 = '[ruby-core:33289]'
|
|
|
|
tailcall(<<-EOF)
|
|
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
|
|
EOF
|
|
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)
|
|
def identity(val)
|
|
val
|
|
end
|
|
|
|
def delay
|
|
-> {
|
|
identity(yield)
|
|
}
|
|
end
|
|
EOF
|
|
assert_equal(123, delay { 123 }.call, message(bug6901) {disasm(:delay)})
|
|
end
|
|
|
|
def just_yield
|
|
yield
|
|
end
|
|
|
|
def test_tailcall_inhibited_by_block
|
|
tailcall(<<-EOF)
|
|
def yield_result
|
|
just_yield {:ok}
|
|
end
|
|
EOF
|
|
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]'
|
|
|
|
tailcall(<<-'end;')
|
|
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(<<-EOF)
|
|
def apply_one_and_two(&block)
|
|
yield(1, 2)
|
|
end
|
|
|
|
def add_one_and_two
|
|
apply_one_and_two(&:+)
|
|
end
|
|
EOF
|
|
assert_equal(3, add_one_and_two,
|
|
message(bug12565) {disasm(:add_one_and_two)})
|
|
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([], <<-"end;")# do
|
|
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 = <<-EOF
|
|
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
|
|
EOF
|
|
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([], <<-"end;") # do
|
|
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
|
|
end
|