js-slang
Version:
Javascript-based implementations of Source, written in Typescript
764 lines • 30.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.apply = exports.evaluateProgram = exports.evaluators = exports.pushEnvironment = exports.createBlockEnvironment = exports.actualValue = void 0;
const lodash_1 = require("lodash");
const constants_1 = require("../constants");
const createContext_1 = require("../createContext");
const heap_1 = require("../cse-machine/heap");
const utils_1 = require("../cse-machine/utils");
const errors = require("../errors/errors");
const runtimeSourceError_1 = require("../errors/runtimeSourceError");
const inspector_1 = require("../stdlib/inspector");
const types_1 = require("../types");
const create = require("../utils/ast/astCreator");
const astCreator_1 = require("../utils/ast/astCreator");
const helpers_1 = require("../utils/ast/helpers");
const operators_1 = require("../utils/operators");
const rttc = require("../utils/rttc");
const closure_1 = require("./closure");
class BreakValue {
}
class ContinueValue {
}
class ReturnValue {
constructor(value) {
this.value = value;
}
}
class TailCallReturnValue {
constructor(callee, args, node) {
this.callee = callee;
this.args = args;
this.node = node;
}
}
class Thunk {
constructor(exp, env) {
this.exp = exp;
this.env = env;
this.isMemoized = false;
this.value = null;
}
}
const delayIt = (exp, env) => new Thunk(exp, env);
function* forceIt(val, context) {
if (val instanceof Thunk) {
if (val.isMemoized)
return val.value;
(0, exports.pushEnvironment)(context, val.env);
const evalRes = yield* actualValue(val.exp, context);
popEnvironment(context);
val.value = evalRes;
val.isMemoized = true;
return evalRes;
}
else
return val;
}
function* actualValue(exp, context) {
const evalResult = yield* evaluate(exp, context);
const forced = yield* forceIt(evalResult, context);
return forced;
}
exports.actualValue = actualValue;
const createEnvironment = (context, closure, args, callExpression) => {
const environment = {
name: closure.functionName,
tail: closure.environment,
head: {},
heap: new heap_1.default(),
id: (0, utils_1.uniqueId)(context)
};
if (callExpression) {
environment.callExpression = {
...callExpression,
arguments: args.map(astCreator_1.primitive)
};
}
closure.node.params.forEach((param, index) => {
if (param.type === 'RestElement') {
environment.head[param.argument.name] = args.slice(index);
}
else {
environment.head[param.name] = args[index];
}
});
return environment;
};
const createBlockEnvironment = (context, name = 'blockEnvironment') => {
return {
name,
tail: currentEnvironment(context),
head: {},
heap: new heap_1.default(),
id: (0, utils_1.uniqueId)(context)
};
};
exports.createBlockEnvironment = createBlockEnvironment;
const handleRuntimeError = (context, error) => {
context.errors.push(error);
context.runtime.environments = context.runtime.environments.slice(-context.numberOfOuterEnvironments);
throw error;
};
const DECLARED_BUT_NOT_YET_ASSIGNED = Symbol('Used to implement block scope');
function declareIdentifier(context, name, node) {
const environment = currentEnvironment(context);
if (environment.head.hasOwnProperty(name)) {
const descriptors = Object.getOwnPropertyDescriptors(environment.head);
return handleRuntimeError(context, new errors.VariableRedeclaration(node, name, descriptors[name].writable));
}
environment.head[name] = DECLARED_BUT_NOT_YET_ASSIGNED;
return environment;
}
function declareVariables(context, node) {
for (const declaration of node.declarations) {
declareIdentifier(context, declaration.id.name, node);
}
}
function declareFunctionsAndVariables(context, node) {
for (const statement of node.body) {
switch (statement.type) {
case 'VariableDeclaration':
declareVariables(context, statement);
break;
case 'FunctionDeclaration':
if (statement.id === null) {
throw new Error('Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.');
}
declareIdentifier(context, statement.id.name, statement);
break;
}
}
}
function defineVariable(context, name, value, constant = false) {
const environment = currentEnvironment(context);
if (environment.head[name] !== DECLARED_BUT_NOT_YET_ASSIGNED) {
return handleRuntimeError(context, new errors.VariableRedeclaration(context.runtime.nodes[0], name, !constant));
}
Object.defineProperty(environment.head, name, {
value,
writable: !constant,
enumerable: true
});
return environment;
}
function* visit(context, node) {
(0, inspector_1.checkEditorBreakpoints)(context, node);
context.runtime.nodes.unshift(node);
yield context;
}
function* leave(context) {
context.runtime.break = false;
context.runtime.nodes.shift();
yield context;
}
const currentEnvironment = (context) => context.runtime.environments[0];
const replaceEnvironment = (context, environment) => {
context.runtime.environments[0] = environment;
context.runtime.environmentTree.insert(environment);
};
const popEnvironment = (context) => context.runtime.environments.shift();
const pushEnvironment = (context, environment) => {
context.runtime.environments.unshift(environment);
context.runtime.environmentTree.insert(environment);
};
exports.pushEnvironment = pushEnvironment;
const getVariable = (context, name) => {
let environment = currentEnvironment(context);
while (environment) {
if (environment.head.hasOwnProperty(name)) {
if (environment.head[name] === DECLARED_BUT_NOT_YET_ASSIGNED) {
return handleRuntimeError(context, new errors.UnassignedVariable(name, context.runtime.nodes[0]));
}
else {
return environment.head[name];
}
}
else {
environment = environment.tail;
}
}
return handleRuntimeError(context, new errors.UndefinedVariable(name, context.runtime.nodes[0]));
};
const setVariable = (context, name, value) => {
let environment = currentEnvironment(context);
while (environment) {
if (environment.head.hasOwnProperty(name)) {
if (environment.head[name] === DECLARED_BUT_NOT_YET_ASSIGNED) {
break;
}
const descriptors = Object.getOwnPropertyDescriptors(environment.head);
if (descriptors[name].writable) {
environment.head[name] = value;
return undefined;
}
return handleRuntimeError(context, new errors.ConstAssignment(context.runtime.nodes[0], name));
}
else {
environment = environment.tail;
}
}
return handleRuntimeError(context, new errors.UndefinedVariable(name, context.runtime.nodes[0]));
};
const checkNumberOfArguments = (context, callee, args, exp) => {
if (callee instanceof closure_1.default) {
const params = callee.node.params;
const hasVarArgs = params[params.length - 1]?.type === 'RestElement';
if (hasVarArgs ? params.length - 1 > args.length : params.length !== args.length) {
return handleRuntimeError(context, new errors.InvalidNumberOfArguments(exp, hasVarArgs ? params.length - 1 : params.length, args.length, hasVarArgs));
}
}
else {
const hasVarArgs = callee.minArgsNeeded != undefined;
if (hasVarArgs ? callee.minArgsNeeded > args.length : callee.length !== args.length) {
return handleRuntimeError(context, new errors.InvalidNumberOfArguments(exp, hasVarArgs ? callee.minArgsNeeded : callee.length, args.length, hasVarArgs));
}
}
return undefined;
};
function* getArgs(context, call) {
const args = [];
for (const arg of call.arguments) {
if (context.variant === types_1.Variant.LAZY) {
args.push(delayIt(arg, currentEnvironment(context)));
}
else if (arg.type === 'SpreadElement') {
args.push(...(yield* actualValue(arg.argument, context)));
}
else {
args.push(yield* actualValue(arg, context));
}
}
return args;
}
function transformLogicalExpression(node) {
if (node.operator === '&&') {
return (0, astCreator_1.conditionalExpression)(node.left, node.right, (0, astCreator_1.literal)(false), node.loc);
}
else {
return (0, astCreator_1.conditionalExpression)(node.left, (0, astCreator_1.literal)(true), node.right, node.loc);
}
}
function* reduceIf(node, context) {
const test = yield* actualValue(node.test, context);
const error = rttc.checkIfStatement(node, test, context.chapter);
if (error) {
return handleRuntimeError(context, error);
}
return test ? node.consequent : node.alternate;
}
function* evaluateBlockStatement(context, node) {
declareFunctionsAndVariables(context, node);
let result;
for (const statement of node.body) {
result = yield* evaluate(statement, context);
if (result instanceof ReturnValue ||
result instanceof TailCallReturnValue ||
result instanceof BreakValue ||
result instanceof ContinueValue) {
break;
}
}
return result;
}
/**
* WARNING: Do not use object literal shorthands, e.g.
* {
* *Literal(node: es.Literal, ...) {...},
* *ThisExpression(node: es.ThisExpression, ..._ {...},
* ...
* }
* They do not minify well, raising uncaught syntax errors in production.
* See: https://github.com/webpack/webpack/issues/7566
*/
// tslint:disable:object-literal-shorthand
// prettier-ignore
exports.evaluators = {
/** Simple Values */
Literal: function* (node, _context) {
return node.value;
},
TemplateLiteral: function* (node) {
// Expressions like `${1}` are not allowed, so no processing needed
return node.quasis[0].value.cooked;
},
ThisExpression: function* (node, context) {
return currentEnvironment(context).thisContext;
},
ArrayExpression: function* (node, context) {
const res = [];
for (const n of node.elements) {
res.push(yield* evaluate(n, context));
}
return res;
},
DebuggerStatement: function* (node, context) {
context.runtime.break = true;
yield;
},
FunctionExpression: function* (node, context) {
return new closure_1.default(node, currentEnvironment(context), context);
},
ArrowFunctionExpression: function* (node, context) {
return closure_1.default.makeFromArrowFunction(node, currentEnvironment(context), context);
},
Identifier: function* (node, context) {
return getVariable(context, node.name);
},
CallExpression: function* (node, context) {
const callee = yield* actualValue(node.callee, context);
const args = yield* getArgs(context, node);
let thisContext;
if (node.callee.type === 'MemberExpression') {
thisContext = yield* actualValue(node.callee.object, context);
}
const result = yield* apply(context, callee, args, node, thisContext);
return result;
},
NewExpression: function* (node, context) {
const callee = yield* evaluate(node.callee, context);
const args = [];
for (const arg of node.arguments) {
args.push(yield* evaluate(arg, context));
}
const obj = {};
if (callee instanceof closure_1.default) {
obj.__proto__ = callee.fun.prototype;
callee.fun.apply(obj, args);
}
else {
obj.__proto__ = callee.prototype;
callee.apply(obj, args);
}
return obj;
},
UnaryExpression: function* (node, context) {
const value = yield* actualValue(node.argument, context);
const error = rttc.checkUnaryExpression(node, node.operator, value, context.chapter);
if (error) {
return handleRuntimeError(context, error);
}
return (0, operators_1.evaluateUnaryExpression)(node.operator, value);
},
BinaryExpression: function* (node, context) {
const left = yield* actualValue(node.left, context);
const right = yield* actualValue(node.right, context);
const error = rttc.checkBinaryExpression(node, node.operator, context.chapter, left, right);
if (error) {
return handleRuntimeError(context, error);
}
return (0, operators_1.evaluateBinaryExpression)(node.operator, left, right);
},
ConditionalExpression: function* (node, context) {
return yield* this.IfStatement(node, context);
},
LogicalExpression: function* (node, context) {
return yield* this.ConditionalExpression(transformLogicalExpression(node), context);
},
VariableDeclaration: function* (node, context) {
const declaration = node.declarations[0];
const constant = node.kind === 'const';
const id = declaration.id;
const value = yield* evaluate(declaration.init, context);
defineVariable(context, id.name, value, constant);
return undefined;
},
ContinueStatement: function* (_node, _context) {
return new ContinueValue();
},
BreakStatement: function* (_node, _context) {
return new BreakValue();
},
ForStatement: function* (node, context) {
// Create a new block scope for the loop variables
const loopEnvironment = (0, exports.createBlockEnvironment)(context, 'forLoopEnvironment');
(0, exports.pushEnvironment)(context, loopEnvironment);
const initNode = node.init;
const testNode = node.test;
const updateNode = node.update;
if (initNode.type === 'VariableDeclaration') {
declareVariables(context, initNode);
}
yield* actualValue(initNode, context);
let value;
while (yield* actualValue(testNode, context)) {
// create block context and shallow copy loop environment head
// see https://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation
// and https://hacks.mozilla.org/2015/07/es6-in-depth-let-and-const/
// We copy this as a const to avoid ES6 funkiness when mutating loop vars
// https://github.com/source-academy/js-slang/issues/65#issuecomment-425618227
const environment = (0, exports.createBlockEnvironment)(context, 'forBlockEnvironment');
(0, exports.pushEnvironment)(context, environment);
for (const name in loopEnvironment.head) {
if (loopEnvironment.head.hasOwnProperty(name)) {
declareIdentifier(context, name, node);
defineVariable(context, name, loopEnvironment.head[name], true);
}
}
value = yield* actualValue(node.body, context);
// Remove block context
popEnvironment(context);
if (value instanceof ContinueValue) {
value = undefined;
}
if (value instanceof BreakValue) {
value = undefined;
break;
}
if (value instanceof ReturnValue || value instanceof TailCallReturnValue) {
break;
}
yield* actualValue(updateNode, context);
}
popEnvironment(context);
return value;
},
MemberExpression: function* (node, context) {
let obj = yield* actualValue(node.object, context);
if (obj instanceof closure_1.default) {
obj = obj.fun;
}
let prop;
if (node.computed) {
prop = yield* actualValue(node.property, context);
}
else {
prop = node.property.name;
}
const error = rttc.checkMemberAccess(node, obj, prop);
if (error) {
return handleRuntimeError(context, error);
}
if (obj !== null &&
obj !== undefined &&
typeof obj[prop] !== 'undefined' &&
!obj.hasOwnProperty(prop)) {
return handleRuntimeError(context, new errors.GetInheritedPropertyError(node, obj, prop));
}
try {
return obj[prop];
}
catch {
return handleRuntimeError(context, new errors.GetPropertyError(node, obj, prop));
}
},
AssignmentExpression: function* (node, context) {
if (node.left.type === 'MemberExpression') {
const left = node.left;
const obj = yield* actualValue(left.object, context);
let prop;
if (left.computed) {
prop = yield* actualValue(left.property, context);
}
else {
prop = left.property.name;
}
const error = rttc.checkMemberAccess(node, obj, prop);
if (error) {
return handleRuntimeError(context, error);
}
const val = yield* evaluate(node.right, context);
try {
obj[prop] = val;
}
catch {
return handleRuntimeError(context, new errors.SetPropertyError(node, obj, prop));
}
return val;
}
const id = node.left;
// Make sure it exist
const value = yield* evaluate(node.right, context);
setVariable(context, id.name, value);
return value;
},
FunctionDeclaration: function* (node, context) {
const id = node.id;
if (id === null) {
throw new Error("Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.");
}
// tslint:disable-next-line:no-any
const closure = new closure_1.default(node, currentEnvironment(context), context);
defineVariable(context, id.name, closure, true);
return undefined;
},
IfStatement: function* (node, context) {
const result = yield* reduceIf(node, context);
if (result === null) {
return undefined;
}
return yield* evaluate(result, context);
},
ExpressionStatement: function* (node, context) {
return yield* evaluate(node.expression, context);
},
ReturnStatement: function* (node, context) {
let returnExpression = node.argument;
// If we have a conditional expression, reduce it until we get something else
while (returnExpression.type === 'LogicalExpression' ||
returnExpression.type === 'ConditionalExpression') {
if (returnExpression.type === 'LogicalExpression') {
returnExpression = transformLogicalExpression(returnExpression);
}
returnExpression = yield* reduceIf(returnExpression, context);
}
// If we are now left with a CallExpression, then we use TCO
if (returnExpression.type === 'CallExpression' && context.variant !== types_1.Variant.LAZY) {
const callee = yield* actualValue(returnExpression.callee, context);
const args = yield* getArgs(context, returnExpression);
return new TailCallReturnValue(callee, args, returnExpression);
}
else {
return new ReturnValue(yield* evaluate(returnExpression, context));
}
},
WhileStatement: function* (node, context) {
let value; // tslint:disable-line
while (
// tslint:disable-next-line
(yield* actualValue(node.test, context)) &&
!(value instanceof ReturnValue) &&
!(value instanceof BreakValue) &&
!(value instanceof TailCallReturnValue)) {
value = yield* actualValue(node.body, context);
}
if (value instanceof BreakValue) {
return undefined;
}
return value;
},
ObjectExpression: function* (node, context) {
const obj = {};
for (const propUntyped of node.properties) {
// node.properties: es.Property | es.SpreadExpression, but
// our Acorn is set to ES6 which cannot have a es.SpreadExpression
// at this point. Force the type.
const prop = propUntyped;
let key;
if (prop.key.type === 'Identifier') {
key = prop.key.name;
}
else {
key = yield* evaluate(prop.key, context);
}
obj[key] = yield* evaluate(prop.value, context);
}
return obj;
},
BlockStatement: function* (node, context) {
// Create a new environment (block scoping)
const environment = (0, exports.createBlockEnvironment)(context, 'blockEnvironment');
(0, exports.pushEnvironment)(context, environment);
const result = yield* evaluateBlockStatement(context, node);
popEnvironment(context);
return result;
},
ImportDeclaration: function* (node, context) {
throw new Error('ImportDeclarations should already have been removed');
},
ExportNamedDeclaration: function* (_node, _context) {
// Exports are handled as a separate pre-processing step in 'transformImportedFile'.
// Subsequently, they are removed from the AST by 'removeExports' before the AST is evaluated.
// As such, there should be no ExportNamedDeclaration nodes in the AST.
throw new Error('Encountered an ExportNamedDeclaration node in the AST while evaluating. This suggests that an invariant has been broken.');
},
ExportDefaultDeclaration: function* (_node, _context) {
// Exports are handled as a separate pre-processing step in 'transformImportedFile'.
// Subsequently, they are removed from the AST by 'removeExports' before the AST is evaluated.
// As such, there should be no ExportDefaultDeclaration nodes in the AST.
throw new Error('Encountered an ExportDefaultDeclaration node in the AST while evaluating. This suggests that an invariant has been broken.');
},
ExportAllDeclaration: function* (_node, _context) {
// Exports are handled as a separate pre-processing step in 'transformImportedFile'.
// Subsequently, they are removed from the AST by 'removeExports' before the AST is evaluated.
// As such, there should be no ExportAllDeclaration nodes in the AST.
throw new Error('Encountered an ExportAllDeclaration node in the AST while evaluating. This suggests that an invariant has been broken.');
},
Program: function* (node, context) {
throw new Error('A program should not contain another program within itself');
}
};
// tslint:enable:object-literal-shorthand
// TODO: move to util
/**
* Checks if `env` is empty (that is, head of env is an empty object)
*/
function isEmptyEnvironment(env) {
return (0, lodash_1.isEmpty)(env.head);
}
/**
* Extracts the non-empty tail environment from the given environment and
* returns current environment if tail environment is a null.
*/
function getNonEmptyEnv(environment) {
if (isEmptyEnvironment(environment)) {
const tailEnvironment = environment.tail;
if (tailEnvironment === null) {
return environment;
}
return getNonEmptyEnv(tailEnvironment);
}
else {
return environment;
}
}
function* evaluateProgram(program, context) {
yield* visit(context, program);
context.numberOfOuterEnvironments += 1;
const environment = (0, exports.createBlockEnvironment)(context, 'programEnvironment');
(0, exports.pushEnvironment)(context, environment);
const otherNodes = [];
try {
for (const node of program.body) {
if (node.type !== 'ImportDeclaration') {
otherNodes.push(node);
continue;
}
yield* visit(context, node);
const moduleName = (0, helpers_1.getModuleDeclarationSource)(node);
const functions = context.nativeStorage.loadedModules[moduleName];
for (const spec of node.specifiers) {
declareIdentifier(context, spec.local.name, node);
let obj;
switch (spec.type) {
case 'ImportSpecifier': {
obj = functions[spec.imported.name];
break;
}
case 'ImportDefaultSpecifier': {
obj = functions.default;
break;
}
case 'ImportNamespaceSpecifier': {
obj = functions;
break;
}
}
defineVariable(context, spec.local.name, obj, true);
}
yield* leave(context);
}
}
catch (error) {
handleRuntimeError(context, error);
}
const newProgram = create.blockStatement(otherNodes);
const result = yield* forceIt(yield* evaluateBlockStatement(context, newProgram), context);
yield* leave(context); // Done visiting program
if (result instanceof closure_1.default) {
Object.defineProperty(getNonEmptyEnv(currentEnvironment(context)).head, (0, utils_1.uniqueId)(context), {
value: result,
writable: false,
enumerable: true
});
}
return result;
}
exports.evaluateProgram = evaluateProgram;
function* evaluate(node, context) {
yield* visit(context, node);
const result = yield* exports.evaluators[node.type](node, context);
yield* leave(context);
if (result instanceof closure_1.default) {
Object.defineProperty(getNonEmptyEnv(currentEnvironment(context)).head, (0, utils_1.uniqueId)(context), {
value: result,
writable: false,
enumerable: true
});
}
return result;
}
function* apply(context, fun, args, node, thisContext) {
let result;
let total = 0;
while (!(result instanceof ReturnValue)) {
if (fun instanceof closure_1.default) {
checkNumberOfArguments(context, fun, args, node);
const environment = createEnvironment(context, fun, args, node);
if (result instanceof TailCallReturnValue) {
replaceEnvironment(context, environment);
}
else {
(0, exports.pushEnvironment)(context, environment);
total++;
}
const bodyEnvironment = (0, exports.createBlockEnvironment)(context, 'functionBodyEnvironment');
bodyEnvironment.thisContext = thisContext;
(0, exports.pushEnvironment)(context, bodyEnvironment);
result = yield* evaluateBlockStatement(context, fun.node.body);
popEnvironment(context);
if (result instanceof TailCallReturnValue) {
fun = result.callee;
node = result.node;
args = result.args;
}
else if (!(result instanceof ReturnValue)) {
// No Return Value, set it as undefined
result = new ReturnValue(undefined);
}
}
else if (fun instanceof createContext_1.LazyBuiltIn) {
try {
let finalArgs = args;
if (fun.evaluateArgs) {
finalArgs = [];
for (const arg of args) {
finalArgs.push(yield* forceIt(arg, context));
}
}
result = fun.func.apply(thisContext, finalArgs);
break;
}
catch (e) {
// Recover from exception
context.runtime.environments = context.runtime.environments.slice(-context.numberOfOuterEnvironments);
const loc = node.loc ?? constants_1.UNKNOWN_LOCATION;
if (!(e instanceof runtimeSourceError_1.RuntimeSourceError || e instanceof errors.ExceptionError)) {
// The error could've arisen when the builtin called a source function which errored.
// If the cause was a source error, we don't want to include the error.
// However if the error came from the builtin itself, we need to handle it.
return handleRuntimeError(context, new errors.ExceptionError(e, loc));
}
result = undefined;
throw e;
}
}
else if (typeof fun === 'function') {
checkNumberOfArguments(context, fun, args, node);
try {
const forcedArgs = [];
for (const arg of args) {
forcedArgs.push(yield* forceIt(arg, context));
}
result = fun.apply(thisContext, forcedArgs);
break;
}
catch (e) {
// Recover from exception
context.runtime.environments = context.runtime.environments.slice(-context.numberOfOuterEnvironments);
const loc = node.loc ?? constants_1.UNKNOWN_LOCATION;
if (!(e instanceof runtimeSourceError_1.RuntimeSourceError || e instanceof errors.ExceptionError)) {
// The error could've arisen when the builtin called a source function which errored.
// If the cause was a source error, we don't want to include the error.
// However if the error came from the builtin itself, we need to handle it.
return handleRuntimeError(context, new errors.ExceptionError(e, loc));
}
result = undefined;
throw e;
}
}
else {
return handleRuntimeError(context, new errors.CallingNonFunctionValue(fun, node));
}
}
// Unwraps return value and release stack environment
if (result instanceof ReturnValue) {
result = result.value;
}
for (let i = 1; i <= total; i++) {
popEnvironment(context);
}
return result;
}
exports.apply = apply;
//# sourceMappingURL=interpreter.js.map