UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

653 lines 27.3 kB
"use strict"; 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