UNPKG

@nano-utils/op

Version:

Operator overloading in JS

235 lines (234 loc) 9.03 kB
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;