#!/usr/bin/env ruby

# Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.

require 'rubygems'

require 'readline'

begin
    require 'json'
    require 'highline'
rescue LoadError
    $stderr.puts "Error: some required gems are not installed!"
    $stderr.puts
    $stderr.puts "Try running:"
    $stderr.puts
    $stderr.puts "sudo gem install json"
    $stderr.puts "sudo gem install highline"
    exit 1
end

class Bytecode
    attr_accessor :bytecodes, :bytecodeIndex, :opcode, :description, :topCounts, :bottomCounts, :machineInlinees, :osrExits

    def initialize(bytecodes, bytecodeIndex, opcode, description)
        @bytecodes = bytecodes
        @bytecodeIndex = bytecodeIndex
        @opcode = opcode
        @description = description
        @topCounts = [] # "source" counts
        @bottomCounts = {} # "machine" counts, maps compilations to counts
        @machineInlinees = {} # maps my compilation to a set of inlinees
        @osrExits = []
    end

    def shouldHaveCounts?
        @opcode != "op_call_put_result"
    end

    def addTopCount(count)
        @topCounts << count
    end

    def addBottomCountForCompilation(count, compilation)
        @bottomCounts[compilation] = [] unless @bottomCounts[compilation]
        @bottomCounts[compilation] << count
    end

    def addMachineInlinee(compilation, inlinee)
        @machineInlinees[compilation] = {} unless @machineInlinees[compilation]
        @machineInlinees[compilation][inlinee] = true
    end

    def totalTopExecutionCount
        sum = 0
        @topCounts.each {
            | value |
            sum += value.count
        }
        sum
    end

    def topExecutionCount(engine)
        sum = 0
        @topCounts.each {
            | value |
            if value.engine == engine
                sum += value.count
            end
        }
        sum
    end

    def totalBottomExecutionCount
        sum = 0
        @bottomCounts.each_value {
            | counts |
            max = 0
            counts.each {
                | value |
                max = [max, value.count].max
            }
            sum += max
        }
        sum
    end

    def bottomExecutionCount(engine)
        sum = 0
        @bottomCounts.each_pair {
            | compilation, counts |
            if compilation.engine == engine
                max = 0
                counts.each {
                    | value |
                    max = [max, value.count].max
                }
                sum += max
            end
        }
        sum
    end

    def totalExitCount
        sum = 0
        @osrExits.each {
            | exit |
            sum += exit.count
        }
        sum
    end
end

class Bytecodes
    attr_accessor :codeHash, :inferredName, :source, :instructionCount, :machineInlineSites, :compilations

    def initialize(json)
        @codeHash = json["hash"].to_s
        @inferredName = json["inferredName"].to_s
        @source = json["sourceCode"].to_s
        @instructionCount = json["instructionCount"].to_i
        @bytecode = {}
        json["bytecode"].each {
            | subJson |
            index = subJson["bytecodeIndex"].to_i
            @bytecode[index] = Bytecode.new(self, index, subJson["opcode"].to_s, subJson["description"].to_s)
        }
        @machineInlineSites = {} # maps compilation to a set of origins
        @compilations = []
    end

    def name(limit)
        if to_s.size > limit
            "\##{@codeHash}"
        else
            to_s
        end
    end

    def to_s
        "#{@inferredName}\##{@codeHash}"
    end

    def matches(pattern)
        if pattern =~ /^#/
            $~.post_match == @codeHash
        elsif pattern =~ /#/
            pattern == to_s
        else
            pattern == @inferredName or pattern == @codeHash
        end
    end

    def each
        @bytecode.values.sort{|a, b| a.bytecodeIndex <=> b.bytecodeIndex}.each {
            | value |
            yield value
        }
    end

    def bytecode(bytecodeIndex)
        @bytecode[bytecodeIndex]
    end

    def addMachineInlineSite(compilation, origin)
        @machineInlineSites[compilation] = {} unless @machineInlineSites[compilation]
        @machineInlineSites[compilation][origin] = true
    end

    def totalMachineInlineSites
        sum = 0
        @machineInlineSites.each_value {
            | set |
            sum += set.size
        }
        sum
    end

    def sourceMachineInlineSites
        set = {}
        @machineInlineSites.each_value {
            | mySet |
            set.merge!(mySet)
        }
        set.size
    end

    def totalMaxTopExecutionCount
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.totalTopExecutionCount].max
        }
        max
    end

    def maxTopExecutionCount(engine)
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.topExecutionCount(engine)].max
        }
        max
    end

    def totalMaxBottomExecutionCount
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.totalBottomExecutionCount].max
        }
        max
    end

    def maxBottomExecutionCount(engine)
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.bottomExecutionCount(engine)].max
        }
        max
    end

    def totalExitCount
        sum = 0
        each {
            | bytecode |
            sum += bytecode.totalExitCount
        }
        sum
    end
