verifiable-thinking-mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/package.json +75 -0
  4. package/src/index.ts +38 -0
  5. package/src/lib/cache.ts +246 -0
  6. package/src/lib/compression.ts +804 -0
  7. package/src/lib/compute/cache.ts +86 -0
  8. package/src/lib/compute/classifier.ts +555 -0
  9. package/src/lib/compute/confidence.ts +79 -0
  10. package/src/lib/compute/context.ts +154 -0
  11. package/src/lib/compute/extract.ts +200 -0
  12. package/src/lib/compute/filter.ts +224 -0
  13. package/src/lib/compute/index.ts +171 -0
  14. package/src/lib/compute/math.ts +247 -0
  15. package/src/lib/compute/patterns.ts +564 -0
  16. package/src/lib/compute/registry.ts +145 -0
  17. package/src/lib/compute/solvers/arithmetic.ts +65 -0
  18. package/src/lib/compute/solvers/calculus.ts +249 -0
  19. package/src/lib/compute/solvers/derivation-core.ts +371 -0
  20. package/src/lib/compute/solvers/derivation-latex.ts +160 -0
  21. package/src/lib/compute/solvers/derivation-mistakes.ts +1046 -0
  22. package/src/lib/compute/solvers/derivation-simplify.ts +451 -0
  23. package/src/lib/compute/solvers/derivation-transform.ts +620 -0
  24. package/src/lib/compute/solvers/derivation.ts +67 -0
  25. package/src/lib/compute/solvers/facts.ts +120 -0
  26. package/src/lib/compute/solvers/formula.ts +728 -0
  27. package/src/lib/compute/solvers/index.ts +36 -0
  28. package/src/lib/compute/solvers/logic.ts +422 -0
  29. package/src/lib/compute/solvers/probability.ts +307 -0
  30. package/src/lib/compute/solvers/statistics.ts +262 -0
  31. package/src/lib/compute/solvers/word-problems.ts +408 -0
  32. package/src/lib/compute/types.ts +107 -0
  33. package/src/lib/concepts.ts +111 -0
  34. package/src/lib/domain.ts +731 -0
  35. package/src/lib/extraction.ts +912 -0
  36. package/src/lib/index.ts +122 -0
  37. package/src/lib/judge.ts +260 -0
  38. package/src/lib/math/ast.ts +842 -0
  39. package/src/lib/math/index.ts +8 -0
  40. package/src/lib/math/operators.ts +171 -0
  41. package/src/lib/math/tokenizer.ts +477 -0
  42. package/src/lib/patterns.ts +200 -0
  43. package/src/lib/session.ts +825 -0
  44. package/src/lib/think/challenge.ts +323 -0
  45. package/src/lib/think/complexity.ts +504 -0
  46. package/src/lib/think/confidence-drift.ts +507 -0
  47. package/src/lib/think/consistency.ts +347 -0
  48. package/src/lib/think/guidance.ts +188 -0
  49. package/src/lib/think/helpers.ts +568 -0
  50. package/src/lib/think/hypothesis.ts +216 -0
  51. package/src/lib/think/index.ts +127 -0
  52. package/src/lib/think/prompts.ts +262 -0
  53. package/src/lib/think/route.ts +358 -0
  54. package/src/lib/think/schema.ts +98 -0
  55. package/src/lib/think/scratchpad-schema.ts +662 -0
  56. package/src/lib/think/spot-check.ts +961 -0
  57. package/src/lib/think/types.ts +93 -0
  58. package/src/lib/think/verification.ts +260 -0
  59. package/src/lib/tokens.ts +177 -0
  60. package/src/lib/verification.ts +620 -0
  61. package/src/prompts/index.ts +10 -0
  62. package/src/prompts/templates.ts +336 -0
  63. package/src/resources/index.ts +8 -0
  64. package/src/resources/sessions.ts +196 -0
  65. package/src/tools/compress.ts +138 -0
  66. package/src/tools/index.ts +5 -0
  67. package/src/tools/scratchpad.ts +2659 -0
  68. package/src/tools/sessions.ts +144 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Math module barrel export
