1 /* -*- indent-tabs-mode: nil; js-indent-level: 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;
30 var options
= { 'pretty': true,
31 'emacs': !!os
.getenv('INSIDE_EMACS') };
34 // Cleanup functions to run when we next re-enter the repl.
35 var replCleanups
= [];
37 // Redirect debugger printing functions to go to the original output
38 // destination, unaffected by any redirects done by the debugged script.
39 var initialOut
= os
.file
.redirect();
40 var initialErr
= os
.file
.redirectErr();
42 function wrap(global
, name
) {
43 var orig
= global
[name
];
44 global
[name
] = function(...args
) {
46 var oldOut
= os
.file
.redirect(initialOut
);
47 var oldErr
= os
.file
.redirectErr(initialErr
);
49 return orig
.apply(global
, args
);
51 os
.file
.redirect(oldOut
);
52 os
.file
.redirectErr(oldErr
);
57 wrap(this, 'printErr');
60 // Convert a debuggee value v to a string.
61 function dvToString(v
) {
62 if (typeof(v
) === 'object' && v
!== null) {
63 return `[object ${v.class}]`;
67 return s
.substr(0, 400) + "...<" + (s
.length
- 400) + " more bytes>...";
72 function summaryObject(dv
) {
74 for (var name
of dv
.getOwnPropertyNames()) {
75 var v
= dv
.getOwnPropertyDescriptor(name
).value
;
76 if (v
instanceof Debugger
.Object
) {
84 function debuggeeValueToString(dv
, style
) {
85 var dvrepr
= dvToString(dv
);
86 if (!style
.pretty
|| (typeof dv
!== 'object') || (dv
=== null))
87 return [dvrepr
, undefined];
89 const exec
= debuggeeGlobalWrapper
.executeInGlobalWithBindings
.bind(debuggeeGlobalWrapper
);
91 if (dv
.class == "Error") {
92 let errval
= exec("$$.toString()", debuggeeValues
);
93 return [dvrepr
, errval
.return];
97 return [dvrepr
, JSON
.stringify(summaryObject(dv
), null, 4)];
99 let str
= exec("JSON.stringify(v, null, 4)", {v
: dv
});
100 if ('throw' in str
) {
102 return [dvrepr
, undefined];
105 Object
.assign(substyle
, style
);
106 substyle
.noerror
= true;
107 return [dvrepr
, debuggeeValueToString(str
.throw, substyle
)];
110 return [dvrepr
, str
.return];
113 // Problem! Used to do [object Object] followed by details. Now just details?
115 function showDebuggeeValue(dv
, style
={pretty
: options
.pretty
}) {
116 var i
= nextDebuggeeValueIndex
++;
117 debuggeeValues
["$" + i
] = dv
;
118 debuggeeValues
["$$"] = dv
;
119 let [brief
, full
] = debuggeeValueToString(dv
, style
);
120 print("$" + i
+ " = " + brief
);
121 if (full
!== undefined)
125 Object
.defineProperty(Debugger
.Frame
.prototype, "num", {
130 for (var f
= topFrame
; f
&& f
!== this; f
= f
.older
)
132 return f
=== null ? undefined : i
;
136 Debugger
.Frame
.prototype.frameDescription
= function frameDescription() {
137 if (this.type
== "call")
138 return ((this.callee
.name
|| '<anonymous>') +
139 "(" + this.arguments
.map(dvToString
).join(", ") + ")");
141 return this.type
+ " code";
144 Debugger
.Frame
.prototype.positionDescription
= function positionDescription() {
146 var line
= this.script
.getOffsetLocation(this.offset
).lineNumber
;
148 return this.script
.url
+ ":" + line
;
149 return "line " + line
;
154 Debugger
.Frame
.prototype.location = function () {
156 var { lineNumber
, columnNumber
, isEntryPoint
} = this.script
.getOffsetLocation(this.offset
);
158 return this.script
.url
+ ":" + lineNumber
;
164 Debugger
.Frame
.prototype.fullDescription
= function fullDescription() {
165 var fr
= this.frameDescription();
166 var pos
= this.positionDescription();
168 return fr
+ ", " + pos
;
172 Object
.defineProperty(Debugger
.Frame
.prototype, "line", {
177 return this.script
.getOffsetLocation(this.offset
).lineNumber
;
183 function callDescription(f
) {
184 return ((f
.callee
.name
|| '<anonymous>') +
185 "(" + f
.arguments
.map(dvToString
).join(", ") + ")");
188 function showFrame(f
, n
) {
189 if (f
=== undefined || f
=== null) {
196 if (n
=== undefined) {
199 throw new Error("Internal error: frame not on stack");
202 print('#' + n
+ " " + f
.fullDescription());
205 function saveExcursion(fn
) {
206 var tf
= topFrame
, ff
= focusedFrame
;
215 function parseArgs(str
) {
216 return str
.split(" ");
219 function describedRv(r
, desc
) {
220 desc
= "[" + desc
+ "] ";
221 if (r
=== undefined) {
222 print(desc
+ "Returning undefined");
223 } else if (r
=== null) {
224 print(desc
+ "Returning null");
225 } else if (r
.length
=== undefined) {
226 print(desc
+ "Returning object " + JSON
.stringify(r
));
228 print(desc
+ "Returning length-" + r
.length
+ " list");
236 // Rerun the program (reloading it from the file)
237 function runCommand(args
) {
238 print(`Restarting program (${args})`);
240 activeTask
.scriptArgs
= parseArgs(args
);
242 activeTask
.scriptArgs
= [...actualScriptArgs
];
244 for (var f
= topFrame
; f
; f
= f
.older
) {
246 f
.onPop
= () => null;
248 f
.onPop
= () => ({ 'return': 0 });
251 //return describedRv([{ 'return': 0 }], "runCommand");
255 // Evaluate an expression in the Debugger global
256 function evalCommand(expr
) {
260 function quitCommand() {
261 dbg
.removeAllDebuggees();
265 function backtraceCommand() {
266 if (topFrame
=== null)
268 for (var i
= 0, f
= topFrame
; f
; i
++, f
= f
.older
)
272 function setCommand(rest
) {
273 var space
= rest
.indexOf(' ');
275 print("Invalid set <option> <value> command");
277 var name
= rest
.substr(0, space
);
278 var value
= rest
.substr(space
+ 1);
280 if (name
== 'args') {
281 activeTask
.scriptArgs
= parseArgs(value
);
283 var yes
= ["1", "yes", "true", "on"];
284 var no
= ["0", "no", "false", "off"];
286 if (yes
.includes(value
))
287 options
[name
] = true;
288 else if (no
.includes(value
))
289 options
[name
] = false;
291 options
[name
] = value
;
296 function split_print_options(s
, style
) {
297 var m
= /^\/(\w+)/.exec(s
);
300 if (m
[1].includes("p"))
302 if (m
[1].includes("b"))
304 return [ s
.substr(m
[0].length
).trimLeft(), style
];
307 function doPrint(expr
, style
) {
308 // This is the real deal.
309 var cv
= saveExcursion(
310 () => focusedFrame
== null
311 ? debuggeeGlobalWrapper
.executeInGlobalWithBindings(expr
, debuggeeValues
)
312 : focusedFrame
.evalWithBindings(expr
, debuggeeValues
));
314 print("Debuggee died.");
315 } else if ('return' in cv
) {
316 showDebuggeeValue(cv
.return, style
);
318 print("Exception caught. (To rethrow it, type 'throw'.)");
320 showDebuggeeValue(lastExc
, style
);
324 function printCommand(rest
) {
325 var [expr
, style
] = split_print_options(rest
, {pretty
: options
.pretty
});
326 return doPrint(expr
, style
);
329 function keysCommand(rest
) { return doPrint("Object.keys(" + rest
+ ")"); }
331 function detachCommand() {
332 dbg
.removeAllDebuggees();
336 function continueCommand(rest
) {
337 if (focusedFrame
=== null) {
342 var match
= rest
.match(/^(\d+)$/);
344 return doStepOrNext({upto
:true, stopLine
:match
[1]});
350 function throwCommand(rest
) {
352 if (focusedFrame
!== topFrame
) {
353 print("To throw, you must select the newest frame (use 'frame 0').");
355 } else if (focusedFrame
=== null) {
358 } else if (rest
=== '') {
359 return [{throw: lastExc
}];
361 var cv
= saveExcursion(function () { return focusedFrame
.eval(rest
); });
363 print("Debuggee died while determining what to throw. Stopped.");
364 } else if ('return' in cv
) {
365 return [{throw: cv
.return}];
367 print("Exception determining what to throw. Stopped.");
368 showDebuggeeValue(cv
.throw);
374 function frameCommand(rest
) {
376 if (rest
.match(/[0-9]+/)) {
383 for (var i
= 0; i
< n
&& f
; i
++) {
385 print("There is no frame " + rest
+ ".");
392 updateLocation(focusedFrame
);
394 } else if (rest
=== '') {
395 if (topFrame
=== null) {
398 updateLocation(focusedFrame
);
402 print("do what now?");
406 function upCommand() {
407 if (focusedFrame
=== null)
409 else if (focusedFrame
.older
=== null)
410 print("Initial frame selected; you cannot go up.");
412 focusedFrame
.older
.younger
= focusedFrame
;
413 focusedFrame
= focusedFrame
.older
;
414 updateLocation(focusedFrame
);
419 function downCommand() {
420 if (focusedFrame
=== null)
422 else if (!focusedFrame
.younger
)
423 print("Youngest frame selected; you cannot go down.");
425 focusedFrame
= focusedFrame
.younger
;
426 updateLocation(focusedFrame
);
431 function forcereturnCommand(rest
) {
433 var f
= focusedFrame
;
434 if (f
!== topFrame
) {
435 print("To forcereturn, you must select the newest frame (use 'frame 0').");
436 } else if (f
=== null) {
437 print("Nothing on the stack.");
438 } else if (rest
=== '') {
439 return [{return: undefined}];
441 var cv
= saveExcursion(function () { return f
.eval(rest
); });
443 print("Debuggee died while determining what to forcereturn. Stopped.");
444 } else if ('return' in cv
) {
445 return [{return: cv
.return}];
447 print("Error determining what to forcereturn. Stopped.");
448 showDebuggeeValue(cv
.throw);
453 function printPop(f
, c
) {
454 var fdesc
= f
.fullDescription();
456 print("frame returning (still selected): " + fdesc
);
457 showDebuggeeValue(c
.return, {brief
: true});
458 } else if (c
.throw) {
459 print("frame threw exception: " + fdesc
);
460 showDebuggeeValue(c
.throw);
461 print("(To rethrow it, type 'throw'.)");
464 print("frame was terminated: " + fdesc
);
468 // Set |prop| on |obj| to |value|, but then restore its current value
469 // when we next enter the repl.
470 function setUntilRepl(obj
, prop
, value
) {
471 var saved
= obj
[prop
];
473 replCleanups
.push(function () { obj
[prop
] = saved
; });
476 function updateLocation(frame
) {
478 var loc
= frame
.location();
480 print("\032\032" + loc
+ ":1");
484 function doStepOrNext(kind
) {
485 var startFrame
= topFrame
;
486 var startLine
= startFrame
.line
;
487 // print("stepping in: " + startFrame.fullDescription());
488 // print("starting line: " + uneval(startLine));
490 function stepPopped(completion
) {
491 // Note that we're popping this frame; we need to watch for
492 // subsequent step events on its caller.
493 this.reportedPop
= true;
494 printPop(this, completion
);
495 topFrame
= focusedFrame
= this;
497 // We want to continue, but this frame is going to be invalid as
498 // soon as this function returns, which will make the replCleanups
499 // assert when it tries to access the dead frame's 'onPop'
500 // property. So clear it out now while the frame is still valid,
501 // and trade it for an 'onStep' callback on the frame we're popping to.
503 setUntilRepl(this.older
, 'onStep', stepStepped
);
506 updateLocation(this);
510 function stepEntered(newFrame
) {
511 print("entered frame: " + newFrame
.fullDescription());
512 updateLocation(newFrame
);
513 topFrame
= focusedFrame
= newFrame
;
517 function stepStepped() {
518 // print("stepStepped: " + this.fullDescription());
519 updateLocation(this);
523 // 'finish' set a one-time onStep for stopping at the frame it
524 // wants to return to
526 } else if (kind
.upto
) {
527 // running until a given line is reached
528 if (this.line
== kind
.stopLine
)
531 // regular step; stop whenever the line number changes
532 if ((this.line
!= startLine
) || (this != startFrame
))
537 topFrame
= focusedFrame
= this;
538 if (focusedFrame
!= startFrame
)
539 print(focusedFrame
.fullDescription());
543 // Otherwise, let execution continue.
548 setUntilRepl(dbg
, 'onEnterFrame', stepEntered
);
550 // If we're stepping after an onPop, watch for steps and pops in the
551 // next-older frame; this one is done.
552 var stepFrame
= startFrame
.reportedPop
? startFrame
.older
: startFrame
;
553 if (!stepFrame
|| !stepFrame
.script
)
557 setUntilRepl(stepFrame
, 'onStep', stepStepped
);
558 setUntilRepl(stepFrame
, 'onPop', stepPopped
);
561 // Let the program continue!
565 function stepCommand() { return doStepOrNext({step
:true}); }
566 function nextCommand() { return doStepOrNext({next
:true}); }
567 function finishCommand() { return doStepOrNext({finish
:true}); }
569 // FIXME: DOES NOT WORK YET
570 function breakpointCommand(where
) {
571 print("Sorry, breakpoints don't work yet.");
572 var script
= focusedFrame
.script
;
573 var offsets
= script
.getLineOffsets(Number(where
));
574 if (offsets
.length
== 0) {
575 print("Unable to break at line " + where
);
578 for (var offset
of offsets
) {
579 script
.setBreakpoint(offset
, { hit
: handleBreakpoint
});
581 print("Set breakpoint in " + script
.url
+ ":" + script
.startLine
+ " at line " + where
+ ", " + offsets
.length
);
584 // Build the table of commands.
587 backtraceCommand
, "bt", "where",
588 breakpointCommand
, "b", "break",
589 continueCommand
, "c",
595 finishCommand
, "fin",
607 var currentCmd
= null;
608 for (var i
= 0; i
< commandArray
.length
; i
++) {
609 var cmd
= commandArray
[i
];
610 if (typeof cmd
=== "string")
611 commands
[cmd
] = currentCmd
;
613 currentCmd
= commands
[cmd
.name
.replace(/Command$/, '')] = cmd
;
616 function helpCommand(rest
) {
617 print("Available commands:");
618 var printcmd = function(group
) {
619 print(" " + group
.join(", "));
623 for (var cmd
of commandArray
) {
624 if (typeof cmd
=== "string") {
627 if (group
.length
) printcmd(group
);
628 group
= [ cmd
.name
.replace(/Command$/, '') ];
634 // Break cmd into two parts: its first word and everything else. If it begins
635 // with punctuation, treat that as a separate word. The first word is
636 // terminated with whitespace or the '/' character. So:
638 // print x => ['print', 'x']
639 // print => ['print', '']
640 // !print x => ['!', 'print x']
641 // ?!wtf!? => ['?', '!wtf!?']
642 // print/b x => ['print', '/b x']
644 function breakcmd(cmd
) {
645 cmd
= cmd
.trimLeft();
646 if ("!@#$%^&*_+=/?.,<>:;'\"".includes(cmd
.substr(0, 1)))
647 return [cmd
.substr(0, 1), cmd
.substr(1).trimLeft()];
648 var m
= /\s+|(?=\/)/.exec(cmd
);
651 return [cmd
.slice(0, m
.index
), cmd
.slice(m
.index
+ m
[0].length
)];
654 function runcmd(cmd
) {
655 var pieces
= breakcmd(cmd
);
656 if (pieces
[0] === "")
659 var first
= pieces
[0], rest
= pieces
[1];
660 if (!commands
.hasOwnProperty(first
)) {
661 print("unrecognized command '" + first
+ "'");
665 var cmd
= commands
[first
];
666 if (cmd
.length
=== 0 && rest
!== '') {
667 print("this command cannot take an argument");
674 function preReplCleanups() {
675 while (replCleanups
.length
> 0)
676 replCleanups
.pop()();
679 var prevcmd
= undefined;
685 putstr("\n" + prompt
);
694 var result
= runcmd(cmd
);
695 if (result
=== undefined)
696 ; // do nothing, return to prompt
697 else if (Array
.isArray(result
))
699 else if (result
=== null)
702 throw new Error("Internal error: result of runcmd wasn't array or undefined: " + result
);
704 print("*** Internal error: exception in the debugger code.");
711 var dbg
= new Debugger();
712 dbg
.onDebuggerStatement = function (frame
) {
713 return saveExcursion(function () {
714 topFrame
= focusedFrame
= frame
;
715 print("'debugger' statement hit.");
717 updateLocation(focusedFrame
);
719 return describedRv(repl(), "debugger.saveExc");
722 dbg
.onThrow = function (frame
, exc
) {
723 return saveExcursion(function () {
724 topFrame
= focusedFrame
= frame
;
725 print("Unwinding due to exception. (Type 'c' to continue unwinding.)");
727 print("Exception value is:");
728 showDebuggeeValue(exc
);
733 function handleBreakpoint (frame
) {
734 print("Breakpoint hit!");
735 return saveExcursion(() => {
736 topFrame
= focusedFrame
= frame
;
737 print("breakpoint hit.");
739 updateLocation(focusedFrame
);
744 // The depth of jorendb nesting.
746 if (typeof jorendbDepth
== 'undefined') jorendbDepth
= 0;
748 var debuggeeGlobal
= newGlobal({newCompartment
: true});
749 debuggeeGlobal
.jorendbDepth
= jorendbDepth
+ 1;
750 var debuggeeGlobalWrapper
= dbg
.addDebuggee(debuggeeGlobal
);
752 print("jorendb version -0.0");
753 prompt
= '(' + Array(jorendbDepth
+1).join('meta-') + 'jorendb) ';
755 var args
= scriptArgs
.slice(0);
756 print("INITIAL ARGS: " + args
);
758 // Find the script to run and its arguments. The script may have been given as
759 // a plain script name, in which case all remaining arguments belong to the
760 // script. Or there may have been any number of arguments to the JS shell,
761 // followed by -f scriptName, followed by additional arguments to the JS shell,
762 // followed by the script arguments. There may be multiple -e or -f options in
763 // the JS shell arguments, and we want to treat each one as a debuggable
766 // The difficulty is that the JS shell has a mixture of
774 // parameters, and there's no way to know whether --option takes an argument or
775 // not. We will assume that VAL will never end in .js, or rather that the first
776 // argument that does not start with "-" but does end in ".js" is the name of
779 // If you need to pass other options and not have them given to the script,
780 // pass them before the -f jorendb.js argument. Thus, the safe ways to pass
783 // js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)+ -- [script args]
784 // js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)* script.js [script args]
786 // Additionally, if you want to run a script that is *NOT* debugged, put it in
787 // as part of the leading [JS shell options].
790 // Compute actualScriptArgs by finding the script to be run and grabbing every
791 // non-script argument. The script may be given by -f scriptname or just plain
792 // scriptname. In the latter case, it will be in the global variable
793 // 'scriptPath' (and NOT in scriptArgs.)
794 var actualScriptArgs
= [];
797 if (scriptPath
!== undefined) {
800 'script': scriptPath
,
805 while(args
.length
> 0) {
806 var arg
= args
.shift();
807 print("arg: " + arg
);
814 } else if (arg
== '-f') {
815 var script
= args
.shift();
816 print(" load -f " + script
);
822 } else if (arg
.indexOf("-") == 0) {
824 print(" pass remaining args to script");
825 actualScriptArgs
.push(...args
);
827 } else if ((args
.length
> 0) && (args
[0].indexOf(".js") + 3 == args
[0].length
)) {
828 // Ends with .js, assume we are looking at --boolean script.js
829 print(" load script.js after --boolean");
832 'script': args
.shift(),
836 // Does not end with .js, assume we are looking at JS shell arg
843 print(" load general");
844 actualScriptArgs
.push(...args
);
851 print(" arg " + arg
);
852 actualScriptArgs
.push(arg
);
856 print("jorendb: scriptPath = " + scriptPath
);
857 print("jorendb: scriptArgs = " + scriptArgs
);
858 print("jorendb: actualScriptArgs = " + actualScriptArgs
);
860 for (var task
of todo
) {
861 task
['scriptArgs'] = [...actualScriptArgs
];
864 // Always drop into a repl at the end. Especially if the main script throws an
866 todo
.push({ 'action': 'repl' });
869 print("Top of run loop");
871 for (var task
of todo
) {
873 if (task
.action
== 'eval') {
874 debuggeeGlobal
.eval(task
.code
);
875 } else if (task
.action
== 'load') {
876 debuggeeGlobal
['scriptArgs'] = task
.scriptArgs
;
877 debuggeeGlobal
['scriptPath'] = task
.script
;
878 print("Loading JavaScript file " + task
.script
);
880 debuggeeGlobal
.evaluate(read(task
.script
), { 'fileName': task
.script
, 'lineNumber': 1 });
882 print("Caught exception " + exc
);
886 } else if (task
.action
== 'repl') {