1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * vim: set ts=8 sw=4 et tw=78:
4 * jorendb - A toy command-line debugger for shell-js programs.
6 * This Source Code Form is subject to the terms of the Mozilla Public
7 * License, v. 2.0. If a copy of the MPL was not distributed with this
8 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
12 * jorendb is a simple command-line debugger for shell-js programs. It is
13 * intended as a demo of the Debugger object (as there are no shell js programs
16 * To run it: $JS -d path/to/this/file/jorendb.js
17 * To run some JS code under it, try:
18 * (jorendb) print load("my-script-to-debug.js")
19 * Execution will stop at debugger statements and you'll get a jorendb prompt.
23 var focusedFrame = null;
25 var debuggeeValues = {};
26 var nextDebuggeeValueIndex = 1;
29 // Cleanup functions to run when we next re-enter the repl.
30 var replCleanups = [];
32 // Convert a debuggee value v to a string.
33 function dvToString(v) {
34 return (typeof v !== 'object' || v === null) ? uneval(v) : "[object " + v.class + "]";
37 function showDebuggeeValue(dv) {
38 var dvrepr = dvToString(dv);
39 var i = nextDebuggeeValueIndex++;
40 debuggeeValues["$" + i] = dv;
41 print("$" + i + " = " + dvrepr);
44 Object.defineProperty(Debugger.Frame.prototype, "num", {
49 for (var f = topFrame; f && f !== this; f = f.older)
51 return f === null ? undefined : i;
55 Debugger.Frame.prototype.frameDescription = function frameDescription() {
56 if (this.type == "call")
57 return ((this.callee.name || '<anonymous>') +
58 "(" + this.arguments.map(dvToString).join(", ") + ")");
60 return this.type + " code";
63 Debugger.Frame.prototype.positionDescription = function positionDescription() {
65 var line = this.script.getOffsetLine(this.offset);
67 return this.script.url + ":" + line;
68 return "line " + line;
73 Debugger.Frame.prototype.fullDescription = function fullDescription() {
74 var fr = this.frameDescription();
75 var pos = this.positionDescription();
77 return fr + ", " + pos;
81 Object.defineProperty(Debugger.Frame.prototype, "line", {
86 return this.script.getOffsetLine(this.offset);
92 function callDescription(f) {
93 return ((f.callee.name || '<anonymous>') +
94 "(" + f.arguments.map(dvToString).join(", ") + ")");
97 function showFrame(f, n) {
98 if (f === undefined || f === null) {
105 if (n === undefined) {
108 throw new Error("Internal error: frame not on stack");
111 print('#' + n + " " + f.fullDescription());
114 function saveExcursion(fn) {
115 var tf = topFrame, ff = focusedFrame;
124 // Evaluate an expression in the Debugger global
125 function evalCommand(expr) {
129 function quitCommand() {
134 function backtraceCommand() {
135 if (topFrame === null)
137 for (var i = 0, f = topFrame; f; i++, f = f.older)
141 function printCommand(rest) {
142 // This is the real deal.
143 var cv = saveExcursion(
144 () => focusedFrame == null
145 ? debuggeeGlobalWrapper.evalInGlobalWithBindings(rest, debuggeeValues)
146 : focusedFrame.evalWithBindings(rest, debuggeeValues));
150 print("Debuggee died.");
151 } else if ('return' in cv) {
154 showDebuggeeValue(cv.return);
158 print("Exception caught. (To rethrow it, type 'throw'.)");
160 showDebuggeeValue(lastExc);
164 function detachCommand() {
169 function continueCommand() {
170 if (focusedFrame === null) {
177 function throwCommand(rest) {
179 if (focusedFrame !== topFrame) {
180 print("To throw, you must select the newest frame (use 'frame 0').");
182 } else if (focusedFrame === null) {
185 } else if (rest === '') {
186 return [{throw: lastExc}];
188 var cv = saveExcursion(function () { return focusedFrame.eval(rest); });
192 print("Debuggee died while determining what to throw. Stopped.");
193 } else if ('return' in cv) {
194 return [{throw: cv.return}];
198 print("Exception determining what to throw. Stopped.");
199 showDebuggeeValue(cv.throw);
205 function frameCommand(rest) {
207 if (rest.match(/[0-9]+/)) {
214 for (var i = 0; i < n && f; i++) {
216 print("There is no frame " + rest + ".");
224 } else if (rest !== '') {
225 if (topFrame === null)
230 print("do what now?");
234 function upCommand() {
235 if (focusedFrame === null)
237 else if (focusedFrame.older === null)
238 print("Initial frame selected; you cannot go up.");
240 focusedFrame.older.younger = focusedFrame;
241 focusedFrame = focusedFrame.older;
246 function downCommand() {
247 if (focusedFrame === null)
249 else if (!focusedFrame.younger)
250 print("Youngest frame selected; you cannot go down.");
252 focusedFrame = focusedFrame.younger;
257 function forcereturnCommand(rest) {
259 var f = focusedFrame;
260 if (f !== topFrame) {
261 print("To forcereturn, you must select the newest frame (use 'frame 0').");
262 } else if (f === null) {
263 print("Nothing on the stack.");
264 } else if (rest === '') {
265 return [{return: undefined}];
267 var cv = saveExcursion(function () { return f.eval(rest); });
271 print("Debuggee died while determining what to forcereturn. Stopped.");
272 } else if ('return' in cv) {
273 return [{return: cv.return}];
277 print("Error determining what to forcereturn. Stopped.");
278 showDebuggeeValue(cv.throw);
283 function printPop(f, c) {
284 var fdesc = f.fullDescription();
286 print("frame returning (still selected): " + fdesc);
287 showDebuggeeValue(c.return);
288 } else if (c.throw) {
289 print("frame threw exception: " + fdesc);
290 showDebuggeeValue(c.throw);
291 print("(To rethrow it, type 'throw'.)");
294 print("frame was terminated: " + fdesc);
298 // Set |prop| on |obj| to |value|, but then restore its current value
299 // when we next enter the repl.
300 function setUntilRepl(obj, prop, value) {
301 var saved = obj[prop];
303 replCleanups.push(function () { obj[prop] = saved; });
306 function doStepOrNext(kind) {
307 var startFrame = topFrame;
308 var startLine = startFrame.line;
309 print("stepping in: " + startFrame.fullDescription());
310 print("starting line: " + uneval(startLine));
312 function stepPopped(completion) {
313 // Note that we're popping this frame; we need to watch for
314 // subsequent step events on its caller.
315 this.reportedPop = true;
316 printPop(this, completion);
317 topFrame = focusedFrame = this;
321 function stepEntered(newFrame) {
322 print("entered frame: " + newFrame.fullDescription());
323 topFrame = focusedFrame = newFrame;
327 function stepStepped() {
328 print("stepStepped: " + this.fullDescription());
329 // If we've changed frame or line, then report that.
330 if (this !== startFrame || this.line != startLine) {
331 topFrame = focusedFrame = this;
332 if (focusedFrame != startFrame)
333 print(focusedFrame.fullDescription());
337 // Otherwise, let execution continue.
342 setUntilRepl(dbg, 'onEnterFrame', stepEntered);
344 // If we're stepping after an onPop, watch for steps and pops in the
345 // next-older frame; this one is done.
346 var stepFrame = startFrame.reportedPop ? startFrame.older : startFrame;
347 if (!stepFrame || !stepFrame.script)
350 setUntilRepl(stepFrame, 'onStep', stepStepped);
351 setUntilRepl(stepFrame, 'onPop', stepPopped);
354 // Let the program continue!
358 function stepCommand() { return doStepOrNext({step:true}); }
359 function nextCommand() { return doStepOrNext({next:true}); }
361 // Build the table of commands.
364 backtraceCommand, "bt", "where",
365 continueCommand, "c",
380 for (var i = 0; i < commandArray.length; i++) {
381 var cmd = commandArray[i];
382 if (typeof cmd === "string")
383 commands[cmd] = last;
385 last = commands[cmd.name.replace(/Command$/, '')] = cmd;
388 function helpCommand(rest) {
389 print("Available commands:");
390 var printcmd = function(group) {
391 print(" " + group.join(", "));
395 for (var cmd of commandArray) {
396 if (typeof cmd === "string") {
399 if (group.length) printcmd(group);
400 group = [ cmd.name.replace(/Command$/, '') ];
406 // Break cmd into two parts: its first word and everything else. If it begins
407 // with punctuation, treat that as a separate word.
408 function breakcmd(cmd) {
409 cmd = cmd.trimLeft();
410 if ("!@#$%^&*_+=/?.,<>:;'\"".indexOf(cmd.substr(0, 1)) != -1)
411 return [cmd.substr(0, 1), cmd.substr(1).trimLeft()];
412 var m = /\s/.exec(cmd);
415 return [cmd.slice(0, m.index), cmd.slice(m.index).trimLeft()];
418 function runcmd(cmd) {
419 var pieces = breakcmd(cmd);
420 if (pieces[0] === "")
423 var first = pieces[0], rest = pieces[1];
424 if (!commands.hasOwnProperty(first)) {
425 print("unrecognized command '" + first + "'");
429 var cmd = commands[first];
430 if (cmd.length === 0 && rest !== '') {
431 print("this command cannot take an argument");
439 while (replCleanups.length > 0)
440 replCleanups.pop()();
444 putstr("\n" + prompt);
450 var result = runcmd(cmd);
451 if (result === undefined)
453 else if (Array.isArray(result))
456 throw new Error("Internal error: result of runcmd wasn't array or undefined");
458 print("*** Internal error: exception in the debugger code.");
465 var dbg = new Debugger();
466 dbg.onDebuggerStatement = function (frame) {
467 return saveExcursion(function () {
468 topFrame = focusedFrame = frame;
469 print("'debugger' statement hit.");
474 dbg.onThrow = function (frame, exc) {
475 return saveExcursion(function () {
476 topFrame = focusedFrame = frame;
477 print("Unwinding due to exception. (Type 'c' to continue unwinding.)");
479 print("Exception value is:");
480 showDebuggeeValue(exc);
485 // The depth of jorendb nesting.
487 if (typeof jorendbDepth == 'undefined') jorendbDepth = 0;
489 var debuggeeGlobal = newGlobal("new-compartment");
490 debuggeeGlobal.jorendbDepth = jorendbDepth + 1;
491 var debuggeeGlobalWrapper = dbg.addDebuggee(debuggeeGlobal);
493 print("jorendb version -0.0");
494 prompt = '(' + Array(jorendbDepth+1).join('meta-') + 'jorendb) ';
496 var args = arguments;
497 while(args.length > 0) {
498 var arg = args.shift();
501 debuggeeGlobal.evaluate(read(arg), { fileName: arg, lineNumber: 1 });
502 } else if (arg == '-e') {
504 debuggeeGlobal.eval(arg);
506 throw("jorendb does not implement command-line argument '" + arg + "'");