UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

519 lines 25.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertToEvalExpression = exports.makeDummyIdentifierNode = exports.schemeEval = exports.isEval = exports.cset_eval = exports.Eval = exports.isApply = exports.cset_apply = exports.Apply = void 0; const errors = require("../errors/errors"); 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 src_1 = require("../alt-langs/scheme/scm-slang/src"); const utils_1 = require("./utils"); const patterns_1 = require("./patterns"); const macro_utils_1 = require("./macro-utils"); const instrCreator_1 = require("./instrCreator"); /** * A metaprocedure used to detect for the apply function object. * If the interpreter sees this specific function, * it will take all of the operands, and apply the second to second last operands as well as the last operand (must be a list) * to the first operand (which must be a function). */ class Apply extends Function { constructor() { super(); } static get() { return Apply.instance; } toString() { return 'apply'; } } exports.Apply = Apply; Apply.instance = new Apply(); exports.cset_apply = Apply.get(); function isApply(value) { return value === exports.cset_apply; } exports.isApply = isApply; /** * A metaprocedure used to detect for the eval function object. * If the interpreter sees this specific function, * it will transfer its operand to the control component. */ class Eval extends Function { constructor() { super(); } static get() { return Eval.instance; } toString() { return 'eval'; } } exports.Eval = Eval; Eval.instance = new Eval(); exports.cset_eval = Eval.get(); function isEval(value) { return value === exports.cset_eval; } exports.isEval = isEval; function isSpecialForm(sym) { return [ 'lambda', 'define', 'set!', 'if', 'begin', 'quote', 'quasiquote', 'define-syntax', 'syntax-rules', 'eval' ].includes(sym.sym); } function schemeEval(command, context, control, stash, isPrelude) { const transformers = (0, utils_1.currentTransformers)(context); // scheme CSE machine will only ever encounter // lists or primitives like symbols, booleans or strings. // if its a list, we can parse the list and evaluate each item as necessary // if its a symbol, we can look up the symbol in the environment. // for either of these operations, if our list matches some pattern in // the T component, then we can apply the corresponding rule. // if its a number, boolean, or string, we can just shift the value // onto the stash. if (command === null) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Cannot evaluate null'))); } if ((0, macro_utils_1.isList)(command)) { // do something const parsedList = (0, macro_utils_1.flattenList)(command); const elem = parsedList[0]; // do work based on the first element of the list. // it should match some symbol "define", "set", "lambda", etc... // or if it doesn't match any of these, then it is a function call. if (elem instanceof base_1._Symbol) { // check if elem matches any defined syntax in the T component. // if it does, then apply the corresponding rule. if (transformers.hasPattern(elem.sym)) { // get the relevant transformers const transformerList = transformers.getPattern(elem.sym); // find the first matching transformer for (const transformer of transformerList) { // check if the transformer matches the list try { if ((0, patterns_1.match)(command, transformer.pattern, transformer.literals)) { // if it does, apply the transformer const transformedMacro = (0, patterns_1.macro_transform)(command, transformer); control.push(transformedMacro); return; } } catch (e) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Error in macro-expanding ' + elem.sym + '! Are the template and pattern well formed?'))); } } // there is an error if we get to here return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('No matching transformer found for macro ' + elem.sym))); } // else, this is a standard special form. // we attempt to piggyback on the standard CSE machine to // handle the basic special forms. // however, for more advanced stuff like quotes or definitions, // the logic will be handled here. switch (parsedList[0].sym) { case 'lambda': if (parsedList.length < 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('lambda requires at least 2 arguments!'))); } // return a lambda expression that takes // in the arguments, and returns the body // as an eval of the body. const args = parsedList[1]; let argsList = []; let rest = null; if (args instanceof base_1._Symbol) { // if the args is a symbol, then it is a variadic function. // we can just set the args to a list of the symbol. rest = args; } else if ((0, macro_utils_1.isImproperList)(args)) { ; [argsList, rest] = (0, macro_utils_1.flattenImproperList)(args); argsList.forEach((arg) => { if (!(arg instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid arguments for lambda!'))); } return; }); if (rest !== null && !(rest instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid arguments for lambda!'))); } } else if ((0, macro_utils_1.isList)(args)) { argsList = (0, macro_utils_1.flattenList)(args); argsList.forEach((arg) => { if (!(arg instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid arguments for lambda!'))); } return; }); } else { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid arguments for lambda!'))); } // convert the args to estree pattern const params = argsList.map(arg => makeDummyIdentifierNode((0, src_1.encode)(arg.sym))); let body_elements = parsedList.slice(2); let body = (0, macro_utils_1.arrayToList)([new base_1._Symbol('begin'), ...body_elements]); // if there is a rest argument, we need to wrap it in a rest element. // we also need to add another element to the body, // to convert the rest element into a list. if (rest !== null) { params.push({ type: 'RestElement', argument: makeDummyIdentifierNode((0, src_1.encode)(rest.sym)) }); body = (0, macro_utils_1.arrayToList)([ new base_1._Symbol('begin'), (0, macro_utils_1.arrayToList)([ new base_1._Symbol('set!'), rest, (0, macro_utils_1.arrayToList)([new base_1._Symbol('vector->list'), rest]) ]), ...body_elements ]); } // estree ArrowFunctionExpression const lambda = { type: 'ArrowFunctionExpression', params: params, body: convertToEvalExpression(body) }; control.push(lambda); return; case 'define': if (parsedList.length < 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define requires at least 2 arguments!'))); } const variable = parsedList[1]; if ((0, macro_utils_1.isList)(variable)) { // then this define is actually a function definition const varList = (0, macro_utils_1.flattenList)(variable); const name = varList[0]; const params = varList.slice(1); const body = parsedList.slice(2); const define_function = (0, macro_utils_1.arrayToList)([ new base_1._Symbol('define'), name, (0, macro_utils_1.arrayToList)([new base_1._Symbol('lambda'), (0, macro_utils_1.arrayToList)(params), ...body]) ]); control.push(define_function); return; } else if ((0, macro_utils_1.isImproperList)(variable)) { const [varList, rest] = (0, macro_utils_1.flattenImproperList)(variable); const name = varList[0]; const params = varList.slice(1); const body = parsedList.slice(2); const define_function = (0, macro_utils_1.arrayToList)([ new base_1._Symbol('define'), name, (0, macro_utils_1.arrayToList)([new base_1._Symbol('lambda'), (0, macro_utils_1.arrayToImproperList)(params, rest), ...body]) ]); control.push(define_function); return; } else if (!(variable instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid variable for define!'))); } if (parsedList.length !== 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define requires 2 arguments!'))); } // make sure variable does not shadow a special form if (isSpecialForm(variable) && !isPrelude) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Cannot shadow special form ' + variable.sym + ' with a definition!'))); } const value = parsedList[2]; // estree VariableDeclaration const definition = { type: 'VariableDeclaration', kind: 'let', declarations: [ { type: 'VariableDeclarator', id: makeDummyIdentifierNode((0, src_1.encode)(variable.sym)), init: convertToEvalExpression(value) } ] }; control.push(definition); return; case 'set!': if (parsedList.length !== 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('set! requires 2 arguments!'))); } const set_variable = parsedList[1]; if (!(set_variable instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid arguments for set!'))); } // make sure set_variable does not shadow a special form if (isSpecialForm(set_variable) && !isPrelude) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Cannot overwrite special form ' + set_variable.sym + ' with a value!'))); } const set_value = parsedList[2]; // estree AssignmentExpression const assignment = { type: 'AssignmentExpression', operator: '=', left: makeDummyIdentifierNode((0, src_1.encode)(set_variable.sym)), right: convertToEvalExpression(set_value) }; control.push(assignment); return; case 'if': if (parsedList.length < 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('if requires at least 2 arguments!'))); } if (parsedList.length > 4) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('if requires at most 3 arguments!'))); } const condition = parsedList[1]; const consequent = parsedList[2]; // check if there is an alternate const alternate = parsedList.length > 3 ? parsedList[3] : undefined; // evaluate the condition with truthy const truthyCondition = (0, macro_utils_1.arrayToList)([new base_1._Symbol('truthy'), condition]); // estree ConditionalExpression const conditional = { type: 'ConditionalExpression', test: convertToEvalExpression(truthyCondition), consequent: convertToEvalExpression(consequent), alternate: alternate ? convertToEvalExpression(alternate) : undefined }; control.push(conditional); return; case 'begin': // begin is a sequence of expressions // that are evaluated in order. // push the expressions to the control in reverse // order. if (parsedList.length < 2) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('begin requires at least 1 argument!'))); } control.push(parsedList[parsedList.length - 1]); for (let i = parsedList.length - 2; i > 0; i--) { control.push((0, instrCreator_1.popInstr)(makeDummyIdentifierNode('pop'))); control.push(parsedList[i]); } return; case 'quote': // quote is a special form that returns the expression // as is, without evaluating it. // we can just push the expression to the stash. if (parsedList.length !== 2) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('quote requires 1 argument!'))); } stash.push(parsedList[1]); return; // case 'quasiquote': quasiquote is implemented as a macro. case 'define-syntax': if (parsedList.length !== 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define-syntax requires 2 arguments!'))); } // parse the pattern and template here, // generate a list of transformers from it, // and add it to the Patterns component. const syntaxName = parsedList[1]; if (!(syntaxName instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define-syntax requires a symbol!'))); } // make sure syntaxName does not shadow a special form if (isSpecialForm(syntaxName) && !isPrelude) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Cannot shadow special form ' + syntaxName.sym + ' with a macro!'))); } const syntaxRules = parsedList[2]; if (!(0, macro_utils_1.isList)(syntaxRules)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define-syntax requires a syntax-rules transformer!'))); } // at this point, we assume that syntax-rules is verified // and parsed correctly already. const syntaxRulesList = (0, macro_utils_1.flattenList)(syntaxRules); if (!(syntaxRulesList[0] instanceof base_1._Symbol) || syntaxRulesList[0].sym !== 'syntax-rules') { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('define-syntax requires a syntax-rules transformer!'))); } if (syntaxRulesList.length < 3) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('syntax-rules requires at least 2 arguments!'))); } if (!(0, macro_utils_1.isList)(syntaxRulesList[1])) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid syntax-rules literals!'))); } const literalList = (0, macro_utils_1.flattenList)(syntaxRulesList[1]); const literals = literalList.map((literal) => { if (!(literal instanceof base_1._Symbol)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid syntax-rules literals!'))); } return literal.sym; }); const rules = syntaxRulesList.slice(2); // rules are set as a list of patterns and templates. // we need to convert these into transformers. const transformerList = rules.map(rule => { if (!(0, macro_utils_1.isList)(rule)) { return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('Invalid syntax-rules rule!'))); } const ruleList = (0, macro_utils_1.flattenList)(rule); const pattern = ruleList[0]; const template = ruleList[1]; return new patterns_1.Transformer(literals, pattern, template); }); // now we can add the transformers to the transformers component. transformers.addPattern(syntaxName.sym, transformerList); // for consistency with scheme expecting undefined return values, push undefined to the stash. stash.push(undefined); return; case 'syntax-rules': // syntax-rules is a special form that is used to define // a set of transformers. // it isn't meant to be evaluated outside of a define-syntax. // throw an error. return (0, utils_1.handleRuntimeError)(context, new errors.ExceptionError(new Error('syntax-rules must only exist within define-syntax!'))); } } // if we get to this point, then it is a function call. // convert it into an es.CallExpression and push it to the control. const procedure = parsedList[0]; const args = parsedList.slice(1); const appln = { type: 'CallExpression', optional: false, callee: convertToEvalExpression(procedure), arguments: args.map(convertToEvalExpression) // unfortunately, each one needs to be converted. }; control.push(appln); return; } else if (command instanceof base_1._Symbol) { // get the value of the symbol from the environment // associated with this symbol. const encodedName = (0, src_1.encode)(command.sym); stash.push((0, utils_1.getVariable)(context, encodedName, makeDummyIdentifierNode(command.sym))); return; } // if we get to this point of execution, it is just some primitive value. // just push it to the stash. stash.push(command); return; } exports.schemeEval = schemeEval; function makeDummyIdentifierNode(name) { return { type: 'Identifier', name }; } exports.makeDummyIdentifierNode = makeDummyIdentifierNode; /** * Convert a scheme expression (that is meant to be evaluated) * into an estree expression, using eval. * this will let us avoid the "hack" of storing Scheme lists * in estree nodes. * @param expression * @returns estree expression */ function convertToEvalExpression(expression) { function convertToEstreeExpression(expression) { /* cases to consider: - list - pair/improper list - symbol - number - boolean - string */ if ((0, macro_utils_1.isList)(expression)) { // make a call expression to list // with the elements of the list as its arguments. const args = (0, macro_utils_1.flattenList)(expression).map(convertToEstreeExpression); return { type: 'CallExpression', optional: false, callee: { type: 'Identifier', name: 'list' }, arguments: args }; } else if ((0, macro_utils_1.isImproperList)(expression)) { // make a call to cons // with the car and cdr as its arguments. const [car, cdr] = expression; return { type: 'CallExpression', optional: false, callee: { type: 'Identifier', name: 'cons' }, arguments: [convertToEstreeExpression(car), convertToEstreeExpression(cdr)] }; } else if (expression instanceof base_1._Symbol) { // make a call to string->symbol // with the symbol name as its argument. return { type: 'CallExpression', optional: false, callee: { type: 'Identifier', name: (0, src_1.encode)('string->symbol') }, arguments: [ { type: 'Literal', value: expression.sym } ] }; } else if ((0, core_math_1.is_number)(expression)) { // make a call to string->number // with the number toString() as its argument. return { type: 'CallExpression', optional: false, callee: { type: 'Identifier', name: (0, src_1.encode)('string->number') }, arguments: [ { type: 'Literal', value: expression.toString() } ] }; } // if we're here, then it is a boolean or string. // just return the literal value. return { type: 'Literal', value: expression }; } // make a call expression to eval with the single expression as its component. return { type: 'CallExpression', optional: false, callee: { type: 'Identifier', name: (0, src_1.encode)('eval') }, arguments: [convertToEstreeExpression(expression)] }; } exports.convertToEvalExpression = convertToEvalExpression; //# sourceMappingURL=scheme-macros.js.map