diff options
Diffstat (limited to 'test/ruby/test_iseq.rb')
| -rw-r--r-- | test/ruby/test_iseq.rb | 1045 |
1 files changed, 1045 insertions, 0 deletions
diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb new file mode 100644 index 0000000000..b4760dc412 --- /dev/null +++ b/test/ruby/test_iseq.rb @@ -0,0 +1,1045 @@ +require 'test/unit' +require 'tempfile' + +class TestISeq < Test::Unit::TestCase + ISeq = RubyVM::InstructionSequence + + def test_no_linenum + bug5894 = '[ruby-dev:45130]' + assert_normal_exit('p RubyVM::InstructionSequence.compile("1", "mac", "", 0).to_a', bug5894) + end + + def compile(src, line = nil, opt = nil) + unless line + line = caller_locations(1).first.lineno + end + EnvUtil.suppress_warning do + ISeq.new(src, __FILE__, __FILE__, line, opt) + end + end + + def lines src, lines = nil + body = compile(src, lines).to_a[13] + body.find_all{|e| e.kind_of? Integer} + end + + def test_allocate + assert_raise(TypeError) {ISeq.allocate} + end + + def test_to_a_lines + assert_equal [__LINE__+1, __LINE__+2, __LINE__+4], lines(<<-EOS, __LINE__+1) + p __LINE__ # 1 + p __LINE__ # 2 + # 3 + p __LINE__ # 4 + EOS + + assert_equal [__LINE__+2, __LINE__+4], lines(<<-EOS, __LINE__+1) + # 1 + p __LINE__ # 2 + # 3 + p __LINE__ # 4 + # 5 + EOS + + assert_equal [__LINE__+3, __LINE__+4, __LINE__+7, __LINE__+9], lines(<<~EOS, __LINE__+1) + 1 # should be optimized out + 2 # should be optimized out + p __LINE__ # 3 + p __LINE__ # 4 + 5 # should be optimized out + 6 # should be optimized out + p __LINE__ # 7 + 8 # should be optimized out + 9 + EOS + end + + def test_unsupported_type + ary = compile("p").to_a + ary[9] = :foobar + assert_raise_with_message(TypeError, /:foobar/) {ISeq.load(ary)} + end if defined?(RubyVM::InstructionSequence.load) + + def test_loaded_cdhash_mark + iseq = compile(<<-'end;', __LINE__+1) + def bug(kw) + case kw + when "false" then false + when "true" then true + when "nil" then nil + else raise("unhandled argument: #{kw.inspect}") + end + end + end; + assert_separately([], <<-"end;") + iseq = #{iseq.to_a.inspect} + RubyVM::InstructionSequence.load(iseq).eval + assert_equal(false, bug("false")) + GC.start + assert_equal(false, bug("false")) + end; + end if defined?(RubyVM::InstructionSequence.load) + + def test_cdhash_after_roundtrip + # CDHASH was not built properly when loading from binary and + # was causing opt_case_dispatch to clobber its stack canary + # for its "leaf" instruction attribute. + iseq = compile(<<~EOF, __LINE__+1) + case Class.new(String).new("foo") + when "foo" + 42 + end + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_forwardable + iseq = compile(<<~EOF, __LINE__+1) + Class.new { + def bar(a, b); a + b; end + def foo(...); bar(...); end + } + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval.new.foo(40, 2)) + end + + def test_super_with_block + iseq = compile(<<~EOF, __LINE__+1) + def (Object.new).touch(*) # :nodoc: + foo { super } + end + 42 + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_super_with_block_hash_0 + iseq = compile(<<~EOF, __LINE__+1) + # [Bug #18250] `req` specifically cause `Assertion failed: (key != 0), function hash_table_raw_insert` + def (Object.new).touch(req, *) + foo { super } + end + 42 + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_super_with_block_and_kwrest + iseq = compile(<<~EOF, __LINE__+1) + def (Object.new).touch(**) # :nodoc: + foo { super } + end + 42 + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_lambda_with_ractor_roundtrip + iseq = compile(<<~EOF, __LINE__+1) + x = 42 + y = Ractor.shareable_lambda{x} + y.call + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_super_with_anonymous_block + iseq = compile(<<~EOF, __LINE__+1) + def (Object.new).touch(&) # :nodoc: + foo { super } + end + 42 + EOF + assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) + end + + def test_ractor_unshareable_outer_variable + name = "\u{2603 26a1}" + assert_raise_with_message(Ractor::IsolationError, /\(#{name}\)/) do + eval("#{name} = nil; Ractor.shareable_proc{#{name} = nil}") + end + + assert_raise_with_message(Ractor::IsolationError, /\'#{name}\'/) do + eval("#{name} = []; Ractor.shareable_proc{#{name}}") + end + + obj = Object.new + def obj.foo(*) Ractor.shareable_proc{super} end + assert_raise_with_message(Ractor::IsolationError, /cannot make a shareable Proc because it can refer unshareable object \[\]/) do + obj.foo(*[]) + end + end + + def test_ractor_shareable_value_frozen_core + iseq = RubyVM::InstructionSequence.compile(<<~'RUBY') + # shareable_constant_value: literal + REGEX = /#{}/ # [Bug #20569] + RUBY + assert_includes iseq_to_binary(iseq), "REGEX".b + end + + def test_disasm_encoding + src = +"\u{3042} = 1; \u{3042}; \u{3043}" + asm = compile(src).disasm + assert_equal(src.encoding, asm.encoding) + assert_predicate(asm, :valid_encoding?) + src.encode!(Encoding::Shift_JIS) + asm = compile(src).disasm + assert_equal(src.encoding, asm.encoding) + assert_predicate(asm, :valid_encoding?) + + obj = Object.new + name = "\u{2603 26a1}" + obj.instance_eval("def #{name}; tap {}; end") + assert_include(RubyVM::InstructionSequence.of(obj.method(name)).disasm, name) + end + + def test_compile_file_encoding + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts "{ '\u00de' => 'Th', '\u00df' => 'ss', '\u00e0' => 'a' }" + f.close + + EnvUtil.with_default_external(Encoding::US_ASCII) do + assert_warn('') { + load f.path + } + assert_nothing_raised(SyntaxError) { + RubyVM::InstructionSequence.compile_file(f.path) + } + end + end + end + + def test_compile_file_options + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts('_ = "test"') + f.close + iseq = RubyVM::InstructionSequence.compile_file(f.path, { frozen_string_literal: false }) + refute_predicate iseq.eval, :frozen? + + iseq = RubyVM::InstructionSequence.compile_file(f.path, { frozen_string_literal: true }) + assert_predicate iseq.eval, :frozen? + end + end + + def test_compile_options + iseq = RubyVM::InstructionSequence.compile("'test'", nil, nil, nil, { frozen_string_literal: false }) + refute_predicate iseq.eval, :frozen? + + iseq = RubyVM::InstructionSequence.compile("'test'", nil, nil, nil, { frozen_string_literal: true }) + assert_predicate iseq.eval, :frozen? + end + + LINE_BEFORE_METHOD = __LINE__ + def method_test_line_trace + + _a = 1 + + _b = 2 + + end + + def test_line_trace + iseq = compile(<<~EOF, __LINE__+1) + a = 1 + b = 2 + c = 3 + # d = 4 + e = 5 + # f = 6 + g = 7 + EOF + + assert_equal([1, 2, 3, 5, 7], iseq.line_trace_all) + iseq.line_trace_specify(1, true) # line 2 + iseq.line_trace_specify(3, true) # line 5 + + result = [] + TracePoint.new(:specified_line){|tp| + result << tp.lineno + }.enable{ + iseq.eval + } + assert_equal([2, 5], result) + + iseq = ISeq.of(self.class.instance_method(:method_test_line_trace)) + assert_equal([LINE_BEFORE_METHOD + 3, LINE_BEFORE_METHOD + 5], iseq.line_trace_all) + end if false # TODO: now, it is only for C APIs. + + LINE_OF_HERE = __LINE__ + def test_location + iseq = ISeq.of(method(:test_location)) + + assert_equal(__FILE__, iseq.path) + assert_match(/#{__FILE__}/, iseq.absolute_path) + assert_equal("test_location", iseq.label) + assert_equal("test_location", iseq.base_label) + assert_equal(LINE_OF_HERE+1, iseq.first_lineno) + + line = __LINE__ + iseq = ISeq.of(Proc.new{}) + assert_equal(__FILE__, iseq.path) + assert_match(/#{__FILE__}/, iseq.absolute_path) + assert_equal("test_location", iseq.base_label) + assert_equal("block in test_location", iseq.label) + assert_equal(line+1, iseq.first_lineno) + end + + def test_label_fstring + c = Class.new{ def foobar() end } + + a, b = eval("# encoding: us-ascii\n'foobar'.freeze"), + ISeq.of(c.instance_method(:foobar)).label + assert_same a, b + end + + def test_disable_opt + src = "a['foo'] = a['bar']; 'a'.freeze" + body= compile(src, __LINE__, false).to_a[13] + body.each{|insn| + next unless Array === insn + op = insn.first + assert(!op.to_s.match(/^opt_/), "#{op}") + } + end + + def test_invalid_source + bug11159 = '[ruby-core:69219] [Bug #11159]' + assert_raise(TypeError, bug11159) {compile(nil)} + assert_raise(TypeError, bug11159) {compile(:foo)} + assert_raise(TypeError, bug11159) {compile(1)} + end + + def test_invalid_source_no_memory_leak + # [Bug #21394] + assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + code = proc do |t| + RubyVM::InstructionSequence.new(nil) + rescue TypeError + else + raise "TypeError was not raised during RubyVM::InstructionSequence.new" + end + + 10.times(&code) + begin; + 1_000_000.times(&code) + end; + + # [Bug #21394] + # RubyVM::InstructionSequence.new calls rb_io_path, which dups the string + # and can leak memory if the dup raises + assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true) + MyError = Class.new(StandardError) + String.prepend(Module.new do + def initialize_dup(_) + if $raise_on_dup + raise MyError + else + super + end + end + end) + + code = proc do |t| + Tempfile.create do |f| + $raise_on_dup = true + t.times do + RubyVM::InstructionSequence.new(f) + rescue MyError + else + raise "MyError was not raised during RubyVM::InstructionSequence.new" + end + ensure + $raise_on_dup = false + end + end + + code.call(100) + begin; + code.call(1_000_000) + end; + end + + def test_frozen_string_literal_compile_option + $f = 'f' + line = __LINE__ + 2 + code = <<-'EOS' + ['foo', 'foo', "#{$f}foo", "#{'foo'}"] + EOS + s1, s2, s3, s4 = compile(code, line, {frozen_string_literal: true}).eval + assert_predicate(s1, :frozen?) + assert_predicate(s2, :frozen?) + assert_not_predicate(s3, :frozen?) + assert_not_predicate(s4, :frozen?) + end + + def test_frozen_string_literal_compile_option_file + Tempfile.create(%w[fsl .rb]) do |f| + f.write("['foo', 'foo', \"\#{$f}foo\", \"\#{'foo'}\"]\n") + f.flush + $f = 'f' + s1, s2, s3, s4 = RubyVM::InstructionSequence + .compile_file(f.path, frozen_string_literal: true).eval + assert_predicate(s1, :frozen?) + assert_predicate(s2, :frozen?) + assert_not_predicate(s3, :frozen?) + assert_not_predicate(s4, :frozen?) + end + end + + # Safe call chain is not optimized when Coverage is running. + # So we can test it only when Coverage is not running. + def test_safe_call_chain + src = "a&.a&.a&.a&.a&.a" + body = compile(src, __LINE__, {peephole_optimization: true}).to_a[13] + labels = body.select {|op, arg| op == :branchnil}.map {|op, arg| arg} + assert_equal(1, labels.uniq.size) + end if (!defined?(Coverage) || !Coverage.running?) + + def test_parent_iseq_mark + assert_separately([], <<-'end;', timeout: 20) + ->{ + ->{ + ->{ + eval <<-EOS + class Segfault + define_method :segfault do + x = nil + GC.disable + 1000.times do |n| + n.times do + x = (foo rescue $!).local_variables + end + GC.start + end + x + end + end + EOS + }.call + }.call + }.call + at_exit { assert_equal([:n, :x], Segfault.new.segfault.sort) } + end; + end + + def test_syntax_error_message + feature11951 = '[Feature #11951]' + + src, line = <<-'end;', __LINE__+1 + def x@;end + def y@;end + end; + e1 = e2 = nil + m1 = EnvUtil.verbose_warning do + e1 = assert_raise(SyntaxError) do + eval(src, nil, __FILE__, line) + end + end + m2 = EnvUtil.verbose_warning do + e2 = assert_raise(SyntaxError) do + ISeq.new(src, __FILE__, __FILE__, line) + end + end + assert_equal([m1, e1.message], [m2, e2.message], feature11951) + + if e1.message.lines[0] == "#{__FILE__}:#{line}: syntax errors found\n" + # Prism lays out the error messages in line with the source, so the + # following assertions do not make sense in that context. + else + message = e1.message.each_line + message.with_index(1) do |line, i| + next if /^ / =~ line + assert_send([line, :start_with?, __FILE__], + proc {message.map {|l, j| (i == j ? ">" : " ") + l}.join("")}) + end + end + end + + # [Bug #19173] + def test_compile_error + assert_raise SyntaxError do + RubyVM::InstructionSequence.compile 'using Module.new; yield' + end + end + + def test_compile_file_error + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts "end" + f.close + path = f.path + assert_in_out_err(%W[- #{path}], "#{<<-"begin;"}\n#{<<-"end;"}", /unexpected 'end'/, [], success: true) + begin; + path = ARGV[0] + begin + RubyVM::InstructionSequence.compile_file(path) + rescue SyntaxError => e + puts e.message + end + end; + end + end + + def test_translate_by_object + assert_separately([], <<-"end;") + class Object + def translate + end + end + assert_equal(0, eval("0")) + end; + end + + def test_inspect + %W[foo \u{30d1 30b9}].each do |name| + assert_match(/@#{name}/, ISeq.compile("", name).inspect, name) + m = ISeq.compile("class TestISeq::Inspect; def #{name}; end; instance_method(:#{name}); end").eval + assert_match(/:#{name}@/, ISeq.of(m).inspect, name) + end + end + + def anon_star(*); end + + def test_anon_rest_param_in_disasm + iseq = RubyVM::InstructionSequence.of(method(:anon_star)) + param_names = iseq.to_a[iseq.to_a.index(:method) + 1] + assert_equal [:*], param_names + end + + def anon_keyrest(**); end + + def test_anon_keyrest_param_in_disasm + iseq = RubyVM::InstructionSequence.of(method(:anon_keyrest)) + param_names = iseq.to_a[iseq.to_a.index(:method) + 1] + assert_equal [:**], param_names + end + + def anon_block(&); end + + def test_anon_block_param_in_disasm + iseq = RubyVM::InstructionSequence.of(method(:anon_block)) + param_names = iseq.to_a[iseq.to_a.index(:method) + 1] + assert_equal [:&], param_names + end + + def strip_lineno(source) + source.gsub(/^.*?: /, "") + end + + def sample_iseq + ISeq.compile(strip_lineno(<<-EOS)) + 1: class C + 2: def foo + 3: begin + 4: rescue + 5: p :rescue + 6: ensure + 7: p :ensure + 8: end + 9: end + 10: def bar + 11: 1.times{ + 12: 2.times{ + 13: } + 14: } + 15: end + 16: end + 17: class D < C + 18: end + EOS + end + + def test_each_child + iseq = sample_iseq + + collect_iseq = lambda{|iseq| + iseqs = [] + iseq.each_child{|child_iseq| + iseqs << collect_iseq.call(child_iseq) + } + ["#{iseq.label}@#{iseq.first_lineno}", *iseqs.sort_by{|k, *| k}] + } + + expected = ["<compiled>@1", + ["<class:C>@1", + ["bar@10", ["block in bar@11", + ["block (2 levels) in bar@12"]]], + ["foo@2", ["ensure in foo@7"], + ["rescue in foo@4"]]], + ["<class:D>@17"]] + + assert_equal expected, collect_iseq.call(iseq) + end + + def test_trace_points + collect_iseq = lambda{|iseq| + iseqs = [] + iseq.each_child{|child_iseq| + iseqs << collect_iseq.call(child_iseq) + } + [["#{iseq.label}@#{iseq.first_lineno}", iseq.trace_points], *iseqs.sort_by{|k, *| k}] + } + assert_equal [["<compiled>@1", [[1, :line], + [17, :line]]], + [["<class:C>@1", [[1, :class], + [2, :line], + [10, :line], + [16, :end]]], + [["bar@10", [[10, :call], + [11, :line], + [15, :return]]], + [["block in bar@11", [[11, :b_call], + [12, :line], + [14, :b_return]]], + [["block (2 levels) in bar@12", [[12, :b_call], + [13, :b_return]]]]]], + [["foo@2", [[2, :call], + [4, :line], + [7, :line], + [9, :return]]], + [["ensure in foo@7", [[7, :line]]]], + [["rescue in foo@4", [[5, :line], + [5, :rescue]]]]]], + [["<class:D>@17", [[17, :class], + [18, :end]]]]], collect_iseq.call(sample_iseq) + end + + def test_empty_iseq_lineno + iseq = ISeq.compile(<<-EOS) + # 1 + # 2 + def foo # line 3 empty method + end # line 4 + 1.time do # line 5 empty block + end # line 6 + class C # line 7 empty class + end + EOS + + iseq.each_child{|ci| + ary = ci.to_a + type = ary[9] + name = ary[5] + line = ary[13].first + case type + when :method + assert_equal "foo", name + assert_equal 3, line + when :class + assert_equal '<class:C>', name + assert_equal 7, line + when :block + assert_equal 'block in <compiled>', name + assert_equal 5, line + else + raise "unknown ary: " + ary.inspect + end + } + end + + def hexdump(bin) + bin.unpack1("H*").gsub(/.{1,32}/) {|s| + "#{'%04x:' % $~.begin(0)}#{s.gsub(/../, " \\&").tap{|_|_[24]&&="-"}}\n" + } + end + + def iseq_to_binary(iseq) + iseq.to_binary + rescue RuntimeError => e + omit e.message if /compile with coverage/ =~ e.message + raise + end + + def assert_iseq_to_binary(code, mesg = nil) + iseq = RubyVM::InstructionSequence.compile(code) + bin = assert_nothing_raised(mesg) do + iseq_to_binary(iseq) + end + 10.times do + bin2 = iseq_to_binary(iseq) + assert_equal(bin, bin2, message(mesg) {diff hexdump(bin), hexdump(bin2)}) + end + iseq2 = RubyVM::InstructionSequence.load_from_binary(bin) + a1 = iseq.to_a + a2 = iseq2.to_a + assert_equal(a1, a2, message(mesg) {diff iseq.disassemble, iseq2.disassemble}) + if iseq2.script_lines + assert_kind_of(Array, iseq2.script_lines) + else + assert_nil(iseq2.script_lines) + end + iseq2 + end + + def test_to_binary_with_hidden_local_variables + assert_iseq_to_binary("for _foo in bar; end") + + bin = iseq_to_binary(RubyVM::InstructionSequence.compile(<<-RUBY)) + Object.new.instance_eval do + a = [] + def self.bar; [1] end + for foo in bar + a << (foo * 2) + end + a + end + RUBY + v = RubyVM::InstructionSequence.load_from_binary(bin).eval + assert_equal([2], v) + end + + def test_to_binary_with_objects + assert_iseq_to_binary("[]"+100.times.map{|i|"<</#{i}/"}.join) + assert_iseq_to_binary("@x ||= (1..2)") + end + + def test_to_binary_pattern_matching + code = "case foo; in []; end" + iseq = compile(code) + assert_include(iseq.disasm, "TypeError") + assert_include(iseq.disasm, "NoMatchingPatternError") + EnvUtil.suppress_warning do + assert_iseq_to_binary(code, "[Feature #14912]") + end + end + + def test_to_binary_dumps_nokey + iseq = assert_iseq_to_binary(<<-RUBY) + o = Object.new + class << o + def foo(**nil); end + end + o + RUBY + assert_equal([[:nokey]], iseq.eval.singleton_method(:foo).parameters) + end + + def test_to_binary_dumps_noblock + iseq = assert_iseq_to_binary(<<-RUBY) + o = Object.new + class << o + def foo(&nil); end + end + o + RUBY + assert_equal([[:noblock]], iseq.eval.singleton_method(:foo).parameters) + end + + def test_to_binary_line_info + assert_iseq_to_binary("#{<<~"begin;"}\n#{<<~'end;'}", '[Bug #14660]').eval + begin; + class P + def p; end + def q; end + E = "" + N = "#{E}" + attr_reader :i + end + end; + + # cleanup + ::Object.class_eval do + remove_const :P + end + end + + def collect_from_binary_tracepoint_lines(tracepoint_type, filename) + iseq = RubyVM::InstructionSequence.compile(strip_lineno(<<-RUBY), filename) + class A + class B + 2.times { + def self.foo + _a = 'good day' + raise + rescue + 'dear reader' + end + } + end + B.foo + end + RUBY + + iseq_bin = iseq_to_binary(iseq) + iseq = ISeq.load_from_binary(iseq_bin) + lines = [] + TracePoint.new(tracepoint_type){|tp| + next unless tp.path == filename + lines << tp.lineno + }.enable{ + EnvUtil.suppress_warning {iseq.eval} + } + + lines + ensure + Object.send(:remove_const, :A) rescue nil + end + + def test_to_binary_line_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:line, filename) + + assert_equal [1, 2, 3, 4, 4, 12, 5, 6, 8], lines, '[Bug #14702]' + end + + def test_to_binary_class_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:class, filename) + + assert_equal [1, 2], lines, '[Bug #14702]' + end + + def test_to_binary_end_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:end, filename) + + assert_equal [11, 13], lines, '[Bug #14702]' + end + + def test_to_binary_return_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:return, filename) + + assert_equal [9], lines, '[Bug #14702]' + end + + def test_to_binary_b_call_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:b_call, filename) + + assert_equal [3, 3], lines, '[Bug #14702]' + end + + def test_to_binary_b_return_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:b_return, filename) + + assert_equal [10, 10], lines, '[Bug #14702]' + end + + def test_iseq_of + [ + proc{}, + method(:test_iseq_of), + RubyVM::InstructionSequence.compile("p 1", __FILE__), + begin; raise "error"; rescue => error; error.backtrace_locations[0]; end + ].each{|src| + iseq = RubyVM::InstructionSequence.of(src) + assert_equal __FILE__, iseq.path + } + end + + def test_iseq_of_twice_for_same_code + [ + proc{}, + method(:test_iseq_of_twice_for_same_code), + RubyVM::InstructionSequence.compile("p 1"), + begin; raise "error"; rescue => error; error.backtrace_locations[0]; end + ].each{|src| + iseq1 = RubyVM::InstructionSequence.of(src) + iseq2 = RubyVM::InstructionSequence.of(src) + + # ISeq objects should be same for same src + assert_equal iseq1.object_id, iseq2.object_id + } + end + + def test_iseq_builtin_to_a + invokebuiltin = eval(EnvUtil.invoke_ruby(['-e', <<~EOS], '', true).first) + insns = RubyVM::InstructionSequence.of([].method(:pack)).to_a.last + p insns.find { |insn| insn.is_a?(Array) && insn[0] == :opt_invokebuiltin_delegate_leave } + EOS + assert_not_nil(invokebuiltin) + assert_equal([:func_ptr, :argc, :index, :name], invokebuiltin[1].keys) + end + + def test_iseq_builtin_load + Tempfile.create(["builtin", ".iseq"]) do |f| + f.binmode + f.write(iseq_to_binary(RubyVM::InstructionSequence.of(1.method(:abs)))) + f.close + assert_separately(["-", f.path], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + bin = File.binread(ARGV[0]) + assert_raise(ArgumentError) do + RubyVM::InstructionSequence.load_from_binary(bin) + end + end; + end + end + + def test_iseq_option_debug_level + assert_raise(TypeError) {ISeq.compile("", debug_level: "")} + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + RubyVM::InstructionSequence.compile("", debug_level: 5) + end; + end + + def test_mandatory_only + assert_separately [], <<~RUBY + at0 = Time.at(0) + assert_equal at0, Time.public_send(:at, 0, 0) + RUBY + end + + def test_mandatory_only_redef + assert_separately ['-W0'], <<~RUBY + r = Ractor.new{ + Float(10) + module Kernel + undef Float + def Float(n) + :new + end + end + GC.start + Float(30) + } + assert_equal :new, r.value + RUBY + end + + def test_ever_condition_loop + assert_ruby_status([], "BEGIN {exit}; while true && true; end") + end + + def test_short_circuited_loop_condition + assert_ruby_status([], "while true || true; exit; end; abort") + end + + def test_unreachable_syntax_error + mesg = /Invalid break/ + assert_syntax_error("false and break", mesg) + assert_syntax_error("if false and break; end", mesg) + end + + def test_unreachable_pattern_matching + assert_in_out_err([], "true or 1 in 1") + assert_in_out_err([], "true or (case 1; in 1; 1; in 2; 2; end)") + end + + def test_unreachable_pattern_matching_in_if_condition + assert_in_out_err([], "#{<<~"begin;"}\n#{<<~'end;'}", %w[1]) + begin; + if true or {a: 0} in {a:} + p 1 + else + p a + end + end; + end + + def test_unreachable_next_in_block + bug20344 = '[ruby-core:117210] [Bug #20344]' + assert_nothing_raised(SyntaxError, bug20344) do + compile(<<~RUBY) + proc do + next + + case nil + when "a" + next + when "b" + when "c" + proc {} + end + + next + end + RUBY + end + end + + def test_serialize_anonymous_outer_variables + iseq = RubyVM::InstructionSequence.compile(<<~'RUBY') + obj = Object.new + def obj.test + [1].each do + raise "Oops" + rescue + return it + end + end + obj + RUBY + + binary = iseq.to_binary # [Bug # 21370] + roundtripped_iseq = RubyVM::InstructionSequence.load_from_binary(binary) + object = roundtripped_iseq.eval + assert_equal 1, object.test + end + + def test_loading_kwargs_memory_leak + assert_no_memory_leak([], "#{<<~"begin;"}", "#{<<~'end;'}", rss: true) + a = RubyVM::InstructionSequence.compile("foo(bar: :baz)").to_binary + begin; + 1_000_000.times do + RubyVM::InstructionSequence.load_from_binary(a) + end + end; + end + + def test_ibf_bignum + iseq = RubyVM::InstructionSequence.compile("0x0"+"_0123_4567_89ab_cdef"*5) + expected = iseq.eval + result = RubyVM::InstructionSequence.load_from_binary(iseq_to_binary(iseq)).eval + assert_equal expected, result, proc {sprintf("expected: %x, result: %x", expected, result)} + end + + def test_compile_prism_with_file + Tempfile.create(%w"test_iseq .rb") do |f| + f.puts "_name = 'Prism'; puts 'hello'" + f.close + + assert_nothing_raised(TypeError) do + RubyVM::InstructionSequence.compile_prism(f) + end + end + end + + def block_using_method + yield + end + + def block_unused_method + end + + def test_unused_param + a = RubyVM::InstructionSequence.of(method(:block_using_method)).to_a + + assert_equal true, a.dig(11, :use_block) + + b = RubyVM::InstructionSequence.of(method(:block_unused_method)).to_a + assert_equal nil, b.dig(11, :use_block) + end + + def test_compile_prism_with_invalid_object_type + assert_raise(TypeError) do + RubyVM::InstructionSequence.compile_prism(Object.new) + end + end + + def test_load_from_binary_only_accepts_string_param + assert_raise(TypeError) do + var_0 = 0 + RubyVM::InstructionSequence.load_from_binary(var_0) + end + end + + def test_while_in_until_condition + assert_in_out_err(["--dump=i", "-e", "until while 1; end; end"]) do |stdout, stderr, status| + assert_include(stdout.shift, "== disasm:") + assert_include(stdout.pop, "leave") + assert_predicate(status, :success?) + end + end + + def test_compile_empty_under_gc_stress + EnvUtil.under_gc_stress do + RubyVM::InstructionSequence.compile_file(File::NULL) + end + end +end |
