2024-05-17 20:26:54 -04:00
|
|
|
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);
|
|
|
|
}
|
2024-05-17 20:26:54 -04:00
|
|
|
|
|
|
|
const expression = argv.map(AsText).join(" ").trim();
|
|
|
|
|
|
|
|
const parser = new ExpressionParser(expression);
|
2024-05-17 20:50:54 -04:00
|
|
|
const result = parser.parseSubExpression(0);
|
2024-05-17 20:26:54 -04:00
|
|
|
|
2024-05-20 20:39:12 -04:00
|
|
|
if (parser.pos != expression.length) {
|
|
|
|
return { error: "Couldn't parse full expression" };
|
2024-05-18 21:24:19 -04:00
|
|
|
}
|
|
|
|
|
2024-05-17 20:26:54 -04:00
|
|
|
if ("value" in result) {
|
|
|
|
return { text: String(result.value) };
|
|
|
|
} else {
|
|
|
|
return result;
|
|
|
|
}
|
2023-10-20 21:02:27 -04:00
|
|
|
}
|
2024-05-17 20:26:54 -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;
|
2024-05-18 23:22:50 -04:00
|
|
|
const MOD_TOKEN = /\s*(\%)/y;
|
2024-05-17 20:26:54 -04:00
|
|
|
|
|
|
|
const NUMBER_TOKEN = /\s*(\d+)/y;
|
|
|
|
|
|
|
|
type Value = { value: number };
|
|
|
|
|
|
|
|
type TokenHandler = {
|
|
|
|
leftBindingPower: number;
|
|
|
|
token: RegExp;
|
2024-05-17 20:50:54 -04:00
|
|
|
parse: (
|
|
|
|
left: Value,
|
|
|
|
matched: string,
|
|
|
|
parser: ExpressionParser
|
|
|
|
) => Value | ErrorResult;
|
2024-05-17 20:26:54 -04:00
|
|
|
};
|
|
|
|
|
2024-05-17 20:50:54 -04:00
|
|
|
function map(
|
|
|
|
value: Value | ErrorResult,
|
|
|
|
op: (value: number) => Value | ErrorResult
|
|
|
|
): Value | ErrorResult {
|
|
|
|
if ("error" in value) {
|
|
|
|
return value;
|
|
|
|
} else {
|
|
|
|
return op(value.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-20 20:39:12 -04:00
|
|
|
function makeInfixOp(
|
|
|
|
token: RegExp,
|
|
|
|
leftBindingPower: number,
|
|
|
|
rightBindingPower: number,
|
|
|
|
op: (left: number, right: number) => Value | ErrorResult
|
|
|
|
): TokenHandler {
|
2024-05-18 21:33:17 -04:00
|
|
|
return {
|
|
|
|
leftBindingPower,
|
|
|
|
token,
|
|
|
|
parse: ({ value: left }, matched, parser) =>
|
2024-05-20 20:39:12 -04:00
|
|
|
map(parser.parseSubExpression(rightBindingPower), (right) =>
|
|
|
|
op(left, right)
|
|
|
|
),
|
|
|
|
};
|
2024-05-18 21:33:17 -04:00
|
|
|
}
|
|
|
|
|
2024-05-17 20:26:54 -04:00
|
|
|
const Operators: TokenHandler[] = [
|
|
|
|
{
|
|
|
|
leftBindingPower: -1,
|
|
|
|
token: NUMBER_TOKEN,
|
2024-05-17 20:50:54 -04:00
|
|
|
parse: (left, matched) => ({ value: Number(matched) }),
|
|
|
|
},
|
2024-05-18 21:53:23 -04:00
|
|
|
{
|
|
|
|
leftBindingPower: -1,
|
|
|
|
token: MINUS_TOKEN,
|
|
|
|
parse: (left, matched, parser) =>
|
2024-05-20 20:39:12 -04:00
|
|
|
map(parser.parseSubExpression(99), (right) => ({ value: -right })),
|
2024-05-18 21:53:23 -04:00
|
|
|
},
|
2024-05-18 21:33:17 -04:00
|
|
|
makeInfixOp(PLUS_TOKEN, 10, 11, (left, right) => ({ value: left + right })),
|
|
|
|
makeInfixOp(MINUS_TOKEN, 10, 11, (left, right) => ({ value: left - right })),
|
2024-05-18 23:15:46 -04:00
|
|
|
makeInfixOp(TIMES_TOKEN, 20, 21, (left, right) => ({ value: left * right })),
|
2024-05-20 20:39:12 -04:00
|
|
|
makeInfixOp(FLOOR_TOKEN, 20, 21, (left, right) => ({
|
|
|
|
value: Math.floor(left / right),
|
|
|
|
})),
|
2024-05-18 23:15:46 -04:00
|
|
|
makeInfixOp(DIV_TOKEN, 20, 21, (left, right) => ({ value: left / right })),
|
2024-05-20 20:39:12 -04:00
|
|
|
makeInfixOp(MOD_TOKEN, 20, 21, (left, right) => ({
|
|
|
|
value: ((left % right) + right) % right,
|
|
|
|
})),
|
2024-05-17 20:26:54 -04:00
|
|
|
];
|
|
|
|
|
2024-05-17 20:50:54 -04:00
|
|
|
const ZERO = { value: 0 };
|
|
|
|
|
2024-05-17 20:26:54 -04:00
|
|
|
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. */
|
2024-05-17 20:50:54 -04:00
|
|
|
tryAll(left: Value, options: TokenHandler[]): Value | ErrorResult | null {
|
2024-05-17 20:26:54 -04:00
|
|
|
for (const option of options) {
|
|
|
|
const match = this.next(option.token);
|
|
|
|
if (match) {
|
2024-05-17 20:50:54 -04:00
|
|
|
return option.parse(left, match, this);
|
2024-05-17 20:26:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
parsePrefixOp(): Value | ErrorResult {
|
|
|
|
return (
|
|
|
|
this.tryAll(
|
2024-05-17 20:50:54 -04:00
|
|
|
ZERO,
|
2024-05-17 20:26:54 -04:00
|
|
|
Operators.filter((operator) => operator.leftBindingPower == -1)
|
|
|
|
) ?? { error: "Couldn't parse number" }
|
|
|
|
);
|
|
|
|
}
|
2024-05-17 20:50:54 -04:00
|
|
|
|
|
|
|
parseSubExpression(rightBindingPower: number): Value | ErrorResult {
|
|
|
|
let value = this.parsePrefixOp();
|
|
|
|
if ("error" in value) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
const newValue = this.tryAll(
|
|
|
|
value,
|
|
|
|
Operators.filter(
|
|
|
|
(operator) => operator.leftBindingPower > rightBindingPower
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (newValue == null) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
value = newValue;
|
|
|
|
} while (!("error" in value));
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
2024-05-17 20:26:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|