/* -*- 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 || '') + "(" + 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 || '') + "(" + 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