# Module for working with instruction sequences.

class Rubinius::SetTrace
  module ISeq
    OP_GOTO = Rubinius::InstructionSet.opcodes_map[:goto]
    OP_GOTO_IF_TRUE  = Rubinius::InstructionSet.opcodes_map[:goto_if_true]
    OP_GOTO_IF_FALSE = Rubinius::InstructionSet.opcodes_map[:goto_if_false]
    OP_RET           = Rubinius::InstructionSet.opcodes_map[:ret]
    OP_YIELD_STACK   = Rubinius::InstructionSet.opcodes_map[:yield_stack]

    module_function

    # Returns prefix string to indicate whether a breakpoint has been
    # set at this ip and or whether we are currently stopped at this ip.
    def disasm_prefix(ip, frame_ip, cm)
      prefix = cm.breakpoint?(ip) ? 'B' : ' ' 
      prefix += 
        if ip == frame_ip
          '-->'
        else
          '   '
        end
    end

    def goto_op?(cm, ip)
      [OP_GOTO, OP_GOTO_IF_TRUE, OP_GOTO_IF_FALSE].member?(cm.iseq[ip])
    end

    def goto_between(cm, start, fin)
      
      iseq = cm.iseq
      
      i = start
      while i < fin
        op = iseq[i]
        case op
        when OP_GOTO
          return next_interesting(cm, iseq[i + 1]) # goto target
        when OP_RET
          return -2
        when OP_GOTO_IF_TRUE, OP_GOTO_IF_FALSE
          return [next_interesting(cm, iseq[i + 1]),
                  next_interesting(cm, i + 2)] # target and next ip
        when nil
          return -1
        else
          # Rubinius is getting an error here sometimes. Need to figure
          # out what's wrong.
          begin
            op = Rubinius::InstructionSet[op]
          rescue TypeError
            return -1
          end
          i += (op.arg_count + 1)
        end
      end

      if fin == cm.lines.last
        return -1
      else
        return next_interesting(cm, fin)
      end
    end

    def ip_ranges_for_line(lines, line)
      result = []
      in_range = false
      start_ip = nil
      total = lines.size
      i = 1
      while i < total
        cur_line = lines.at(i)
        if cur_line == line
          start_ip = lines.at(i-1)
          in_range = true
        elsif cur_line > line && in_range
          result << [start_ip, lines.at(i-1)]
          start_ip = nil
          in_range = false
        end
        i += 2
      end
      if in_range && start_ip
        result << [start_ip, lines.at(total-1)]
      end
      result
    end
    
    # Return range of ips covering ip. There will be only one of these.
    # We surround this in another list to match the format of 
    # ip_ranges_for_line.
    def ip_ranges_for_ip(lines, ip)
      total = lines.size
      i = 0
      while i < total and lines.at(i) <= ip
        i += 2
      end
      [[lines.at(i-2), lines.at(i)]]
    end
  
    def next_interesting(cm, ip)
      pop = Rubinius::InstructionSet.opcodes_map[:pop]
      
      if cm.iseq[ip] == pop
        return ip + 1
      end
      
      return ip
    end

    # start is assumed to be on a tail code "synthesized" line, which
    # has line number 0. Follow opcodes until we get to a line that is
    # not 0. 
    def tail_code_line(cm, start)
      
      iseq = cm.iseq
      
      i = start
      fin = cm.lines.last
      while i < fin
        line_no = cm.line_from_ip(i)
        return line_no if line_no > 0
        op = iseq[i]
        case op
        when OP_GOTO_IF_TRUE, OP_GOTO_IF_FALSE, OP_GOTO
        when OP_GOTO
          i = iseq[i+1]
        when OP_RET
          return -2
        when nil
          return -1
        else
          # Rubinius is getting an error here sometimes. Need to figure
          # out what's wrong.
          begin
            op = Rubinius::InstructionSet[op]
          rescue TypeError
            return -1
          end
          i += (op.arg_count + 1)
        end
      end
      return 0
    end
    
    def yield_or_return_between(cm, start, fin)
      iseq = cm.iseq
      ips = []
      i = start
      while i < fin
        op = iseq[i]
        if [OP_RET, OP_YIELD_STACK].member?(op)
          ips << i 
        end
        op = Rubinius::InstructionSet[op]
        i += (op.arg_count + 1)
      end
      return ips
    end

  end
end
if __FILE__ == $0
  vm_locations = Rubinius::VM.backtrace(0, true)
  include Rubinius::SetTrace::ISeq
  call_loc = vm_locations[1]
  cm = call_loc.method
  puts cm.decode
  ips = nil
  puts '-' * 20
  0.upto((cm.lines.last+1)/2) do |i|
    ip = cm.lines[i*2]
    unless -1 == ip
      ips = yield_or_return_between(cm, ip, cm.lines.last) 
      puts "return: #{ips.inspect}"
      break 
    end
  end
  puts '-' * 20
  0.upto((cm.lines.last+1)/2) do |i|
    ip = cm.lines[i*2]
    unless -1 == ip
      ips = goto_between(cm, ip, cm.lines.last)
      puts "goto: #{ips.inspect}"
      break 
    end
  end
  puts disasm_prefix(ips[0], ips[0], cm)
  p disasm_prefix(10, ips[0], cm)
end
