expr/expr.ts

239 lines
7 KiB
TypeScript

// an expression language
function run(code: string, ctx: { [name: string]: any } = {}): any {
const tokens = lex(code);
const tree = parse(tokens);
const result = expr(tree, ctx);
return result;
}
function expr(tree, ctx: { [name: string]: any } = {}): any {
switch (tree.type) {
case "int": return parseInt(tree.val);
case "float": return parseFloat(tree.val);
case "str": return tree.val.slice(1, -1);
case "ident":
switch (tree.val) {
case "true": return true;
case "false": return false;
case "null": return null;
default: return Object.hasOwn(ctx, tree.val) && ctx[tree.val];
}
}
if (tree.op) {
if (tree.op.val === ".") {
if (tree.cons[1].type !== "ident") throw "expected ident";
const obj = expr(tree.cons[0], ctx);
const idx = tree.cons[1].val;
if (!Object.hasOwn(obj, idx)) throw `${idx} doesn't exist on object`;
return obj[idx];
}
const op = ({
"**": (a, b) => a ** b,
"*": (a, b) => a * b,
"/": (a, b) => a / b,
"%": (a, b) => a % b,
"+": (a, b) => a + b,
"-": (a, b) => b === undefined ? -a : a - b,
"==": (a, b) => a == b,
"!=": (a, b) => a != b,
">=": (a, b) => a >= b,
"<=": (a, b) => a <= b,
">": (a, b) => a > b,
"<": (a, b) => a < b,
"in": (a, b) => {
if (!Array.isArray(b)) throw "can only use `in` on arrays";
return b.includes(a);
},
"&&": (a, b) => a && b,
"||": (a, b) => a || b,
"?": (cond, a, b) => cond ? a : b,
",": (a, b) => (a, b),
})[tree.op.val];
if (!op) throw `invalid operator ${op}`;
return op(...tree.cons.map(i => expr(i, ctx)));
}
if (tree.array) return tree.array.map(i => expr(i, ctx));
if (tree.func) return expr(tree.func, ctx)(...tree.cons.map(i => expr(i, ctx)));
if (tree.index) {
const obj = expr(tree.index, ctx);
const idx = expr(tree.with, ctx);
if (!Object.hasOwn(obj, idx)) throw `${idx} doesn't exist on object`;
return obj[idx];
}
}
function parse(tokens, min = 0) {
const bp = {
".": [24, 25],
"(": [22, 23],
"[": [20, 21],
// negation/not: 19
"**": [18, 17],
"*": [15, 16],
"/": [15, 16],
"%": [15, 16],
"+": [13, 14],
"-": [13, 14],
"==": [11, 12],
"!=": [11, 12],
">=": [11, 12],
"<=": [11, 12],
">": [11, 12],
"<": [11, 12],
"in": [9, 10],
"&&": [7, 8],
"||": [5, 6],
"?": [4, 3],
",": [2, 1],
};
let lhs = tokens.next();
if (lhs.val === "-" || lhs.val === "!") {
lhs = { op: lhs, cons: [parse(tokens, 19)] };
} else if (lhs.val === "(") {
if (tokens.peek()?.val === ")") throw "empty paranthases";
const newlhs = parse(tokens, 0);
if (tokens.next()?.val !== ")") throw "missing a closing paranthase";
lhs = newlhs;
} else if (lhs.val === "[") {
const cons = [];
let next;
if (tokens.peek()?.val === "]") {
next = tokens.peek();
} else {
do {
cons.push(parse(tokens, 0));
next = tokens.next();
} while (next.val === ",");
}
if (next?.val !== "]") throw "missing a closing bracket";
lhs = { array: cons };
} else if (lhs.type === "symb" || lhs.val === "in") {
throw "unexpected token " + lhs.val;
}
while (true) {
const next = tokens.peek();
if (!next) break;
if (next.type !== "symb" && next.type !== "paran" && next.val !== "in") throw "unexpected token " + next.val;
if ([")", "]", ",", ":"].includes(next.val)) break;
if (!bp[next.val]) throw "invalid symbol";
const [left, right] = bp[next.val];
if (left < min) break;
if (next.val === "(" || next.val === "[") {
const cons = [];
do {
tokens.next();
if (tokens.peek()?.val === ")") break;
cons.push(parse(tokens, 0));
} while (next.val === "(" && tokens.peek()?.val === ",");
if (next.val === "(") {
if (tokens.next()?.val !== ")") throw "missing a closing paranthase";
lhs = { func: lhs, cons };
} else {
if (tokens.next()?.val !== "]") throw "missing a closing bracket";
lhs = { index: lhs, with: cons[0] };
}
continue;
} else if (next.val === "?") {
tokens.next();
const middle = parse(tokens, 0);
if (tokens.next()?.val !== ":") throw "missing a colon";
lhs = { op: next, cons: [lhs, middle, parse(tokens, right)] };
} else {
tokens.next();
lhs = { op: next, cons: [lhs, parse(tokens, right)] };
}
}
return lhs;
}
function lex(input: string) {
let code = input;
const tokens = [];
while (code.length) {
for (let [name, reg] of [
["space", /^[ \n\t]+/],
["int", /^0(x[0-9a-f]+|b[01]+|o[0-7]+)/i],
["float", /^[0-9]+(\.[0-9]+)?/],
["str", /^"(.*?[^\\])?"/],
["str", /^'(.*?[^\\])?'/],
["ident", /^[a-z_][a-z0-9_]*/i],
["paran", /^[\[\]\(\)]/i],
["symb", /^[^ \n\ta-z0-9_]{1,2}/i],
]) {
const match = code.match(reg);
if (match) {
tokens.push({ type: name, val: match[0] });
code = code.slice(match[0].length);
break;
}
}
}
return {
i: 0,
tokens: tokens.filter(i => i.type !== "space"),
peek() { return this.tokens[this.i] },
next() { return this.tokens[this.i++] },
};
}
function test(code: string) {
const ctx = {
math: {
rand: () => Math.random(),
abs: (n) => Math.abs(n),
sin: (n) => Math.sin(n),
cos: (n) => Math.cos(n),
tan: (n) => Math.tan(n),
log: (n) => Math.log10(n),
log2: (n) => Math.log2(n),
sqrt: (n) => Math.sqrt(n),
floor: (n) => Math.floor(n),
ceil: (n) => Math.ceil(n),
round: (n) => Math.round(n),
max: (...n) => Math.max(...n),
min: (...n) => Math.min(...n),
sum: (...n) => n.reduce((a, i) => a + i),
avg: (...n) => n.reduce((a, i) => a + i) / n,
pi: Math.PI,
tau: Math.PI * 2,
e: Math.E,
},
obj: {
keys: (obj) => [...Object.keys(obj)],
vals: (obj) => [...Object.values(obj)],
entries: (obj) => [...Object.entries(obj)],
has: (obj) => Object.hasOwn(obj),
},
str: {
capitalize: (str) => str.split(" ").map(i => i[0].toUpperCase() + i.slice(1).toLowerCase()).join(" "),
upper: (str) => str.toUpperCase(),
lower: (str) => str.toLowerCase(),
trim: (str) => str.trim(),
trimStart: (str) => str.trimStart(),
trimEnd: (str) => str.trimEnd(),
split: (str, by = " ") => str.split(by),
slice: (str, start, end) => str.slice(by),
},
};
console.log(code, "\x1b[90m=>\x1b[0m", run(code, ctx));
}
test("2 + 3 * 4");
test("(2 + 3) * 4");
test("math.sin");
test("math.sin(10 * math.pi)");
test('"foo" in ["fooo", "bar", "baz"]');
test('"foo" ? 1 : 4');
test('"hello" + ", " + "world"');
test('"hello".length');
test('[1, 2, 3].length');
test('str.capitalize("hello world")');
test('"apple" in str.split("apple banana orange") ? "apple exists" : "apple doesn\'t exist"');