fune/js/examples/jorendb.js
Marian-Vasile Laza 5e48e30b98 Backed out 10 changesets (bug 1746090) for causing build bustages on TestingFunctions.cpp. CLOSED TREE
Backed out changeset edbf96722e4b (bug 1746090)
Backed out changeset f4e4bf6ba8ff (bug 1746090)
Backed out changeset c288fe1c6c84 (bug 1746090)
Backed out changeset 2b0caa13d0fc (bug 1746090)
Backed out changeset 1ed9d77885c6 (bug 1746090)
Backed out changeset 54a60388fb11 (bug 1746090)
Backed out changeset a9c16e721533 (bug 1746090)
Backed out changeset 774bdb9939a9 (bug 1746090)
Backed out changeset 5c5742535301 (bug 1746090)
Backed out changeset ff509fe4671d (bug 1746090)
2022-01-20 02:39:17 +02:00

886 lines
25 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) {
return (typeof v !== 'object' || v === null) ? uneval(v) : "[object " + v.class + "]";
}
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'))
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);
}
} else if (task.action == 'repl') {
repl();
}
if (rerun)
break;
}
}
quit(0);