3
+ * Re-exports operator utilities, tokenizer, and AST functionality
4
+ */
5
+
6
+ export * from "./ast.ts";
7
+ export * from "./operators.ts";
8
+ export * from "./tokenizer.ts";
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Math Operator Utilities
3
+ * Constants and functions for handling mathematical operators (ASCII + Unicode)
4
+ */
5
+
6
+ // Common math operators including Unicode variants from LaTeX/Word
7
+ // ASCII: + - * / ^ %
8
+ // Unicode: × ÷ − · √ ² ³ ⁿ ± ∓
9
+ // Also handles √ before numbers (prefix operator)
10
+
11
+ /** All recognized math operator characters (ASCII + Unicode) */
12
+ export const MATH_OPERATORS = "+-*/^%×÷−·√²³⁺⁻±∓" as const;
13
+
14
+ /** Pattern to check if text ends with a math operator */
15
+ export const MATH_OPERATOR_PATTERN = /[+\-*/^%×÷−·√²³⁺⁻±∓]\s*$/;
16
+
17
+ /** Pattern to match a single math operator character */
18
+ export const SINGLE_OPERATOR_PATTERN = /^[+\-*/^%×÷−·√²³⁺⁻±∓]$/;
19
+
20
+ /**
21
+ * Operator precedence levels (higher = binds tighter)
22
+ * Based on standard mathematical conventions:
23
+ * - Level 1: Addition/subtraction (lowest)
24
+ * - Level 2: Multiplication/division
25
+ * - Level 3: Exponentiation
26
+ * - Level 4: Unary/prefix operators (highest)
27
+ */
28
+ export const OPERATOR_PRECEDENCE: Record<string, number> = {
29
+ // Addition/subtraction - Level 1
30
+ "+": 1,
31
+ "-": 1,
32
+ "−": 1, // Unicode minus
33
+ "±": 1,
34
+ "∓": 1,
35
+ "⁺": 1, // Superscript plus
36
+ "⁻": 1, // Superscript minus
37
+
38
+ // Multiplication/division - Level 2
39
+ "*": 2,
40
+ "/": 2,
41
+ "%": 2,
42
+ "×": 2,
43
+ "÷": 2,
44
+ "·": 2, // Middle dot (multiplication)
45
+
46
+ // Exponentiation - Level 3
47
+ "^": 3,
48
+ "²": 3, // Superscript 2
49
+ "³": 3, // Superscript 3
50
+
51
+ // Unary/prefix - Level 4 (highest)
52
+ "√": 4, // Square root (prefix)
53
+ };
54
+
55
+ /**
56
+ * Right-associative operators (evaluated right-to-left)
57
+ * e.g., 2^3^4 = 2^(3^4) = 2^81, not (2^3)^4 = 8^4 = 4096
58
+ */
59
+ export const RIGHT_ASSOCIATIVE = new Set(["^", "²", "³"]);
60
+
61
+ /**
62
+ * Unary operators (take one operand)
63
+ * - Prefix: √4, -5 (when at start or after operator)
64
+ * - Postfix: 5², 3³
65
+ */
66
+ export const UNARY_OPERATORS = new Set(["√", "²", "³", "⁺", "⁻"]);
67
+
68
+ /**
69
+ * Operators that can be either unary or binary depending on context
70
+ * e.g., "-" is binary in "5-3" but unary in "-5" or "5*-3"
71
+ */
72
+ export const AMBIGUOUS_OPERATORS = new Set(["-", "−", "+", "±", "∓"]);
73
+
74
+ /**
75
+ * Check if a character is a recognized math operator
76
+ * Supports ASCII (+, -, *, /, ^, %) and Unicode (×, ÷, −, ·, √, ², ³, ⁺, ⁻, ±, ∓)
77
+ */
78
+ export function isMathOperator(char: string): boolean {
79
+ return SINGLE_OPERATOR_PATTERN.test(char);
80
+ }
81
+
82
+ /**
83
+ * Get the precedence level of a math operator
84
+ * Higher values bind tighter (e.g., * before +)
85
+ * Returns null for unrecognized characters
86
+ *
87
+ * Precedence levels:
88
+ * - 1: +, -, −, ±, ∓, ⁺, ⁻ (addition/subtraction)
89
+ * - 2: *, /, %, ×, ÷, · (multiplication/division)
90
+ * - 3: ^, ², ³ (exponentiation)
91
+ * - 4: √ (unary/prefix operators)
92
+ */
93
+ export function getOperatorPrecedence(char: string): number | null {
94
+ return OPERATOR_PRECEDENCE[char] ?? null;
95
+ }
96
+
97
+ /**
98
+ * Compare two operators by precedence
99
+ * Returns: negative if a < b, 0 if equal, positive if a > b
100
+ * Returns null if either is not a valid operator
101
+ */
102
+ export function compareOperatorPrecedence(a: string, b: string): number | null {
103
+ const precA = OPERATOR_PRECEDENCE[a];
104
+ const precB = OPERATOR_PRECEDENCE[b];
105
+ if (precA === undefined || precB === undefined) return null;
106
+ return precA - precB;
107
+ }
108
+
109
+ /**
110
+ * Check if an operator is right-associative
111
+ * Right-associative: 2^3^4 = 2^(3^4)
112
+ * Left-associative (default): 2-3-4 = (2-3)-4
113
+ */
114
+ export function isRightAssociative(char: string): boolean {
115
+ return RIGHT_ASSOCIATIVE.has(char);
116
+ }
117
+
118
+ /**
119
+ * Get the arity of an operator (number of operands)
120
+ * Returns 1 for unary, 2 for binary, null for non-operators
121
+ *
122
+ * Note: Some operators like "-" can be both unary and binary.
123
+ * Use getOperatorArityInContext() for context-aware detection.
124
+ */
125
+ export function getOperatorArity(char: string): 1 | 2 | null {
126
+ if (!isMathOperator(char)) return null;
127
+ if (UNARY_OPERATORS.has(char)) return 1;
128
+ return 2;
129
+ }
130
+
131
+ /**
132
+ * Check if an operator can be used as unary (context-dependent)
133
+ * "-" and "+" can be unary at expression start or after another operator
134
+ */
135
+ export function canBeUnary(char: string): boolean {
136
+ return UNARY_OPERATORS.has(char) || AMBIGUOUS_OPERATORS.has(char);
137
+ }
138
+
139
+ /**
140
+ * Determine operator arity based on context
141
+ * @param char - The operator character
142
+ * @param afterOperator - Whether this operator follows another operator or is at start
143
+ */
144
+ export function getOperatorArityInContext(char: string, afterOperator: boolean): 1 | 2 | null {
145
+ if (!isMathOperator(char)) return null;
146
+ if (UNARY_OPERATORS.has(char)) return 1;
147
+ if (afterOperator && AMBIGUOUS_OPERATORS.has(char)) return 1;
148
+ return 2;
149
+ }
150
+
151
+ /**
152
+ * Normalize operator to canonical ASCII form
153
+ * Converts Unicode operators to their ASCII equivalents:
154
+ * - − → -
155
+ * - × → *
156
+ * - · → *
157
+ * - ÷ → /
158
+ */
159
+ export function normalizeOperator(op: string): string {
160
+ switch (op) {
161
+ case "−":
162
+ return "-";
163
+ case "×":
164
+ case "·":
165
+ return "*";
166
+ case "÷":
167
+ return "/";
168
+ default:
169
+ return op;
170
+ }
171
+ }
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Math Expression Tokenizer
3
+ * Tokenizes mathematical expressions into structured tokens with operator metadata
4
+ */
5
+
6
+ import {
7
+ getOperatorArityInContext,
8
+ getOperatorPrecedence,
9
+ isMathOperator,
10
+ isRightAssociative,
11
+ } from "./operators.ts";
12
+
13
+ // =============================================================================
14
+ // EXPRESSION VALIDATION
15
+ // =============================================================================
16
+
17
+ /** Result of expression validation */
18
+ export interface ExpressionValidation {
19
+ valid: boolean;
20
+ error?: string;
21
+ /** Position in expression where error was detected */
22
+ errorIndex?: number;
23
+ }
24
+
25
+ /**
26
+ * Validate a math expression for structural correctness
27
+ * Checks for:
28
+ * - Consecutive binary operators (e.g., "2 + * 3")
29
+ * - Missing operands (e.g., "5 +", "+ 5" without unary context)
30
+ * - Mismatched parentheses
31
+ * - Postfix operator without operand (e.g., "² 5")
32
+ */
33
+ export function validateExpression(expr: string): ExpressionValidation {
34
+ const trimmed = expr.trim();
35
+ if (!trimmed) {
36
+ return { valid: false, error: "Empty expression" };
37
+ }
38
+
39
+ const state = { parenDepth: 0, expectOperand: true, lastWasOperator: true };
40
+ let i = 0;
41
+
42
+ while (i < trimmed.length) {
43
+ const char = trimmed[i] as string;
44
+
45
+ // Skip whitespace
46
+ if (/\s/.test(char)) {
47
+ i++;
48
+ continue;
49
+ }
50
+
51
+ // Handle different token types
52
+ const result = processValidationChar(char, trimmed, i, state);
53
+ if (result.error) {
54
+ return { valid: false, error: result.error, errorIndex: result.errorIndex };
55
+ }
56
+ i = result.nextIndex;
57
+ }
58
+
59
+ // Final checks
60
+ if (state.parenDepth > 0) {
61
+ return { valid: false, error: "Unclosed parenthesis" };
62
+ }
63
+
64
+ // Trailing binary operator (but allow expressions like "5²")
65
+ if (state.expectOperand && state.lastWasOperator) {
66
+ const lastNonSpace = trimmed.trimEnd().slice(-1);
67
+ if (isMathOperator(lastNonSpace) && lastNonSpace !== "²" && lastNonSpace !== "³") {
68
+ return { valid: false, error: "Expression ends with operator" };
69
+ }
70
+ }
71
+
72
+ return { valid: true };
73
+ }
74
+
75
+ /** State for expression validation */
76
+ interface ValidationState {
77
+ parenDepth: number;
78
+ expectOperand: boolean;
79
+ lastWasOperator: boolean;
80
+ }
81
+
82
+ /** Process a single character during validation */
83
+ function processValidationChar(
84
+ char: string,
85
+ expr: string,
86
+ i: number,
87
+ state: ValidationState,
88
+ ): { nextIndex: number; error?: string; errorIndex?: number } {
89
+ // Handle parentheses
90
+ if (char === "(" || char === "[" || char === "{") {
91
+ state.parenDepth++;
92
+ state.expectOperand = true;
93
+ state.lastWasOperator = true;
94
+ return { nextIndex: i + 1 };
95
+ }
96
+
97
+ if (char === ")" || char === "]" || char === "}") {
98
+ state.parenDepth--;
99
+ if (state.parenDepth < 0) {
100
+ return { nextIndex: i, error: "Unmatched closing parenthesis", errorIndex: i };
101
+ }
102
+ if (state.expectOperand) {
103
+ return { nextIndex: i, error: "Empty parentheses or missing operand", errorIndex: i };
104
+ }
105
+ state.expectOperand = false;
106
+ state.lastWasOperator = false;
107
+ return { nextIndex: i + 1 };
108
+ }
109
+
110
+ // Handle operators
111
+ if (isMathOperator(char)) {
112
+ return processOperatorValidation(char, i, state);
113
+ }
114
+
115
+ // Handle numbers
116
+ if (/[\d.]/.test(char)) {
117
+ let j = i;
118
+ while (j < expr.length && /[\d.eE+-]/.test(expr[j] as string)) j++;
119
+ state.expectOperand = false;
120
+ state.lastWasOperator = false;
121
+ return { nextIndex: j };
122
+ }
123
+
124
+ // Handle variables
125
+ if (/[a-zA-Z_]/.test(char)) {
126
+ let j = i;
127
+ while (j < expr.length && /[a-zA-Z0-9_]/.test(expr[j] as string)) j++;
128
+ state.expectOperand = false;
129
+ state.lastWasOperator = false;
130
+ return { nextIndex: j };
131
+ }
132
+
133
+ // Unknown character - skip
134
+ return { nextIndex: i + 1 };
135
+ }
136
+
137
+ /** Process operator during validation */
138
+ function processOperatorValidation(
139
+ char: string,
140
+ i: number,
141
+ state: ValidationState,
142
+ ): { nextIndex: number; error?: string; errorIndex?: number } {
143
+ const arity = getOperatorArityInContext(char, state.lastWasOperator);
144
+
145
+ // Postfix operators need a preceding operand
146
+ if ((char === "²" || char === "³") && state.expectOperand) {
147
+ return { nextIndex: i, error: `Postfix operator '${char}' without operand`, errorIndex: i };
148
+ }
149
+
150
+ // Unary prefix operators are okay when expecting operand
151
+ if (arity === 1 && state.expectOperand && char !== "²" && char !== "³") {
152
+ state.lastWasOperator = true;
153
+ return { nextIndex: i + 1 };
154
+ }
155
+
156
+ // Binary operator when expecting operand = error
157
+ if (arity === 2 && state.expectOperand) {
158
+ return { nextIndex: i, error: `Unexpected operator '${char}'`, errorIndex: i };
159
+ }
160
+
161
+ // Update state based on operator type
162
+ if (char === "²" || char === "³") {
163
+ state.expectOperand = false;
164
+ state.lastWasOperator = false;
165
+ } else {
166
+ state.expectOperand = true;
167
+ state.lastWasOperator = true;
168
+ }
169
+
170
+ return { nextIndex: i + 1 };
171
+ }
172
+
173
+ // =============================================================================
174
+ // TOKEN TYPES
175
+ // =============================================================================
176
+
177
+ /** Token types for math expression tokenization */
178
+ export type MathTokenType = "number" | "operator" | "variable" | "paren" | "unknown";
179
+
180
+ /** A single token from a math expression */
181
+ export interface MathToken {
182
+ type: MathTokenType;
183
+ value: string;
184
+ position: number;
185
+ /** For operators: precedence level (1-4) */
186
+ precedence?: number;
187
+ /** For operators: arity in context (1 or 2) */
188
+ arity?: 1 | 2;
189
+ /** For operators: whether right-associative */
190
+ rightAssociative?: boolean;
191
+ }
192
+
193
+ /** Result of tokenizing an expression */
194
+ export interface TokenizeResult {
195
+ tokens: MathToken[];
196
+ /** Any errors encountered during tokenization */
197
+ errors: string[];
198
+ }
199
+
200
+ // =============================================================================
201
+ // TOKENIZER
202
+ // =============================================================================
203
+
204
+ /**
205
+ * Tokenize a math expression into structured tokens
206
+ * Returns tokens with type, value, position, and operator metadata
207
+ *
208
+ * @example
209
+ * tokenizeMathExpression("2 + 3 * -4")
210
+ * // Returns tokens: [
211
+ * // { type: "number", value: "2", position: 0 },
212
+ * // { type: "operator", value: "+", position: 2, precedence: 1, arity: 2 },
213
+ * // { type: "number", value: "3", position: 4 },
214
+ * // { type: "operator", value: "*", position: 6, precedence: 2, arity: 2 },
215
+ * // { type: "operator", value: "-", position: 8, precedence: 1, arity: 1 },
216
+ * // { type: "number", value: "4", position: 9 },
217
+ * // ]
218
+ */
219
+ export function tokenizeMathExpression(expr: string): TokenizeResult {
220
+ const tokens: MathToken[] = [];
221
+ const errors: string[] = [];
222
+ let i = 0;
223
+ let lastWasOperator = true; // Start as if after operator (for unary detection)
224
+ let lastWasOperand = false;
225
+
226
+ while (i < expr.length) {
227
+ const char = expr[i] as string;
228
+ const startPos = i;
229
+
230
+ // Skip whitespace
231
+ if (/\s/.test(char)) {
232
+ i++;
233
+ continue;
234
+ }
235
+
236
+ // Parentheses and brackets
237
+ if (/[()[\]{}]/.test(char)) {
238
+ tokens.push({
239
+ type: "paren",
240
+ value: char,
241
+ position: startPos,
242
+ });
243
+ // Opening paren acts like "after operator" for unary detection
244
+ lastWasOperator = char === "(" || char === "[" || char === "{";
245
+ lastWasOperand = char === ")" || char === "]" || char === "}";
246
+ i++;
247
+ continue;
248
+ }
249
+
250
+ // Operators
251
+ if (isMathOperator(char)) {
252
+ const arity = getOperatorArityInContext(char, lastWasOperator && !lastWasOperand);
253
+ tokens.push({
254
+ type: "operator",
255
+ value: char,
256
+ position: startPos,
257
+ precedence: getOperatorPrecedence(char) ?? undefined,
258
+ arity: arity ?? undefined,
259
+ rightAssociative: isRightAssociative(char) || undefined,
260
+ });
261
+ // Postfix operators (², ³) don't change lastWasOperator
262
+ if (char === "²" || char === "³") {
263
+ lastWasOperator = false;
264
+ lastWasOperand = true;
265
+ } else {
266
+ lastWasOperator = true;
267
+ lastWasOperand = false;
268
+ }
269
+ i++;
270
+ continue;
271
+ }
272
+
273
+ // Numbers (including decimals and scientific notation)
274
+ if (/[\d.]/.test(char)) {
275
+ let numStr = "";
276
+ while (i < expr.length) {
277
+ const c = expr[i] as string;
278
+ // Handle scientific notation: 1e10, 2.5e-3
279
+ if (/[\d.]/.test(c)) {
280
+ numStr += c;
281
+ i++;
282
+ } else if (/[eE]/.test(c) && i + 1 < expr.length) {
283
+ const next = expr[i + 1] as string;
284
+ if (/[\d+-]/.test(next)) {
285
+ numStr += c + next;
286
+ i += 2;
287
+ } else {
288
+ break;
289
+ }
290
+ } else {
291
+ break;
292
+ }
293
+ }
294
+ tokens.push({
295
+ type: "number",
296
+ value: numStr,
297
+ position: startPos,
298
+ });
299
+ lastWasOperator = false;
300
+ lastWasOperand = true;
301
+ continue;
302
+ }
303
+
304
+ // Variables/identifiers
305
+ if (/[a-zA-Z_]/.test(char)) {
306
+ let varStr = "";
307
+ while (i < expr.length) {
308
+ const c = expr[i] as string;
309
+ if (/[a-zA-Z0-9_]/.test(c)) {
310
+ varStr += c;
311
+ i++;
312
+ } else {
313
+ break;
314
+ }
315
+ }
316
+ tokens.push({
317
+ type: "variable",
318
+ value: varStr,
319
+ position: startPos,
320
+ });
321
+ lastWasOperator = false;
322
+ lastWasOperand = true;
323
+ continue;
324
+ }
325
+
326
+ // Unknown character
327
+ tokens.push({
328
+ type: "unknown",
329
+ value: char,
330
+ position: startPos,
331
+ });
332
+ errors.push(`Unknown character '${char}' at position ${startPos}`);
333
+ i++;
334
+ }
335
+
336
+ // Post-process: insert implicit multiplication operators
337
+ // E.g., "2x" → "2 * x", "x(y)" → "x * (y)", "(a)(b)" → "(a) * (b)"
338
+ const processed = insertImplicitMultiplication(tokens);
339
+
340
+ return { tokens: processed, errors };
341
+ }
342
+
343
+ /**
344
+ * Insert implicit multiplication operators between adjacent operands
345
+ * Handles: number-variable (2x), variable-paren (x(y)), paren-paren ((a)(b))
346
+ * @internal
347
+ */
348
+ function insertImplicitMultiplication(tokens: MathToken[]): MathToken[] {
349
+ const result: MathToken[] = [];
350
+
351
+ for (let i = 0; i < tokens.length; i++) {
352
+ const curr = tokens[i] as MathToken;
353
+ result.push(curr);
354
+
355
+ // Check if we need to insert implicit multiplication after this token
356
+ const next = tokens[i + 1];
357
+ if (!next) continue;
358
+
359
+ // Conditions where implicit multiplication applies:
360
+ // 1. number followed by variable: 2x
361
+ // 2. number followed by opening paren: 2(x)
362
+ // 3. variable followed by opening paren: x(y)
363
+ // 4. variable followed by variable: xy (though this could be a variable name)
364
+ // 5. closing paren followed by opening paren: (a)(b)
365
+ // 6. closing paren followed by number: (a)2
366
+ // 7. closing paren followed by variable: (a)x
367
+ // 8. number followed by number (rare, but possible in some contexts)
368
+ // 9. postfix operator followed by operand: x²y
369
+
370
+ const currIsOperand =
371
+ curr.type === "number" ||
372
+ curr.type === "variable" ||
373
+ (curr.type === "paren" && /[)\]}]/.test(curr.value)) ||
374
+ (curr.type === "operator" && (curr.value === "²" || curr.value === "³"));
375
+
376
+ const nextIsOperand =
377
+ next.type === "number" ||
378
+ next.type === "variable" ||
379
+ (next.type === "paren" && /[([{]/.test(next.value));
380
+
381
+ if (currIsOperand && nextIsOperand) {
382
+ // Insert implicit multiplication
383
+ result.push({
384
+ type: "operator",
385
+ value: "*",
386
+ position: curr.position + curr.value.length,
387
+ precedence: 2, // Same as explicit multiplication
388
+ arity: 2,
389
+ });
390
+ }
391
+ }
392
+
393
+ return result;
394
+ }
395
+
396
+ // =============================================================================
397
+ // TOKEN FORMATTING
398
+ // =============================================================================
399
+
400
+ /** Options for formatting expressions */
401
+ export interface FormatOptions {
402
+ /** Use Unicode operators (× instead of *, ÷ instead of /) */
403
+ useUnicode?: boolean;
404
+ /** Add spaces around binary operators */
405
+ spaces?: boolean;
406
+ /** Normalize minus sign to Unicode − */
407
+ normalizeMinus?: boolean;
408
+ }
409
+
410
+ /** Map ASCII operators to Unicode equivalents */
411
+ const UNICODE_OPERATORS: Record<string, string> = {
412
+ "*": "×",
413
+ "/": "÷",
414
+ "-": "−",
415
+ };
416
+
417
+ /**
418
+ * Format tokens into a pretty-printed expression string
419
+ *
420
+ * @example
421
+ * const tokens = tokenizeMathExpression("2*3/4").tokens;
422
+ * formatExpression(tokens, { useUnicode: true, spaces: true });
423
+ * // Returns: "2 × 3 ÷ 4"
424
+ */
425
+ export function formatExpression(tokens: MathToken[], options: FormatOptions = {}): string {
426
+ const { useUnicode = false, spaces = true, normalizeMinus = false } = options;
427
+
428
+ let result = "";
429
+ let prevToken: MathToken | null = null;
430
+
431
+ for (const token of tokens) {
432
+ let value = token.value;
433
+
434
+ // Convert to Unicode if requested
435
+ if (token.type === "operator") {
436
+ if (useUnicode && UNICODE_OPERATORS[value]) {
437
+ value = UNICODE_OPERATORS[value] as string;
438
+ } else if (normalizeMinus && value === "-") {
439
+ value = "−";
440
+ }
441
+ }
442
+
443
+ // Add spacing
444
+ if (prevToken && spaces) {
445
+ const needsSpace = shouldAddSpace(prevToken, token);
446
+ if (needsSpace) {
447
+ result += " ";
448
+ }
449
+ }
450
+
451
+ result += value;
452
+ prevToken = token;
453
+ }
454
+
455
+ return result;
456
+ }
457
+
458
+ /** Determine if a space should be added between two tokens */
459
+ function shouldAddSpace(prev: MathToken, curr: MathToken): boolean {
460
+ // No space after opening paren or before closing paren
461
+ if (prev.type === "paren" && /[([{]/.test(prev.value)) return false;
462
+ if (curr.type === "paren" && /[)\]}]/.test(curr.value)) return false;
463
+
464
+ // No space before postfix operators
465
+ if (curr.type === "operator" && (curr.value === "²" || curr.value === "³")) return false;
466
+
467
+ // No space after unary prefix operators
468
+ if (prev.type === "operator" && prev.arity === 1 && prev.value !== "²" && prev.value !== "³") {
469
+ return false;
470
+ }
471
+
472
+ // Space around binary operators
473
+ if (curr.type === "operator" && curr.arity === 2) return true;
474
+ if (prev.type === "operator" && prev.arity === 2) return true;
475
+
476
+ return false;
477
+ }