@nano-utils/op
Version:
Operator overloading in JS
235 lines (234 loc) • 9.03 kB
JavaScript
import { isNumber, OP } from './utils.js';
export function makeOp(ops) {
return function op(strs, ...vals) {
let strIdx = 0, charIdx = 0;
function lex() {
if (strs[strIdx] === undefined)
return undefined;
while (strs[strIdx][charIdx] !== undefined && strs[strIdx][charIdx].trim() === '')
charIdx++;
if (strs[strIdx][charIdx] === undefined) {
return vals[strIdx];
}
else {
const op = ops
.flat()
.concat('(', ')')
.find((op) => strs[strIdx].startsWith(op, charIdx));
if (op !== undefined) {
return { [OP]: op };
}
else if (strs[strIdx][charIdx] === "'" || strs[strIdx][charIdx] === '"') {
const ci = charIdx, quote = strs[strIdx][charIdx];
charIdx++;
if (strs[strIdx][charIdx] === undefined)
throw new Error('Syntax error: string literal expected');
let str = '';
while (strs[strIdx][charIdx] !== quote) {
str += strs[strIdx][charIdx];
charIdx++;
if (strs[strIdx][charIdx] === undefined)
throw new Error('Syntax error: unterminated string literal');
}
charIdx = ci;
return str;
}
else if (isNumber(strs[strIdx][charIdx])) {
const ci = charIdx;
let str = '';
while (strs[strIdx][charIdx] !== undefined && strs[strIdx][charIdx].trim() !== '' && isNumber(strs[strIdx][charIdx])) {
str += strs[strIdx][charIdx];
charIdx++;
}
charIdx = ci;
return Number(str);
}
else {
throw new Error(`Syntax error: invalid token '${strs[strIdx][charIdx]}'`);
}
}
}
function next() {
if (strs[strIdx] !== undefined) {
if (strs[strIdx][charIdx] === undefined) {
strIdx++;
charIdx = 0;
}
else {
const consumedTok = lex();
if (typeof consumedTok === 'object' && OP in consumedTok) {
charIdx += consumedTok[OP].length;
}
else {
charIdx += typeof consumedTok === 'string' ? consumedTok.length + 2 : consumedTok.toString().length;
}
}
}
}
const parsers = [
() => {
const tok = lex();
if (typeof tok === 'object' && OP in tok) {
if (tok[OP] === '(') {
next();
const expr = parsers.at(-1)();
const cp = lex();
if (typeof cp === 'object' && cp[OP] === ')') {
next();
return expr;
}
else {
// throw new Error(`Syntax error: missing ')' before ${strs[strIdx].slice(charIdx)}${vals[strIdx]}, got ${cp[OP]}`);
// should be end of input because top level parse will consume everything until ')'
throw new Error("Syntax error: unterminated '('");
}
}
else {
throw new Error('Syntax error: expected value');
}
}
else {
next();
if (tok !== undefined) {
return tok;
}
else {
throw new Error('Syntax error: expected value');
}
}
}
].concat(ops.map((tier, i) => () => {
const left = parsers[i]();
let tok = lex();
if (tok === undefined) {
next();
return left;
}
if (typeof tok !== 'object' || !(OP in tok))
throw new Error('Syntax error: expected operator');
const clauses = [left];
while (tok !== undefined && tier.includes(tok[OP])) {
next();
clauses.push(tok[OP]);
clauses.push(parsers[i]());
tok = lex();
}
while (clauses.length > 1) {
const left = clauses.shift(), op = clauses.shift(), right = clauses.shift();
clauses.unshift({
[OP]: op,
left,
right
});
}
return clauses[0];
}));
const tree = parsers.at(-1)();
return evalOps(tree);
};
}
function evalOps(node) {
if (typeof node === 'object' && OP in node) {
const left = evalOps(node.left), right = evalOps(node.right), op = 'operator' + node[OP];
if (typeof left === 'object') {
if (op in left) {
return left[op](right);
}
else {
if (op in left.constructor) {
const Class = left.constructor;
try {
return Class[op](left, right);
}
catch (e) { }
}
if (typeof right === 'object' && op in right.constructor) {
const Class = right.constructor;
try {
return Class[op](left, right);
}
catch (e) { }
}
throw new Error(`Operator error: ${op} is not a callable on left operand ${toErrorDisplay(left)} or a static function on either operand's types`);
}
}
else if (node[OP] === '+') {
if ((typeof left === 'string' || typeof left === 'number') && (typeof right === 'string' || typeof right === 'number')) {
// need as any because typescript disallows (string | number) + (string | number) even though it's perfectly fine in JS
return left + right;
}
else {
if (typeof right === 'object' && op in right) {
try {
return right[op](left);
}
catch (e) { }
}
if (typeof right === 'object' && op in right.constructor) {
const Class = right.constructor;
try {
return Class[op](left, right);
}
catch (e) { }
}
throw new Error(`Operator error: cannot evaluate ${toErrorDisplay(left)} + ${toErrorDisplay(right)}`);
}
}
else if (['-', '*', '/'].includes(node[OP])) {
if (typeof left === 'number' && typeof right === 'number') {
switch (node[OP]) {
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
}
}
else {
if (typeof right === 'object' && op in right) {
try {
return right[op](left);
}
catch (e) { }
}
if (typeof right === 'object' && op in right.constructor) {
const Class = right.constructor;
try {
return Class[op](left, right);
}
catch (e) { }
}
throw new Error(`Operator error: cannot evaluate ${toErrorDisplay(left)} ${node[OP]} ${toErrorDisplay(right)}`);
}
}
else {
throw new Error(`Operator error: ${op} is not a builtin or a callable on left operand ${toErrorDisplay(left)}`);
}
}
else {
return node;
}
}
function toErrorDisplay(obj) {
if (typeof obj === 'object') {
if (Symbol.toPrimitive in obj) {
return `${obj}`;
}
else if ('toString' in obj && obj.toString !== Object.prototype.toString) {
return obj.toString();
}
else {
return obj.constructor.name;
}
}
else {
return `${obj}`;
}
}
export const op = makeOp([
['*', '/'],
['+', '-'],
['==', '!=']
]);
op.make = makeOp;