デバッガのソースコード

とりあえず、デバッガのソースコードを掲載します。

instruction.rb

#
#  instruction.rb - structured bytecode library
#
module VMLib
  class InstSeqTree
    Headers = %w(magic major_version minor_version format_type
                 misc name filename type locals args exception_table)
#                 misc name filename line type locals args exception_table)
#  call-seq:
#     VMLib::InstSeqTree.new(parent, iseq)
#        parent  Partent of InstSeqTree
#                For example, when you will construct InstSeqTree of
#                the method body, you must 'parent' is InstSeqTree of definition
#                code of the method.
#                If parent is none, 'parent' is nil.
#        iseq    Instruction Sequence, Normally the result of 
#                VM::InstructionSequence.compile(...) or 
#                VM::InstructionSequence.compile_file(...)
    def initialize(parent = nil, iseq = nil)
      @klasses = {}
      @methodes = {}
      @blockes = {}
      @line = {}
      @line[nil] = []
      @line_list = []
      
      @header = {}
      @body = nil
      @parent = parent
      @cur_send_no = 0

      Headers.each do |name|
        @header[name] = nil
      end
      
      if iseq then
        init_from_ary(iseq.to_a)
      end
    end

    attr :header
    attr :klasses
    attr :methodes
    attr :blockes
    attr :line
    attr :line_list
    attr :body
    attr :parent
    
    def init_from_ary(ary)
      i = 0
      Headers.each do |name|
        @header[name] = ary[i]
        i = i + 1
      end

      @body = ary[i]
      curlinno = nil
      @body.each do |inst|
        if inst.is_a? Integer then
          # Line number
          curlinno = "#{inst}"
          i = 1
          while @line_list.include?(curlinno) do
            curlinno = "#{inst}-#{i}"
            i = i + 1
          end
          @line_list.push curlinno
          @line[curlinno] = []

        elsif inst.is_a? Array
          case inst[0]
          when :defineclass
            if inst[2] then
              obj = InstSeqTree.new(self)
              obj.init_from_ary(inst[2])
              @klasses[inst[1]] = obj
            end
            
          when :definemethod
            if inst[2] then
              obj = InstSeqTree.new(self)
              obj.init_from_ary(inst[2])
              @methodes[inst[1]] = obj
            end
            
          # inst[3]にはブロック内のメソッドの通し番号が入る
          # この通し番号は例外処理でreturn pointの確定などに
          # 使われる。
          when :send
            if inst[3] then
              obj = InstSeqTree.new(self)
              obj.init_from_ary(inst[3])
              @blockes[@cur_send_no]= obj
              inst[3] = [inst[3], @cur_send_no]
            else
              inst[3] = [nil, @cur_send_no]
            end
            @cur_send_no += 1
            
          when :invokesuper
            if inst[2] then
              obj = InstSeqTree.new(self)
              obj.init_from_ary(inst[2])
              @blockes[@cur_send_no] = obj
              inst[2] = [inst[3], @cur_send_no]
            else
              inst[2] = [nil, @cur_send_no]
            end
            @cur_send_no += 1
          end

          @line[curlinno].push inst
          
        elsif inst.is_a? Symbol
          # Label
          if !@line_list.include?(curlinno) then
            @line_list.push curlinno
          end
          @line[curlinno].push inst
            
        else
          raise inst
        end
      end
    end

    def to_a
      res = []
      Headers.each do |name|
        res.push @header[name]
      end
      
      body = []
      @line_list.each do |ln|
        body.push ln.to_i
        @line[ln].each do |inst|
          if inst.is_a? Array then
            cinst = inst.clone
            case inst[0]
            when :defineclass
              if inst[2] then
                cinst[2] = @klasses[inst[1]].to_a
              end
              
            when :definemethod
              if inst[2] then
                cinst[2] = @methodes[inst[1]].to_a
              end
              
            when :send
              if inst[3] and inst[3][0] then
                cinst[3] = @blockes[inst[3][1]].to_a
              else
                cinst[3] = nil
              end
              
            when :invokesuper
              if inst[2] and inst[2][0] then
                cinst[2] = @blockes[inst[2][1]].to_a
              else
                cinst[2] = nil
              end
            end
            body.push cinst
          else
            body.push inst
          end
        end
      end
      
      res.push body
      res
    end

    def add_code_all_before_block(&action)
      traverse_code([nil, nil, nil]) do |code, info|
        if code.line_list[0] != :before then
          code.line_list.unshift :before
          code.line[:before] = []
        end
        
        curlno = 0
        code.line_list.sort_by {|item| item.to_i}.each do |n|
          if n.to_i != 0 then
            curlno = n
            break
          end
        end
        line = code.line
        inscode = action.call(header['filename'], info, curlno)
        line[:before] =  insert_code(inscode, line[:before], 0)
      end
    end

    # 命令の最後にコードを付加するのではなく、:leave命令の直前に
    # 命令を付加しているのに注意
    def add_code_all_before_return(&action)
      traverse_code([nil, nil, nil]) do |code, info|
        line = code.line
        code.line.each do |no, cont|
          insact = action.call(header['filename'], info, no)
          line[no] = insert_code_before_return(insact, line[no], code)
        end
      end
    end

    def add_code_all_around_send(&action)
      traverse_code([nil, nil, nil]) do |code, info|
        line = code.line
        code.line.each do |no, cont|
          insact = action.call(header['filename'], info, no)
          line[no] = insert_code_around_send(insact, line[no], code)
        end
      end
    end

    def add_code_all_before_line(&action)
      traverse_code([nil, nil, nil]) do |code, info|
        line = code.line
        code.line.each do |no, cont|
          inscode = action.call(header['filename'], info, no)
          line[no] = insert_code(inscode, line[no], 0)
        end
      end
    end

    def traverse_code(info, &action)
      action.call(self, info)
      
      blockes.each do |sno, cont|
        cont.traverse_code([info[0], info[1], sno], &action)
      end

      klasses.each do |name, cont|
        cont.traverse_code([name, nil, nil], &action)
      end
      
      methodes.each do |name, cont|
        cont.traverse_code([info[0], name, nil], &action)
      end
    end


    private

    def join(*arg)
      res = []
      arg.each do |n|
        if n then
          res += n
        end
      end
      
      res
    end

    # ラベルと:getinlinecache命令の間に命令を挿入するとコア吐くので
    # それを防ぐ
    # insposは挿入地点で0が先頭
    def insert_code(code, line, inspos)
      if line[inspos].is_a? Array and line[inspos][0] == :getinlinecache then
        join(line[0, inspos + 1], code, line[inspos+1..-1])
      else
        join(line[0, inspos], code, line[inspos..-1])
      end
    end

    # sendの前後にコードを挿入する
    # スタックフレームの作成に使う
    # insact: 実行するとsendの前、後の2つのコードを返す
    def insert_code_around_send(insact, line, ctree)
      line.inject([]) do |res, inst|
        issend = false
        sendno = nil
        mname = nil
        if inst.is_a? Array then
          if inst[0] == :send then
            issend = true
            sendno = inst[3][1]
            mname = inst[1]
          elsif inst[0] == :sendsuper then
            issend = true
            sendno = inst[2][1]
            mname = inst[1]
          end
        end

        if issend then
          res = res + insact.call(mname, sendno, inst)
        else
          res.push inst
        end
        
        res
      end
    end

    TAG_RETURN = 1
    TAG_BREAK = 2
    def insert_code_before_return(insact, line, ctree)
      line.inject([]) do |res, inst|
        if inst.is_a? Array then
          if inst[0] == :leave then
            res = res + insact.call(1)
          elsif inst[0] == :throw then
            if inst[1] == TAG_RETURN then
              # return 
              # この場合は一番外側のmethodまでたどってそこにreturn
              itree = ctree
              n = 1
              while itree do
                if itree.header['type'] == :method then
                  break
                end
