function createFunctionFactory(functionLibrary) {
return function serializeToFunction(node) {
const vars = new Set();
const outputs = new Map();
const orderedArgs = [];
const arrayOutputs = [];
function serialize(node, parentPrec = -1) {
const parts = [];
const thisPrec = getNodePrecedence(node);
const wrap = parentPrec > thisPrec;
if (wrap) parts.push("(");
switch (node.type) {
case "UnaryExpression":
parts.push(node.operator);
parts.push(serialize(node.argument, thisPrec));
break;
case "BinaryExpression":
if (node.operator === "^") parts.push("(");
parts.push(serialize(node.left, thisPrec));
if (node.operator === "^") {
parts.push("**");
} else {
parts.push(node.operator);
}
parts.push(serialize(node.right, thisPrec));
if (node.operator === "^") parts.push(")");
break;
case "Literal":
parts.push(node.raw);
break;
case "Identifier":
vars.add(node.name);
parts.push(`args.${node.name}`);
break;
case "CallExpression":
const func = functionLibrary.functions[node.callee.name];
if (!func) {
throw new Error(`Unknown function '${node.callee.name}'`);
}
const funcMeta = functionLibrary.metadata[node.callee.name];
if (funcMeta) {
if (typeof funcMeta.arity === "number") {
if (node.arguments.length !== funcMeta.arity) {
throw new Error(
`Expected ${funcMeta.arity} arguments for function '${node.callee.name}' but got ${node.arguments.length}`
);
}
} else if (Array.isArray(funcMeta.arity)) {
if (
node.arguments.length < funcMeta.arity[0] ||
node.arguments.length > funcMeta.arity[1]
) {
throw new Error(
`Expected between ${funcMeta.arity[0]} and ${funcMeta.arity[1]} arguments for function '${node.callee.name}' but got ${node.arguments.length}`
);
}
}
}
parts.push(
`lib.${node.callee.name}(${node.arguments
.map(serialize)
.join(", ")})`
);
break;
case "AssignmentExpression":
if (node.left.type === "Identifier") {
outputs.set(node.left.name, serialize(node.right, thisPrec));
} else if (node.left.type === "ArrayExpression") {
if (node.right.type === "ArrayExpression") {
for (const element of node.right.elements) {
arrayOutputs.push(serialize(element));
}
} else {
throw new Error(
`ArrayExpression assignment lvalue must be accompanied by ArrayExpression rvalue but got ${node.left.type}.`
);
}
} else if (node.left.type === "CallExpression") {
if (orderedArgs.length) {
throw new Error(
`Only one lvalue CallExpression may be present in an equation. Try removing the arguments from your lvalues.`
);
}
outputs.set(node.left.callee.name, serialize(node.right, thisPrec));
if (node.left.callee.type !== "Identifier") {
throw new Error(
`Name of lvalue CallExpression must be an Identifier but got ${node.left.callee.type}.`
);
}
for (let i = 0; i < node.left.arguments.length; i++) {
const arg = node.left.arguments[i];
if (arg.type !== "Identifier") {
throw new Error(
`Arguments of lvalue CallExpression must have type Identifier, but argument ${
i + 1
} has type ${arg.type}`
);
}
orderedArgs.push(arg.name);
}
} else {
throw new Error(
`Expected Identifier or ArrayExpression for assignment lvalue but got ${node.left.type}.`
);
}
break;
case "Compound":
for (const part of node.body) {
parts.push(serialize(part, thisPrec));
}
break;
case "ArrayExpression":
parts.push(
`[${node.elements
.map((node) => serialize(node, thisPrec))
.join(",")}]`
);
break;
default:
throw new Error(
`Internal error in createFunction: Unhandled node: ${JSON.stringify(
node,
null,
2
)}`
);
}
if (wrap) parts.push(")");
return parts.join(" ");
}
if (outputs.length > 1) orderedArgs.length = 0;
let str = serialize(node, vars);
const hasArgs = orderedArgs.length;
const singleOutput = outputs.size === 1;
// Major hack! Remove `args.` from args if the argument order is specified
// by providing a lvalue CallExpression
function fixArgs(str) {
return hasArgs ? str.replace(/\bargs\./g, "") : str;
}
// prettier-ignore
const code = `return function (${
hasArgs ? orderedArgs.join(", ") : "args"
}) {
${
hasArgs
? ""
: Array.from(vars)
.map(
(v) =>
` if (typeof args['${v}'] !== 'number') throw new Error("Expected numeric value for ${v} but got " + (typeof args['${v}']));`
)
.join("\n")
}
${
outputs.size > 1
? ` const out = {};
${[...outputs].map(([id, fn]) => ` out['${id}'] = ${fixArgs(fn)};`).join("\n")}
return out;`
: arrayOutputs.length
? ` return [
${arrayOutputs.map((fn) => ` ${fixArgs(fn)},`).join("\n")}
];`
: ` return ${fixArgs(
singleOutput ? [...outputs.values()][0] : str
)};`
}
}`;
console.log(code);
return new Function("lib", code)(functionLibrary.functions);
};
}