michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- michael@0: * vim: set ts=8 sw=4 et tw=78: michael@0: * michael@0: * jorendb - A toy command-line debugger for shell-js programs. michael@0: * michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: */ michael@0: michael@0: /* michael@0: * jorendb is a simple command-line debugger for shell-js programs. It is michael@0: * intended as a demo of the Debugger object (as there are no shell js programs michael@0: * to speak of). michael@0: * michael@0: * To run it: $JS -d path/to/this/file/jorendb.js michael@0: * To run some JS code under it, try: michael@0: * (jorendb) print load("my-script-to-debug.js") michael@0: * Execution will stop at debugger statements and you'll get a jorendb prompt. michael@0: */ michael@0: michael@0: // Debugger state. michael@0: var focusedFrame = null; michael@0: var topFrame = null; michael@0: var debuggeeValues = {}; michael@0: var nextDebuggeeValueIndex = 1; michael@0: var lastExc = null; michael@0: michael@0: // Cleanup functions to run when we next re-enter the repl. michael@0: var replCleanups = []; michael@0: michael@0: // Convert a debuggee value v to a string. michael@0: function dvToString(v) { michael@0: return (typeof v !== 'object' || v === null) ? uneval(v) : "[object " + v.class + "]"; michael@0: } michael@0: michael@0: function showDebuggeeValue(dv) { michael@0: var dvrepr = dvToString(dv); michael@0: var i = nextDebuggeeValueIndex++; michael@0: debuggeeValues["$" + i] = dv; michael@0: print("$" + i + " = " + dvrepr); michael@0: } michael@0: michael@0: Object.defineProperty(Debugger.Frame.prototype, "num", { michael@0: configurable: true, michael@0: enumerable: false, michael@0: get: function () { michael@0: var i = 0; michael@0: for (var f = topFrame; f && f !== this; f = f.older) michael@0: i++; michael@0: return f === null ? undefined : i; michael@0: } michael@0: }); michael@0: michael@0: Debugger.Frame.prototype.frameDescription = function frameDescription() { michael@0: if (this.type == "call") michael@0: return ((this.callee.name || '') + michael@0: "(" + this.arguments.map(dvToString).join(", ") + ")"); michael@0: else michael@0: return this.type + " code"; michael@0: } michael@0: michael@0: Debugger.Frame.prototype.positionDescription = function positionDescription() { michael@0: if (this.script) { michael@0: var line = this.script.getOffsetLine(this.offset); michael@0: if (this.script.url) michael@0: return this.script.url + ":" + line; michael@0: return "line " + line; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: Debugger.Frame.prototype.fullDescription = function fullDescription() { michael@0: var fr = this.frameDescription(); michael@0: var pos = this.positionDescription(); michael@0: if (pos) michael@0: return fr + ", " + pos; michael@0: return fr; michael@0: } michael@0: michael@0: Object.defineProperty(Debugger.Frame.prototype, "line", { michael@0: configurable: true, michael@0: enumerable: false, michael@0: get: function() { michael@0: if (this.script) michael@0: return this.script.getOffsetLine(this.offset); michael@0: else michael@0: return null; michael@0: } michael@0: }); michael@0: michael@0: function callDescription(f) { michael@0: return ((f.callee.name || '') + michael@0: "(" + f.arguments.map(dvToString).join(", ") + ")"); michael@0: } michael@0: michael@0: function showFrame(f, n) { michael@0: if (f === undefined || f === null) { michael@0: f = focusedFrame; michael@0: if (f === null) { michael@0: print("No stack."); michael@0: return; michael@0: } michael@0: } michael@0: if (n === undefined) { michael@0: n = f.num; michael@0: if (n === undefined) michael@0: throw new Error("Internal error: frame not on stack"); michael@0: } michael@0: michael@0: print('#' + n + " " + f.fullDescription()); michael@0: } michael@0: michael@0: function saveExcursion(fn) { michael@0: var tf = topFrame, ff = focusedFrame; michael@0: try { michael@0: return fn(); michael@0: } finally { michael@0: topFrame = tf; michael@0: focusedFrame = ff; michael@0: } michael@0: } michael@0: michael@0: // Evaluate an expression in the Debugger global michael@0: function evalCommand(expr) { michael@0: eval(expr); michael@0: } michael@0: michael@0: function quitCommand() { michael@0: dbg.enabled = false; michael@0: quit(0); michael@0: } michael@0: michael@0: function backtraceCommand() { michael@0: if (topFrame === null) michael@0: print("No stack."); michael@0: for (var i = 0, f = topFrame; f; i++, f = f.older) michael@0: showFrame(f, i); michael@0: } michael@0: michael@0: function printCommand(rest) { michael@0: // This is the real deal. michael@0: var cv = saveExcursion( michael@0: () => focusedFrame == null michael@0: ? debuggeeGlobalWrapper.evalInGlobalWithBindings(rest, debuggeeValues) michael@0: : focusedFrame.evalWithBindings(rest, debuggeeValues)); michael@0: if (cv === null) { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Debuggee died."); michael@0: } else if ('return' in cv) { michael@0: if (!dbg.enabled) michael@0: return [undefined]; michael@0: showDebuggeeValue(cv.return); michael@0: } else { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Exception caught. (To rethrow it, type 'throw'.)"); michael@0: lastExc = cv.throw; michael@0: showDebuggeeValue(lastExc); michael@0: } michael@0: } michael@0: michael@0: function detachCommand() { michael@0: dbg.enabled = false; michael@0: return [undefined]; michael@0: } michael@0: michael@0: function continueCommand() { michael@0: if (focusedFrame === null) { michael@0: print("No stack."); michael@0: return; michael@0: } michael@0: return [undefined]; michael@0: } michael@0: michael@0: function throwCommand(rest) { michael@0: var v; michael@0: if (focusedFrame !== topFrame) { michael@0: print("To throw, you must select the newest frame (use 'frame 0')."); michael@0: return; michael@0: } else if (focusedFrame === null) { michael@0: print("No stack."); michael@0: return; michael@0: } else if (rest === '') { michael@0: return [{throw: lastExc}]; michael@0: } else { michael@0: var cv = saveExcursion(function () { return focusedFrame.eval(rest); }); michael@0: if (cv === null) { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Debuggee died while determining what to throw. Stopped."); michael@0: } else if ('return' in cv) { michael@0: return [{throw: cv.return}]; michael@0: } else { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Exception determining what to throw. Stopped."); michael@0: showDebuggeeValue(cv.throw); michael@0: } michael@0: return; michael@0: } michael@0: } michael@0: michael@0: function frameCommand(rest) { michael@0: var n, f; michael@0: if (rest.match(/[0-9]+/)) { michael@0: n = +rest; michael@0: f = topFrame; michael@0: if (f === null) { michael@0: print("No stack."); michael@0: return; michael@0: } michael@0: for (var i = 0; i < n && f; i++) { michael@0: if (!f.older) { michael@0: print("There is no frame " + rest + "."); michael@0: return; michael@0: } michael@0: f.older.younger = f; michael@0: f = f.older; michael@0: } michael@0: focusedFrame = f; michael@0: showFrame(f, n); michael@0: } else if (rest !== '') { michael@0: if (topFrame === null) michael@0: print("No stack."); michael@0: else michael@0: showFrame(); michael@0: } else { michael@0: print("do what now?"); michael@0: } michael@0: } michael@0: michael@0: function upCommand() { michael@0: if (focusedFrame === null) michael@0: print("No stack."); michael@0: else if (focusedFrame.older === null) michael@0: print("Initial frame selected; you cannot go up."); michael@0: else { michael@0: focusedFrame.older.younger = focusedFrame; michael@0: focusedFrame = focusedFrame.older; michael@0: showFrame(); michael@0: } michael@0: } michael@0: michael@0: function downCommand() { michael@0: if (focusedFrame === null) michael@0: print("No stack."); michael@0: else if (!focusedFrame.younger) michael@0: print("Youngest frame selected; you cannot go down."); michael@0: else { michael@0: focusedFrame = focusedFrame.younger; michael@0: showFrame(); michael@0: } michael@0: } michael@0: michael@0: function forcereturnCommand(rest) { michael@0: var v; michael@0: var f = focusedFrame; michael@0: if (f !== topFrame) { michael@0: print("To forcereturn, you must select the newest frame (use 'frame 0')."); michael@0: } else if (f === null) { michael@0: print("Nothing on the stack."); michael@0: } else if (rest === '') { michael@0: return [{return: undefined}]; michael@0: } else { michael@0: var cv = saveExcursion(function () { return f.eval(rest); }); michael@0: if (cv === null) { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Debuggee died while determining what to forcereturn. Stopped."); michael@0: } else if ('return' in cv) { michael@0: return [{return: cv.return}]; michael@0: } else { michael@0: if (!dbg.enabled) michael@0: return [cv]; michael@0: print("Error determining what to forcereturn. Stopped."); michael@0: showDebuggeeValue(cv.throw); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function printPop(f, c) { michael@0: var fdesc = f.fullDescription(); michael@0: if (c.return) { michael@0: print("frame returning (still selected): " + fdesc); michael@0: showDebuggeeValue(c.return); michael@0: } else if (c.throw) { michael@0: print("frame threw exception: " + fdesc); michael@0: showDebuggeeValue(c.throw); michael@0: print("(To rethrow it, type 'throw'.)"); michael@0: lastExc = c.throw; michael@0: } else { michael@0: print("frame was terminated: " + fdesc); michael@0: } michael@0: } michael@0: michael@0: // Set |prop| on |obj| to |value|, but then restore its current value michael@0: // when we next enter the repl. michael@0: function setUntilRepl(obj, prop, value) { michael@0: var saved = obj[prop]; michael@0: obj[prop] = value; michael@0: replCleanups.push(function () { obj[prop] = saved; }); michael@0: } michael@0: michael@0: function doStepOrNext(kind) { michael@0: var startFrame = topFrame; michael@0: var startLine = startFrame.line; michael@0: print("stepping in: " + startFrame.fullDescription()); michael@0: print("starting line: " + uneval(startLine)); michael@0: michael@0: function stepPopped(completion) { michael@0: // Note that we're popping this frame; we need to watch for michael@0: // subsequent step events on its caller. michael@0: this.reportedPop = true; michael@0: printPop(this, completion); michael@0: topFrame = focusedFrame = this; michael@0: return repl(); michael@0: } michael@0: michael@0: function stepEntered(newFrame) { michael@0: print("entered frame: " + newFrame.fullDescription()); michael@0: topFrame = focusedFrame = newFrame; michael@0: return repl(); michael@0: } michael@0: michael@0: function stepStepped() { michael@0: print("stepStepped: " + this.fullDescription()); michael@0: // If we've changed frame or line, then report that. michael@0: if (this !== startFrame || this.line != startLine) { michael@0: topFrame = focusedFrame = this; michael@0: if (focusedFrame != startFrame) michael@0: print(focusedFrame.fullDescription()); michael@0: return repl(); michael@0: } michael@0: michael@0: // Otherwise, let execution continue. michael@0: return undefined; michael@0: } michael@0: michael@0: if (kind.step) michael@0: setUntilRepl(dbg, 'onEnterFrame', stepEntered); michael@0: michael@0: // If we're stepping after an onPop, watch for steps and pops in the michael@0: // next-older frame; this one is done. michael@0: var stepFrame = startFrame.reportedPop ? startFrame.older : startFrame; michael@0: if (!stepFrame || !stepFrame.script) michael@0: stepFrame = null; michael@0: if (stepFrame) { michael@0: setUntilRepl(stepFrame, 'onStep', stepStepped); michael@0: setUntilRepl(stepFrame, 'onPop', stepPopped); michael@0: } michael@0: michael@0: // Let the program continue! michael@0: return [undefined]; michael@0: } michael@0: michael@0: function stepCommand() { return doStepOrNext({step:true}); } michael@0: function nextCommand() { return doStepOrNext({next:true}); } michael@0: michael@0: // Build the table of commands. michael@0: var commands = {}; michael@0: var commandArray = [ michael@0: backtraceCommand, "bt", "where", michael@0: continueCommand, "c", michael@0: detachCommand, michael@0: downCommand, "d", michael@0: forcereturnCommand, michael@0: frameCommand, "f", michael@0: nextCommand, "n", michael@0: printCommand, "p", michael@0: quitCommand, "q", michael@0: stepCommand, "s", michael@0: throwCommand, "t", michael@0: upCommand, "u", michael@0: helpCommand, "h", michael@0: evalCommand, "!", michael@0: ]; michael@0: var last = null; michael@0: for (var i = 0; i < commandArray.length; i++) { michael@0: var cmd = commandArray[i]; michael@0: if (typeof cmd === "string") michael@0: commands[cmd] = last; michael@0: else michael@0: last = commands[cmd.name.replace(/Command$/, '')] = cmd; michael@0: } michael@0: michael@0: function helpCommand(rest) { michael@0: print("Available commands:"); michael@0: var printcmd = function(group) { michael@0: print(" " + group.join(", ")); michael@0: } michael@0: michael@0: var group = []; michael@0: for (var cmd of commandArray) { michael@0: if (typeof cmd === "string") { michael@0: group.push(cmd); michael@0: } else { michael@0: if (group.length) printcmd(group); michael@0: group = [ cmd.name.replace(/Command$/, '') ]; michael@0: } michael@0: } michael@0: printcmd(group); michael@0: } michael@0: michael@0: // Break cmd into two parts: its first word and everything else. If it begins michael@0: // with punctuation, treat that as a separate word. michael@0: function breakcmd(cmd) { michael@0: cmd = cmd.trimLeft(); michael@0: if ("!@#$%^&*_+=/?.,<>:;'\"".indexOf(cmd.substr(0, 1)) != -1) michael@0: return [cmd.substr(0, 1), cmd.substr(1).trimLeft()]; michael@0: var m = /\s/.exec(cmd); michael@0: if (m === null) michael@0: return [cmd, '']; michael@0: return [cmd.slice(0, m.index), cmd.slice(m.index).trimLeft()]; michael@0: } michael@0: michael@0: function runcmd(cmd) { michael@0: var pieces = breakcmd(cmd); michael@0: if (pieces[0] === "") michael@0: return undefined; michael@0: michael@0: var first = pieces[0], rest = pieces[1]; michael@0: if (!commands.hasOwnProperty(first)) { michael@0: print("unrecognized command '" + first + "'"); michael@0: return undefined; michael@0: } michael@0: michael@0: var cmd = commands[first]; michael@0: if (cmd.length === 0 && rest !== '') { michael@0: print("this command cannot take an argument"); michael@0: return undefined; michael@0: } michael@0: michael@0: return cmd(rest); michael@0: } michael@0: michael@0: function repl() { michael@0: while (replCleanups.length > 0) michael@0: replCleanups.pop()(); michael@0: michael@0: var cmd; michael@0: for (;;) { michael@0: putstr("\n" + prompt); michael@0: cmd = readline(); michael@0: if (cmd === null) michael@0: return null; michael@0: michael@0: try { michael@0: var result = runcmd(cmd); michael@0: if (result === undefined) michael@0: ; // do nothing michael@0: else if (Array.isArray(result)) michael@0: return result[0]; michael@0: else michael@0: throw new Error("Internal error: result of runcmd wasn't array or undefined"); michael@0: } catch (exc) { michael@0: print("*** Internal error: exception in the debugger code."); michael@0: print(" " + exc); michael@0: print(exc.stack); michael@0: } michael@0: } michael@0: } michael@0: michael@0: var dbg = new Debugger(); michael@0: dbg.onDebuggerStatement = function (frame) { michael@0: return saveExcursion(function () { michael@0: topFrame = focusedFrame = frame; michael@0: print("'debugger' statement hit."); michael@0: showFrame(); michael@0: return repl(); michael@0: }); michael@0: }; michael@0: dbg.onThrow = function (frame, exc) { michael@0: return saveExcursion(function () { michael@0: topFrame = focusedFrame = frame; michael@0: print("Unwinding due to exception. (Type 'c' to continue unwinding.)"); michael@0: showFrame(); michael@0: print("Exception value is:"); michael@0: showDebuggeeValue(exc); michael@0: return repl(); michael@0: }); michael@0: }; michael@0: michael@0: // The depth of jorendb nesting. michael@0: var jorendbDepth; michael@0: if (typeof jorendbDepth == 'undefined') jorendbDepth = 0; michael@0: michael@0: var debuggeeGlobal = newGlobal("new-compartment"); michael@0: debuggeeGlobal.jorendbDepth = jorendbDepth + 1; michael@0: var debuggeeGlobalWrapper = dbg.addDebuggee(debuggeeGlobal); michael@0: michael@0: print("jorendb version -0.0"); michael@0: prompt = '(' + Array(jorendbDepth+1).join('meta-') + 'jorendb) '; michael@0: michael@0: var args = arguments; michael@0: while(args.length > 0) { michael@0: var arg = args.shift(); michael@0: if (arg == '-f') { michael@0: arg = args.shift(); michael@0: debuggeeGlobal.evaluate(read(arg), { fileName: arg, lineNumber: 1 }); michael@0: } else if (arg == '-e') { michael@0: arg = args.shift(); michael@0: debuggeeGlobal.eval(arg); michael@0: } else { michael@0: throw("jorendb does not implement command-line argument '" + arg + "'"); michael@0: } michael@0: } michael@0: michael@0: repl();