prototype-3x5/src/lib/expr.ts

99 lines
2.4 KiB
TypeScript
Raw Normal View History

import { AsText, ErrorResult, ProcResult, TextPiece } from "../words";
2023-10-20 21:02:27 -04:00
export function Expr({}, argv: TextPiece[]): ProcResult {
const name = argv[0];
if ("bare" in name && name.bare == "expr") {
// being called as [expr ...], not fallback for math
argv.splice(0, 1);
}
const expression = argv.map(AsText).join(" ").trim();
const parser = new ExpressionParser(expression);
const result = parser.parsePrefixOp();
if ("value" in result) {
return { text: String(result.value) };
} else {
return result;
}
2023-10-20 21:02:27 -04:00
}
// ---------------------------
// Pratt parser for evaluating arithmatic expressions
const PLUS_TOKEN = /\s*(\+)/y;
const MINUS_TOKEN = /\s*(\-)/y;
const TIMES_TOKEN = /\s*(\*)/y;
const FLOOR_TOKEN = /\s*(\/\/)/y;
const DIV_TOKEN = /\s*(\/)/y;
const NUMBER_TOKEN = /\s*(\d+)/y;
type Value = { value: number };
type TokenHandler = {
leftBindingPower: number;
token: RegExp;
parse: (matched: string, parser: ExpressionParser) => Value | ErrorResult;
};
const Operators: TokenHandler[] = [
{
leftBindingPower: -1,
token: NUMBER_TOKEN,
parse: (matched) => ({ value: Number(matched) }),
},
];
class ExpressionParser {
pos: number = 0;
constructor(public text: string) {}
next(token: RegExp): string | null {
token.lastIndex = this.pos;
const matches = token.exec(this.text);
if (matches) {
this.pos = token.lastIndex;
return matches[1];
} else {
return null;
}
}
/** Tries to match one of the given tokens & returns the value of the handler. If none match, returns null. */
tryAll(options: TokenHandler[]): Value | ErrorResult | null {
for (const option of options) {
const match = this.next(option.token);
if (match) {
return option.parse(match, this);
}
}
return null;
}
parsePrefixOp(): Value | ErrorResult {
return (
this.tryAll(
Operators.filter((operator) => operator.leftBindingPower == -1)
) ?? { error: "Couldn't parse number" }
);
}
}
// parse expression:
// must parse sub-expression with right binding power 0
// must be at EOF
// parse sub-expression with right binding power :
// must parse a prefix op
// parse as many postfix ops w/ left binding power > right binding power
// parse op:
// must parse associated token (if success- failing the rest of the op fails *everything*)
// depending on op:
// maybe parse sub-expressions with op's right-binding power
// maybe consume other tokens