forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			894 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			894 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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);
 | 
