add expr
This commit is contained in:
commit
53716f7731
1 changed files with 234 additions and 0 deletions
234
expr.ts
Normal file
234
expr.ts
Normal file
|
@ -0,0 +1,234 @@
|
|||
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[0].toUpperCase() + str.slice(1).toLowerCase(),
|
||||
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');
|
Loading…
Reference in a new issue