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,842 @@
1
+ /**
2
+ * Math Expression AST (Abstract Syntax Tree)
3
+ * Building, simplification, formatting, comparison, and evaluation of math expressions
4
+ */
5
+
6
+ import { isRightAssociative, normalizeOperator } from "./operators.ts";
7
+ import { type MathToken, tokenizeMathExpression, validateExpression } from "./tokenizer.ts";
8
+
9
+ // =============================================================================
10
+ // AST NODE TYPES
11
+ // =============================================================================
12
+
13
+ /** AST node types */
14
+ export type ASTNodeType = "number" | "variable" | "unary" | "binary";
15
+
16
+ /** Base AST node */
17
+ export interface ASTNodeBase {
18
+ type: ASTNodeType;
19
+ }
20
+
21
+ /** Number literal node */
22
+ export interface NumberNode extends ASTNodeBase {
23
+ type: "number";
24
+ value: number;
25
+ }
26
+
27
+ /** Variable reference node */
28
+ export interface VariableNode extends ASTNodeBase {
29
+ type: "variable";
30
+ name: string;
31
+ }
32
+
33
+ /** Unary operation node */
34
+ export interface UnaryNode extends ASTNodeBase {
35
+ type: "unary";
36
+ operator: string;
37
+ operand: ASTNode;
38
+ }
39
+
40
+ /** Binary operation node */
41
+ export interface BinaryNode extends ASTNodeBase {
42
+ type: "binary";
43
+ operator: string;
44
+ left: ASTNode;
45
+ right: ASTNode;
46
+ }
47
+
48
+ /** Union of all AST node types */
49
+ export type ASTNode = NumberNode | VariableNode | UnaryNode | BinaryNode;
50
+
51
+ /** Result of AST building */
52
+ export interface ASTResult {
53
+ ast: ASTNode | null;
54
+ error?: string;
55
+ }
56
+
57
+ // =============================================================================
58
+ // AST BUILDING (Shunting-Yard Algorithm)
59
+ // =============================================================================
60
+
61
+ /**
62
+ * Build an Abstract Syntax Tree from tokens using the shunting-yard algorithm
63
+ * Respects operator precedence and associativity
64
+ *
65
+ * @example
66
+ * const tokens = tokenizeMathExpression("2 + 3 * 4").tokens;
67
+ * const { ast } = buildAST(tokens);
68
+ * // ast = { type: "binary", operator: "+", left: 2, right: { type: "binary", operator: "*", left: 3, right: 4 } }
69
+ */
70
+ export function buildAST(tokens: MathToken[]): ASTResult {
71
+ if (tokens.length === 0) {
72
+ return { ast: null, error: "Empty expression" };
73
+ }
74
+
75
+ const outputStack: ASTNode[] = [];
76
+ const operatorStack: MathToken[] = [];
77
+
78
+ for (const token of tokens) {
79
+ const result = processASTToken(token, outputStack, operatorStack);
80
+ if (result?.error) return { ast: null, error: result.error };
81
+ }
82
+
83
+ // Pop remaining operators
84
+ while (operatorStack.length > 0) {
85
+ const op = operatorStack.pop() as MathToken;
86
+ if (op.type === "paren") {
87
+ return { ast: null, error: "Mismatched parentheses" };
88
+ }
89
+ const result = applyOperator(op, outputStack);
90
+ if (result.error) return { ast: null, error: result.error };
91
+ }
92
+
93
+ if (outputStack.length !== 1) {
94
+ return { ast: null, error: "Invalid expression structure" };
95
+ }
96
+
97
+ return { ast: outputStack[0] as ASTNode };
98
+ }
99
+
100
+ /** Process a single token for AST building */
101
+ function processASTToken(
102
+ token: MathToken,
103
+ outputStack: ASTNode[],
104
+ operatorStack: MathToken[],
105
+ ): { error?: string } | null {
106
+ switch (token.type) {
107
+ case "number":
108
+ outputStack.push({ type: "number", value: parseFloat(token.value) });
109
+ return null;
110
+
111
+ case "variable":
112
+ outputStack.push({ type: "variable", name: token.value });
113
+ return null;
114
+
115
+ case "operator":
116
+ return processASTOperator(token, outputStack, operatorStack);
117
+
118
+ case "paren":
119
+ return processASTParen(token, outputStack, operatorStack);
120
+
121
+ case "unknown":
122
+ return { error: `Unknown token: ${token.value}` };
123
+ }
124
+ }
125
+
126
+ /** Process operator token for AST */
127
+ function processASTOperator(
128
+ token: MathToken,
129
+ outputStack: ASTNode[],
130
+ operatorStack: MathToken[],
131
+ ): { error?: string } | null {
132
+ if (token.arity === 1) {
133
+ operatorStack.push(token);
134
+ return null;
135
+ }
136
+
137
+ // Binary operator - pop higher/equal precedence operators
138
+ while (operatorStack.length > 0) {
139
+ const top = operatorStack[operatorStack.length - 1] as MathToken;
140
+ if (top.type === "paren") break;
141
+
142
+ const topPrec = top.precedence ?? 0;
143
+ const currPrec = token.precedence ?? 0;
144
+ const shouldPop =
145
+ top.arity === 1 || topPrec > currPrec || (topPrec === currPrec && !token.rightAssociative);
146
+
147
+ if (!shouldPop) break;
148
+
149
+ operatorStack.pop();
150
+ const result = applyOperator(top, outputStack);
151
+ if (result.error) return result;
152
+ }
153
+
154
+ operatorStack.push(token);
155
+ return null;
156
+ }
157
+
158
+ /** Process parenthesis token for AST */
159
+ function processASTParen(
160
+ token: MathToken,
161
+ outputStack: ASTNode[],
162
+ operatorStack: MathToken[],
163
+ ): { error?: string } | null {
164
+ if (token.value === "(" || token.value === "[" || token.value === "{") {
165
+ operatorStack.push(token);
166
+ return null;
167
+ }
168
+
169
+ // Closing paren - pop until matching open
170
+ while (operatorStack.length > 0) {
171
+ const top = operatorStack[operatorStack.length - 1] as MathToken;
172
+ if (top.type === "paren") {
173
+ operatorStack.pop();
174
+ return null;
175
+ }
176
+ operatorStack.pop();
177
+ const result = applyOperator(top, outputStack);
178
+ if (result.error) return result;
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /** Apply an operator to operands on the stack */
184
+ function applyOperator(op: MathToken, stack: ASTNode[]): { error?: string } {
185
+ if (op.arity === 1) {
186
+ // Unary operator
187
+ if (stack.length < 1) {
188
+ return { error: `Missing operand for unary operator ${op.value}` };
189
+ }
190
+ const operand = stack.pop() as ASTNode;
191
+ stack.push({
192
+ type: "unary",
193
+ operator: op.value,
194
+ operand,
195
+ });
196
+ } else {
197
+ // Binary operator
198
+ if (stack.length < 2) {
199
+ return { error: `Missing operands for binary operator ${op.value}` };
200
+ }
201
+ const right = stack.pop() as ASTNode;
202
+ const left = stack.pop() as ASTNode;
203
+ stack.push({
204
+ type: "binary",
205
+ operator: op.value,
206
+ left,
207
+ right,
208
+ });
209
+ }
210
+ return {};
211
+ }
212
+
213
+ // =============================================================================
214
+ // AST SIMPLIFICATION
215
+ // =============================================================================
216
+
217
+ /**
218
+ * Simplify an AST by performing constant folding and algebraic simplification
219
+ * Transformations applied:
220
+ * - Constant folding: 2 + 3 → 5
221
+ * - Identity: x + 0 → x, x * 1 → x, x ^ 1 → x
222
+ * - Zero: x * 0 → 0, 0 / x → 0
223
+ * - Power of zero: x ^ 0 → 1
224
+ * - Double negation: --x → x
225
+ * - Self subtraction: x - x → 0
226
+ * - Self division: x / x → 1
227
+ *
228
+ * @example
229
+ * const tokens = tokenizeMathExpression("x + 0 * 2").tokens;
230
+ * const { ast } = buildAST(tokens);
231
+ * const simplified = simplifyAST(ast);
232
+ * // simplified represents just "x"
233
+ */
234
+ export function simplifyAST(node: ASTNode): ASTNode {
235
+ switch (node.type) {
236
+ case "number":
237
+ case "variable":
238
+ return node;
239
+
240
+ case "unary":
241
+ return simplifyUnary(node);
242
+
243
+ case "binary":
244
+ return simplifyBinary(node);
245
+ }
246
+ }
247
+
248
+ /** Simplify unary node */
249
+ function simplifyUnary(node: UnaryNode): ASTNode {
250
+ const operand = simplifyAST(node.operand);
251
+
252
+ // Double negation: --x → x
253
+ if (
254
+ (node.operator === "-" || node.operator === "−") &&
255
+ operand.type === "unary" &&
256
+ (operand.operator === "-" || operand.operator === "−")
257
+ ) {
258
+ return operand.operand;
259
+ }
260
+
261
+ // Constant folding for unary operators
262
+ if (operand.type === "number") {
263
+ const result = evaluateUnaryOp(node.operator, operand.value);
264
+ if (result !== null) {
265
+ return { type: "number", value: result };
266
+ }
267
+ }
268
+
269
+ // +x → x
270
+ if (node.operator === "+") {
271
+ return operand;
272
+ }
273
+
274
+ return { ...node, operand };
275
+ }
276
+
277
+ /** Simplify binary node - apply identity and zero rules */
278
+ function simplifyBinary(node: BinaryNode): ASTNode {
279
+ const left = simplifyAST(node.left);
280
+ const right = simplifyAST(node.right);
281
+
282
+ // Constant folding: both operands are numbers
283
+ if (left.type === "number" && right.type === "number") {
284
+ const result = evaluateBinaryOp(node.operator, left.value, right.value);
285
+ if (result !== null) {
286
+ return { type: "number", value: result };
287
+ }
288
+ }
289
+
290
+ const op = normalizeOperator(node.operator);
291
+
292
+ // Try operator-specific simplifications
293
+ const simplified = simplifyByOperator(op, left, right);
294
+ if (simplified) return simplified;
295
+
296
+ return { ...node, left, right };
297
+ }
298
+
299
+ /** Apply operator-specific simplification rules */
300
+ function simplifyByOperator(op: string, left: ASTNode, right: ASTNode): ASTNode | null {
301
+ switch (op) {
302
+ case "+":
303
+ if (right.type === "number" && right.value === 0) return left;
304
+ if (left.type === "number" && left.value === 0) return right;
305
+ break;
306
+ case "-":
307
+ if (right.type === "number" && right.value === 0) return left;
308
+ if (astEqual(left, right)) return { type: "number", value: 0 };
309
+ break;
310
+ case "*":
311
+ if (right.type === "number" && right.value === 0) return { type: "number", value: 0 };
312
+ if (left.type === "number" && left.value === 0) return { type: "number", value: 0 };
313
+ if (right.type === "number" && right.value === 1) return left;
314
+ if (left.type === "number" && left.value === 1) return right;
315
+ break;
316
+ case "/":
317
+ if (left.type === "number" && left.value === 0) return { type: "number", value: 0 };
318
+ if (right.type === "number" && right.value === 1) return left;
319
+ if (astEqual(left, right)) return { type: "number", value: 1 };
320
+ break;
321
+ case "^":
322
+ if (right.type === "number" && right.value === 0) return { type: "number", value: 1 };
323
+ if (right.type === "number" && right.value === 1) return left;
324
+ if (left.type === "number" && left.value === 1) return { type: "number", value: 1 };
325
+ if (
326
+ left.type === "number" &&
327
+ left.value === 0 &&
328
+ right.type === "number" &&
329
+ right.value > 0
330
+ ) {
331
+ return { type: "number", value: 0 };
332
+ }
333
+ break;
334
+ }
335
+ return null;
336
+ }
337
+
338
+ /** Evaluate a unary operation on a number */
339
+ function evaluateUnaryOp(op: string, value: number): number | null {
340
+ switch (op) {
341
+ case "-":
342
+ case "−":
343
+ return -value;
344
+ case "+":
345
+ return value;
346
+ case "√":
347
+ return value >= 0 ? Math.sqrt(value) : null;
348
+ case "²":
349
+ return value * value;
350
+ case "³":
351
+ return value * value * value;
352
+ default:
353
+ return null;
354
+ }
355
+ }
356
+
357
+ /** Evaluate a binary operation on two numbers */
358
+ function evaluateBinaryOp(op: string, left: number, right: number): number | null {
359
+ switch (normalizeOperator(op)) {
360
+ case "+":
361
+ return left + right;
362
+ case "-":
363
+ return left - right;
364
+ case "*":
365
+ return left * right;
366
+ case "/":
367
+ return right !== 0 ? left / right : null;
368
+ case "^":
369
+ return left ** right;
370
+ case "%":
371
+ return right !== 0 ? left % right : null;
372
+ default:
373
+ return null;
374
+ }
375
+ }
376
+
377
+ /** Check if two AST nodes are structurally equal */
378
+ function astEqual(a: ASTNode, b: ASTNode): boolean {
379
+ if (a.type !== b.type) return false;
380
+
381
+ switch (a.type) {
382
+ case "number":
383
+ return a.value === (b as NumberNode).value;
384
+ case "variable":
385
+ return a.name === (b as VariableNode).name;
386
+ case "unary":
387
+ return (
388
+ normalizeOperator(a.operator) === normalizeOperator((b as UnaryNode).operator) &&
389
+ astEqual(a.operand, (b as UnaryNode).operand)
390
+ );
391
+ case "binary":
392
+ return (
393
+ normalizeOperator(a.operator) === normalizeOperator((b as BinaryNode).operator) &&
394
+ astEqual(a.left, (b as BinaryNode).left) &&
395
+ astEqual(a.right, (b as BinaryNode).right)
396
+ );
397
+ }
398
+ }
399
+
400
+ // =============================================================================
401
+ // AST FORMATTING
402
+ // =============================================================================
403
+
404
+ /** Options for AST formatting */
405
+ export interface FormatASTOptions {
406
+ /** Use Unicode operators (× instead of *, ÷ instead of /, − instead of -) */
407
+ useUnicode?: boolean;
408
+ /** Add spaces around binary operators */
409
+ spaces?: boolean;
410
+ /** Use minimal parentheses based on precedence */
411
+ minimalParens?: boolean;
412
+ }
413
+
414
+ /** Map ASCII operators to Unicode equivalents for formatting */
415
+ const UNICODE_OPS: Record<string, string> = {
416
+ "*": "×",
417
+ "/": "÷",
418
+ "-": "−",
419
+ };
420
+
421
+ /** Get precedence for an operator string */
422
+ function opPrecedence(op: string): number {
423
+ const normalized = normalizeOperator(op);
424
+ switch (normalized) {
425
+ case "+":
426
+ case "-":
427
+ return 1;
428
+ case "*":
429
+ case "/":
430
+ case "%":
431
+ return 2;
432
+ case "^":
433
+ return 3;
434
+ default:
435
+ return 4; // Unary operators
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Format an AST node back to a human-readable expression string
441
+ *
442
+ * @param node The AST node to format
443
+ * @param options Formatting options
444
+ * @returns Formatted expression string
445
+ *
446
+ * @example
447
+ * const tokens = tokenizeMathExpression("2 * x + 3").tokens;
448
+ * const { ast } = buildAST(tokens);
449
+ * formatAST(ast, { useUnicode: true, spaces: true });
450
+ * // Returns: "2 × x + 3"
451
+ *
452
+ * @example
453
+ * formatAST(ast, { minimalParens: true });
454
+ * // Returns: "2 * x + 3" (no parens needed due to precedence)
455
+ */
456
+ export function formatAST(node: ASTNode, options: FormatASTOptions = {}): string {
457
+ const { useUnicode = false, spaces = true, minimalParens = true } = options;
458
+
459
+ function formatOp(op: string): string {
460
+ if (useUnicode && UNICODE_OPS[op]) {
461
+ return UNICODE_OPS[op] as string;
462
+ }
463
+ return op;
464
+ }
465
+
466
+ function fmt(n: ASTNode, parentPrec: number = 0, isRight: boolean = false): string {
467
+ switch (n.type) {
468
+ case "number":
469
+ return n.value.toString();
470
+ case "variable":
471
+ return n.name;
472
+ case "unary":
473
+ return formatUnaryNode(n, formatOp);
474
+ case "binary":
475
+ return formatBinaryNode(n, parentPrec, isRight, formatOp, spaces, minimalParens);
476
+ }
477
+ }
478
+
479
+ return fmt(node);
480
+ }
481
+
482
+ /** Format a unary AST node */
483
+ function formatUnaryNode(n: UnaryNode, formatOp: (op: string) => string): string {
484
+ const operand = formatASTInternal(n.operand, formatOp, false, false);
485
+ const op = formatOp(n.operator);
486
+
487
+ // Postfix operators (², ³)
488
+ if (n.operator === "²" || n.operator === "³") {
489
+ if (n.operand.type === "binary" || n.operand.type === "unary") {
490
+ return `(${operand})${op}`;
491
+ }
492
+ return `${operand}${op}`;
493
+ }
494
+
495
+ // Square root
496
+ if (n.operator === "√") {
497
+ return `${op}${operand}`;
498
+ }
499
+
500
+ // Unary minus/plus
501
+ if (n.operand.type === "binary") {
502
+ return `${op}(${operand})`;
503
+ }
504
+ return `${op}${operand}`;
505
+ }
506
+
507
+ /** Format a binary AST node */
508
+ function formatBinaryNode(
509
+ n: BinaryNode,
510
+ parentPrec: number,
511
+ isRight: boolean,
512
+ formatOp: (op: string) => string,
513
+ spaces: boolean,
514
+ minimalParens: boolean,
515
+ ): string {
516
+ const prec = opPrecedence(n.operator);
517
+ const op = formatOp(n.operator);
518
+ const sp = spaces ? " " : "";
519
+
520
+ const left = formatASTInternal(n.left, formatOp, spaces, minimalParens);
521
+ const right = formatASTInternal(n.right, formatOp, spaces, minimalParens);
522
+
523
+ const leftStr = wrapLeftChild(n.left, left, prec, minimalParens);
524
+ const rightStr = wrapRightChild(n.right, right, prec, n.operator, minimalParens);
525
+
526
+ const result = `${leftStr}${sp}${op}${sp}${rightStr}`;
527
+
528
+ // Check if THIS node needs parens due to parent context
529
+ if (minimalParens && parentPrec > 0) {
530
+ if (parentPrec > prec) return `(${result})`;
531
+ if (parentPrec === prec && isRight && !isRightAssociative(n.operator)) {
532
+ return `(${result})`;
533
+ }
534
+ }
535
+
536
+ return result;
537
+ }
538
+
539
+ /** Internal formatting helper */
540
+ function formatASTInternal(
541
+ node: ASTNode,
542
+ formatOp: (op: string) => string,
543
+ spaces: boolean,
544
+ minimalParens: boolean,
545
+ ): string {
546
+ switch (node.type) {
547
+ case "number":
548
+ return node.value.toString();
549
+ case "variable":
550
+ return node.name;
551
+ case "unary":
552
+ return formatUnaryNode(node, formatOp);
553
+ case "binary":
554
+ return formatBinaryNode(node, 0, false, formatOp, spaces, minimalParens);
555
+ }
556
+ }
557
+
558
+ /** Wrap left child with parens if needed */
559
+ function wrapLeftChild(
560
+ left: ASTNode,
561
+ leftStr: string,
562
+ parentPrec: number,
563
+ minimalParens: boolean,
564
+ ): string {
565
+ if (minimalParens) {
566
+ const leftPrec = left.type === "binary" ? opPrecedence((left as BinaryNode).operator) : 99;
567
+ if (leftPrec < parentPrec) return `(${leftStr})`;
568
+ return leftStr;
569
+ }
570
+ return left.type === "binary" ? `(${leftStr})` : leftStr;
571
+ }
572
+
573
+ /** Wrap right child with parens if needed */
574
+ function wrapRightChild(
575
+ right: ASTNode,
576
+ rightStr: string,
577
+ parentPrec: number,
578
+ parentOp: string,
579
+ minimalParens: boolean,
580
+ ): string {
581
+ if (minimalParens) {
582
+ const rightPrec = right.type === "binary" ? opPrecedence((right as BinaryNode).operator) : 99;
583
+ if (rightPrec < parentPrec) return `(${rightStr})`;
584
+ if (rightPrec === parentPrec && !isRightAssociative(parentOp) && right.type === "binary") {
585
+ return `(${rightStr})`;
586
+ }
587
+ return rightStr;
588
+ }
589
+ return right.type === "binary" ? `(${rightStr})` : rightStr;
590
+ }
591
+
592
+ // =============================================================================
593
+ // EXPRESSION COMPARISON
594
+ // =============================================================================
595
+
596
+ /**
597
+ * Compare two expressions for algebraic equivalence using random test values
598
+ * Useful for verifying mathematical derivations like "x + x" = "2*x"
599
+ *
600
+ * @param a First expression string
601
+ * @param b Second expression string
602
+ * @param numTests Number of random test points (default: 10)
603
+ * @param tolerance Numeric tolerance for comparison (default: 1e-9)
604
+ * @returns true if expressions are equivalent at all test points
605
+ *
606
+ * @example
607
+ * compareExpressions("x + x", "2 * x"); // true
608
+ * compareExpressions("(a + b)²", "a² + 2*a*b + b²"); // true
609
+ * compareExpressions("x + 1", "x"); // false
610
+ */
611
+ export function compareExpressions(
612
+ a: string,
613
+ b: string,
614
+ numTests: number = 10,
615
+ tolerance: number = 1e-9,
616
+ ): boolean {
617
+ // Parse both expressions
618
+ const tokensA = tokenizeMathExpression(a);
619
+ const tokensB = tokenizeMathExpression(b);
620
+
621
+ if (tokensA.errors.length > 0 || tokensB.errors.length > 0) {
622
+ return false;
623
+ }
624
+
625
+ const astResultA = buildAST(tokensA.tokens);
626
+ const astResultB = buildAST(tokensB.tokens);
627
+
628
+ if (!astResultA.ast || !astResultB.ast) {
629
+ return false;
630
+ }
631
+
632
+ // Collect all variables from both expressions
633
+ const varsA = collectVariables(astResultA.ast);
634
+ const varsB = collectVariables(astResultB.ast);
635
+ const allVars = new Set([...varsA, ...varsB]);
636
+
637
+ // If no variables, just compare values directly
638
+ if (allVars.size === 0) {
639
+ const resultA = evaluateExpression(a);
640
+ const resultB = evaluateExpression(b);
641
+ if (resultA.value === null || resultB.value === null) return false;
642
+ return Math.abs(resultA.value - resultB.value) <= tolerance;
643
+ }
644
+
645
+ // Generate random test points and compare
646
+ // Use a seeded sequence for reproducibility
647
+ const testPoints = generateTestPoints(allVars, numTests);
648
+
649
+ for (const point of testPoints) {
650
+ const resultA = evaluateExpression(a, point);
651
+ const resultB = evaluateExpression(b, point);
652
+
653
+ // Skip invalid points (division by zero, sqrt of negative, etc.)
654
+ if (resultA.value === null || resultB.value === null) {
655
+ continue;
656
+ }
657
+
658
+ // Compare with tolerance
659
+ if (Math.abs(resultA.value - resultB.value) > tolerance) {
660
+ return false;
661
+ }
662
+ }
663
+
664
+ return true;
665
+ }
666
+
667
+ /** Collect all variable names from an AST */
668
+ function collectVariables(node: ASTNode): Set<string> {
669
+ const vars = new Set<string>();
670
+
671
+ function traverse(n: ASTNode): void {
672
+ switch (n.type) {
673
+ case "number":
674
+ break;
675
+ case "variable":
676
+ vars.add(n.name);
677
+ break;
678
+ case "unary":
679
+ traverse(n.operand);
680
+ break;
681
+ case "binary":
682
+ traverse(n.left);
683
+ traverse(n.right);
684
+ break;
685
+ }
686
+ }
687
+
688
+ traverse(node);
689
+ return vars;
690
+ }
691
+
692
+ /** Generate random test points for a set of variables */
693
+ function generateTestPoints(vars: Set<string>, numTests: number): Record<string, number>[] {
694
+ const points: Record<string, number>[] = [];
695
+ const varList = Array.from(vars);
696
+
697
+ // Use a deterministic sequence for reproducibility
698
+ // Mix of positive, negative, fractional values - designed to avoid cancellation
699
+ // Each row represents values for up to 8 variables that won't accidentally cancel
700
+ const testRows = [
701
+ [0.5, 1.3, 2.1, 0.7, 1.9, 3.2, 0.4, 2.7],
702
+ [1.1, -0.6, 2.3, 1.5, -0.8, 1.7, 2.9, -0.3],
703
+ [-0.7, 1.8, -1.2, 2.4, 0.9, -1.6, 3.1, 1.4],
704
+ [2.2, 0.4, -1.5, 0.8, 2.6, -0.9, 1.3, -1.1],
705
+ [0.3, -1.4, 3.0, -0.5, 1.2, 2.5, -1.8, 0.6],
706
+ [-1.3, 2.0, 0.6, -1.7, 0.2, 1.1, -2.1, 3.3],
707
+ [1.7, -0.2, 1.4, 3.5, -1.0, 0.5, 2.3, -0.4],
708
+ [0.9, 1.6, -0.8, 1.1, 2.8, -1.3, 0.7, 2.0],
709
+ [-0.4, 2.7, 1.0, -0.9, 1.5, 3.0, -0.6, 1.8],
710
+ [2.5, -1.1, 0.3, 2.2, -0.7, 1.9, 0.1, -1.5],
711
+ ];
712
+
713
+ for (let i = 0; i < numTests; i++) {
714
+ const point: Record<string, number> = {};
715
+ const row = testRows[i % testRows.length]!;
716
+ for (let j = 0; j < varList.length; j++) {
717
+ const varName = varList[j] as string;
718
+ // Use values from the row, cycling if more variables than row length
719
+ point[varName] = row[j % row.length]!;
720
+ }
721
+ points.push(point);
722
+ }
723
+
724
+ return points;
725
+ }
726
+
727
+ // =============================================================================
728
+ // EXPRESSION EVALUATION
729
+ // =============================================================================
730
+
731
+ /** Result of expression evaluation */
732
+ export interface EvalResult {
733
+ value: number | null;
734
+ error?: string;
735
+ }
736
+
737
+ /**
738
+ * Evaluate a math expression with optional variable bindings
739
+ * Returns null if evaluation fails (e.g., unbound variables, division by zero)
740
+ *
741
+ * @example
742
+ * evaluateExpression("2 + 3 * 4"); // { value: 14 }
743
+ * evaluateExpression("x² + y²", { x: 3, y: 4 }); // { value: 25 }
744
+ * evaluateExpression("10 / 0"); // { value: null, error: "Division by zero" }
745
+ */
746
+ export function evaluateExpression(expr: string, vars: Record<string, number> = {}): EvalResult {
747
+ // Tokenize
748
+ const { tokens, errors } = tokenizeMathExpression(expr);
749
+ if (errors.length > 0) {
750
+ return { value: null, error: errors[0] };
751
+ }
752
+
753
+ // Validate
754
+ const validation = validateExpression(expr);
755
+ if (!validation.valid) {
756
+ return { value: null, error: validation.error };
757
+ }
758
+
759
+ // Build AST
760
+ const { ast, error } = buildAST(tokens);
761
+ if (error || !ast) {
762
+ return { value: null, error: error ?? "Failed to build AST" };
763
+ }
764
+
765
+ // Evaluate
766
+ return evaluateAST(ast, vars);
767
+ }
768
+
769
+ /** Evaluate an AST node */
770
+ function evaluateAST(node: ASTNode, vars: Record<string, number>): EvalResult {
771
+ switch (node.type) {
772
+ case "number":
773
+ return { value: node.value };
774
+
775
+ case "variable": {
776
+ const val = vars[node.name];
777
+ if (val === undefined) {
778
+ return { value: null, error: `Unbound variable: ${node.name}` };
779
+ }
780
+ return { value: val };
781
+ }
782
+
783
+ case "unary": {
784
+ const operand = evaluateAST(node.operand, vars);
785
+ if (operand.error || operand.value === null) return operand;
786
+
787
+ switch (node.operator) {
788
+ case "-":
789
+ case "−":
790
+ return { value: -operand.value };
791
+ case "+":
792
+ return { value: operand.value };
793
+ case "√":
794
+ if (operand.value < 0) {
795
+ return { value: null, error: "Square root of negative number" };
796
+ }
797
+ return { value: Math.sqrt(operand.value) };
798
+ case "²":
799
+ return { value: operand.value * operand.value };
800
+ case "³":
801
+ return { value: operand.value * operand.value * operand.value };
802
+ default:
803
+ return { value: null, error: `Unknown unary operator: ${node.operator}` };
804
+ }
805
+ }
806
+
807
+ case "binary": {
808
+ const left = evaluateAST(node.left, vars);
809
+ if (left.error || left.value === null) return left;
810
+
811
+ const right = evaluateAST(node.right, vars);
812
+ if (right.error || right.value === null) return right;
813
+
814
+ switch (node.operator) {
815
+ case "+":
816
+ return { value: left.value + right.value };
817
+ case "-":
818
+ case "−":
819
+ return { value: left.value - right.value };
820
+ case "*":
821
+ case "×":
822
+ case "·":
823
+ return { value: left.value * right.value };
824
+ case "/":
825
+ case "÷":
826
+ if (right.value === 0) {
827
+ return { value: null, error: "Division by zero" };
828
+ }
829
+ return { value: left.value / right.value };
830
+ case "%":
831
+ if (right.value === 0) {
832
+ return { value: null, error: "Modulo by zero" };
833
+ }
834
+ return { value: left.value % right.value };
835
+ case "^":
836
+ return { value: left.value ** right.value };
837
+ default:
838
+ return { value: null, error: `Unknown binary operator: ${node.operator}` };
839
+ }
840
+ }
841
+ }
842
+ }