UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

692 lines 28 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hasContinueStatement = exports.hasBreakStatement = exports.hasReturnStatement = exports.hasReturnStatementIf = exports.checkStackOverFlow = exports.checkNumberOfArguments = exports.handleRuntimeError = exports.setVariable = exports.getVariable = exports.createProgramEnvironment = exports.createBlockEnvironment = exports.pushEnvironment = exports.popEnvironment = exports.createEnvironment = exports.currentEnvironment = exports.setTransformers = exports.currentTransformers = 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.isNode = exports.isInstr = exports.isSchemeValue = void 0; exports.declareIdentifier = declareIdentifier; exports.declareFunctionsAndVariables = declareFunctionsAndVariables; exports.defineVariable = defineVariable; exports.isEnvDependent = isEnvDependent; const lodash_1 = require("lodash"); const base_1 = require("../alt-langs/scheme/scm-slang/src/stdlib/base"); const core_math_1 = require("../alt-langs/scheme/scm-slang/src/stdlib/core-math"); const errors = require("../errors/errors"); const langs_1 = require("../langs"); const ast = require("../utils/ast/astCreator"); const typeGuards_1 = require("../utils/ast/typeGuards"); const closure_1 = require("./closure"); const continuations_1 = require("./continuations"); const heap_1 = require("./heap"); const instr = require("./instrCreator"); const scheme_macros_1 = require("./scheme-macros"); const types_1 = require("./types"); /** * Typeguard for commands to check if they are scheme values. * * @param command A ControlItem * @returns true if the ControlItem is a scheme value, false otherwise. */ const isSchemeValue = (command) => { return (command === null || typeof command === 'string' || typeof command === 'boolean' || Array.isArray(command) || command instanceof base_1._Symbol || (0, core_math_1.is_number)(command)); }; exports.isSchemeValue = isSchemeValue; /** * 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) => { // this prevents us from reading properties of null if ((0, exports.isSchemeValue)(command)) { return false; } return 'instrType' in command; }; 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) => { // this prevents us from reading properties of null if ((0, exports.isSchemeValue)(command)) { return false; } return 'type' in command; }; exports.isNode = isNode; /** * 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 (0, exports.isNode)(node) && 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 (Array.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 || !Array.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 (!(0, typeGuards_1.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 if ((0, exports.isInstr)(command)) { 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)); } else { // TODO deal with scheme control items // for now, as per the CSE machine paper, // we decide to ignore environment optimizations // for scheme control items :P return true; } }; exports.envChanging = envChanging; // TODO: This type guard does not seem to be doing what it thinks its doing /** * 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; /** * Transformers */ const currentTransformers = (context) => context.runtime.transformers; exports.currentTransformers = currentTransformers; const setTransformers = (context, transformers) => { context.runtime.transformers = transformers; }; exports.setTransformers = setTransformers; /** * Environments */ const currentEnvironment = (context) => context.runtime.environments[0]; exports.currentEnvironment = currentEnvironment; const createEnvironment = (context, closure, args, callExpression) => { const environment = { name: (0, typeGuards_1.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; } 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; } } } function defineVariable(context, name, value, constant = false, node) { const environment = (0, exports.currentEnvironment)(context); // we disable this check for full scheme due to the inability to scan for variables before usage if (environment.head[name] !== UNASSIGNED_CONST && environment.head[name] !== UNASSIGNED_LET && context.chapter !== langs_1.Chapter.FULL_SCHEME) { 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; } 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 ((0, scheme_macros_1.isEval)(callee)) { // eval 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 ((0, scheme_macros_1.isApply)(callee)) { // apply should have at least two arguments if (args.length < 2) { return (0, exports.handleRuntimeError)(context, new errors.InvalidNumberOfArguments(exp, 2, 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; function nodeVisitor(node, type) { switch (node.type) { case 'BlockStatement': case 'StatementSequence': case 'Program': return node.body.some(each => nodeVisitor(each, type)); case 'IfStatement': { const { consequent, alternate } = node; if (nodeVisitor(consequent, type)) return true; return !!alternate && nodeVisitor(alternate, type); } default: return node.type === type; } } /** * 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) => { return nodeVisitor(block, 'BreakStatement'); }; exports.hasBreakStatement = hasBreakStatement; /** * 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) => { return nodeVisitor(block, 'ContinueStatement'); }; exports.hasContinueStatement = hasContinueStatement; const envCalculators = { ArrayExpression: ({ elements }) => elements.some(isEnvDependent), ArrowFunctionExpression: true, AssignmentExpression: ['left', 'right'], BlockStatement: ({ body }) => body.some(isEnvDependent), BinaryExpression: ['left', 'right'], BreakStatement: false, CallExpression: node => [node.callee, ...node.arguments].some(isEnvDependent), ConditionalExpression: ['alternate', 'consequent', 'test'], ContinueStatement: false, DebuggerStatement: false, ExpressionStatement: 'expression', ForStatement: ['body', 'init', 'test', 'update'], FunctionDeclaration: true, Identifier: true, IfStatement: ['alternate', 'consequent', 'test'], ImportDeclaration: ({ specifiers }) => specifiers.some(isEnvDependent), ImportDefaultSpecifier: true, ImportSpecifier: true, Literal: false, LogicalExpression: ['left', 'right'], MemberExpression: ['object', 'property'], Program: ({ body }) => body.some(isEnvDependent), ReturnStatement: 'argument', StatementSequence: ({ body }) => body.some(isEnvDependent), UnaryExpression: 'argument', VariableDeclaration: true, WhileStatement: ['body', 'test'], //Instruction [types_1.InstrType.APPLICATION]: true, [types_1.InstrType.ARRAY_ACCESS]: false, [types_1.InstrType.ARRAY_ASSIGNMENT]: false, [types_1.InstrType.ARRAY_LITERAL]: true, [types_1.InstrType.ASSIGNMENT]: true, [types_1.InstrType.BINARY_OP]: false, [types_1.InstrType.BRANCH]: ['alternate', 'consequent'], [types_1.InstrType.BREAK_MARKER]: false, [types_1.InstrType.CONTINUE]: false, [types_1.InstrType.CONTINUE_MARKER]: false, [types_1.InstrType.ENVIRONMENT]: false, [types_1.InstrType.MARKER]: false, [types_1.InstrType.POP]: false, [types_1.InstrType.RESET]: false, [types_1.InstrType.SPREAD]: false, [types_1.InstrType.UNARY_OP]: false, [types_1.InstrType.WHILE]: ['body', 'test'], [types_1.InstrType.FOR]: ['body', 'init', 'test', 'update'] }; /** * Checks whether the evaluation of the given control item depends on the current environment. * The item is also considered environment dependent if its evaluation introduces * environment dependent items * @param item The control item to be checked * @return `true` if the item is environment depedent, else `false`. */ function isEnvDependent(item) { if (item === null || item === undefined) { return false; } // Scheme primitives are not environment dependent. if (typeof item === 'string' || typeof item === 'boolean') { return false; } // Scheme symbols represent identifiers, which are environment dependent. if (item instanceof base_1._Symbol) { return true; } // We assume no optimisations for scheme lists. if (Array.isArray(item)) { return true; } // If result is already calculated, return it if (item.isEnvDependent !== undefined) { return item.isEnvDependent; } let calculator; if ((0, exports.isNode)(item)) { calculator = envCalculators[item.type]; } else if ((0, exports.isInstr)(item)) { calculator = envCalculators[item.instrType]; } switch (typeof calculator) { case 'boolean': { item.isEnvDependent = calculator; break; } case 'string': { // @ts-expect-error Indexing with an arbitrary index item.isEnvDependent = isEnvDependent(item[calculator]); break; } case 'function': { // @ts-expect-error Function parameter gets narrowed to never item.isEnvDependent = calculator(item); break; } case 'undefined': { item.isEnvDependent = false; break; } default: { if (!Array.isArray(calculator)) throw new Error(`Invalid setter for ${item}: ${calculator}`); // @ts-expect-error Indexing with an arbitrary index item.isEnvDependent = calculator.some(each => isEnvDependent(item[each])); break; } } return item.isEnvDependent; } //# sourceMappingURL=utils.js.map