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