#                p itree.header['type']
                n += 1
                itree = itree.parent
              end
              
              if itree then
                res = res + insact.call(n)
              end
            elsif inst[1] == TAG_BREAK then
              # break
              # この場合はブロックにreturnする
              res = res + insact.call(1)
            end
          end
        end
        res.push inst
        
        res
      end
    end
  end
end

debug.rb

require 'instruction'
require 'pp'

module Depatch
  class SourceManager
    def initialize(fname)
      @source_line = File.readlines(fname)
    end

    def list(center, len = 10, cur = nil)
      ll = 0
      if center < len / 2 then
        ll = 0
      else
        ll = center - len / 2 - 1
      end

      len.times do |n|
        if ll == cur then
          printf "=> %4d   %s" % [ll + 1, @source_line[ll]]
        else
          printf "   %4d   %s" % [ll + 1, @source_line[ll]]
        end

        ll = ll + 1
      end
    end
  end

  class DebugEnv
    def initialize(fname, line, bind)
      @fname = fname
      @line = line
      @bind = bind

      # ブレークポイントのハンドラ
      # デバッガのContinuationが入る
      @break_handler = nil
    end

    def set_line(lin)
      @line = lin
    end

    def set_fname(fname)
      @fname = fname
    end

    attr :fname
    attr :line
    attr :bind
  end

  class DebugContext
    def initialize
      @frame = []
      @frame_sp = -1
      @frame_curp = -1
      @top_frame = [nil, 0, TOPLEVEL_BINDING]
      @context_table = []
      @max_context_no = -1
      @env = DebugEnv.new(*@top_frame)
      @debugger = nil
      @required_file = []
    end

    attr :env

    def set_debugger(debugger)
      @debugger = debugger
    end

    def reset_env
      @frame_curp = @frame_sp + 1
      set_env
      p @env
    end

    def set_env
      if @frame_curp <= @frame_sp  then
        cnt = @frame[@frame_curp]
        cno = cnt[0]
        fi = @context_table[cno]
        @env = DebugEnv.new(fi[0], fi[2], cnt[1])
      else
        @env = DebugEnv.new(*@top_frame)
      end
    end

    def run(fname)
      @env.set_fname(fname)
      @top_frame[0] = fname
    end

    def frame_up
      if @frame_curp < @frame_sp + 1 then
        @frame_curp += 1
        set_env
      else
        print "Here is top frame\n"
      end
    end

    def frame_down
      if @frame_curp > 0 then
        @frame_curp -= 1
        set_env
      else
        print "Here is bottom frame\n"
      end
    end
    
    def add_context(fname, info, no, sendno)
      @max_context_no += 1
      @context_table[@max_context_no] = [fname, info, no, sendno]
      #    print "#{@context_table[@max_context_no]} : #{@max_context_no} \n"
      @max_context_no
    end
    
    # ブロック内でのreturnで、catchポイントを検索する
    # sendnoにreturnを含むブロックを起動したメソッドのシーケンス番号
    # が入っているので、@context_tableからそのシーケンス番号のメソッドを
    # 検索する。
    def search_return_pos(fname, klass, method, sendno)
      @context_table.each_with_index {|cont, idx|
        info = cont[1]
        if  fname == cont[0] and
            klass == info[0] and
            method == info[1] and
            info[2] == nil and
            sendno == cont[3] then

          # returnはmethodから抜けるのでmethodのトップレベルまでスタックを
          # 巻き戻す必要がある。
          # methodのトップレベルのcontextでは現在のブロックを起動した
          # メソッドのシーケンス番号をあらわすinfo[2]はnilになっていなければ
          # ならない
          return idx
        end

        if  fname == cont[0] and
            klass == info[0] and
            method == info[1] and
            sendno == cont[3] then

          # ブロックがレキシカルにネスとしてる場合の処理
          # info[2]がnilでないときはmethodのトップレベルじゃない
          # でも、呼び出したメソッドなので、sendnoを書き換える
          sendno = info[2]
          retry
        end
      }

      # たぶん内部エラー、原因追及のため情報をたんまり表示する
      p @context_table
      raise "Can't return #{fname} #{klass} #{method} #{sendno}"
    end

    def require_load(fname, no, bind)
      rfname = fname
      if !FileTest.exist?(rfname) then
        rfname = fname + ".rb"
        if !FileTest.exist?(rfname) then
          require fname
          return false
        end
      end
      if !@required_file.include?(rfname)
        @debugger.load_file(rfname)
        @required_file.push rfname
      end
      return rfname
    end

    def require_run(fname, no, bind)
      if fname = require_load(fname, no, bind) then
        method_enter(no, bind)
        @debugger.run(fname)
        method_leave(no)
      end
    end
    
    def method_enter(no, bind)
      @frame_sp += 1
      #    print "enter #{@context_table[no]} #{no}\n"
      if @frame[@frame_sp] then
        @frame[@frame_sp][0] = no
        if bind then
          @frame[@frame_sp][1] = bind
        end
      else
        @frame[@frame_sp] = [no, bind]
      end
      #    @frame[@frame_sp] = @context_table[no]
      nil
    end
    
    def exception_throw(no, catchno)
      lvl = 0
      @frame_sp.downto(0) do |n|
        lvl += 1
        if @frame[n][0] == catchno then
          @frame_sp -= lvl
          return
        end
      end
    end
    
    def method_leave(no)
      @frame_sp -= 1
    end

    def set_break_handler(cont)
      @break_handler = cont
    end

    def backtrace
      @frame_sp.downto(0) do |n|
        yield @context_table[@frame[n][0]], @frame[n][1]
      end
    end

    def handle_break(fname, lno, klass, method, bind)
      @top_frame = [fname, lno, bind]
      @env = DebugEnv.new(*@top_frame)
      @frame_curp = @frame_sp + 1
      print "\nbreak in #{lno} at #{fname} (#{klass}##{method})\n"
      print eval("local_variables", bind)

      if @break_handler then
        callcc {|c|
          @break_handler.call(c)
        }
      end
    end
  end
  
  class Debugger
    include VMLib
    
    def initialize
      @source = {}
      @file_iseqt = {}
      $__debug__ = DebugContext.new
      @curfname = nil

      # PP.pp @iseqt.to_a

      @debugee_cont = nil
    end

    def load_file(fname)
      if @source[fname] == nil then
        @source[fname] = SourceManager.new(fname)
        iseq = VM::InstructionSequence.compile_file(fname)
