js-slang
Version:
Javascript-based implementations of Source, written in Typescript
959 lines • 43.6 kB
JavaScript
"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