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);