diff options
Diffstat (limited to 'js/examples/jorendb.js')
-rw-r--r-- | js/examples/jorendb.js | 894 |
1 files changed, 894 insertions, 0 deletions
diff --git a/js/examples/jorendb.js b/js/examples/jorendb.js new file mode 100644 index 0000000000..33d6c27316 --- /dev/null +++ b/js/examples/jorendb.js @@ -0,0 +1,894 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- + * vim: set ts=8 sw=4 et tw=78: + * + * jorendb - A toy command-line debugger for shell-js programs. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * jorendb is a simple command-line debugger for shell-js programs. It is + * intended as a demo of the Debugger object (as there are no shell js programs + * to speak of). + * + * To run it: $JS -d path/to/this/file/jorendb.js + * To run some JS code under it, try: + * (jorendb) print load("my-script-to-debug.js") + * Execution will stop at debugger statements and you'll get a jorendb prompt. + */ + +// Debugger state. +var focusedFrame = null; +var topFrame = null; +var debuggeeValues = {}; +var nextDebuggeeValueIndex = 1; +var lastExc = null; +var todo = []; +var activeTask; +var options = { 'pretty': true, + 'emacs': !!os.getenv('INSIDE_EMACS') }; +var rerun = true; + +// Cleanup functions to run when we next re-enter the repl. +var replCleanups = []; + +// Redirect debugger printing functions to go to the original output +// destination, unaffected by any redirects done by the debugged script. +var initialOut = os.file.redirect(); +var initialErr = os.file.redirectErr(); + +function wrap(global, name) { + var orig = global[name]; + global[name] = function(...args) { + + var oldOut = os.file.redirect(initialOut); + var oldErr = os.file.redirectErr(initialErr); + try { + return orig.apply(global, args); + } finally { + os.file.redirect(oldOut); + os.file.redirectErr(oldErr); + } + }; +} +wrap(this, 'print'); +wrap(this, 'printErr'); +wrap(this, 'putstr'); + +// Convert a debuggee value v to a string. +function dvToString(v) { + if (typeof(v) === 'object' && v !== null) { + return `[object ${v.class}]`; + } + const s = uneval(v); + if (s.length > 400) { + return s.substr(0, 400) + "...<" + (s.length - 400) + " more bytes>..."; + } + return s; +} + +function summaryObject(dv) { + var obj = {}; + for (var name of dv.getOwnPropertyNames()) { + var v = dv.getOwnPropertyDescriptor(name).value; + if (v instanceof Debugger.Object) { + v = "(...)"; + } + obj[name] = v; + } + return obj; +} + +function debuggeeValueToString(dv, style) { + var dvrepr = dvToString(dv); + if (!style.pretty || (typeof dv !== 'object') || (dv === null)) + return [dvrepr, undefined]; + + const exec = debuggeeGlobalWrapper.executeInGlobalWithBindings.bind(debuggeeGlobalWrapper); + + if (dv.class == "Error") { + let errval = exec("$$.toString()", debuggeeValues); + return [dvrepr, errval.return]; + } + + if (style.brief) + return [dvrepr, JSON.stringify(summaryObject(dv), null, 4)]; + + let str = exec("JSON.stringify(v, null, 4)", {v: dv}); + if ('throw' in str) { + if (style.noerror) + return [dvrepr, undefined]; + + let substyle = {}; + Object.assign(substyle, style); + substyle.noerror = true; + return [dvrepr, debuggeeValueToString(str.throw, substyle)]; + } + + return [dvrepr, str.return]; +} + +// Problem! Used to do [object Object] followed by details. Now just details? + +function showDebuggeeValue(dv, style={pretty: options.pretty}) { + var i = nextDebuggeeValueIndex++; + debuggeeValues["$" + i] = dv; + debuggeeValues["$$"] = dv; + let [brief, full] = debuggeeValueToString(dv, style); + print("$" + i + " = " + brief); + if (full !== undefined) + print(full); +} + +Object.defineProperty(Debugger.Frame.prototype, "num", { + configurable: true, + enumerable: false, + get: function () { + var i = 0; + for (var f = topFrame; f && f !== this; f = f.older) + i++; + return f === null ? undefined : i; + } + }); + +Debugger.Frame.prototype.frameDescription = function frameDescription() { + if (this.type == "call") + return ((this.callee.name || '<anonymous>') + + "(" + this.arguments.map(dvToString).join(", ") + ")"); + else + return this.type + " code"; +} + +Debugger.Frame.prototype.positionDescription = function positionDescription() { + if (this.script) { + var line = this.script.getOffsetLocation(this.offset).lineNumber; + if (this.script.url) + return this.script.url + ":" + line; + return "line " + line; + } + return null; +} + +Debugger.Frame.prototype.location = function () { + if (this.script) { + var { lineNumber, columnNumber, isEntryPoint } = this.script.getOffsetLocation(this.offset); + if (this.script.url) + return this.script.url + ":" + lineNumber; + return null; + } + return null; +} + +Debugger.Frame.prototype.fullDescription = function fullDescription() { + var fr = this.frameDescription(); + var pos = this.positionDescription(); + if (pos) + return fr + ", " + pos; + return fr; +} + +Object.defineProperty(Debugger.Frame.prototype, "line", { + configurable: true, + enumerable: false, + get: function() { + if (this.script) + return this.script.getOffsetLocation(this.offset).lineNumber; + else + return null; + } + }); + +function callDescription(f) { + return ((f.callee.name || '<anonymous>') + + "(" + f.arguments.map(dvToString).join(", ") + ")"); +} + +function showFrame(f, n) { + if (f === undefined || f === null) { + f = focusedFrame; + if (f === null) { + print("No stack."); + return; + } + } + if (n === undefined) { + n = f.num; + if (n === undefined) + throw new Error("Internal error: frame not on stack"); + } + + print('#' + n + " " + f.fullDescription()); +} + +function saveExcursion(fn) { + var tf = topFrame, ff = focusedFrame; + try { + return fn(); + } finally { + topFrame = tf; + focusedFrame = ff; + } +} + +function parseArgs(str) { + return str.split(" "); +} + +function describedRv(r, desc) { + desc = "[" + desc + "] "; + if (r === undefined) { + print(desc + "Returning undefined"); + } else if (r === null) { + print(desc + "Returning null"); + } else if (r.length === undefined) { + print(desc + "Returning object " + JSON.stringify(r)); + } else { + print(desc + "Returning length-" + r.length + " list"); + if (r.length > 0) { + print(" " + r[0]); + } + } + return r; +} + +// Rerun the program (reloading it from the file) +function runCommand(args) { + print(`Restarting program (${args})`); + if (args) + activeTask.scriptArgs = parseArgs(args); + else + activeTask.scriptArgs = [...actualScriptArgs]; + rerun = true; + for (var f = topFrame; f; f = f.older) { + if (f.older) { + f.onPop = () => null; + } else { + f.onPop = () => ({ 'return': 0 }); + } + } + //return describedRv([{ 'return': 0 }], "runCommand"); + return null; +} + +// Evaluate an expression in the Debugger global +function evalCommand(expr) { + eval(expr); +} + +function quitCommand() { + dbg.removeAllDebuggees(); + quit(0); +} + +function backtraceCommand() { + if (topFrame === null) + print("No stack."); + for (var i = 0, f = topFrame; f; i++, f = f.older) + showFrame(f, i); +} + +function setCommand(rest) { + var space = rest.indexOf(' '); + if (space == -1) { + print("Invalid set <option> <value> command"); + } else { + var name = rest.substr(0, space); + var value = rest.substr(space + 1); + + if (name == 'args') { + activeTask.scriptArgs = parseArgs(value); + } else { + var yes = ["1", "yes", "true", "on"]; + var no = ["0", "no", "false", "off"]; + + if (yes.includes(value)) + options[name] = true; + else if (no.includes(value)) + options[name] = false; + else + options[name] = value; + } + } +} + +function split_print_options(s, style) { + var m = /^\/(\w+)/.exec(s); + if (!m) + return [ s, style ]; + if (m[1].includes("p")) + style.pretty = true; + if (m[1].includes("b")) + style.brief = true; + return [ s.substr(m[0].length).trimLeft(), style ]; +} + +function doPrint(expr, style) { + // This is the real deal. + var cv = saveExcursion( + () => focusedFrame == null + ? debuggeeGlobalWrapper.executeInGlobalWithBindings(expr, debuggeeValues) + : focusedFrame.evalWithBindings(expr, debuggeeValues)); + if (cv === null) { + print("Debuggee died."); + } else if ('return' in cv) { + showDebuggeeValue(cv.return, style); + } else { + print("Exception caught. (To rethrow it, type 'throw'.)"); + lastExc = cv.throw; + showDebuggeeValue(lastExc, style); + } +} + +function printCommand(rest) { + var [expr, style] = split_print_options(rest, {pretty: options.pretty}); + return doPrint(expr, style); +} + +function keysCommand(rest) { return doPrint("Object.keys(" + rest + ")"); } + +function detachCommand() { + dbg.removeAllDebuggees(); + return [undefined]; +} + +function continueCommand(rest) { + if (focusedFrame === null) { + print("No stack."); + return; + } + + var match = rest.match(/^(\d+)$/); + if (match) { + return doStepOrNext({upto:true, stopLine:match[1]}); + } + + return [undefined]; +} + +function throwCommand(rest) { + var v; + if (focusedFrame !== topFrame) { + print("To throw, you must select the newest frame (use 'frame 0')."); + return; + } else if (focusedFrame === null) { + print("No stack."); + return; + } else if (rest === '') { + return [{throw: lastExc}]; + } else { + var cv = saveExcursion(function () { return focusedFrame.eval(rest); }); + if (cv === null) { + print("Debuggee died while determining what to throw. Stopped."); + } else if ('return' in cv) { + return [{throw: cv.return}]; + } else { + print("Exception determining what to throw. Stopped."); + showDebuggeeValue(cv.throw); + } + return; + } +} + +function frameCommand(rest) { + var n, f; + if (rest.match(/[0-9]+/)) { + n = +rest; + f = topFrame; + if (f === null) { + print("No stack."); + return; + } + for (var i = 0; i < n && f; i++) { + if (!f.older) { + print("There is no frame " + rest + "."); + return; + } + f.older.younger = f; + f = f.older; + } + focusedFrame = f; + updateLocation(focusedFrame); + showFrame(f, n); + } else if (rest === '') { + if (topFrame === null) { + print("No stack."); + } else { + updateLocation(focusedFrame); + showFrame(); + } + } else { + print("do what now?"); + } +} + +function upCommand() { + if (focusedFrame === null) + print("No stack."); + else if (focusedFrame.older === null) + print("Initial frame selected; you cannot go up."); + else { + focusedFrame.older.younger = focusedFrame; + focusedFrame = focusedFrame.older; + updateLocation(focusedFrame); + showFrame(); + } +} + +function downCommand() { + if (focusedFrame === null) + print("No stack."); + else if (!focusedFrame.younger) + print("Youngest frame selected; you cannot go down."); + else { + focusedFrame = focusedFrame.younger; + updateLocation(focusedFrame); + showFrame(); + } +} + +function forcereturnCommand(rest) { + var v; + var f = focusedFrame; + if (f !== topFrame) { + print("To forcereturn, you must select the newest frame (use 'frame 0')."); + } else if (f === null) { + print("Nothing on the stack."); + } else if (rest === '') { + return [{return: undefined}]; + } else { + var cv = saveExcursion(function () { return f.eval(rest); }); + if (cv === null) { + print("Debuggee died while determining what to forcereturn. Stopped."); + } else if ('return' in cv) { + return [{return: cv.return}]; + } else { + print("Error determining what to forcereturn. Stopped."); + showDebuggeeValue(cv.throw); + } + } +} + +function printPop(f, c) { + var fdesc = f.fullDescription(); + if (c.return) { + print("frame returning (still selected): " + fdesc); + showDebuggeeValue(c.return, {brief: true}); + } else if (c.throw) { + print("frame threw exception: " + fdesc); + showDebuggeeValue(c.throw); + print("(To rethrow it, type 'throw'.)"); + lastExc = c.throw; + } else { + print("frame was terminated: " + fdesc); + } +} + +// Set |prop| on |obj| to |value|, but then restore its current value +// when we next enter the repl. +function setUntilRepl(obj, prop, value) { + var saved = obj[prop]; + obj[prop] = value; + replCleanups.push(function () { obj[prop] = saved; }); +} + +function updateLocation(frame) { + if (options.emacs) { + var loc = frame.location(); + if (loc) + print("\032\032" + loc + ":1"); + } +} + +function doStepOrNext(kind) { + var startFrame = topFrame; + var startLine = startFrame.line; + // print("stepping in: " + startFrame.fullDescription()); + // print("starting line: " + uneval(startLine)); + + function stepPopped(completion) { + // Note that we're popping this frame; we need to watch for + // subsequent step events on its caller. + this.reportedPop = true; + printPop(this, completion); + topFrame = focusedFrame = this; + if (kind.finish) { + // We want to continue, but this frame is going to be invalid as + // soon as this function returns, which will make the replCleanups + // assert when it tries to access the dead frame's 'onPop' + // property. So clear it out now while the frame is still valid, + // and trade it for an 'onStep' callback on the frame we're popping to. + preReplCleanups(); + setUntilRepl(this.older, 'onStep', stepStepped); + return undefined; + } + updateLocation(this); + return repl(); + } + + function stepEntered(newFrame) { + print("entered frame: " + newFrame.fullDescription()); + updateLocation(newFrame); + topFrame = focusedFrame = newFrame; + return repl(); + } + + function stepStepped() { + // print("stepStepped: " + this.fullDescription()); + updateLocation(this); + var stop = false; + + if (kind.finish) { + // 'finish' set a one-time onStep for stopping at the frame it + // wants to return to + stop = true; + } else if (kind.upto) { + // running until a given line is reached + if (this.line == kind.stopLine) + stop = true; + } else { + // regular step; stop whenever the line number changes + if ((this.line != startLine) || (this != startFrame)) + stop = true; + } + + if (stop) { + topFrame = focusedFrame = this; + if (focusedFrame != startFrame) + print(focusedFrame.fullDescription()); + return repl(); + } + + // Otherwise, let execution continue. + return undefined; + } + + if (kind.step) + setUntilRepl(dbg, 'onEnterFrame', stepEntered); + + // If we're stepping after an onPop, watch for steps and pops in the + // next-older frame; this one is done. + var stepFrame = startFrame.reportedPop ? startFrame.older : startFrame; + if (!stepFrame || !stepFrame.script) + stepFrame = null; + if (stepFrame) { + if (!kind.finish) + setUntilRepl(stepFrame, 'onStep', stepStepped); + setUntilRepl(stepFrame, 'onPop', stepPopped); + } + + // Let the program continue! + return [undefined]; +} + +function stepCommand() { return doStepOrNext({step:true}); } +function nextCommand() { return doStepOrNext({next:true}); } +function finishCommand() { return doStepOrNext({finish:true}); } + +// FIXME: DOES NOT WORK YET +function breakpointCommand(where) { + print("Sorry, breakpoints don't work yet."); + var script = focusedFrame.script; + var offsets = script.getLineOffsets(Number(where)); + if (offsets.length == 0) { + print("Unable to break at line " + where); + return; + } + for (var offset of offsets) { + script.setBreakpoint(offset, { hit: handleBreakpoint }); + } + print("Set breakpoint in " + script.url + ":" + script.startLine + " at line " + where + ", " + offsets.length); +} + +// Build the table of commands. +var commands = {}; +var commandArray = [ + backtraceCommand, "bt", "where", + breakpointCommand, "b", "break", + continueCommand, "c", + detachCommand, + downCommand, "d", + evalCommand, "!", + forcereturnCommand, + frameCommand, "f", + finishCommand, "fin", + nextCommand, "n", + printCommand, "p", + keysCommand, "k", + quitCommand, "q", + runCommand, "run", + stepCommand, "s", + setCommand, + throwCommand, "t", + upCommand, "u", + helpCommand, "h", +]; +var currentCmd = null; +for (var i = 0; i < commandArray.length; i++) { + var cmd = commandArray[i]; + if (typeof cmd === "string") + commands[cmd] = currentCmd; + else + currentCmd = commands[cmd.name.replace(/Command$/, '')] = cmd; +} + +function helpCommand(rest) { + print("Available commands:"); + var printcmd = function(group) { + print(" " + group.join(", ")); + } + + var group = []; + for (var cmd of commandArray) { + if (typeof cmd === "string") { + group.push(cmd); + } else { + if (group.length) printcmd(group); + group = [ cmd.name.replace(/Command$/, '') ]; + } + } + printcmd(group); +} + +// Break cmd into two parts: its first word and everything else. If it begins +// with punctuation, treat that as a separate word. The first word is +// terminated with whitespace or the '/' character. So: +// +// print x => ['print', 'x'] +// print => ['print', ''] +// !print x => ['!', 'print x'] +// ?!wtf!? => ['?', '!wtf!?'] +// print/b x => ['print', '/b x'] +// +function breakcmd(cmd) { + cmd = cmd.trimLeft(); + if ("!@#$%^&*_+=/?.,<>:;'\"".includes(cmd.substr(0, 1))) + return [cmd.substr(0, 1), cmd.substr(1).trimLeft()]; + var m = /\s+|(?=\/)/.exec(cmd); + if (m === null) + return [cmd, '']; + return [cmd.slice(0, m.index), cmd.slice(m.index + m[0].length)]; +} + +function runcmd(cmd) { + var pieces = breakcmd(cmd); + if (pieces[0] === "") + return undefined; + + var first = pieces[0], rest = pieces[1]; + if (!commands.hasOwnProperty(first)) { + print("unrecognized command '" + first + "'"); + return undefined; + } + + var cmd = commands[first]; + if (cmd.length === 0 && rest !== '') { + print("this command cannot take an argument"); + return undefined; + } + + return cmd(rest); +} + +function preReplCleanups() { + while (replCleanups.length > 0) + replCleanups.pop()(); +} + +var prevcmd = undefined; +function repl() { + preReplCleanups(); + + var cmd; + for (;;) { + putstr("\n" + prompt); + cmd = readline(); + if (cmd === null) + return null; + else if (cmd === "") + cmd = prevcmd; + + try { + prevcmd = cmd; + var result = runcmd(cmd); + if (result === undefined) + ; // do nothing, return to prompt + else if (Array.isArray(result)) + return result[0]; + else if (result === null) + return null; + else + throw new Error("Internal error: result of runcmd wasn't array or undefined: " + result); + } catch (exc) { + print("*** Internal error: exception in the debugger code."); + print(" " + exc); + print(exc.stack); + } + } +} + +var dbg = new Debugger(); +dbg.onDebuggerStatement = function (frame) { + return saveExcursion(function () { + topFrame = focusedFrame = frame; + print("'debugger' statement hit."); + showFrame(); + updateLocation(focusedFrame); + backtrace(); + return describedRv(repl(), "debugger.saveExc"); + }); +}; +dbg.onThrow = function (frame, exc) { + return saveExcursion(function () { + topFrame = focusedFrame = frame; + print("Unwinding due to exception. (Type 'c' to continue unwinding.)"); + showFrame(); + print("Exception value is:"); + showDebuggeeValue(exc); + return repl(); + }); +}; + +function handleBreakpoint (frame) { + print("Breakpoint hit!"); + return saveExcursion(() => { + topFrame = focusedFrame = frame; + print("breakpoint hit."); + showFrame(); + updateLocation(focusedFrame); + return repl(); + }); +}; + +// The depth of jorendb nesting. +var jorendbDepth; +if (typeof jorendbDepth == 'undefined') jorendbDepth = 0; + +var debuggeeGlobal = newGlobal({newCompartment: true}); +debuggeeGlobal.jorendbDepth = jorendbDepth + 1; +var debuggeeGlobalWrapper = dbg.addDebuggee(debuggeeGlobal); + +print("jorendb version -0.0"); +prompt = '(' + Array(jorendbDepth+1).join('meta-') + 'jorendb) '; + +var args = scriptArgs.slice(0); +print("INITIAL ARGS: " + args); + +// Find the script to run and its arguments. The script may have been given as +// a plain script name, in which case all remaining arguments belong to the +// script. Or there may have been any number of arguments to the JS shell, +// followed by -f scriptName, followed by additional arguments to the JS shell, +// followed by the script arguments. There may be multiple -e or -f options in +// the JS shell arguments, and we want to treat each one as a debuggable +// script. +// +// The difficulty is that the JS shell has a mixture of +// +// --boolean +// +// and +// +// --value VAL +// +// parameters, and there's no way to know whether --option takes an argument or +// not. We will assume that VAL will never end in .js, or rather that the first +// argument that does not start with "-" but does end in ".js" is the name of +// the script. +// +// If you need to pass other options and not have them given to the script, +// pass them before the -f jorendb.js argument. Thus, the safe ways to pass +// arguments are: +// +// js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)+ -- [script args] +// js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)* script.js [script args] +// +// Additionally, if you want to run a script that is *NOT* debugged, put it in +// as part of the leading [JS shell options]. + + +// Compute actualScriptArgs by finding the script to be run and grabbing every +// non-script argument. The script may be given by -f scriptname or just plain +// scriptname. In the latter case, it will be in the global variable +// 'scriptPath' (and NOT in scriptArgs.) +var actualScriptArgs = []; +var scriptSeen; + +if (scriptPath !== undefined) { + todo.push({ + 'action': 'load', + 'script': scriptPath, + }); + scriptSeen = true; +} + +while(args.length > 0) { + var arg = args.shift(); + print("arg: " + arg); + if (arg == '-e') { + print(" eval"); + todo.push({ + 'action': 'eval', + 'code': args.shift() + }); + } else if (arg == '-f') { + var script = args.shift(); + print(" load -f " + script); + scriptSeen = true; + todo.push({ + 'action': 'load', + 'script': script, + }); + } else if (arg.indexOf("-") == 0) { + if (arg == '--') { + print(" pass remaining args to script"); + actualScriptArgs.push(...args); + break; + } else if ((args.length > 0) && (args[0].indexOf(".js") + 3 == args[0].length)) { + // Ends with .js, assume we are looking at --boolean script.js + print(" load script.js after --boolean"); + todo.push({ + 'action': 'load', + 'script': args.shift(), + }); + scriptSeen = true; + } else { + // Does not end with .js, assume we are looking at JS shell arg + // --value VAL + print(" ignore"); + args.shift(); + } + } else { + if (!scriptSeen) { + print(" load general"); + actualScriptArgs.push(...args); + todo.push({ + 'action': 'load', + 'script': arg, + }); + break; + } else { + print(" arg " + arg); + actualScriptArgs.push(arg); + } + } +} +print("jorendb: scriptPath = " + scriptPath); +print("jorendb: scriptArgs = " + scriptArgs); +print("jorendb: actualScriptArgs = " + actualScriptArgs); + +for (var task of todo) { + task['scriptArgs'] = [...actualScriptArgs]; +} + +// Always drop into a repl at the end. Especially if the main script throws an +// exception. +todo.push({ 'action': 'repl' }); + +while (rerun) { + print("Top of run loop"); + rerun = false; + for (var task of todo) { + activeTask = task; + if (task.action == 'eval') { + debuggeeGlobal.eval(task.code); + } else if (task.action == 'load') { + debuggeeGlobal['scriptArgs'] = task.scriptArgs; + debuggeeGlobal['scriptPath'] = task.script; + print("Loading JavaScript file " + task.script); + try { + debuggeeGlobal.evaluate(read(task.script), { 'fileName': task.script, 'lineNumber': 1 }); + } catch (exc) { + print("Caught exception " + exc); + print(exc.stack); + break; + } + } else if (task.action == 'repl') { + repl(); + } + if (rerun) + break; + } +} + +quit(0); |