end

class ProfiledBytecode
    attr_reader :bytecodeIndex, :description

    def initialize(json)
        @bytecodeIndex = json["bytecodeIndex"].to_i
        @description = json["description"].to_s
    end
end

class ProfiledBytecodes
    attr_reader :header, :bytecodes

    def initialize(json)
        @header = json["header"]
        @bytecodes = $bytecodes[json["bytecodesID"].to_i]
        @sequence = json["bytecode"].map {
            | subJson |
            ProfiledBytecode.new(subJson)
        }
    end

    def each
        @sequence.each {
            | description |
            yield description
        }
    end
end

def originStackFromJSON(json)
    json.map {
        | subJson |
        $bytecodes[subJson["bytecodesID"].to_i].bytecode(subJson["bytecodeIndex"].to_i)
    }
end

class CompiledBytecode
    attr_accessor :origin, :description

    def initialize(json)
        @origin = originStackFromJSON(json["origin"])
        @description = json["description"].to_s
    end
end

class ExecutionCounter
    attr_accessor :origin, :engine, :count

    def initialize(origin, engine, count)
        @origin = origin
        @engine = engine
        @count = count
    end
end

class OSRExit
    attr_reader :compilation, :origin, :codeAddresses, :exitKind, :isWatchpoint, :count

    def initialize(compilation, origin, codeAddresses, exitKind, isWatchpoint, count)
        @compilation = compilation
        @origin = origin
        @codeAddresses = codeAddresses
        @exitKind = exitKind
        @isWatchpoint = isWatchpoint
        @count = count
    end

    def dumpForDisplay(prefix)
        puts(prefix + "EXIT: due to #{@exitKind}, #{@count} times")
    end
end

class Compilation
    attr_accessor :bytecode, :engine, :descriptions, :counters, :compilationIndex
    attr_accessor :osrExits, :profiledBytecodes, :numInlinedGetByIds, :numInlinedPutByIds
    attr_accessor :numInlinedCalls

    def initialize(json)
        @bytecode = $bytecodes[json["bytecodesID"].to_i]
        @bytecode.compilations << self
        @compilationIndex = @bytecode.compilations.size
        @engine = json["compilationKind"]
        @descriptions = json["descriptions"].map {
            | subJson |
            CompiledBytecode.new(subJson)
        }
        @descriptions.each {
            | description |
            next if description.origin.empty?
            description.origin[1..-1].each_with_index {
                | inlinee, index |
                description.origin[0].addMachineInlinee(self, inlinee.bytecodes)
                inlinee.bytecodes.addMachineInlineSite(self, description.origin[0...index])
            }
        }
        @counters = {}
        json["counters"].each {
            | subJson |
            origin = originStackFromJSON(subJson["origin"])
            counter = ExecutionCounter.new(origin, @engine, subJson["executionCount"].to_i)
            @counters[origin] = counter
            origin[-1].addTopCount(counter)
            origin[0].addBottomCountForCompilation(counter, self)
        }
        @osrExits = {}
        json["osrExits"].each {
            | subJson |
            osrExit = OSRExit.new(self, originStackFromJSON(subJson["origin"]),
                                  json["osrExitSites"][subJson["id"]].map {
                                      | value |
                                      value.hex
                                  }, subJson["exitKind"], subJson["isWatchpoint"],
                                  subJson["count"])
            osrExit.codeAddresses.each {
                | codeAddress |
                osrExits[codeAddress] = [] unless osrExits[codeAddress]
                osrExits[codeAddress] << osrExit
            }
            osrExit.origin[-1].osrExits << osrExit
        }
        @profiledBytecodes = []
        json["profiledBytecodes"].each {
            | subJson |
            @profiledBytecodes << ProfiledBytecodes.new(subJson)
        }
        @numInlinedGetByIds = json["numInlinedGetByIds"]
        @numInlinedPutByIds = json["numInlinedPutByIds"]
        @numInlinedCalls = json["numInlinedCalls"]
    end

    def counter(origin)
        @counters[origin]
    end

    def to_s
        "#{bytecode}-#{compilationIndex}-#{engine}"
    end
