From 62593b0ab8dfaa63a788702762146803c9845a98 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Fri, 17 May 2024 20:26:54 -0400 Subject: [PATCH 01/12] Start stubbing out Pratt parser - parse integers --- src/lib/expr.test.ts | 23 +++++++++++ src/lib/expr.ts | 94 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/lib/expr.test.ts diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts new file mode 100644 index 0000000..eea76f7 --- /dev/null +++ b/src/lib/expr.test.ts @@ -0,0 +1,23 @@ +import { AsText, TextPiece } from "../words"; +import { Expr } from "./expr"; + +describe("expr", () => { + test.each([ + ["1", "1"], + // ["1 + 2", "3"], + // ["1 - 2", "-1"], + // ["1 * 2", "2"], + // ["1 / 2", "0.5"], + // ["1 // 2", "0"], + // TODO: operator precedence + // TODO: parentheses + // TODO; eror reporting + ])("Expr does math: %s", (expression, result) => { + const actualResult = Expr({}, [{ text: expression }]); + console.log(actualResult) + expect("error" in actualResult).toBeFalsy(); + expect(AsText(actualResult as TextPiece)).toEqual(result); + }); + + // TODO: handle expr prefix +}); diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 59e01f3..27cc2fe 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -1,4 +1,4 @@ -import { AsText, ProcResult, TextPiece } from "../words"; +import { AsText, ErrorResult, ProcResult, TextPiece } from "../words"; export function Expr({}, argv: TextPiece[]): ProcResult { const name = argv[0]; @@ -6,7 +6,93 @@ export function Expr({}, argv: TextPiece[]): ProcResult { // being called as [expr ...], not fallback for math argv.splice(0, 1); } - return { - text: `Will do math to solve ${argv.map(AsText).join(" ")} eventually`, - }; + + 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; + } } + +// --------------------------- + +// 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 From bf44d84e175b6f03ef0760945def3967f3517fa0 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Fri, 17 May 2024 20:50:54 -0400 Subject: [PATCH 02/12] impl + --- src/lib/expr.test.ts | 2 +- src/lib/expr.ts | 57 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) 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: From 1a46130b1d0959d2ec85fa22c151d1d96a59ca8d Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Fri, 17 May 2024 20:52:33 -0400 Subject: [PATCH 03/12] impl - * // / --- src/lib/expr.test.ts | 8 ++++---- src/lib/expr.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index 3b7b4ce..31b4714 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -5,10 +5,10 @@ describe("expr", () => { test.each([ ["1", "1"], ["1 + 2", "3"], - // ["1 - 2", "-1"], - // ["1 * 2", "2"], - // ["1 / 2", "0.5"], - // ["1 // 2", "0"], + ["1 - 2", "-1"], + ["1 * 2", "2"], + ["1 / 2", "0.5"], + ["1 // 2", "0"], // TODO: operator precedence // TODO: parentheses // TODO; eror reporting diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 9926251..880ee2a 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -66,6 +66,32 @@ const Operators: TokenHandler[] = [ parse: ({ value: left }, matched, parser) => map(parser.parseSubExpression(11), (right) => ({ value: left + right })), }, + { + leftBindingPower: 10, + token: MINUS_TOKEN, + parse: ({ value: left }, matched, parser) => + map(parser.parseSubExpression(11), (right) => ({ value: left - right })), + }, + { + leftBindingPower: 20, + token: TIMES_TOKEN, + parse: ({ value: left }, matched, parser) => + map(parser.parseSubExpression(11), (right) => ({ value: left * right })), + }, + { + leftBindingPower: 20, + token: FLOOR_TOKEN, + parse: ({ value: left }, matched, parser) => + map(parser.parseSubExpression(11), (right) => ({ + value: Math.floor(left / right), + })), + }, + { + leftBindingPower: 20, + token: DIV_TOKEN, + parse: ({ value: left }, matched, parser) => + map(parser.parseSubExpression(11), (right) => ({ value: left / right })), + }, ]; const ZERO = { value: 0 }; From 21e8d95f98615bf16af178ce02cece59c1a2306a Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 21:24:19 -0400 Subject: [PATCH 04/12] Fix expression parser to require parsing full string --- src/lib/expr.test.ts | 38 +++++++++++++++++++++++--------------- src/lib/expr.ts | 4 ++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index 31b4714..a4917c9 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -2,21 +2,29 @@ import { AsText, TextPiece } from "../words"; import { Expr } from "./expr"; describe("expr", () => { - test.each([ - ["1", "1"], - ["1 + 2", "3"], - ["1 - 2", "-1"], - ["1 * 2", "2"], - ["1 / 2", "0.5"], - ["1 // 2", "0"], - // TODO: operator precedence - // TODO: parentheses - // TODO; eror reporting - ])("Expr does math: %s", (expression, result) => { - const actualResult = Expr({}, [{ text: expression }]); - console.log(actualResult) - expect("error" in actualResult).toBeFalsy(); - expect(AsText(actualResult as TextPiece)).toEqual(result); + describe("Expr does math", () => { + test.each([ + ["1", "1"], + ["1 + 2", "3"], + ["1 - 2", "-1"], + ["1 * 2", "2"], + ["1 / 2", "0.5"], + ["1 // 2", "0"], + // TODO: operator precedence + // TODO: parentheses + ])("%s", (expression, result) => { + const actualResult = Expr({}, [{ text: expression }]); + expect("error" in actualResult).toBeFalsy(); + expect(AsText(actualResult as TextPiece)).toEqual(result); + }); + }); + + // TODO; error reporting + describe("Expr rejects invalid expressions", () => { + test.each([["1 $ 2"], ["1 1 + 2"], ["$ 1"]])("%s", (expression) => { + const actualResult = Expr({}, [{ text: expression }]); + expect("error" in actualResult).toBeTruthy(); + }); }); // TODO: handle expr prefix diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 880ee2a..f4a72de 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -12,6 +12,10 @@ export function Expr({}, argv: TextPiece[]): ProcResult { 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 { From 1078dc365339c917462e6cd50801a59ad0aa6408 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 21:24:43 -0400 Subject: [PATCH 05/12] Fix binding power of * // / --- src/lib/expr.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/expr.ts b/src/lib/expr.ts index f4a72de..c0d1e96 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -80,13 +80,13 @@ const Operators: TokenHandler[] = [ leftBindingPower: 20, token: TIMES_TOKEN, parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(11), (right) => ({ value: left * right })), + map(parser.parseSubExpression(21), (right) => ({ value: left * right })), }, { leftBindingPower: 20, token: FLOOR_TOKEN, parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(11), (right) => ({ + map(parser.parseSubExpression(21), (right) => ({ value: Math.floor(left / right), })), }, @@ -94,7 +94,7 @@ const Operators: TokenHandler[] = [ leftBindingPower: 20, token: DIV_TOKEN, parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(11), (right) => ({ value: left / right })), + map(parser.parseSubExpression(21), (right) => ({ value: left / right })), }, ]; From 5a34b0f6f7b5a0b6a9a77a8166cb3a3752499b0d Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 21:33:17 -0400 Subject: [PATCH 06/12] Factor out common infix op logic --- src/lib/expr.ts | 46 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/src/lib/expr.ts b/src/lib/expr.ts index c0d1e96..4af04a1 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -58,44 +58,26 @@ function map( } } +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)), + } +} + const Operators: TokenHandler[] = [ { leftBindingPower: -1, token: NUMBER_TOKEN, parse: (left, matched) => ({ value: Number(matched) }), }, - { - leftBindingPower: 10, - token: PLUS_TOKEN, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(11), (right) => ({ value: left + right })), - }, - { - leftBindingPower: 10, - token: MINUS_TOKEN, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(11), (right) => ({ value: left - right })), - }, - { - leftBindingPower: 20, - token: TIMES_TOKEN, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(21), (right) => ({ value: left * right })), - }, - { - leftBindingPower: 20, - token: FLOOR_TOKEN, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(21), (right) => ({ - value: Math.floor(left / right), - })), - }, - { - leftBindingPower: 20, - token: DIV_TOKEN, - parse: ({ value: left }, matched, parser) => - map(parser.parseSubExpression(21), (right) => ({ value: left / right })), - }, + makeInfixOp(PLUS_TOKEN, 10, 11, (left, right) => ({ value: left + right })), + makeInfixOp(MINUS_TOKEN, 10, 11, (left, right) => ({ value: left - right })), + makeInfixOp(TIMES_TOKEN, 10, 11, (left, right) => ({ value: left * right })), + makeInfixOp(FLOOR_TOKEN, 10, 11, (left, right) => ({ value: Math.floor(left / right) })), + makeInfixOp(DIV_TOKEN, 10, 11, (left, right) => ({ value: left / right })), ]; const ZERO = { value: 0 }; From 2770a2de3bdbfa67b66cdd7eff95590bcfa1092f Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 21:53:23 -0400 Subject: [PATCH 07/12] impl unary minus --- src/lib/expr.test.ts | 1 + src/lib/expr.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index a4917c9..e998605 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -5,6 +5,7 @@ describe("expr", () => { describe("Expr does math", () => { test.each([ ["1", "1"], + ["-1", "-1"], ["1 + 2", "3"], ["1 - 2", "-1"], ["1 * 2", "2"], diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 4af04a1..4bd6b09 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -73,6 +73,12 @@ const Operators: TokenHandler[] = [ token: NUMBER_TOKEN, parse: (left, matched) => ({ value: Number(matched) }), }, + { + leftBindingPower: -1, + token: MINUS_TOKEN, + parse: (left, matched, parser) => + map(parser.parseSubExpression(0), (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, 10, 11, (left, right) => ({ value: left * right })), From 2b6aff0dab0ed7fed5410dead2ed4bf15e6b1270 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 23:15:46 -0400 Subject: [PATCH 08/12] Fix some binding powers --- src/lib/expr.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 4bd6b09..00dce77 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -77,13 +77,13 @@ const Operators: TokenHandler[] = [ leftBindingPower: -1, token: MINUS_TOKEN, parse: (left, matched, parser) => - map(parser.parseSubExpression(0), (right) => ({value: -right})), + 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, 10, 11, (left, right) => ({ value: left * right })), - makeInfixOp(FLOOR_TOKEN, 10, 11, (left, right) => ({ value: Math.floor(left / right) })), - makeInfixOp(DIV_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 })), ]; const ZERO = { value: 0 }; From ce991fd12a0bec89f2c7c3857bd50f5d124b454d Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 18 May 2024 23:22:50 -0400 Subject: [PATCH 09/12] impl modulo (handle negative divisors how people expect) --- src/lib/expr.test.ts | 5 +++++ src/lib/expr.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index e998605..3ed361f 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -11,6 +11,11 @@ describe("expr", () => { ["1 * 2", "2"], ["1 / 2", "0.5"], ["1 // 2", "0"], + ["1 % 2", "1"], + ["3 % 2", "1"], + ["-1 % 2", "1"], + // TODO: might change this! negative dividend is weird no matter what, but positive modulo is arguably the better call? + ["1 % -2", "-1"], // TODO: operator precedence // TODO: parentheses ])("%s", (expression, result) => { diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 00dce77..9d6afb2 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -32,6 +32,7 @@ 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; @@ -84,6 +85,7 @@ const Operators: TokenHandler[] = [ 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 }; From 7e21e71a072ad78be0e72480bc531c9505f5d7e2 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Mon, 20 May 2024 20:39:12 -0400 Subject: [PATCH 10/12] Clarify division behavior in tests --- src/lib/expr.test.ts | 24 ++++++++++++++++++------ src/lib/expr.ts | 27 +++++++++++++++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index 3ed361f..fbb0edb 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -10,12 +10,24 @@ describe("expr", () => { ["1 - 2", "-1"], ["1 * 2", "2"], ["1 / 2", "0.5"], - ["1 // 2", "0"], - ["1 % 2", "1"], - ["3 % 2", "1"], - ["-1 % 2", "1"], - // TODO: might change this! negative dividend is weird no matter what, but positive modulo is arguably the better call? - ["1 % -2", "-1"], + // 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"], // TODO: operator precedence // TODO: parentheses ])("%s", (expression, result) => { diff --git a/src/lib/expr.ts b/src/lib/expr.ts index 9d6afb2..81df33e 100644 --- a/src/lib/expr.ts +++ b/src/lib/expr.ts @@ -12,8 +12,8 @@ export function Expr({}, argv: TextPiece[]): ProcResult { const parser = new ExpressionParser(expression); const result = parser.parseSubExpression(0); - if(parser.pos != expression.length) { - return {error: "Couldn't parse full expression"} + if (parser.pos != expression.length) { + return { error: "Couldn't parse full expression" }; } if ("value" in result) { @@ -59,13 +59,20 @@ function map( } } -function makeInfixOp(token:RegExp, leftBindingPower: number,rightBindingPower: number, op: (left: number, right: number) => Value | ErrorResult):TokenHandler { +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)), - } + map(parser.parseSubExpression(rightBindingPower), (right) => + op(left, right) + ), + }; } const Operators: TokenHandler[] = [ @@ -78,14 +85,18 @@ const Operators: TokenHandler[] = [ leftBindingPower: -1, token: MINUS_TOKEN, parse: (left, matched, parser) => - map(parser.parseSubExpression(99), (right) => ({value: -right})), + 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(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 })), + makeInfixOp(MOD_TOKEN, 20, 21, (left, right) => ({ + value: ((left % right) + right) % right, + })), ]; const ZERO = { value: 0 }; From 9acba7431ffaf53ee303a7a7334ba0d7b4ef5087 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Mon, 20 May 2024 20:53:06 -0400 Subject: [PATCH 11/12] More expr test cases --- src/lib/expr.test.ts | 47 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/lib/expr.test.ts b/src/lib/expr.test.ts index fbb0edb..9095d40 100644 --- a/src/lib/expr.test.ts +++ b/src/lib/expr.test.ts @@ -2,7 +2,7 @@ import { AsText, TextPiece } from "../words"; import { Expr } from "./expr"; describe("expr", () => { - describe("Expr does math", () => { + describe("does math", () => { test.each([ ["1", "1"], ["-1", "-1"], @@ -28,8 +28,6 @@ describe("expr", () => { ["6 % -5", "-4"], ["-6 % -5", "-1"], ["-1 % -5", "-1"], - // TODO: operator precedence - // TODO: parentheses ])("%s", (expression, result) => { const actualResult = Expr({}, [{ text: expression }]); expect("error" in actualResult).toBeFalsy(); @@ -37,13 +35,46 @@ describe("expr", () => { }); }); - // TODO; error reporting - describe("Expr rejects invalid expressions", () => { - test.each([["1 $ 2"], ["1 1 + 2"], ["$ 1"]])("%s", (expression) => { + 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).toBeTruthy(); + expect("error" in actualResult).toBeFalsy(); + expect(AsText(actualResult as TextPiece)).toEqual(result); }); }); - // TODO: handle expr prefix + // 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"); + }); + }); }); From 7c74c9e34f18a365eb87e1a99a5b63c9d31bd2f4 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Wed, 5 Jun 2024 00:32:04 -0400 Subject: [PATCH 12/12] Add optional (and unused) offset parameter to parse() signature --- src/parser.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index d9dc4d4..3f7ad78 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -137,10 +137,11 @@ 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 to parse - * @returns - parsed list of commands, or error message on failure + * @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 */ -export function parse(code: string): [true, Script] | [false, string] { +export function parse(code: string, offset = 0): [true, Script] | [false, string] { /* Parse */ const [commands, errorPos, expected] = Script.match(code, 0);