#        print VM::InstructionSequence.load(iseq.to_a).disasm
        @file_iseqt[fname] = InstSeqTree.new(nil, iseq)
        set_trace(@file_iseqt[fname])
      end
    end

    def set_current_file(fname)
      @curfname = fname
    end
    
    def run(fname = @curfname)
#      PP.pp @file_iseqt[fname].to_a
      $__debug__.run(fname)
      VM::InstructionSequence.load(@file_iseqt[fname].to_a).eval
    end

=begin
    begin
      require 'readline'
      def readline(prompt, hist)
        Readline::readline(prompt, hist)
      end
    rescue LoadError
=end
      def readline(prompt, hist)
        STDOUT.print prompt
        STDOUT.flush
        line = STDIN.gets
        exit unless line
        line.chomp!
        line
      end
#    end

    def where
      $__debug__.backtrace {|posinfo, bind|
#        p posinfo
        if posinfo[1][0] and posinfo[1][1] then
          print "#{posinfo[1][0]}##{posinfo[1][1]} #{posinfo[0]}:#{posinfo[2]}\n"
        else
          print "#{posinfo[0]}:#{posinfo[2]}\n"
        end
        print "local variables\n"
        print eval("local_variables",bind) 
        print "\n\n"
      }
    end

    def disasm(iseqt)
      print VM::InstructionSequence.load(iseqt.to_a).disasm
    end

    def up
      $__debug__.frame_up
    end

    def down
      $__debug__.frame_down
    end

    def command_rep
      while command = readline("(dep) ", true)
        case command
        when /^\s*b(?:reak)?\s+(?:(.+):)?([0-9]+)/
          line = $2
          if $1 then
            file = $1
          else
            file = @curfname
          end

          set_break_line(@file_iseqt[file], file, line)
          
        when /^\s*b(?:reak)?\s+(?:(.+)[.#])?(\S+)/
          if $1 then
            klassn = $1.to_sym
          else
            klassn = nil
          end
          methodn = $2
          set_break_method(klassn, methodn.to_sym)
          print "Set break point #{klassn.to_s}##{methodn}\n"

        when /^\s*r(?:un)?/
          @debugee_cont = 
            callcc {|c|
              $__debug__.set_break_handler(c)
              begin
                run
              rescue StandardError, ScriptError
                p $!
                p $!.backtrace
                /^([^:]+):([0-9]+):/ =~ $!.backtrace[0]
                @top_frame = [$1, $2, nil]
                p @top_frame

                $__debug__.reset_env
                $__debug__.frame_down
              end
              nil
            }

        when /^\s*c(?:ont)?/
          if @debugee_cont then
            @debugee_cont.call(nil)
          end

        when /^\s*w(?:here)?/
          where

        when /^\s*l(?:ist)?/
          p $__debug__.env.fname
          @source[$__debug__.env.fname].list($__debug__.env.line.to_i)

        when /^\s*dis(?:asm)?/
          disasm(@file_iseqt[$__debug__.env.fname])

        when /^\s*up$/
          up
          @curfname = $__debug__.env.fname
          p @curfname
          @source[$__debug__.env.fname].list($__debug__.env.line.to_i, 1)

        when /^\s*down$/
          down
          @curfname = $__debug__.env.fname
          @source[$__debug__.env.fname].list($__debug__.env.line.to_i, 1)

        else
          begin
            eval(command, $__debug__.env.bind)
          rescue StandardError, ScriptError
            print "Error: #{command}\n"
            STDOUT.flush
          end
        end
        STDOUT.flush
      end
    end
    
    def set_trace(iseqt)

      iseqt.add_code_all_around_send {|fname, info, no|
        lambda {|mname, sendno, inst|
          cno = $__debug__.add_context(fname, info, no, sendno)
          case mname
          when :require
            [
              [:swap],
              [:pop],
              [:getglobal, :$__debug__],
              [:swap],
              [:putobject, cno],
              [:putnil],
              [:send, :binding, 0, nil, 24, nil],
              [:send, :require_run, 3, nil, 0, nil],
            ]
            
          else
            [
              [:getglobal, :$__debug__],
              [:putobject, cno],
              [:putnil],
              [:send, :binding, 0, nil, 24, nil],
              [:send, :method_enter, 2, nil, 0, nil],
              [:pop],
              inst,
              [:getglobal, :$__debug__],
              [:putobject, cno],
              [:send, :method_leave, 1, nil, 0, nil],
              [:pop],
            ]
          end
        }
      }
      # before_returnといいながら、例外のときだけコードを挿入する
      # returnのスタック調整用
      iseqt.add_code_all_before_return {|fname, info, no|
        lambda {|level|
          if level > 1 then
            cno = $__debug__.add_context(fname, info, no, nil)
            retno = $__debug__.search_return_pos(fname, info[0], info[1], info[2])
            [
              [:getglobal, :$__debug__],
              [:putobject, cno],
              [:putobject, retno],
              [:send, :exception_throw, 2, nil, 0, nil],
              [:pop],
            ]
          else
            []
          end
        }
      }
    end
    
    def set_break_line(iseqt, fname, line)
      if iseqt.nil? then
        return nil
      end

      if fname == nil then
        fname = @curfname
      end
      
      line = line.to_s
      
      iseqt.add_code_all_before_line {|fn, info, no|
        if fname == fn and no == line then
          [
            [:getglobal, :$__debug__],
            [:putobject, fn],
            [:putobject, no],
            [:putobject, info[0]],
            [:putobject, info[1]],
            [:putnil],
            [:send, :binding, 0, nil, 24, nil],
            [:send, :handle_break, 5, nil, 0, nil], 
            [:pop]
          ]
        else
          []
        end
      }
      true
    end
    
    def set_break_method(klass, method)
      
      @file_iseqt.each do |file, iseqt|
        iseqt.add_code_all_before_block {|fn, info, no|
          if info[0] == klass and info[1] == method and info[2] == nil then
            [
              [:getglobal, :$__debug__],
              [:putobject, fn],
              [:putobject, no],
              [:putobject, info[0]],
              [:putobject, info[1]],
              [:putnil],
              [:send, :binding, 0, nil, 24, nil],
              [:send, :handle_break, 5, nil, 0, nil], 
              [:pop]
            ]
          else
            []
          end
        }
      end

      true
    end
  end
end

=begin
VM::InstructionSequence.compile_option = {
  trace_instruction: false,
  peephole_optimization: false,
  inline_const_cache: false,
  specialized_instruction: false,
  operands_unification: false,
  instructions_unification: false,
}
=end
debug = Depatch::Debugger.new
$__debug__.set_debugger(debug)
debug.load_file(ARGV[0])
debug.set_current_file(ARGV[0])
ARGV.shift
debug.command_rep