end

class DescriptionLine
    attr_reader :actualCountsString, :sourceCountsString, :disassembly, :shouldShow

    def initialize(actualCountsString, sourceCountsString, disassembly, shouldShow)
        @actualCountsString = actualCountsString
        @sourceCountsString = sourceCountsString
        @disassembly = disassembly
        @shouldShow = shouldShow
    end

    def codeAddress
        if @disassembly =~ /^\s*(0x[0-9a-fA-F]+):/
            $1.hex
        else
            nil
        end
    end
end

if ARGV.length != 1
    $stderr.puts "Usage: display-profiler-output <path to profiler output file>"
    $stderr.puts
    $stderr.puts "The typical usage pattern for the profiler currently looks something like:"
    $stderr.puts
    $stderr.puts "Path/To/jsc -p profile.json myprogram.js"
    $stderr.puts "display-profiler-output profile.json"
    exit 1
end

$json = JSON::parse(IO::read(ARGV[0]))
$bytecodes = $json["bytecodes"].map {
    | subJson |
    Bytecodes.new(subJson)
}
$compilations = $json["compilations"].map {
    | subJson |
    Compilation.new(subJson)
}
$engines = ["Baseline", "DFG"]

def lpad(str,chars)
  if str.length>chars
    str
  else
    "%#{chars}s"%(str)
  end
end

def rpad(str, chars)
    while str.length < chars
        str += " "
    end
    str
end

def center(str, chars)
    while str.length < chars
        str += " "
        if str.length < chars
            str = " " + str
        end
    end
    str
end

def mayBeHash(hash)
    hash =~ /#/ or hash.size == 6
end

def sourceOnOneLine(source, limit)
    source.gsub(/\s+/, ' ')[0...limit]
end

def screenWidth
    if $stdin.tty?
        HighLine::SystemExtensions.terminal_size[0]
    else
        200
    end
end

