diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts deleted file mode 100644 index 9095d40..0000000 --- a/src/lib/expr.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AsText, TextPiece } from "../words"; -import { Expr } from "./expr"; - -describe("expr", () => { - describe("does math", () => { - test.each([ - ["1", "1"], - ["-1", "-1"], - ["1 + 2", "3"], - ["1 - 2", "-1"], - ["1 * 2", "2"], - ["1 / 2", "0.5"], - // floored division - ["1 // 5", "0"], - ["6 // 5", "1"], - ["-1 // 5", "-1"], - ["-6 // 5", "-2"], - ["1 // -5", "-1"], - ["6 // -5", "-2"], - ["-1 // -5", "0"], - ["-6 // -5", "1"], - // floored modulo - ["1 % 5", "1"], - ["6 % 5", "1"], - ["-6 % 5", "4"], - ["-1 % 5", "4"], - ["1 % -5", "-4"], - ["6 % -5", "-4"], - ["-6 % -5", "-1"], - ["-1 % -5", "-1"], - ])("%s", (expression, result) => { - const actualResult = Expr({}, [{ text: expression }]); - expect("error" in actualResult).toBeFalsy(); - expect(AsText(actualResult as TextPiece)).toEqual(result); - }); - }); - - describe("handles operator precedence", () => { - test.each([ - ["1 - 2 + 1", "0"], - ["1 + 2 * 3", "7"], - ["1 / 2 + 3", "3.5"], - ])("%s", (expression, result) => { - const actualResult = Expr({}, [{ text: expression }]); - expect("error" in actualResult).toBeFalsy(); - expect(AsText(actualResult as TextPiece)).toEqual(result); - }); - }); - - // TODO: parentheses - - // TODO; error reporting - describe("rejects invalid expressions", () => { - test.each([[""], ["1 $ 2"], ["1 1 + 2"], ["1 + + 2"], ["$ 1"]])( - "%s", - (expression) => { - const actualResult = Expr({}, [{ text: expression }]); - expect("error" in actualResult).toBeTruthy(); - } - ); - }); - - // TODO: operators should only be accepted as bare words - - describe("ignores an expr prefix", () => { - test.each([ - [["1", "+", "2"]], - [["expr", "1", "+", "2"]], - [["1 + 2"]], - [["expr", "1 + 2"]], - ])("%s", (argv) => { - const actualResult = Expr( - {}, - argv.map((text) => ({ bare: text })) - ); - expect("error" in actualResult).toBeFalsy(); - expect(AsText(actualResult as TextPiece)).toEqual("3"); - }); - }); -}); diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 81df33e..59e01f3 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -1,4 +1,4 @@ -import { AsText, ErrorResult, ProcResult, TextPiece } from "../words"; +import { AsText, ProcResult, TextPiece } from "../words"; export function Expr({}, argv: TextPiece[]): ProcResult { const name = argv[0]; @@ -6,171 +6,7 @@ export function Expr({}, argv: TextPiece[]): ProcResult { // 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.parseSubExpression(0); - - if (parser.pos != expression.length) { - return { error: "Couldn't parse full expression" }; - } - - if ("value" in result) { - return { text: String(result.value) }; - } else { - return result; - } -} - -// --------------------------- - -// 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 MOD_TOKEN = /\s*(\%)/y; - -const NUMBER_TOKEN = /\s*(\d+)/y; - -type Value = { value: number }; - -type TokenHandler = { - leftBindingPower: number; - token: RegExp; - 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); - } -} - -function makeInfixOp( - token: RegExp, - leftBindingPower: number, - rightBindingPower: number, - op: (left: number, right: number) => Value | ErrorResult -): TokenHandler { return { - leftBindingPower, - token, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(rightBindingPower), (right) => - op(left, right) - ), + text: `Will do math to solve ${argv.map(AsText).join(" ")} eventually`, }; } - -const Operators: TokenHandler[] = [ - { - leftBindingPower: -1, - token: NUMBER_TOKEN, - parse: (left, matched) => ({ value: Number(matched) }), - }, - { - leftBindingPower: -1, - token: MINUS_TOKEN, - parse: (left, matched, parser) => - map(parser.parseSubExpression(99), (right) => ({ value: -right })), - }, - makeInfixOp(PLUS_TOKEN, 10, 11, (left, right) => ({ value: left + right })), - makeInfixOp(MINUS_TOKEN, 10, 11, (left, right) => ({ value: left - right })), - makeInfixOp(TIMES_TOKEN, 20, 21, (left, right) => ({ value: left * right })), - makeInfixOp(FLOOR_TOKEN, 20, 21, (left, right) => ({ - value: Math.floor(left / right), - })), - makeInfixOp(DIV_TOKEN, 20, 21, (left, right) => ({ value: left / right })), - makeInfixOp(MOD_TOKEN, 20, 21, (left, right) => ({ - value: ((left % right) + right) % right, - })), -]; - -const ZERO = { value: 0 }; - -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(left: Value, options: TokenHandler[]): Value | ErrorResult | null { - for (const option of options) { - const match = this.next(option.token); - if (match) { - return option.parse(left, match, this); - } - } - return null; - } - - 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: -// 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 diff --git a/src/parser.ts b/src/parser.ts index 3f7ad78..d9dc4d4 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -137,11 +137,10 @@ const ERROR_CONTEXT = /(?<=([^\n]{0,50}))([^\n]{0,50})/y; * Parse out a Notcl script into an easier-to-interpret representation. * No script is actually executed yet. * - * @param code code to parse - * @param offset source position of code, if embedded in a larger source document - * @returns parsed list of commands, or error message on failure + * @param - code to parse + * @returns - parsed list of commands, or error message on failure */ -export function parse(code: string, offset = 0): [true, Script] | [false, string] { +export function parse(code: string): [true, Script] | [false, string] { /* Parse */ const [commands, errorPos, expected] = Script.match(code, 0);