simple-eval
Version:
Simple JavaScript expression evaluator
230 lines (228 loc) • 6.19 kB
JavaScript
// src/utils.ts
function isConstructable(fn) {
try {
new new Proxy(fn, { construct: () => ({}) })();
return true;
} catch {
return false;
}
}
function isCallable(value) {
return typeof value === "function";
}
function fnToString(fn) {
return Function.toString.call(fn);
}
function toString(value) {
const stringTag = value[Symbol.toStringTag];
if (typeof stringTag === "string") {
return stringTag;
}
return Object.prototype.toString.call(value).slice(8, -1);
}
function printValue(value) {
switch (typeof value) {
case "string":
case "number":
case "boolean":
case "symbol":
case "undefined":
case "bigint":
return String(value);
case "function":
return fnToString(value);
case "object":
if (value === null) {
return "null";
}
return `#<${toString(value)}>`;
}
}
// src/reduce.ts
function reduce(node, ctx) {
switch (node.type) {
case "Program":
return reduceProgram(node, ctx);
case "ExpressionStatement":
return reduce(node.expression, ctx);
case "MemberExpression":
return reduceMemExpr(node, ctx);
case "LogicalExpression":
return reduceLogExpr(node, ctx);
case "ConditionalExpression":
return reduceConExpr(node, ctx);
case "BinaryExpression":
return reduceBinExpr(node, ctx);
case "UnaryExpression":
return reduceUnExpr(node, ctx);
case "CallExpression":
return reduceCallExpr(node, ctx);
case "NewExpression":
return reduceNewExpr(node, ctx);
case "ArrayExpression":
return reduceArrExpr(node, ctx);
case "ThisExpression":
return ctx;
case "Identifier":
return resolveIdentifier(node.name, ctx);
case "Literal":
return node.value;
default:
throw SyntaxError("Unexpected node");
}
}
function reduceProgram(node, ctx) {
if (node.body.length !== 1) {
throw SyntaxError("Too complex expression");
}
return reduce(node.body[0], ctx);
}
function reduceMemExpr(node, ctx) {
const value = reduce(node.object, ctx);
const key = node.property.type === "Identifier" ? node.property.name : reduce(node.property, ctx);
if (value === null || value === void 0) {
throw TypeError(
`Cannot read properties of ${String(value)} (reading '${printValue(value)}')`
);
}
const accessedValue = value[key];
return isCallable(accessedValue) ? accessedValue.bind(value) : accessedValue;
}
function reduceCallExpr(node, ctx) {
const maybeCallable = reduce(node.callee, ctx);
if (!isCallable(maybeCallable)) {
throw TypeError(`${maybeCallable} is not a function`);
}
return Reflect.apply(
maybeCallable,
null,
node.arguments.map((arg) => reduce(arg, ctx))
);
}
function reduceNewExpr(node, ctx) {
const maybeConstructable = reduce(node.callee, ctx);
if (!isConstructable(maybeConstructable)) {
throw TypeError(`${printValue(maybeConstructable)} is not a constructor`);
}
return Reflect.construct(
maybeConstructable,
node.arguments.map((arg) => reduce(arg, ctx))
);
}
function reduceUnExpr(node, ctx) {
if (!node.prefix || node.argument.type === "UnaryExpression") {
throw SyntaxError("Unexpected operator");
}
switch (node.operator) {
case "!":
return !reduce(node.argument, ctx);
case "-":
return -reduce(node.argument, ctx);
case "+":
return +reduce(node.argument, ctx);
case "~":
return ~reduce(node.argument, ctx);
case "typeof":
return typeof reduce(node.argument, ctx);
case "void":
return void reduce(node.argument, ctx);
case "delete": {
if (node.argument.type !== "MemberExpression") {
return true;
}
const obj = reduce(node.argument.object, ctx);
if (typeof obj !== "object" || obj === null) {
return true;
}
return Reflect.deleteProperty(
obj,
node.argument.property.type === "Identifier" ? node.argument.property.name : reduce(node.argument.property, ctx)
);
}
default:
throw SyntaxError(`Unsupported unary operator: ${node.operator}`);
}
}
function reduceConExpr(node, ctx) {
return reduce(node.test, ctx) ? reduce(node.consequent, ctx) : reduce(node.alternate, ctx);
}
function reduceBinExpr(node, ctx) {
return evalLhsRhs(node, ctx);
}
function reduceLogExpr(node, ctx) {
return evalLhsRhs(node, ctx);
}
function reduceArrExpr(node, ctx) {
return node.elements.map((el) => el === null ? null : reduce(el, ctx));
}
function evalLhsRhs(node, ctx) {
const left = reduce(node.left, ctx);
const right = reduce(node.right, ctx);
const isLogicalExpression = node.type === "LogicalExpression";
switch (node.operator) {
case "-":
return left - right;
case "+":
return left + right;
case "==":
return left == right;
case "!=":
return left != right;
case "===":
return left === right;
case "!==":
return left !== right;
case "<":
return left < right;
case "<=":
return left <= right;
case ">":
return left > right;
case ">=":
return left >= right;
case "<<":
return left << right;
case ">>":
return left >> right;
case ">>>":
return left >>> right;
case "*":
return left * right;
case "/":
return left / right;
case "%":
return left % right;
case "**":
return left ** right;
case "|":
return left | right;
case "^":
return left ^ right;
case "&":
return left & right;
case "in":
return left in right;
case "instanceof":
return left instanceof right;
case "&&":
return left && right;
case "||":
return left || right;
case "??":
return left ?? right;
default:
throw SyntaxError(
`Unsupported ${isLogicalExpression ? "logical" : "binary"} operator: ${node["operator"]}`
);
}
}
function resolveIdentifier(name, ctx) {
if (ctx === void 0 || !(name in ctx)) {
throw ReferenceError(`${name} is not defined`);
}
return Reflect.get(ctx, name, ctx);
}
export {
reduce
};
//# sourceMappingURL=chunk-7ZYRIKQM.js.map