def summary(mode)
    remaining = screenWidth

    # Figure out how many columns we need for the code block names, and for counts
    maxCount = 0
    maxName = 0
    $bytecodes.each {
        | bytecodes |
        maxCount = ([maxCount] + $engines.map {
                        | engine |
                        bytecodes.maxTopExecutionCount(engine)
                    } + $engines.map {
                        | engine |
                        bytecodes.maxBottomExecutionCount(engine)
                    }).max
        maxName = [bytecodes.to_s.size, maxName].max
    }
    maxCountDigits = maxCount.to_s.size

    hashCols = [[maxName, 30].min, "CodeBlock".size].max
    remaining -= hashCols + 1

    countCols = [maxCountDigits * $engines.size, "Source Counts".size].max
    remaining -= countCols + 1

    if mode == :full
        instructionCountCols = 6
        remaining -= instructionCountCols + 1

        machineCountCols = [maxCountDigits * $engines.size, "Machine Counts".size].max
        remaining -= machineCountCols + 1

        compilationsCols = 7
        remaining -= compilationsCols + 1

        inlinesCols = 9
        remaining -= inlinesCols + 1

        exitCountCols = 7
        remaining -= exitCountCols + 1

        recentOptsCols = 12
        remaining -= recentOptsCols + 1
    end

    if remaining > 0
        sourceCols = remaining
    else
        sourceCols = nil
    end

    print(center("CodeBlock", hashCols))
    if mode == :full
        print(" " + center("#Instr", instructionCountCols))
    end
    print(" " + center("Source Counts", countCols))
    if mode == :full
        print(" " + center("Machine Counts", machineCountCols))
        print(" " + center("#Compil", compilationsCols))
        print(" " + center("Inlines", inlinesCols))
        print(" " + center("#Exits", exitCountCols))
        print(" " + center("Last Opts", recentOptsCols))
    end
    if sourceCols
        print(" " + center("Source", sourceCols))
    end
    puts

    print(center("", hashCols))
    if mode == :full
        print(" " + (" " * instructionCountCols))
    end
    print(" " + center("Base/DFG", countCols))
    if mode == :full
        print(" " + center("Base/DFG", machineCountCols))
        print(" " + (" " * compilationsCols))
        print(" " + center("Src/Total", inlinesCols))
        print(" " + (" " * exitCountCols))
        print(" " + center("Get/Put/Call", recentOptsCols))
    end
    puts
    $bytecodes.sort {
        | a, b |
        b.totalMaxTopExecutionCount <=> a.totalMaxTopExecutionCount
    }.each {
        | bytecode |
        print(center(bytecode.name(hashCols), hashCols))
        if mode == :full
            print(" " + center(bytecode.instructionCount.to_s, instructionCountCols))
        end
        print(" " +
              center($engines.map {
                         | engine |
                         bytecode.maxTopExecutionCount(engine).to_s
                     }.join("/"), countCols))
        if mode == :full
            print(" " + center($engines.map {
                                   | engine |
                                   bytecode.maxBottomExecutionCount(engine).to_s
                               }.join("/"), machineCountCols))
            print(" " + center(bytecode.compilations.size.to_s, compilationsCols))
            print(" " + center(bytecode.sourceMachineInlineSites.to_s + "/" + bytecode.totalMachineInlineSites.to_s, inlinesCols))
            print(" " + center(bytecode.totalExitCount.to_s, exitCountCols))
            lastCompilation = bytecode.compilations[-1]
            if lastCompilation
                optData = [lastCompilation.numInlinedGetByIds,
                           lastCompilation.numInlinedPutByIds,
                           lastCompilation.numInlinedCalls]
            else
                optData = ["N/A"]
            end
            print(" " + center(optData.join('/'), recentOptsCols))
        end
        if sourceCols
            print(" " + sourceOnOneLine(bytecode.source, sourceCols))
        end
        puts
    }
end

