UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

959 lines 43.6 kB
"use strict"; /** * This interpreter implements an explicit-control evaluator. * * Heavily adapted from https://github.com/source-academy/JSpike/ * and the legacy interpreter at '../interpreter/interpreter' */ Object.defineProperty(exports, "__esModule", { value: true }); exports.generateCSEMachineStateStream = exports.CSEResultPromise = exports.resumeEvaluate = exports.evaluate = exports.Transformers = exports.Stash = exports.Control = void 0; const lodash_1 = require("lodash"); const constants_1 = require("../constants"); const errors = require("../errors/errors"); const runtimeSourceError_1 = require("../errors/runtimeSourceError"); const inspector_1 = require("../stdlib/inspector"); const ast = require("../utils/ast/astCreator"); const helpers_1 = require("../utils/ast/helpers"); const operators_1 = require("../utils/operators"); const rttc = require("../utils/rttc"); const seq = require("../utils/statementSeqTransform"); const validator_1 = require("../validator/validator"); const mapper_1 = require("../alt-langs/mapper"); const closure_1 = require("./closure"); const continuations_1 = require("./continuations"); const instr = require("./instrCreator"); const stack_1 = require("./stack"); const types_1 = require("./types"); const utils_1 = require("./utils"); const scheme_macros_1 = require("./scheme-macros"); const macro_utils_1 = require("./macro-utils"); /** * The control is a list of commands that still needs to be executed by the machine. * It contains syntax tree nodes or instructions. */ class Control extends stack_1.Stack { constructor(program) { super(); this.numEnvDependentItems = 0; // Load program into control stack if (program) this.push(program); } canAvoidEnvInstr() { return this.numEnvDependentItems === 0; } // For testing purposes getNumEnvDependentItems() { return this.numEnvDependentItems; } pop() { const item = super.pop(); if (item !== undefined && (0, utils_1.isEnvDependent)(item)) { this.numEnvDependentItems--; } return item; } push(...items) { const itemsNew = Control.simplifyBlocksWithoutDeclarations(...items); itemsNew.forEach((item) => { if ((0, utils_1.isEnvDependent)(item)) { this.numEnvDependentItems++; } }); super.push(...itemsNew); } /** * Before pushing block statements on the control stack, we check if the block statement has any declarations. * If not, the block is converted to a StatementSequence. * @param items The items being pushed on the control. * @returns The same set of control items, but with block statements without declarations converted to StatementSequences. * NOTE: this function handles any case where StatementSequence has to be converted back into BlockStatement due to type issues */ static simplifyBlocksWithoutDeclarations(...items) { const itemsNew = []; items.forEach(item => { if ((0, utils_1.isNode)(item) && (0, utils_1.isBlockStatement)(item) && !(0, utils_1.hasDeclarations)(item)) { // Push block body as statement sequence const seq = ast.statementSequence(item.body, item.loc); itemsNew.push(seq); } else { itemsNew.push(item); } }); return itemsNew; } copy() { const newControl = new Control(); const stackCopy = super.getStack(); newControl.push(...stackCopy); return newControl; } } exports.Control = Control; /** * The stash is a list of values that stores intermediate results. */ class Stash extends stack_1.Stack { constructor() { super(); } copy() { const newStash = new Stash(); const stackCopy = super.getStack(); newStash.push(...stackCopy); return newStash; } } exports.Stash = Stash; /** * The T component is a dictionary of mappings from syntax names to * their corresponding syntax rule transformers (patterns). * * Similar to the E component, there is a matching * "T" environment tree that is used to store the transformers. * as such, we need to track the transformers and update them with the environment. */ class Transformers { constructor(parent) { this.parent = parent || null; this.items = new Map(); } // only call this if you are sure that the pattern exists. getPattern(name) { // check if the pattern exists in the current transformer if (this.items.has(name)) { return this.items.get(name); } // else check if the pattern exists in the parent transformer if (this.parent) { return this.parent.getPattern(name); } // should not get here. use this properly. throw new Error(`Pattern ${name} not found in transformers`); } hasPattern(name) { // check if the pattern exists in the current transformer if (this.items.has(name)) { return true; } // else check if the pattern exists in the parent transformer if (this.parent) { return this.parent.hasPattern(name); } return false; } addPattern(name, item) { this.items.set(name, item); } } exports.Transformers = Transformers; /** * Function to be called when a program is to be interpreted using * the explicit control evaluator. * * @param program The program to evaluate. * @param context The context to evaluate the program in. * @returns The result of running the CSE machine. */ function evaluate(program, context, options) { try { (0, validator_1.checkProgramForUndefinedVariables)(program, context); } catch (error) { context.errors.push(error); return new types_1.CseError(error); } seq.transform(program); try { context.runtime.isRunning = true; context.runtime.control = new Control(program); context.runtime.stash = new Stash(); // set a global transformer if it does not exist. context.runtime.transformers = context.runtime.transformers ? context.runtime.transformers : new Transformers(); return runCSEMachine(context, context.runtime.control, context.runtime.stash, options.envSteps, options.stepLimit, options.isPrelude); } catch (error) { return new types_1.CseError(error); } finally { context.runtime.isRunning = false; } } exports.evaluate = evaluate; /** * Function that is called when a user wishes to resume evaluation after * hitting a breakpoint. * This should only be called after the first 'evaluate' function has been called so that * context.runtime.control and context.runtime.stash are defined. * @param context The context to continue evaluating the program in. * @returns The result of running the CSE machine. */ function resumeEvaluate(context) { try { context.runtime.isRunning = true; return runCSEMachine(context, context.runtime.control, context.runtime.stash, -1, -1); } catch (error) { return new types_1.CseError(error); } finally { context.runtime.isRunning = false; } } exports.resumeEvaluate = resumeEvaluate; function evaluateImports(program, context) { try { const [importNodeMap] = (0, helpers_1.filterImportDeclarations)(program); const environment = (0, utils_1.currentEnvironment)(context); for (const [moduleName, nodes] of importNodeMap) { const functions = context.nativeStorage.loadedModules[moduleName]; for (const node of nodes) { for (const spec of node.specifiers) { (0, utils_1.declareIdentifier)(context, spec.local.name, node, environment); let obj; switch (spec.type) { case 'ImportSpecifier': { obj = functions[spec.imported.name]; break; } case 'ImportDefaultSpecifier': { obj = functions.default; break; } case 'ImportNamespaceSpecifier': { obj = functions; break; } } (0, utils_1.defineVariable)(context, spec.local.name, obj, true, node); } } } } catch (error) { (0, utils_1.handleRuntimeError)(context, error); } } /** * Function that returns the appropriate Promise<Result> given the output of CSE machine evaluating, depending * on whether the program is finished evaluating, ran into a breakpoint or ran into an error. * @param context The context of the program. * @param value The value of CSE machine evaluating the program. * @returns The corresponding promise. */ function CSEResultPromise(context, value) { return new Promise(resolve => { if (value instanceof types_1.CSEBreak) { resolve({ status: 'suspended-cse-eval', context }); } else if (value instanceof types_1.CseError) { resolve({ status: 'error' }); } else { resolve({ status: 'finished', context, value }); } }); } exports.CSEResultPromise = CSEResultPromise; /** * The primary runner/loop of the explicit control evaluator. * * @param context The context to evaluate the program in. * @param control Points to the current context.runtime.control * @param stash Points to the current context.runtime.stash * @param isPrelude Whether the program we are running is the prelude * @returns A special break object if the program is interrupted by a breakpoint; * else the top value of the stash. It is usually the return value of the program. */ function runCSEMachine(context, control, stash, envSteps, stepLimit, isPrelude = false) { const eceState = generateCSEMachineStateStream(context, control, stash, envSteps, stepLimit, isPrelude); // Done intentionally as the state is not needed // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _ of eceState) { } return stash.peek(); } function* generateCSEMachineStateStream(context, control, stash, envSteps, stepLimit, isPrelude = false) { context.runtime.break = false; context.runtime.nodes = []; // steps: number of steps completed let steps = 0; let command = control.peek(); // Push first node to be evaluated into context. // The typeguard is there to guarantee that we are pushing a node (which should always be the case) if (command !== undefined && (0, utils_1.isNode)(command)) { context.runtime.nodes.unshift(command); } while (command !== undefined) { // Return to capture a snapshot of the control and stash after the target step count is reached if (!isPrelude && steps === envSteps) { yield { stash, control, steps }; return; } // Step limit reached, stop further evaluation if (!isPrelude && steps === stepLimit) { break; } if ((0, utils_1.isNode)(command) && command.type === 'DebuggerStatement') { // steps += 1 // Record debugger step if running for the first time if (envSteps === -1) { context.runtime.breakpointSteps.push(steps); } } if (!isPrelude && (0, utils_1.envChanging)(command)) { // command is evaluated on the next step // Hence, next step will change the environment context.runtime.changepointSteps.push(steps + 1); } control.pop(); if ((0, utils_1.isNode)(command)) { context.runtime.nodes.shift(); context.runtime.nodes.unshift(command); (0, inspector_1.checkEditorBreakpoints)(context, command); cmdEvaluators[command.type](command, context, control, stash, isPrelude); if (context.runtime.break && context.runtime.debuggerOn) { // We can put this under isNode since context.runtime.break // will only be updated after a debugger statement and so we will // run into a node immediately after. // With the new evaluator, we don't return a break // return new CSEBreak() } } else if ((0, utils_1.isInstr)(command)) { // Command is an instruction cmdEvaluators[command.instrType](command, context, control, stash, isPrelude); } else { // this is a scheme value (0, scheme_macros_1.schemeEval)(command, context, control, stash, isPrelude); } // Push undefined into the stack if both control and stash is empty if (control.isEmpty() && stash.isEmpty()) { stash.push(undefined); } command = control.peek(); steps += 1; if (!isPrelude) { context.runtime.envStepsTotal = steps; } yield { stash, control, steps }; } } exports.generateCSEMachineStateStream = generateCSEMachineStateStream; /** * Dictionary of functions which handle the logic for the response of the three registers of * the CSE machine to each ControlItem. */ const cmdEvaluators = { /** * Statements */ Program: function (command, context, control, stash, isPrelude) { // After execution of a program, the current environment might be a local one. // This can cause issues (for example, during execution of consecutive REPL programs) // This piece of code will reset the current environment to either a global one, a program one or a prelude one. while ((0, utils_1.currentEnvironment)(context).name != 'global' && (0, utils_1.currentEnvironment)(context).name != 'programEnvironment' && (0, utils_1.currentEnvironment)(context).name != 'prelude') { (0, utils_1.popEnvironment)(context); } // If the program has outer declarations: // - Create the program environment (if none exists yet), and // - Declare the functions and variables in the program environment. if ((0, utils_1.hasDeclarations)(command) || (0, utils_1.hasImportDeclarations)(command)) { if ((0, utils_1.currentEnvironment)(context).name != 'programEnvironment') { const programEnv = (0, utils_1.createProgramEnvironment)(context, isPrelude); (0, utils_1.pushEnvironment)(context, programEnv); } const environment = (0, utils_1.currentEnvironment)(context); evaluateImports(command, context); (0, utils_1.declareFunctionsAndVariables)(context, command, environment); } if (command.body.length == 1) { // If program only consists of one statement, unwrap outer block control.push(...(0, utils_1.handleSequence)(command.body)); } else { // Push block body as statement sequence const seq = ast.statementSequence(command.body, command.loc); control.push(seq); } }, BlockStatement: function (command, context, control) { // To restore environment after block ends // If there is an env instruction on top of the stack, or if there are no declarations // we do not need to push another one // The no declarations case is handled at the transform stage, so no blockStatement node without declarations should end up here. const next = control.peek(); // Push ENVIRONMENT instruction if needed - if next control stack item // exists and is not an environment instruction, OR the control only contains // environment indepedent items if (next && !((0, utils_1.isInstr)(next) && next.instrType === types_1.InstrType.ENVIRONMENT) && !control.canAvoidEnvInstr()) { control.push(instr.envInstr((0, utils_1.currentEnvironment)(context), context.runtime.transformers, command)); } const environment = (0, utils_1.createBlockEnvironment)(context, 'blockEnvironment'); (0, utils_1.declareFunctionsAndVariables)(context, command, environment); (0, utils_1.pushEnvironment)(context, environment); // Push block body as statement sequence const seq = ast.statementSequence(command.body, command.loc); control.push(seq); }, StatementSequence: function (command, context, control, stash, isPrelude) { if (command.body.length == 1) { // If sequence only consists of one statement, evaluate it immediately const next = command.body[0]; cmdEvaluators[next.type](next, context, control, stash, isPrelude); } else { // unpack and push individual nodes in body control.push(...(0, utils_1.handleSequence)(command.body)); } return; }, WhileStatement: function (command, context, control) { if ((0, utils_1.hasBreakStatement)(command.body)) { control.push(instr.breakMarkerInstr(command)); } control.push(instr.whileInstr(command.test, command.body, command)); control.push(command.test); control.push(ast.identifier('undefined', command.loc)); // Return undefined if there is no loop execution }, ForStatement: function (command, context, control) { // All 3 parts will be defined due to parser rules const init = command.init; const test = command.test; const update = command.update; // Loop control variable present // Refer to Source §3 specifications https://docs.sourceacademy.org/source_3.pdf if (init.type === 'VariableDeclaration' && init.kind === 'let') { const id = init.declarations[0].id; control.push(ast.blockStatement([ init, ast.forStatement(ast.assignmentExpression(id, ast.identifier(id.name, command.loc), command.loc), test, update, ast.blockStatement([ ast.variableDeclaration([ ast.variableDeclarator(ast.identifier(`_copy_of_${id.name}`, command.loc), ast.identifier(id.name, command.loc), command.loc) ], 'const', command.loc), ast.blockStatement([ ast.variableDeclaration([ ast.variableDeclarator(ast.identifier(id.name, command.loc), ast.identifier(`_copy_of_${id.name}`, command.loc), command.loc) ], 'const', command.loc), command.body ], command.loc) ], command.loc), command.loc) ], command.loc)); } else { if ((0, utils_1.hasBreakStatement)(command.body)) { control.push(instr.breakMarkerInstr(command)); } control.push(instr.forInstr(init, test, update, command.body, command)); control.push(test); control.push(instr.popInstr(command)); // Pop value from init assignment control.push(init); control.push(ast.identifier('undefined', command.loc)); // Return undefined if there is no loop execution } }, IfStatement: function (command, context, control) { control.push(...(0, utils_1.reduceConditional)(command)); }, ExpressionStatement: function (command, context, control, stash, isPrelude) { // Fast forward to the expression // If not the next step will look like it's only removing ';' cmdEvaluators[command.expression.type](command.expression, context, control, stash, isPrelude); }, DebuggerStatement: function (command, context) { context.runtime.break = true; }, VariableDeclaration: function (command, context, control) { const declaration = command.declarations[0]; const id = declaration.id; // Parser enforces initialisation during variable declaration const init = declaration.init; control.push(instr.popInstr(command)); control.push(instr.assmtInstr(id.name, command.kind === 'const', true, command)); control.push(init); }, FunctionDeclaration: function (command, context, control) { // Function declaration desugared into constant declaration. const lambdaExpression = ast.blockArrowFunction(command.params, command.body, command.loc); const lambdaDeclaration = ast.constantDeclaration(command.id.name, lambdaExpression, command.loc); control.push(lambdaDeclaration); }, ReturnStatement: function (command, context, control) { // Push return argument onto control as well as Reset Instruction to clear to ignore all statements after the return. const next = control.peek(); if (next && (0, utils_1.isInstr)(next) && next.instrType === types_1.InstrType.MARKER) { control.pop(); } else { control.push(instr.resetInstr(command)); } if (command.argument) { control.push(command.argument); } }, ContinueStatement: function (command, context, control) { control.push(instr.contInstr(command)); }, BreakStatement: function (command, context, control) { control.push(instr.breakInstr(command)); }, ImportDeclaration: function () { }, /** * Expressions */ Literal: function (command, context, control, stash) { stash.push(command.value); }, AssignmentExpression: function (command, context, control) { if (command.left.type === 'MemberExpression') { control.push(instr.arrAssmtInstr(command)); control.push(command.right); control.push(command.left.property); control.push(command.left.object); } else if (command.left.type === 'Identifier') { const id = command.left; control.push(instr.assmtInstr(id.name, false, false, command)); control.push(command.right); } }, SpreadElement: function (command, context, control) { const arr = command.argument; control.push(instr.spreadInstr(arr)); control.push(arr); }, ArrayExpression: function (command, context, control) { const elems = command.elements; const len = elems.length; control.push(instr.arrLitInstr(len, command)); for (let i = len - 1; i >= 0; i--) { control.push(elems[i]); } }, MemberExpression: function (command, context, control) { control.push(instr.arrAccInstr(command)); control.push(command.property); control.push(command.object); }, ConditionalExpression: function (command, context, control) { control.push(...(0, utils_1.reduceConditional)(command)); }, Identifier: function (command, context, control, stash) { stash.push((0, utils_1.getVariable)(context, command.name, command)); }, UnaryExpression: function (command, context, control) { control.push(instr.unOpInstr(command.operator, command)); control.push(command.argument); }, BinaryExpression: function (command, context, control) { control.push(instr.binOpInstr(command.operator, command)); control.push(command.right); control.push(command.left); }, LogicalExpression: function (command, context, control) { if (command.operator === '&&') { control.push(ast.conditionalExpression(command.left, command.right, ast.literal(false), command.loc)); } else { control.push(ast.conditionalExpression(command.left, ast.literal(true), command.right, command.loc)); } }, ArrowFunctionExpression: function (command, context, control, stash, isPrelude) { const closure = closure_1.default.makeFromArrowFunction(command, (0, utils_1.currentEnvironment)(context), (0, utils_1.currentTransformers)(context), context, true, isPrelude); stash.push(closure); }, CallExpression: function (command, context, control) { // Push application instruction, function arguments and function onto control. control.push(instr.appInstr(command.arguments.length, command)); for (let index = command.arguments.length - 1; index >= 0; index--) { control.push(command.arguments[index]); } control.push(command.callee); }, /** * Instructions */ [types_1.InstrType.RESET]: function (command, context, control) { // Keep pushing reset instructions until marker is found. const cmdNext = control.pop(); if (cmdNext && (!(0, utils_1.isInstr)(cmdNext) || cmdNext.instrType !== types_1.InstrType.MARKER)) { control.push(instr.resetInstr(command.srcNode)); } }, [types_1.InstrType.WHILE]: function (command, context, control, stash) { const test = stash.pop(); // Check if test condition is a boolean const error = rttc.checkIfStatement(command.srcNode, test, context.chapter); if (error) { (0, utils_1.handleRuntimeError)(context, error); } if (test) { control.push(command); control.push(command.test); if ((0, utils_1.hasContinueStatement)(command.body)) { control.push(instr.contMarkerInstr(command.srcNode)); } if (!(0, utils_1.valueProducing)(command.body)) { // if loop body is not value-producing, insert undefined expression statement control.push(ast.identifier('undefined', command.body.loc)); } control.push(command.body); control.push(instr.popInstr(command.srcNode)); // Pop previous body value } }, [types_1.InstrType.FOR]: function (command, context, control, stash) { const test = stash.pop(); // Check if test condition is a boolean const error = rttc.checkIfStatement(command.srcNode, test, context.chapter); if (error) { (0, utils_1.handleRuntimeError)(context, error); } if (test) { control.push(command); control.push(command.test); control.push(instr.popInstr(command.srcNode)); // Pop value from update control.push(command.update); if ((0, utils_1.hasContinueStatement)(command.body)) { control.push(instr.contMarkerInstr(command.srcNode)); } if (!(0, utils_1.valueProducing)(command.body)) { // if loop body is not value-producing, insert undefined expression statement control.push(ast.identifier('undefined', command.body.loc)); } control.push(command.body); control.push(instr.popInstr(command.srcNode)); // Pop previous body value } }, [types_1.InstrType.ASSIGNMENT]: function (command, context, control, stash) { if (command.declaration) { (0, utils_1.defineVariable)(context, command.symbol, stash.peek(), command.constant, command.srcNode); } else { (0, utils_1.setVariable)(context, command.symbol, stash.peek(), command.srcNode); } }, [types_1.InstrType.UNARY_OP]: function (command, context, control, stash) { const argument = stash.pop(); const error = rttc.checkUnaryExpression(command.srcNode, command.symbol, argument, context.chapter); if (error) { (0, utils_1.handleRuntimeError)(context, error); } stash.push((0, operators_1.evaluateUnaryExpression)(command.symbol, argument)); }, [types_1.InstrType.BINARY_OP]: function (command, context, control, stash) { const right = stash.pop(); const left = stash.pop(); const error = rttc.checkBinaryExpression(command.srcNode, command.symbol, context.chapter, left, right); if (error) { (0, utils_1.handleRuntimeError)(context, error); } stash.push((0, operators_1.evaluateBinaryExpression)(command.symbol, left, right)); }, [types_1.InstrType.POP]: function (command, context, control, stash) { stash.pop(); }, [types_1.InstrType.APPLICATION]: function (command, context, control, stash) { (0, utils_1.checkStackOverFlow)(context, control); // Get function arguments from the stash const args = []; for (let index = 0; index < command.numOfArgs; index++) { args.unshift(stash.pop()); } // Get function from the stash const func = stash.pop(); if (!(func instanceof closure_1.default || func instanceof Function)) { (0, utils_1.handleRuntimeError)(context, new errors.CallingNonFunctionValue(func, command.srcNode)); } if ((0, scheme_macros_1.isApply)(func)) { // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); // get the procedure from the arguments const proc = args[0]; // get the last list from the arguments // (and it should be a list) const last = args[args.length - 1]; if (!(0, macro_utils_1.isList)(last)) { (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Last argument of apply must be a list'))); } // get the rest of the arguments between the procedure and the last list const rest = args.slice(1, args.length - 1); // convert the last list to an array const lastAsArray = (0, macro_utils_1.flattenList)(last); // combine the rest and the last list const combined = [...rest, ...lastAsArray]; // push the items back onto the stash stash.push(proc); stash.push(...combined); // prepare a function call for the procedure control.push(instr.appInstr(combined.length, command.srcNode)); return; } if ((0, scheme_macros_1.isEval)(func)) { // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); // get the AST from the arguments const AST = args[0]; // move it to the control control.push(AST); return; } if ((0, continuations_1.isCallWithCurrentContinuation)(func)) { // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); // generate a continuation here const contControl = control.copy(); const contStash = stash.copy(); const contEnv = context.runtime.environments.slice(); const contTransformers = (0, utils_1.currentTransformers)(context); // at this point, the extra CALL instruction // has been removed from the control stack. // additionally, the single closure argument has been // removed (as the parameter of call/cc) from the stash // and additionally, call/cc itself has been removed from the stash. // as such, there is no further need to modify the // copied C, S, E and T! const continuation = new continuations_1.Continuation(context, contControl, contStash, contEnv, contTransformers); // Get the callee const cont_callee = args[0]; const dummyFCallExpression = (0, continuations_1.makeDummyContCallExpression)('f', 'cont'); // Prepare a function call for the continuation-consuming function control.push(instr.appInstr(command.numOfArgs, dummyFCallExpression)); // push the argument (the continuation caller) back onto the stash stash.push(cont_callee); // finally, push the continuation onto the stash stash.push(continuation); return; } if (func instanceof continuations_1.Continuation) { // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); // get the C, S, E from the continuation const contControl = func.getControl(); const contStash = func.getStash(); const contEnv = func.getEnv(); const contTransformers = func.getTransformers(); // update the C, S, E of the current context control.setTo(contControl); stash.setTo(contStash); context.runtime.environments = contEnv; (0, utils_1.setTransformers)(context, contTransformers); // push the arguments back onto the stash stash.push(...args); return; } if (func instanceof closure_1.default) { // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); const next = control.peek(); // Push ENVIRONMENT instruction if needed - if next control stack item // exists and is not an environment instruction, OR the control only contains // environment indepedent items // if the current language is a scheme language, don't avoid the environment instruction // as schemers like using the REPL, and that always assumes that the environment is reset // to the main environment. if ((next && !((0, utils_1.isInstr)(next) && next.instrType === types_1.InstrType.ENVIRONMENT) && !control.canAvoidEnvInstr()) || (0, mapper_1.isSchemeLanguage)(context)) { control.push(instr.envInstr((0, utils_1.currentEnvironment)(context), (0, utils_1.currentTransformers)(context), command.srcNode)); } // Create environment for function parameters if the function isn't nullary. // Name the environment if the function call expression is not anonymous if (args.length > 0) { const environment = (0, utils_1.createEnvironment)(context, func, args, command.srcNode); (0, utils_1.pushEnvironment)(context, environment); } else { context.runtime.environments.unshift(func.environment); } // Handle special case if function is simple if ((0, utils_1.isSimpleFunction)(func.node)) { // Closures convert ArrowExpressionStatements to BlockStatements const block = func.node.body; const returnStatement = block.body[0]; control.push(returnStatement.argument ?? ast.identifier('undefined', returnStatement.loc)); } else { if (control.peek()) { // push marker if control not empty control.push(instr.markerInstr(command.srcNode)); } control.push(func.node.body); } // we need to update the transformers environment here. const newTransformers = new Transformers(func.transformers); (0, utils_1.setTransformers)(context, newTransformers); return; } // Value is a built-in function // Check for number of arguments mismatch error (0, utils_1.checkNumberOfArguments)(context, func, args, command.srcNode); // Directly stash result of applying pre-built functions without the CSE machine. try { const result = func(...args); if ((0, utils_1.isStreamFn)(func, result)) { // This is a special case for the `stream` built-in function, since it returns pairs // whose last element is a function. The CSE machine on the frontend will still draw // these functions like closures, and the tail of the "closures" will need to point // to where `stream` was called. // // TODO: remove this condition if `stream` becomes a pre-defined function Object.defineProperties(result[1], { environment: { value: (0, utils_1.currentEnvironment)(context), writable: true } }); } // Recursively adds `environment` and `id` properties to any arrays created, // and also adds them to the heap starting from the arrays that are more deeply nested. const attachEnvToResult = (value) => { // Built-in functions don't instantly create arrays with circular references, so // there is no need to keep track of visited arrays. if ((0, lodash_1.isArray)(value) && !(0, utils_1.isEnvArray)(value)) { for (const item of value) { attachEnvToResult(item); } (0, utils_1.handleArrayCreation)(context, value); } }; attachEnvToResult(result); stash.push(result); } catch (error) { if (!(error instanceof runtimeSourceError_1.RuntimeSourceError || error 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. const loc = command.srcNode.loc ?? constants_1.UNKNOWN_LOCATION; (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(error, loc)); } } }, [types_1.InstrType.BRANCH]: function (command, context, control, stash) { const test = stash.pop(); // Check if test condition is a boolean const error = rttc.checkIfStatement(command.srcNode, test, context.chapter); if (error) { (0, utils_1.handleRuntimeError)(context, error); } if (test) { if (!(0, utils_1.valueProducing)(command.consequent)) { control.push(ast.identifier('undefined', command.consequent.loc)); } control.push(command.consequent); } else if (command.alternate) { if (!(0, utils_1.valueProducing)(command.alternate)) { control.push(ast.identifier('undefined', command.consequent.loc)); } control.push(command.alternate); } else { control.push(ast.identifier('undefined', command.srcNode.loc)); } }, [types_1.InstrType.ENVIRONMENT]: function (command, context) { // Restore environment while ((0, utils_1.currentEnvironment)(context).id !== command.env.id) { (0, utils_1.popEnvironment)(context); } // restore transformers environment (0, utils_1.setTransformers)(context, command.transformers); }, [types_1.InstrType.ARRAY_LITERAL]: function (command, context, control, stash) { const arity = command.arity; const array = []; for (let i = 0; i < arity; ++i) { array.unshift(stash.pop()); } (0, utils_1.handleArrayCreation)(context, array); stash.push(array); }, [types_1.InstrType.ARRAY_ACCESS]: function (command, context, control, stash) { const index = stash.pop(); const array = stash.pop(); //Check if the index is legal const indexRangeError = rttc.checkoutofRange(command.srcNode, index, context.chapter); if (indexRangeError) { (0, utils_1.handleRuntimeError)(context, indexRangeError); } // Check if left-hand side is array const lhsArrayCheckError = rttc.checkArray(command.srcNode, array, context.chapter); if (lhsArrayCheckError) { (0, utils_1.handleRuntimeError)(context, lhsArrayCheckError); } // Check if index is out-of-bounds with array, in which case, returns undefined as per spec if (index >= array.length) { stash.push(undefined); } else { stash.push(array[index]); } }, [types_1.InstrType.ARRAY_ASSIGNMENT]: function (command, context, control, stash) { const value = stash.pop(); const index = stash.pop(); const array = stash.pop(); array[index] = value; stash.push(value); }, [types_1.InstrType.CONTINUE]: function (command, context, control) { const next = control.pop(); if ((0, utils_1.isInstr)(next) && next.instrType == types_1.InstrType.CONTINUE_MARKER) { // Encountered continue mark, stop popping } else if ((0, utils_1.isInstr)(next) && next.instrType == types_1.InstrType.ENVIRONMENT) { control.push(command); control.push(next); // Let instruction evaluate to restore env } else { // Continue popping from control by pushing same instruction on control control.push(command); } }, [types_1.InstrType.CONTINUE_MARKER]: function () { }, [types_1.InstrType.BREAK]: function (command, context, control) { const next = control.pop(); if ((0, utils_1.isInstr)(next) && next.instrType == types_1.InstrType.BREAK_MARKER) { // Encountered break mark, stop popping } else if ((0, utils_1.isInstr)(next) && next.instrType == types_1.InstrType.ENVIRONMENT) { control.push(command); control.push(next); // Let instruction evaluate to restore env } else { // Continue popping from control by pushing same instruction on control control.push(command); } }, [types_1.InstrType.BREAK_MARKER]: function () { }, [types_1.InstrType.SPREAD]: function (command, context, control, stash) { const array = stash.pop(); // Check if right-hand side is array const rhsArrayCheckError = rttc.checkArray(command.srcNode, array, context.chapter); if (rhsArrayCheckError) { (0, utils_1.handleRuntimeError)(context, rhsArrayCheckError); } // spread array for (let i = 0; i < array.length; i++) { stash.push(array[i]); } // update call instr above const cont = control.getStack(); const size = control.size(); for (let i = size - 1; i >= 0; i--) { // guaranteed at least one call instr above, because spread is not allowed inside arrays if (cont[i].instrType === types_1.InstrType.APPLICATION) { ; cont[i].numOfArgs += array.length - 1; break; // only the nearest call instruction above } } } }; //# sourceMappingURL=interpreter.js.map