205 lines
6.5 KiB
JavaScript
205 lines
6.5 KiB
JavaScript
export const treewalk = {};
|
|
export const builtins = {};
|
|
|
|
treewalk.init = (env, input) => {
|
|
return {
|
|
input,
|
|
scopes: [new Map(Object.entries(builtins)), env],
|
|
env,
|
|
};
|
|
};
|
|
|
|
treewalk.lookupVariable = (state, name) => {
|
|
for (let i = state.scopes.length; i-- > 0; ) {
|
|
let scope = state.scopes[i];
|
|
if (scope.has(name)) {
|
|
return scope.get(name);
|
|
}
|
|
}
|
|
console.log(new Error().stack);
|
|
throw new Error(`variable ${name} is undefined`);
|
|
};
|
|
|
|
treewalk.eval = (state, node) => {
|
|
switch (node.kind) {
|
|
case "integer":
|
|
return parseInt(node.source);
|
|
|
|
case "string":
|
|
// NOTE: We chop the quotes off of the string literal here.
|
|
return node.source.substring(1, node.source.length - 1);
|
|
|
|
case "identifier":
|
|
return treewalk.lookupVariable(state, node.source);
|
|
|
|
case "list":
|
|
let functionToCall = treewalk.eval(state, node.children[0]);
|
|
return functionToCall(state, node);
|
|
|
|
case "toplevel":
|
|
let result = undefined;
|
|
for (let i = 0; i < node.children.length; ++i) {
|
|
result = treewalk.eval(state, node.children[i]);
|
|
if (result !== undefined && i != node.children.length - 1)
|
|
throw new Error(`expression ${i + 1} had a result despite not being the last`);
|
|
}
|
|
return result;
|
|
|
|
default:
|
|
throw new Error(`unhandled node kind: ${node.kind}`);
|
|
}
|
|
};
|
|
|
|
export function run(env, input, node) {
|
|
let state = treewalk.init(env, input);
|
|
return treewalk.eval(state, node);
|
|
}
|
|
|
|
function arithmeticBuiltin(op) {
|
|
return (state, node) => {
|
|
if (node.children.length < 3)
|
|
throw new Error("arithmetic operations require at least two arguments");
|
|
|
|
let result = treewalk.eval(state, node.children[1]);
|
|
for (let i = 2; i < node.children.length; ++i) {
|
|
result = op(result, treewalk.eval(state, node.children[i]));
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
function comparisonBuiltin(op) {
|
|
return (state, node) => {
|
|
if (node.children.length != 3)
|
|
throw new Error("comparison operators require exactly two arguments");
|
|
|
|
let a = treewalk.eval(state, node.children[1]);
|
|
let b = treewalk.eval(state, node.children[2]);
|
|
return op(a, b) ? 1 : 0;
|
|
};
|
|
}
|
|
|
|
builtins["+"] = arithmeticBuiltin((a, b) => a + b);
|
|
builtins["-"] = arithmeticBuiltin((a, b) => a - b);
|
|
builtins["*"] = arithmeticBuiltin((a, b) => a * b);
|
|
builtins["/"] = arithmeticBuiltin((a, b) => a / b);
|
|
|
|
builtins["="] = comparisonBuiltin((a, b) => a === b);
|
|
builtins["<"] = comparisonBuiltin((a, b) => a < b);
|
|
|
|
export function makeFunction(state, paramNames, bodyExpr) {
|
|
let capturedScopes = [];
|
|
// Start from 1 to skip builtins, which are always present anyways.
|
|
for (let i = 1; i < state.scopes.length; ++i) {
|
|
// We don't really mutate the scopes after pushing them onto the stack, so keeping
|
|
// references to them is okay.
|
|
capturedScopes.push(state.scopes[i]);
|
|
}
|
|
|
|
return (state, node) => {
|
|
if (node.children.length != paramNames.length + 1)
|
|
throw new Error(
|
|
`incorrect number of arguments: expected ${paramNames.length}, but got ${node.children.length - 1}`,
|
|
);
|
|
|
|
let scope = new Map();
|
|
for (let i = 0; i < paramNames.length; ++i) {
|
|
scope.set(paramNames[i], treewalk.eval(state, node.children[i + 1]));
|
|
}
|
|
|
|
state.scopes.push(...capturedScopes);
|
|
state.scopes.push(scope);
|
|
let result = treewalk.eval(state, bodyExpr);
|
|
state.scopes.pop();
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
builtins.fn = (state, node) => {
|
|
if (node.children.length != 3)
|
|
throw new Error("an `fn` must have an argument list and a result expression");
|
|
|
|
let params = node.children[1];
|
|
if (node.children[1].kind != "list")
|
|
throw new Error("expected parameter list as second argument to `fn`");
|
|
|
|
let paramNames = [];
|
|
for (let param of params.children) {
|
|
if (param.kind != "identifier") {
|
|
throw new Error("`fn` parameters must be identifiers");
|
|
}
|
|
paramNames.push(param.source);
|
|
}
|
|
|
|
let expr = node.children[2];
|
|
|
|
return makeFunction(state, paramNames, expr);
|
|
};
|
|
|
|
builtins["if"] = (state, node) => {
|
|
if (node.children.length != 4)
|
|
throw new Error("an `if` must have a condition, true expression, and false expression");
|
|
|
|
let condition = treewalk.eval(state, node.children[1]);
|
|
if (condition !== 0) {
|
|
return treewalk.eval(state, node.children[2]);
|
|
} else {
|
|
return treewalk.eval(state, node.children[3]);
|
|
}
|
|
};
|
|
|
|
builtins.def = (state, node) => {
|
|
if (node.children.length != 3)
|
|
throw new Error(
|
|
"a `def` expects the name of the variable to assign, and the value to assign to the variable",
|
|
);
|
|
|
|
if (node.children[1].kind != "identifier")
|
|
throw new Error("variable name must be an identifier");
|
|
|
|
let name = node.children[1];
|
|
let value = treewalk.eval(state, node.children[2]);
|
|
state.env.set(name.source, value);
|
|
};
|
|
|
|
export function wrapJavaScriptFunctionVarargs(func) {
|
|
return (state, node) => {
|
|
let args = Array(node.children.length - 1);
|
|
for (let i = 1; i < node.children.length; ++i) {
|
|
args[i - 1] = treewalk.eval(state, node.children[i]);
|
|
}
|
|
return func(...args);
|
|
};
|
|
}
|
|
|
|
export function wrapJavaScriptFunction(func) {
|
|
let inner = wrapJavaScriptFunctionVarargs(func);
|
|
return (state, node) => {
|
|
if (node.children.length != func.length + 1)
|
|
throw new Error(
|
|
`\`${func.name}\` expects ${func.length} arguments, but ${node.children.length - 1} were given`,
|
|
);
|
|
return inner(state, node);
|
|
};
|
|
}
|
|
|
|
builtins.cat = wrapJavaScriptFunctionVarargs((...strings) => {
|
|
return strings.join("");
|
|
});
|
|
|
|
builtins.sub = wrapJavaScriptFunctionVarargs((str, start, end) => {
|
|
if (typeof str != "string") throw new Error("`sub` expects a string as the first argument");
|
|
if (typeof start != "number") throw new Error("`sub` expects a number as the second argument");
|
|
end ??= start + 1;
|
|
return str.substring(start, end);
|
|
});
|
|
|
|
builtins.len = wrapJavaScriptFunction((string) => string.length);
|
|
|
|
builtins.chr = wrapJavaScriptFunction((n) => String.fromCodePoint(n));
|
|
builtins.ord = wrapJavaScriptFunction((s) => s.codePointAt(0));
|
|
|
|
builtins["to-string"] = wrapJavaScriptFunction((n) => n.toString());
|
|
builtins["to-number"] = wrapJavaScriptFunction((s) => parseInt(s));
|