def executeCommand(*commandArray)
    command = commandArray[0]
    args = commandArray[1..-1]
    case command
    when "help", "h", "?"
        puts "summary (s)     Print a summary of code block execution rates."
        puts "full (f)        Same as summary, but prints more information."
        puts "source          Show the source for a code block."
        puts "bytecode (b)    Show the bytecode for a code block, with counts."
        puts "profiling (p)   Show the (internal) profiling data for a code block."
        puts "display (d)     Display details for a code block."
        puts "inlines         Show all inlining stacks that the code block was on."
        puts "help (h)        Print this message."
        puts "quit (q)        Quit."
    when "quit", "q", "exit"
        exit 0
    when "summary", "s"
        summary(:summary)
    when "full", "f"
        summary(:full)
    when "source"
        if args.length != 1
            puts "Usage: source <code block hash>"
            return
        end
        $bytecodes.each {
            | bytecode |
            if bytecode.matches(args[0])
                puts bytecode.source
            end
        }
    when "bytecode", "b"
        if args.length != 1
            puts "Usage: source <code block hash>"
            return
        end

        hash = args[0]

        countCols = 10 * $engines.size
        machineCols = 10 * $engines.size
        pad = 1
        while (countCols + 1 + machineCols + pad) % 8 != 0
            pad += 1
        end

        $bytecodes.each {
            | bytecodes |
            next unless bytecodes.matches(hash)
            puts(center("Source Counts", countCols) + " " + center("Machine Counts", machineCols) +
                 (" " * pad) + center("Bytecode for #{bytecodes}", screenWidth - pad - countCols - 1 - machineCols))
            puts(center("Base/DFG", countCols) + " " + center("Base/DFG", countCols))
            bytecodes.each {
                | bytecode |
                if bytecode.shouldHaveCounts?
                    countsString = $engines.map {
                        | myEngine |
                        bytecode.topExecutionCount(myEngine)
                    }.join("/")
                    machineString = $engines.map {
                        | myEngine |
                        bytecode.bottomExecutionCount(myEngine)
                    }.join("/")
                else
                    countsString = ""
                    machineString = ""
                end
                puts(center(countsString, countCols) + " " + center(machineString, machineCols) + (" " * pad) + bytecode.description.chomp)
                bytecode.osrExits.each {
                    | exit |
                    puts(center("!!!!!", countCols) + " " + center("!!!!!", machineCols) + (" " * (pad + 10)) +
                         "EXIT: in #{exit.compilation} due to #{exit.exitKind}, #{exit.count} times")
                }
            }
        }
    when "profiling", "p"
        if args.length != 1
            puts "Usage: profiling <code block hash>"
            return
        end

        hash = args[0]

        first = true
        $compilations.each {
            | compilation |

            compilation.profiledBytecodes.each {
                | profiledBytecodes |
                if profiledBytecodes.bytecodes.matches(hash)
                    if first
                        first = false
                    else
                        puts
                    end

                    puts "Compilation #{compilation}:"
                    profiledBytecodes.header.each {
                        | header |
                        puts(" " * 6 + header)
                    }
                    profiledBytecodes.each {
                        | bytecode |
                        puts(" " * 8 + bytecode.description)
                        profiledBytecodes.bytecodes.bytecode(bytecode.bytecodeIndex).osrExits.each {
                            | exit |
                            if exit.compilation == compilation
                                puts(" !!!!!           EXIT: due to #{exit.exitKind}, #{exit.count} times")
                            end
                        }
                    }
                end
            }
        }
    when "inlines"
        if args.length != 1
            puts "Usage: inlines <code block hash>"
            return
        end

        hash = args[0]

        $bytecodes.each {
            | bytecodes |
            next unless bytecodes.matches(hash)

            # FIXME: print something useful to say more about which code block this is.

            $compilations.each {
                | compilation |
                myOrigins = []
                compilation.descriptions.each {
                    | description |
                    if description.origin.index {
                            | myBytecode |
                            bytecodes == myBytecode.bytecodes
                        }
                        myOrigins << description.origin
                    end
                }
                myOrigins.uniq!
                myOrigins.sort! {
                    | a, b |
                    result = 0
                    [a.size, b.size].min.times {
                        | index |
                        result = a[index].bytecodeIndex <=> b[index].bytecodeIndex
                        break if result != 0
                    }
                    result
                }

                next if myOrigins.empty?

                printArray = []
                lastPrintStack = []

                def originToPrintStack(origin)
                    (0...(origin.size - 1)).map {
                        | index |
                        "bc\##{origin[index].bytecodeIndex} --> #{origin[index + 1].bytecodes}"
                    }
                end

                def printStack(printArray, stack, lastStack)
                    stillCommon = true
                    stack.each_with_index {
                        | entry, index |
                        next if stillCommon and entry == lastStack[index]
                        printArray << ("    " * (index + 1) + entry)
                        stillCommon = false
                    }
                end

                myOrigins.each {
                    | origin |
                    currentPrintStack = originToPrintStack(origin)
                    printStack(printArray, currentPrintStack, lastPrintStack)
                    lastPrintStack = currentPrintStack
                }

                next if printArray.empty?

                puts "Compilation #{compilation}:"
                printArray.each {
                    | entry |
                    puts entry
                }
            }
        }
    when "display", "d"
        compilationIndex = nil

        case args.length
        when 1
            if args[0] == "*"
                hash = nil
            else
                hash = args[0]
            end
            engine = nil
        when 2
            if mayBeHash(args[0])
                hash = args[0]
                engine = args[1]
            else
                engine = args[0]
                hash = args[1]
            end
        else
            puts "Usage: summary <code block hash> <engine>"
            return
        end

        if hash and hash =~ /-([0-9]+)-/
            hash = $~.pre_match
            engine = $~.post_match
            compilationIndex = $1.to_i
        end

        if engine and not $engines.index(engine)
            pattern = Regexp.new(Regexp.escape(engine), "i")
            trueEngine = nil
            $engines.each {
                | myEngine |
                if myEngine =~ pattern
                    trueEngine = myEngine
                    break
                end
            }
            unless trueEngine
                puts "#{engine} is not a valid engine, try #{$engines.join(' or ')}."
                return
            end
            engine = trueEngine
        end

        actualCountCols = 13
        sourceCountCols = 10 * $engines.size

        first = true
        $compilations.each {
            | compilation |
            next if hash and not compilation.bytecode.matches(hash)
            next if engine and compilation.engine != engine
            next if compilationIndex and compilation.compilationIndex != compilationIndex

            if first
                first = false
            else
                puts
            end

            puts("Compilation #{compilation}:")
            puts("    Num inlined: GetByIds: #{compilation.numInlinedGetByIds}  PutByIds: #{compilation.numInlinedPutByIds}  Calls: #{compilation.numInlinedCalls}")
            puts(center("Actual Counts", actualCountCols) + " " + center("Source Counts", sourceCountCols) + " " + center("Disassembly in #{compilation.engine}", screenWidth - 1 - sourceCountCols - 1 - actualCountCols))
            puts((" " * actualCountCols) + " " + center("Base/DFG", sourceCountCols))

            lines = []

            compilation.descriptions.each {
                | description |
                # FIXME: We should have a better way of detecting things like CountExecution nodes
                # and slow path entries in the baseline JIT.
                if description.description =~ /CountExecution\(/ and compilation.engine == "DFG"
                    shouldShow = false
                else
                    shouldShow = true
                end
                if description.origin.empty? or not description.origin[-1].shouldHaveCounts? or (compilation.engine == "Baseline" and description.description =~ /^\s*\(S\)/)
                    actualCountsString = ""
                    sourceCountsString = ""
                else
                    actualCountsString = compilation.counter(description.origin).count.to_s
                    sourceCountsString = $engines.map {
                        | myEngine |
                        description.origin[-1].topExecutionCount(myEngine)
                    }.join("/")
                end
                description.description.split("\n").each {
                    | line |
                    lines << DescriptionLine.new(actualCountsString, sourceCountsString, line.chomp, shouldShow)
                }
            }

            exitPrefix = center("!!!!!", actualCountCols) + " " + center("!!!!!", sourceCountCols) + (" " * 25)

            lines.each_with_index {
                | line, index |
                codeAddress = line.codeAddress
                if codeAddress
                    list = compilation.osrExits[codeAddress]
                    if list
                        list.each {
                            | exit |
                            if exit.isWatchpoint
                                exit.dumpForDisplay(exitPrefix)
                            end
                        }
                    end
                end
                if line.shouldShow
                    puts(center(line.actualCountsString, actualCountCols) + " " + center(line.sourceCountsString, sourceCountCols) + " " + line.disassembly)
                end
                if codeAddress
                    # Find the next disassembly address.
                    endIndex = index + 1
                    endAddress = nil
                    while endIndex < lines.size
                        myAddress = lines[endIndex].codeAddress
                        if myAddress
                            endAddress = myAddress
                            break
                        end
                        endIndex += 1
                    end

                    if endAddress
                        list = compilation.osrExits[endAddress]
                        if list
                            list.each {
                                | exit |
                                unless exit.isWatchpoint
                                    exit.dumpForDisplay(exitPrefix)
                                end
                            }
                        end
                    end
                end
            }
        }
    else
        puts "Invalid command: #{command}"
    end
end

if $stdin.tty?
    executeCommand("full")
end

while commandLine = Readline.readline("> ", true)
    executeCommand(*commandLine.split)
end