js-slang
Version:
Javascript-based implementations of Source, written in Typescript
564 lines • 21.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.InfiniteLoopRuntimeObjectNames = exports.InfiniteLoopRuntimeFunctions = exports.instrument = exports.getOriginalName = void 0;
const astring_1 = require("astring");
const transpiler_1 = require("../transpiler/transpiler");
const create = require("../utils/ast/astCreator");
const walkers_1 = require("../utils/walkers");
const helpers_1 = require("../utils/ast/helpers");
// transforms AST of program
const globalIds = {
builtinsId: 'builtins',
functionsId: '__InfLoopFns',
stateId: '__InfLoopState',
modulesId: '__modules'
};
exports.InfiniteLoopRuntimeObjectNames = globalIds;
var FunctionNames;
(function (FunctionNames) {
FunctionNames[FunctionNames["nothingFunction"] = 0] = "nothingFunction";
FunctionNames[FunctionNames["concretize"] = 1] = "concretize";
FunctionNames[FunctionNames["hybridize"] = 2] = "hybridize";
FunctionNames[FunctionNames["wrapArg"] = 3] = "wrapArg";
FunctionNames[FunctionNames["dummify"] = 4] = "dummify";
FunctionNames[FunctionNames["saveBool"] = 5] = "saveBool";
FunctionNames[FunctionNames["saveVar"] = 6] = "saveVar";
FunctionNames[FunctionNames["preFunction"] = 7] = "preFunction";
FunctionNames[FunctionNames["returnFunction"] = 8] = "returnFunction";
FunctionNames[FunctionNames["postLoop"] = 9] = "postLoop";
FunctionNames[FunctionNames["enterLoop"] = 10] = "enterLoop";
FunctionNames[FunctionNames["exitLoop"] = 11] = "exitLoop";
FunctionNames[FunctionNames["trackLoc"] = 12] = "trackLoc";
FunctionNames[FunctionNames["evalB"] = 13] = "evalB";
FunctionNames[FunctionNames["evalU"] = 14] = "evalU";
})(FunctionNames || (FunctionNames = {}));
exports.InfiniteLoopRuntimeFunctions = FunctionNames;
/**
* Renames all variables in the program to differentiate shadowed variables and
* variables declared with the same name but in different scopes.
*
* E.g. "function f(f)..." -> "function f_0(f_1)..."
* @param predefined A table of [key: string, value:string], where variables named 'key' will be renamed to 'value'
*/
function unshadowVariables(program, predefined = {}) {
for (const name of Object.values(globalIds)) {
predefined[name] = name;
}
const seenIds = new Set();
const env = [predefined];
const genId = (name) => {
let count = 0;
while (seenIds.has(`${name}_${count}`))
count++;
const newName = `${name}_${count}`;
seenIds.add(newName);
env[0][name] = newName;
return newName;
};
const unshadowFunctionInner = (node, s, callback) => {
env.unshift({ ...env[0] });
for (const id of node.params) {
id.name = genId(id.name);
}
callback(node.body, undefined);
env.shift();
};
const doStatements = (stmts, callback) => {
for (const stmt of stmts) {
if (stmt.type === 'FunctionDeclaration') {
// do hoisting first
if (stmt.id === null) {
throw new Error('Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.');
}
stmt.id.name = genId(stmt.id.name);
}
else if (stmt.type === 'VariableDeclaration') {
for (const decl of stmt.declarations) {
decl.id = decl.id;
const newName = genId(decl.id.name);
decl.id.name = newName;
}
}
}
for (const stmt of stmts) {
callback(stmt, undefined);
}
};
(0, walkers_1.recursive)(program, [{}], {
BlockStatement(node, s, callback) {
env.unshift({ ...env[0] });
doStatements(node.body, callback);
env.shift();
},
VariableDeclarator(node, s, callback) {
node.id = node.id;
if (node.init) {
callback(node.init, s);
}
},
FunctionDeclaration(node, s, callback) {
// note: params can shadow function name
env.unshift({ ...env[0] });
for (const id of node.params) {
id.name = genId(id.name);
}
callback(node.body, undefined);
env.shift();
},
ForStatement(node, s, callback) {
env.unshift({ ...env[0] });
if (node.init?.type === 'VariableDeclaration')
doStatements([node.init], callback);
if (node.test)
callback(node.test, s);
if (node.update)
callback(node.update, s);
callback(node.body, s);
env.shift();
},
ArrowFunctionExpression: unshadowFunctionInner,
FunctionExpression: unshadowFunctionInner,
Identifier(node, _s, _callback) {
if (env[0][node.name]) {
node.name = env[0][node.name];
}
else {
create.mutateToMemberExpression(node, create.identifier(globalIds.functionsId), create.literal(FunctionNames.nothingFunction));
node.computed = true;
}
},
AssignmentExpression(node, s, callback) {
callback(node.left, s);
callback(node.right, s);
},
TryStatement(node, s, callback) {
if (!node.finalizer)
return; // should not happen
env.unshift({ ...env[0] });
doStatements(node.block.body, callback);
doStatements(node.finalizer.body, callback);
env.shift();
}
});
}
/**
* Returns the original name of the variable before
* it was changed during the code instrumentation process.
*/
function getOriginalName(name) {
if (/^anon[0-9]+$/.exec(name)) {
return '(anonymous)';
}
let cutAt = name.length - 1;
while (name.charAt(cutAt) !== '_') {
cutAt--;
if (cutAt < 0)
return '(error)';
}
return name.slice(0, cutAt);
}
exports.getOriginalName = getOriginalName;
function callFunction(fun) {
return create.memberExpression(create.identifier(globalIds.functionsId), fun);
}
/**
* Wrap each argument in every call expression.
*
* E.g. "f(x,y)" -> "f(wrap(x), wrap(y))".
* Ensures we do not test functions passed as arguments
* for infinite loops.
*/
function wrapCallArguments(program) {
(0, walkers_1.simple)(program, {
CallExpression(node) {
if (node.callee.type === 'MemberExpression')
return;
for (const arg of node.arguments) {
create.mutateToCallExpression(arg, callFunction(FunctionNames.wrapArg), [
{ ...arg },
create.identifier(globalIds.stateId)
]);
}
}
});
}
/**
* Turn all "is_null(x)" calls to "is_null(x, stateId)" to
* facilitate checking of infinite streams in stream mode.
*/
function addStateToIsNull(program) {
(0, walkers_1.simple)(program, {
CallExpression(node) {
if (node.callee.type === 'Identifier' && node.callee.name === 'is_null_0') {
node.arguments.push(create.identifier(globalIds.stateId));
}
}
});
}
/**
* Changes logical expressions to the corresponding conditional.
* Reduces the number of types of expressions we have to consider
* for the rest of the code transformations.
*
* E.g. "x && y" -> "x ? y : false"
*/
function transformLogicalExpressions(program) {
(0, walkers_1.simple)(program, {
LogicalExpression(node) {
if (node.operator === '&&') {
create.mutateToConditionalExpression(node, node.left, node.right, create.literal(false));
}
else {
create.mutateToConditionalExpression(node, node.left, create.literal(true), node.right);
}
}
});
}
/**
* Changes -ary operations to functions that accept hybrid values as arguments.
* E.g. "1+1" -> "functions.evalB('+',1,1)"
*/
function hybridizeBinaryUnaryOperations(program) {
(0, walkers_1.simple)(program, {
BinaryExpression(node) {
const { operator, left, right } = node;
create.mutateToCallExpression(node, callFunction(FunctionNames.evalB), [
create.literal(operator),
left,
right
]);
},
UnaryExpression(node) {
const { operator, argument } = node;
create.mutateToCallExpression(node, callFunction(FunctionNames.evalU), [
create.literal(operator),
argument
]);
}
});
}
function hybridizeVariablesAndLiterals(program) {
(0, walkers_1.recursive)(program, true, {
Identifier(node, state, _callback) {
if (state) {
create.mutateToCallExpression(node, callFunction(FunctionNames.hybridize), [
create.identifier(node.name),
create.literal(node.name),
create.identifier(globalIds.stateId)
]);
}
},
Literal(node, state, _callback) {
if (state && (typeof node.value === 'boolean' || typeof node.value === 'number')) {
create.mutateToCallExpression(node, callFunction(FunctionNames.dummify), [
create.literal(node.value)
]);
}
},
CallExpression(node, state, callback) {
// ignore callee
for (const arg of node.arguments) {
callback(arg, state);
}
},
MemberExpression(node, state, callback) {
if (!node.computed)
return;
callback(node.object, false);
callback(node.property, false);
create.mutateToCallExpression(node.object, callFunction(FunctionNames.concretize), [
{ ...node.object }
]);
create.mutateToCallExpression(node.property, callFunction(FunctionNames.concretize), [
{ ...node.property }
]);
}
});
}
/**
* Wraps the RHS of variable assignment with a function to track it.
* E.g. "x = x + 1;" -> "x = saveVar(x + 1, 'x', state)".
* saveVar should return the result of "x + 1".
*
* For assignments to elements of arrays we concretize the RHS.
* E.g. "a[1] = y;" -> "a[1] = concretize(y);"
*/
function trackVariableAssignment(program) {
(0, walkers_1.simple)(program, {
AssignmentExpression(node) {
if (node.left.type === 'Identifier') {
node.right = create.callExpression(callFunction(FunctionNames.saveVar), [
node.right,
create.literal(node.left.name),
create.identifier(globalIds.stateId)
]);
}
else if (node.left.type === 'MemberExpression') {
node.right = create.callExpression(callFunction(FunctionNames.concretize), [
{ ...node.right }
]);
}
}
});
}
/**
* Replaces the test of the node with a function to track the result in the state.
*
* E.g. "x===0 ? 1 : 0;" -> "saveBool(x === 0, state) ? 1 : 0;".
* saveBool should return the result of "x === 0"
*/
function saveTheTest(node) {
if (node.test === null || node.test === undefined) {
return;
}
const newTest = create.callExpression(callFunction(FunctionNames.saveBool), [
node.test,
create.identifier(globalIds.stateId)
]);
node.test = newTest;
}
/**
* Mutates a node in-place, turning it into a block statement.
* @param node Node to mutate.
* @param prepend Optional statement to prepend in the result.
* @param append Optional statement to append in the result.
*/
function inPlaceEnclose(node, prepend, append) {
const shallowCopy = { ...node };
node.type = 'BlockStatement';
node = node;
node.body = [shallowCopy];
if (prepend !== undefined) {
node.body.unshift(prepend);
}
if (append !== undefined) {
node.body.push(append);
}
}
/**
* Add tracking to if statements and conditional expressions in the state using saveTheTest.
*/
function trackIfStatements(program) {
const theFunction = (node) => saveTheTest(node);
(0, walkers_1.simple)(program, { IfStatement: theFunction, ConditionalExpression: theFunction });
}
/**
* Tracks loop iterations by adding saveTheTest, postLoop functions.
* postLoop will be executed after the body (and the update if it is a for loop).
* Also adds enter/exitLoop before/after the loop.
*
* E.g. "for(let i=0;i<10;i=i+1) {display(i);}"
* -> "enterLoop(state);
* for(let i=0;i<10; postLoop(state, i=i+1)) {display(i);};
* exitLoop(state);"
* Where postLoop should return the value of its (optional) second argument.
*/
function trackLoops(program) {
const makeCallStatement = (name, args) => create.expressionStatement(create.callExpression(callFunction(name), args));
const stateExpr = create.identifier(globalIds.stateId);
(0, walkers_1.simple)(program, {
WhileStatement: (node) => {
saveTheTest(node);
inPlaceEnclose(node.body, undefined, makeCallStatement(FunctionNames.postLoop, [stateExpr]));
inPlaceEnclose(node, makeCallStatement(FunctionNames.enterLoop, [stateExpr]), makeCallStatement(FunctionNames.exitLoop, [stateExpr]));
},
ForStatement: (node) => {
saveTheTest(node);
const theUpdate = node.update ? node.update : create.identifier('undefined');
node.update = create.callExpression(callFunction(FunctionNames.postLoop), [
stateExpr,
theUpdate
]);
inPlaceEnclose(node, makeCallStatement(FunctionNames.enterLoop, [stateExpr]), makeCallStatement(FunctionNames.exitLoop, [stateExpr]));
}
});
}
/**
* Tracks function iterations by adding preFunction and returnFunction functions.
* preFunction is prepended to every function body, and returnFunction is used to
* wrap the argument of return statements.
*
* E.g. "function f(x) {return x;}"
* -> "function f(x) {
* preFunction('f',[x], state);
* return returnFunction(x, state);
* }"
* where returnFunction should return its first argument 'x'.
*/
function trackFunctions(program) {
const preFunction = (name, params) => {
const args = params
.filter(x => x.type === 'Identifier')
.map(x => x.name)
.map(x => create.arrayExpression([create.literal(x), create.identifier(x)]));
return create.expressionStatement(create.callExpression(callFunction(FunctionNames.preFunction), [
create.literal(name),
create.arrayExpression(args),
create.identifier(globalIds.stateId)
]));
};
let counter = 0;
const anonFunction = (node) => {
if (node.body.type !== 'BlockStatement') {
create.mutateToReturnStatement(node.body, { ...node.body });
}
inPlaceEnclose(node.body, preFunction(`anon${counter++}`, node.params));
};
(0, walkers_1.simple)(program, {
ArrowFunctionExpression: anonFunction,
FunctionExpression: anonFunction,
FunctionDeclaration(node) {
if (node.id === null) {
throw new Error('Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.');
}
const name = node.id.name;
inPlaceEnclose(node.body, preFunction(name, node.params));
}
});
(0, walkers_1.simple)(program, {
ReturnStatement(node) {
const hasNoArgs = node.argument === null || node.argument === undefined;
const arg = hasNoArgs ? create.identifier('undefined') : node.argument;
const argsForCall = [arg, create.identifier(globalIds.stateId)];
node.argument = create.callExpression(callFunction(FunctionNames.returnFunction), argsForCall);
}
});
}
function builtinsToStmts(builtins) {
const makeDecl = (name) => create.declaration(name, 'const', create.callExpression(create.memberExpression(create.identifier(globalIds.builtinsId), 'get'), [create.literal(name)]));
return [...builtins].map(makeDecl);
}
/**
* Make all variables in the 'try' block function-scoped so they
* can be accessed in the 'finally' block
*/
function toVarDeclaration(stmt) {
(0, walkers_1.simple)(stmt, {
VariableDeclaration(node) {
node.kind = 'var';
}
});
}
/**
* There may have been other programs run in the REPL. This hack
* 'combines' the other programs and the current program into a single
* large program by enclosing the past programs in 'try' blocks, and the
* current program in a 'finally' block. Any errors (including detected
* infinite loops) in the past code will be ignored in the empty 'catch'
* block.
*/
function wrapOldCode(current, toWrap) {
for (const stmt of toWrap) {
toVarDeclaration(stmt);
}
const tryStmt = {
type: 'TryStatement',
block: create.blockStatement([...toWrap]),
handler: {
type: 'CatchClause',
param: create.identifier('e'),
body: create.blockStatement([])
},
finalizer: create.blockStatement([...current.body])
};
current.body = [tryStmt];
}
function makePositions(position) {
return create.objectExpression([
create.property('line', create.literal(position.line)),
create.property('column', create.literal(position.column))
]);
}
function savePositionAsExpression(loc) {
if (loc !== undefined && loc !== null) {
return create.objectExpression([
create.property('start', makePositions(loc.start)),
create.property('end', makePositions(loc.end))
]);
}
else {
return create.identifier('undefined');
}
}
/**
* Wraps every callExpression and prepends every loop body
* with a function that saves the callExpression/loop's SourceLocation
* (line number etc) in the state. This location will be used in the
* error given to the user.
*
* E.g. "f(x);" -> "trackLoc({position object}, state, ()=>f(x))".
* where trackLoc should return the result of "(()=>f(x))();".
*/
function trackLocations(program) {
// Note: only add locations for most recently entered code
const trackerFn = callFunction(FunctionNames.trackLoc);
const stateExpr = create.identifier(globalIds.stateId);
const doLoops = (node, _state, _callback) => {
inPlaceEnclose(node.body, create.expressionStatement(create.callExpression(trackerFn, [savePositionAsExpression(node.loc), stateExpr])));
};
(0, walkers_1.recursive)(program, undefined, {
CallExpression(node, _state, _callback) {
if (node.callee.type === 'MemberExpression')
return;
const copy = { ...node };
const lazyCall = create.arrowFunctionExpression([], copy);
create.mutateToCallExpression(node, trackerFn, [
savePositionAsExpression(node.loc),
stateExpr,
lazyCall
]);
},
ForStatement: doLoops,
WhileStatement: doLoops
});
}
function handleImports(programs) {
const imports = programs.flatMap(program => {
const [importsToAdd, otherNodes] = (0, transpiler_1.transformImportDeclarations)(program, create.identifier(globalIds.modulesId));
program.body = [...importsToAdd, ...otherNodes];
return importsToAdd.flatMap(decl => {
const ids = (0, helpers_1.getIdsFromDeclaration)(decl);
return ids.map(id => id.name);
});
});
return [...new Set(imports)];
}
/**
* Instruments the given code with functions that track the state of the program.
*
* @param previous programs that were previously executed in the REPL, most recent first (at ix 0).
* @param program most recent program executed.
* @param builtins Names of builtin functions.
* @returns code with instrumentations.
*/
function instrument(previous, program, builtins) {
const { builtinsId, functionsId, stateId } = globalIds;
const predefined = {};
predefined[builtinsId] = builtinsId;
predefined[functionsId] = functionsId;
predefined[stateId] = stateId;
const innerProgram = { ...program };
const moduleNames = handleImports([program].concat(previous));
for (const name of moduleNames) {
predefined[name] = name;
}
for (const toWrap of previous) {
wrapOldCode(program, toWrap.body);
}
wrapOldCode(program, builtinsToStmts(builtins));
unshadowVariables(program, predefined);
transformLogicalExpressions(program);
hybridizeBinaryUnaryOperations(program);
hybridizeVariablesAndLiterals(program);
// tracking functions: add functions to record runtime data.
trackVariableAssignment(program);
trackIfStatements(program);
trackLoops(program);
trackFunctions(program);
trackLocations(innerProgram);
addStateToIsNull(program);
wrapCallArguments(program);
return (0, astring_1.generate)(program);
}
exports.instrument = instrument;
//# sourceMappingURL=instrument.js.map
;