236 lines
6.9 KiB
TypeScript
236 lines
6.9 KiB
TypeScript
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(),
|
|
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")');
|