js-slang
Version:
Javascript-based implementations of Source, written in Typescript
653 lines • 27.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.canAvoidEnvInstr = exports.isEnvDependent = exports.hasContinueStatement = exports.hasContinueStatementIf = exports.hasBreakStatement = exports.hasBreakStatementIf = exports.hasReturnStatement = exports.hasReturnStatementIf = exports.checkStackOverFlow = exports.checkNumberOfArguments = exports.handleRuntimeError = exports.setVariable = exports.getVariable = exports.defineVariable = exports.hasImportDeclarations = exports.hasDeclarations = exports.declareFunctionsAndVariables = exports.declareIdentifier = exports.createProgramEnvironment = exports.createBlockEnvironment = exports.pushEnvironment = exports.popEnvironment = exports.createEnvironment = exports.currentEnvironment = exports.isSimpleFunction = exports.envChanging = exports.valueProducing = exports.reduceConditional = exports.handleSequence = exports.handleArrayCreation = exports.isStreamFn = exports.isEnvArray = exports.uniqueId = exports.isRestElement = exports.isStatementSequence = exports.isBlockStatement = exports.isIfStatement = exports.isReturnStatement = exports.isIdentifier = exports.isNode = exports.isInstr = void 0;
const lodash_1 = require("lodash");
const errors = require("../errors/errors");
const ast = require("../utils/ast/astCreator");
const heap_1 = require("./heap");
const instr = require("./instrCreator");
const types_1 = require("./types");
const closure_1 = require("./closure");
const continuations_1 = require("./continuations");
/**
* Typeguard for Instr to distinguish between program statements and instructions.
*
* @param command A ControlItem
* @returns true if the ControlItem is an instruction and false otherwise.
*/
const isInstr = (command) => {
return command.instrType !== undefined;
};
exports.isInstr = isInstr;
/**
* Typeguard for Node to distinguish between program statements and instructions.
*
* @param command A ControlItem
* @returns true if the ControlItem is a Node or StatementSequence, false if it is an instruction.
*/
const isNode = (command) => {
return command.type !== undefined;
};
exports.isNode = isNode;
/**
* Typeguard for esIdentifier. To verify if a Node is an esIdentifier.
*
* @param node a Node
* @returns true if node is an esIdentifier, false otherwise.
*/
const isIdentifier = (node) => {
return node.name !== undefined;
};
exports.isIdentifier = isIdentifier;
/**
* Typeguard for esReturnStatement. To verify if a Node is an esReturnStatement.
*
* @param node a Node
* @returns true if node is an esReturnStatement, false otherwise.
*/
const isReturnStatement = (node) => {
return node.type == 'ReturnStatement';
};
exports.isReturnStatement = isReturnStatement;
/**
* Typeguard for esIfStatement. To verify if a Node is an esIfStatement.
*
* @param node a Node
* @returns true if node is an esIfStatement, false otherwise.
*/
const isIfStatement = (node) => {
return node.type == 'IfStatement';
};
exports.isIfStatement = isIfStatement;
/**
* Typeguard for esBlockStatement. To verify if a Node is a block statement.
*
* @param node a Node
* @returns true if node is an esBlockStatement, false otherwise.
*/
const isBlockStatement = (node) => {
return node.type == 'BlockStatement';
};
exports.isBlockStatement = isBlockStatement;
/**
* Typeguard for StatementSequence. To verify if a ControlItem is a statement sequence.
*
* @param node a ControlItem
* @returns true if node is a StatementSequence, false otherwise.
*/
const isStatementSequence = (node) => {
return node.type == 'StatementSequence';
};
exports.isStatementSequence = isStatementSequence;
/**
* Typeguard for esRestElement. To verify if a Node is a block statement.
*
* @param node a Node
* @returns true if node is an esRestElement, false otherwise.
*/
const isRestElement = (node) => {
return node.type == 'RestElement';
};
exports.isRestElement = isRestElement;
/**
* Generate a unique id, for use in environments, arrays and closures.
*
* @param context the context used to provide the new unique id
* @returns a unique id
*/
const uniqueId = (context) => {
return `${context.runtime.objectCount++}`;
};
exports.uniqueId = uniqueId;
/**
* Returns whether `item` is an array with `id` and `environment` properties already attached.
*/
const isEnvArray = (item) => {
return ((0, lodash_1.isArray)(item) &&
{}.hasOwnProperty.call(item, 'id') &&
{}.hasOwnProperty.call(item, 'environment'));
};
exports.isEnvArray = isEnvArray;
/**
* Returns whether `item` is a non-closure function that returns a stream.
* If the function has been called already and we have the result, we can
* pass it in here as a 2nd argument for stronger checking
*/
const isStreamFn = (item, result) => {
if (result == null || !(0, lodash_1.isArray)(result) || result.length !== 2)
return false;
return ((0, lodash_1.isFunction)(item) &&
!(item instanceof closure_1.default) &&
(item.name === 'stream' || {}.hasOwnProperty.call(item, 'environment')));
};
exports.isStreamFn = isStreamFn;
/**
* Adds the properties `id` and `environment` to the given array, and adds the array to the
* current environment's heap. Adds the array to the heap of `envOverride` instead if it's defined.
*
* @param context the context used to provide the current environment and new unique id
* @param array the array to attach properties to, and for addition to the heap
*/
const handleArrayCreation = (context, array, envOverride) => {
const environment = envOverride ?? (0, exports.currentEnvironment)(context);
// Both id and environment are non-enumerable so iterating
// through the array will not return these values
Object.defineProperties(array, {
id: { value: (0, exports.uniqueId)(context) },
// Make environment writable as there are cases on the frontend where
// environments of objects need to be modified
environment: { value: environment, writable: true }
});
environment.heap.add(array);
};
exports.handleArrayCreation = handleArrayCreation;
/**
* A helper function for handling sequences of statements.
* Statements must be pushed in reverse order, and each statement is separated by a pop
* instruction so that only the result of the last statement remains on stash.
* Value producing statements have an extra pop instruction.
*
* @param seq Array of statements.
* @returns Array of commands to be pushed into control.
*/
const handleSequence = (seq) => {
const result = [];
let valueProduced = false;
for (const command of seq) {
if (!isImportDeclaration(command)) {
if ((0, exports.valueProducing)(command)) {
// Value producing statements have an extra pop instruction
if (valueProduced) {
result.push(instr.popInstr(command));
}
else {
valueProduced = true;
}
}
result.push(command);
}
}
// Push statements in reverse order
return result.reverse();
};
exports.handleSequence = handleSequence;
/**
* This function is used for ConditionalExpressions and IfStatements, to create the sequence
* of control items to be added.
*/
const reduceConditional = (node) => {
return [instr.branchInstr(node.consequent, node.alternate, node), node.test];
};
exports.reduceConditional = reduceConditional;
/**
* To determine if a control item is value producing. JavaScript distinguishes value producing
* statements and non-value producing statements.
* Refer to https://sourceacademy.nus.edu.sg/sicpjs/4.1.2 exercise 4.8.
*
* @param command Control item to determine if it is value producing.
* @returns true if it is value producing, false otherwise.
*/
const valueProducing = (command) => {
const type = command.type;
return (type !== 'VariableDeclaration' &&
type !== 'FunctionDeclaration' &&
type !== 'ContinueStatement' &&
type !== 'BreakStatement' &&
type !== 'DebuggerStatement' &&
(type !== 'BlockStatement' || command.body.some(exports.valueProducing)));
};
exports.valueProducing = valueProducing;
/**
* To determine if a control item changes the environment.
* There is a change in the environment when
* 1. pushEnvironment() is called when creating a new frame, if there are variable declarations.
* Called in Program, BlockStatement, and Application instructions.
* 2. there is an assignment.
* Called in Assignment and Array Assignment instructions.
* 3. a new object is created.
* Called in ExpressionStatement that contains an ArrowFunctionExpression, or an ArrowFunctionExpression
*
* @param command Control item to check against.
* @returns true if it changes the environment, false otherwise.
*/
const envChanging = (command) => {
if ((0, exports.isNode)(command)) {
const type = command.type;
return (type === 'Program' ||
type === 'BlockStatement' ||
type === 'ArrowFunctionExpression' ||
(type === 'ExpressionStatement' && command.expression.type === 'ArrowFunctionExpression'));
}
else {
const type = command.instrType;
return (type === types_1.InstrType.ENVIRONMENT ||
type === types_1.InstrType.ARRAY_LITERAL ||
type === types_1.InstrType.ASSIGNMENT ||
type === types_1.InstrType.ARRAY_ASSIGNMENT ||
(type === types_1.InstrType.APPLICATION && command.numOfArgs > 0));
}
};
exports.envChanging = envChanging;
/**
* To determine if the function is simple.
* Simple functions contain a single return statement.
*
* @param node The function to check against.
* @returns true if the function is simple, false otherwise.
*/
const isSimpleFunction = (node) => {
if (node.body.type !== 'BlockStatement' && node.body.type !== 'StatementSequence') {
return true;
}
else {
const block = node.body;
return block.body.length === 1 && block.body[0].type === 'ReturnStatement';
}
};
exports.isSimpleFunction = isSimpleFunction;
/**
* Environments
*/
const currentEnvironment = (context) => context.runtime.environments[0];
exports.currentEnvironment = currentEnvironment;
const createEnvironment = (context, closure, args, callExpression) => {
const environment = {
name: (0, exports.isIdentifier)(callExpression.callee)
? callExpression.callee.name
: closure.declaredName ?? closure.functionName,
tail: closure.environment,
head: {},
heap: new heap_1.default(),
id: (0, exports.uniqueId)(context),
callExpression: {
...callExpression,
arguments: args.map(ast.primitive)
}
};
closure.node.params.forEach((param, index) => {
if ((0, exports.isRestElement)(param)) {
const array = args.slice(index);
(0, exports.handleArrayCreation)(context, array, environment);
environment.head[param.argument.name] = array;
}
else {
environment.head[param.name] = args[index];
}
});
return environment;
};
exports.createEnvironment = createEnvironment;
const popEnvironment = (context) => context.runtime.environments.shift();
exports.popEnvironment = popEnvironment;
const pushEnvironment = (context, environment) => {
context.runtime.environments.unshift(environment);
context.runtime.environmentTree.insert(environment);
};
exports.pushEnvironment = pushEnvironment;
const createBlockEnvironment = (context, name = 'blockEnvironment') => {
return {
name,
tail: (0, exports.currentEnvironment)(context),
head: {},
heap: new heap_1.default(),
id: (0, exports.uniqueId)(context)
};
};
exports.createBlockEnvironment = createBlockEnvironment;
const createProgramEnvironment = (context, isPrelude) => {
return (0, exports.createBlockEnvironment)(context, isPrelude ? 'prelude' : 'programEnvironment');
};
exports.createProgramEnvironment = createProgramEnvironment;
/**
* Variables
*/
const UNASSIGNED_CONST = Symbol('const declaration');
const UNASSIGNED_LET = Symbol('let declaration');
function declareIdentifier(context, name, node, environment, constant = false) {
if (environment.head.hasOwnProperty(name)) {
const descriptors = Object.getOwnPropertyDescriptors(environment.head);
return (0, exports.handleRuntimeError)(context, new errors.VariableRedeclaration(node, name, descriptors[name].writable));
}
environment.head[name] = constant ? UNASSIGNED_CONST : UNASSIGNED_LET;
return environment;
}
exports.declareIdentifier = declareIdentifier;
function declareVariables(context, node, environment) {
for (const declaration of node.declarations) {
// Retrieve declaration type from node
const constant = node.kind === 'const';
declareIdentifier(context, declaration.id.name, node, environment, constant);
}
}
function declareFunctionsAndVariables(context, node, environment) {
for (const statement of node.body) {
switch (statement.type) {
case 'VariableDeclaration':
declareVariables(context, statement, environment);
break;
case 'FunctionDeclaration':
// FunctionDeclaration is always of type constant
declareIdentifier(context, statement.id.name, statement, environment, true);
break;
}
}
}
exports.declareFunctionsAndVariables = declareFunctionsAndVariables;
function hasDeclarations(node) {
for (const statement of node.body) {
if (statement.type === 'VariableDeclaration' || statement.type === 'FunctionDeclaration') {
return true;
}
}
return false;
}
exports.hasDeclarations = hasDeclarations;
function hasImportDeclarations(node) {
for (const statement of node.body) {
if (statement.type === 'ImportDeclaration') {
return true;
}
}
return false;
}
exports.hasImportDeclarations = hasImportDeclarations;
function isImportDeclaration(node) {
return node.type === 'ImportDeclaration';
}
function defineVariable(context, name, value, constant = false, node) {
const environment = (0, exports.currentEnvironment)(context);
if (environment.head[name] !== UNASSIGNED_CONST && environment.head[name] !== UNASSIGNED_LET) {
return (0, exports.handleRuntimeError)(context, new errors.VariableRedeclaration(node, name, !constant));
}
if (constant && value instanceof closure_1.default) {
value.declaredName = name;
}
Object.defineProperty(environment.head, name, {
value,
writable: !constant,
enumerable: true
});
return environment;
}
exports.defineVariable = defineVariable;
const getVariable = (context, name, node) => {
let environment = (0, exports.currentEnvironment)(context);
while (environment) {
if (environment.head.hasOwnProperty(name)) {
if (environment.head[name] === UNASSIGNED_CONST ||
environment.head[name] === UNASSIGNED_LET) {
return (0, exports.handleRuntimeError)(context, new errors.UnassignedVariable(name, node));
}
else {
return environment.head[name];
}
}
else {
environment = environment.tail;
}
}
return (0, exports.handleRuntimeError)(context, new errors.UndefinedVariable(name, node));
};
exports.getVariable = getVariable;
const setVariable = (context, name, value, node) => {
let environment = (0, exports.currentEnvironment)(context);
while (environment) {
if (environment.head.hasOwnProperty(name)) {
if (environment.head[name] === UNASSIGNED_CONST ||
environment.head[name] === UNASSIGNED_LET) {
break;
}
const descriptors = Object.getOwnPropertyDescriptors(environment.head);
if (descriptors[name].writable) {
environment.head[name] = value;
return undefined;
}
return (0, exports.handleRuntimeError)(context, new errors.ConstAssignment(node, name));
}
else {
environment = environment.tail;
}
}
return (0, exports.handleRuntimeError)(context, new errors.UndefinedVariable(name, node));
};
exports.setVariable = setVariable;
const handleRuntimeError = (context, error) => {
context.errors.push(error);
throw error;
};
exports.handleRuntimeError = handleRuntimeError;
const checkNumberOfArguments = (context, callee, args, exp) => {
if (callee instanceof closure_1.default) {
// User-defined or Pre-defined functions
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 (0, exports.handleRuntimeError)(context, new errors.InvalidNumberOfArguments(exp, hasVarArgs ? params.length - 1 : params.length, args.length, hasVarArgs));
}
}
else if ((0, continuations_1.isCallWithCurrentContinuation)(callee)) {
// call/cc should have a single argument
if (args.length !== 1) {
return (0, exports.handleRuntimeError)(context, new errors.InvalidNumberOfArguments(exp, 1, args.length, false));
}
return undefined;
}
else if (callee instanceof continuations_1.Continuation) {
// Continuations have variadic arguments,
// and so we can let it pass
// TODO: in future, if we can somehow check the number of arguments
// expected by the continuation, we can add a check here.
return undefined;
}
else {
// Pre-built functions
const hasVarArgs = callee.minArgsNeeded != undefined;
if (hasVarArgs ? callee.minArgsNeeded > args.length : callee.length !== args.length) {
return (0, exports.handleRuntimeError)(context, new errors.InvalidNumberOfArguments(exp, hasVarArgs ? callee.minArgsNeeded : callee.length, args.length, hasVarArgs));
}
}
return undefined;
};
exports.checkNumberOfArguments = checkNumberOfArguments;
/**
* This function can be used to check for a stack overflow.
* The current limit is set to be a control size of 1.0 x 10^5, if the control
* flows beyond this limit an error is thrown.
* This corresponds to about 10mb of space according to tests ran.
*/
const checkStackOverFlow = (context, control) => {
if (control.size() > 100000) {
const stacks = [];
let counter = 0;
for (let i = 0; counter < errors.MaximumStackLimitExceeded.MAX_CALLS_TO_SHOW &&
i < context.runtime.environments.length; i++) {
if (context.runtime.environments[i].callExpression) {
stacks.unshift(context.runtime.environments[i].callExpression);
counter++;
}
}
(0, exports.handleRuntimeError)(context, new errors.MaximumStackLimitExceeded(context.runtime.nodes[0], stacks));
}
};
exports.checkStackOverFlow = checkStackOverFlow;
/**
* Checks whether an `if` statement returns in every possible branch.
* @param body The `if` statement to be checked
* @return `true` if every branch has a return statement, else `false`.
*/
const hasReturnStatementIf = (statement) => {
let hasReturn = true;
// Parser enforces that if/else have braces (block statement)
hasReturn = hasReturn && (0, exports.hasReturnStatement)(statement.consequent);
if (statement.alternate) {
if ((0, exports.isIfStatement)(statement.alternate)) {
hasReturn = hasReturn && (0, exports.hasReturnStatementIf)(statement.alternate);
}
else if ((0, exports.isBlockStatement)(statement.alternate) || (0, exports.isStatementSequence)(statement.alternate)) {
hasReturn = hasReturn && (0, exports.hasReturnStatement)(statement.alternate);
}
}
return hasReturn;
};
exports.hasReturnStatementIf = hasReturnStatementIf;
/**
* Checks whether a block returns in every possible branch.
* @param body The block to be checked
* @return `true` if every branch has a return statement, else `false`.
*/
const hasReturnStatement = (block) => {
let hasReturn = false;
for (const statement of block.body) {
if ((0, exports.isReturnStatement)(statement)) {
hasReturn = true;
}
else if ((0, exports.isIfStatement)(statement)) {
// Parser enforces that if/else have braces (block statement)
hasReturn = hasReturn || (0, exports.hasReturnStatementIf)(statement);
}
else if ((0, exports.isBlockStatement)(statement) || (0, exports.isStatementSequence)(statement)) {
hasReturn = hasReturn && (0, exports.hasReturnStatement)(statement);
}
}
return hasReturn;
};
exports.hasReturnStatement = hasReturnStatement;
const hasBreakStatementIf = (statement) => {
let hasBreak = false;
// Parser enforces that if/else have braces (block statement)
hasBreak = hasBreak || (0, exports.hasBreakStatement)(statement.consequent);
if (statement.alternate) {
if ((0, exports.isIfStatement)(statement.alternate)) {
hasBreak = hasBreak || (0, exports.hasBreakStatementIf)(statement.alternate);
}
else if ((0, exports.isBlockStatement)(statement.alternate) || (0, exports.isStatementSequence)(statement.alternate)) {
hasBreak = hasBreak || (0, exports.hasBreakStatement)(statement.alternate);
}
}
return hasBreak;
};
exports.hasBreakStatementIf = hasBreakStatementIf;
/**
* Checks whether a block OR any of its child blocks has a `break` statement.
* @param body The block to be checked
* @return `true` if there is a `break` statement, else `false`.
*/
const hasBreakStatement = (block) => {
let hasBreak = false;
for (const statement of block.body) {
if (statement.type === 'BreakStatement') {
hasBreak = true;
}
else if ((0, exports.isIfStatement)(statement)) {
// Parser enforces that if/else have braces (block statement)
hasBreak = hasBreak || (0, exports.hasBreakStatementIf)(statement);
}
else if ((0, exports.isBlockStatement)(statement) || (0, exports.isStatementSequence)(statement)) {
hasBreak = hasBreak || (0, exports.hasBreakStatement)(statement);
}
}
return hasBreak;
};
exports.hasBreakStatement = hasBreakStatement;
const hasContinueStatementIf = (statement) => {
let hasContinue = false;
// Parser enforces that if/else have braces (block statement)
hasContinue = hasContinue || (0, exports.hasContinueStatement)(statement.consequent);
if (statement.alternate) {
if ((0, exports.isIfStatement)(statement.alternate)) {
hasContinue = hasContinue || (0, exports.hasContinueStatementIf)(statement.alternate);
}
else if ((0, exports.isBlockStatement)(statement.alternate) || (0, exports.isStatementSequence)(statement.alternate)) {
hasContinue = hasContinue || (0, exports.hasContinueStatement)(statement.alternate);
}
}
return hasContinue;
};
exports.hasContinueStatementIf = hasContinueStatementIf;
/**
* Checks whether a block OR any of its child blocks has a `continue` statement.
* @param body The block to be checked
* @return `true` if there is a `continue` statement, else `false`.
*/
const hasContinueStatement = (block) => {
let hasContinue = false;
for (const statement of block.body) {
if (statement.type === 'ContinueStatement') {
hasContinue = true;
}
else if ((0, exports.isIfStatement)(statement)) {
// Parser enforces that if/else have braces (block statement)
hasContinue = hasContinue || (0, exports.hasContinueStatementIf)(statement);
}
else if ((0, exports.isBlockStatement)(statement) || (0, exports.isStatementSequence)(statement)) {
hasContinue = hasContinue || (0, exports.hasContinueStatement)(statement);
}
}
return hasContinue;
};
exports.hasContinueStatement = hasContinueStatement;
/**
* Checks whether the evaluation of the given command depends on the current environment.
* @param command The command to be checked
* @return `true` if the command is environment depedent, else `false`.
* NOTE: this check is meant to detect and avoid pushing environment instruction onto the
* control in SIMPLE CASES, so it might not be exhaustive
*/
const isEnvDependent = (command) => {
// If the result is already calculated, return it
if (command.isEnvDependent != undefined) {
return command.isEnvDependent;
}
// Otherwise, calculate and store the result
let isDependent = true;
if ((0, exports.isInstr)(command)) {
const type = command.instrType;
isDependent = !(type === types_1.InstrType.UNARY_OP ||
type === types_1.InstrType.BINARY_OP ||
type === types_1.InstrType.POP ||
type === types_1.InstrType.ARRAY_ACCESS ||
type === types_1.InstrType.ARRAY_ASSIGNMENT ||
type === types_1.InstrType.RESET ||
type === types_1.InstrType.CONTINUE_MARKER ||
type === types_1.InstrType.BREAK_MARKER);
}
else {
const type = command.type;
switch (type) {
case 'StatementSequence':
isDependent = command.body.some((statement) => (0, exports.isEnvDependent)(statement));
case 'Literal':
isDependent = false;
break;
case 'BinaryExpression':
isDependent = (0, exports.isEnvDependent)(command.left) || (0, exports.isEnvDependent)(command.right);
break;
case 'LogicalExpression':
isDependent = (0, exports.isEnvDependent)(command.left) || (0, exports.isEnvDependent)(command.right);
break;
case 'UnaryExpression':
isDependent = (0, exports.isEnvDependent)(command.argument);
break;
case 'ExpressionStatement':
isDependent = (0, exports.isEnvDependent)(command.expression);
break;
default:
break;
}
}
command.isEnvDependent = isDependent;
return isDependent;
};
exports.isEnvDependent = isEnvDependent;
/**
* Checks whether an environment instruction needs to be pushed onto the control.
* @param control The current control to be checked
* @return `true` if the environment instruction can be avoided, else `false`.
* NOTE: this check is meant to detect and avoid pushing environment instruction onto the
* control in SIMPLE CASES, so it might not be exhaustive
*/
const canAvoidEnvInstr = (control) => {
return !control.getStack().some((command) => (0, exports.isEnvDependent)(command));
};
exports.canAvoidEnvInstr = canAvoidEnvInstr;
//# sourceMappingURL=utils.js.map
;