diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index eea76f7..3b7b4ce 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -4,7 +4,7 @@ import { Expr } from "./expr"; describe("expr", () => { test.each([ ["1", "1"], - // ["1 + 2", "3"], + ["1 + 2", "3"], // ["1 - 2", "-1"], // ["1 * 2", "2"], // ["1 / 2", "0.5"], diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 27cc2fe..9926251 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -10,7 +10,7 @@ export function Expr({}, argv: TextPiece[]): ProcResult { const expression = argv.map(AsText).join(" ").trim(); const parser = new ExpressionParser(expression); - const result = parser.parsePrefixOp(); + const result = parser.parseSubExpression(0); if ("value" in result) { return { text: String(result.value) }; @@ -36,17 +36,40 @@ type Value = { value: number }; type TokenHandler = { leftBindingPower: number; token: RegExp; - parse: (matched: string, parser: ExpressionParser) => Value | ErrorResult; + parse: ( + left: Value, + matched: string, + parser: ExpressionParser + ) => Value | ErrorResult; }; +function map( + value: Value | ErrorResult, + op: (value: number) => Value | ErrorResult +): Value | ErrorResult { + if ("error" in value) { + return value; + } else { + return op(value.value); + } +} + const Operators: TokenHandler[] = [ { leftBindingPower: -1, token: NUMBER_TOKEN, - parse: (matched) => ({ value: Number(matched) }), + parse: (left, matched) => ({ value: Number(matched) }), + }, + { + leftBindingPower: 10, + token: PLUS_TOKEN, + parse: ({ value: left }, matched, parser) => + map(parser.parseSubExpression(11), (right) => ({ value: left + right })), }, ]; +const ZERO = { value: 0 }; + class ExpressionParser { pos: number = 0; @@ -64,11 +87,11 @@ class ExpressionParser { } /** 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 { + tryAll(left: Value, options: TokenHandler[]): Value | ErrorResult | null { for (const option of options) { const match = this.next(option.token); if (match) { - return option.parse(match, this); + return option.parse(left, match, this); } } return null; @@ -77,10 +100,34 @@ class ExpressionParser { parsePrefixOp(): Value | ErrorResult { return ( this.tryAll( + ZERO, Operators.filter((operator) => operator.leftBindingPower == -1) ) ?? { error: "Couldn't parse number" } ); } + + 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; + } } // parse expression: