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.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/package.json +75 -0
- package/src/index.ts +38 -0
- package/src/lib/cache.ts +246 -0
- package/src/lib/compression.ts +804 -0
- package/src/lib/compute/cache.ts +86 -0
- package/src/lib/compute/classifier.ts +555 -0
- package/src/lib/compute/confidence.ts +79 -0
- package/src/lib/compute/context.ts +154 -0
- package/src/lib/compute/extract.ts +200 -0
- package/src/lib/compute/filter.ts +224 -0
- package/src/lib/compute/index.ts +171 -0
- package/src/lib/compute/math.ts +247 -0
- package/src/lib/compute/patterns.ts +564 -0
- package/src/lib/compute/registry.ts +145 -0
- package/src/lib/compute/solvers/arithmetic.ts +65 -0
- package/src/lib/compute/solvers/calculus.ts +249 -0
- package/src/lib/compute/solvers/derivation-core.ts +371 -0
- package/src/lib/compute/solvers/derivation-latex.ts +160 -0
- package/src/lib/compute/solvers/derivation-mistakes.ts +1046 -0
- package/src/lib/compute/solvers/derivation-simplify.ts +451 -0
- package/src/lib/compute/solvers/derivation-transform.ts +620 -0
- package/src/lib/compute/solvers/derivation.ts +67 -0
- package/src/lib/compute/solvers/facts.ts +120 -0
- package/src/lib/compute/solvers/formula.ts +728 -0
- package/src/lib/compute/solvers/index.ts +36 -0
- package/src/lib/compute/solvers/logic.ts +422 -0
- package/src/lib/compute/solvers/probability.ts +307 -0
- package/src/lib/compute/solvers/statistics.ts +262 -0
- package/src/lib/compute/solvers/word-problems.ts +408 -0
- package/src/lib/compute/types.ts +107 -0
- package/src/lib/concepts.ts +111 -0
- package/src/lib/domain.ts +731 -0
- package/src/lib/extraction.ts +912 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/judge.ts +260 -0
- package/src/lib/math/ast.ts +842 -0
- package/src/lib/math/index.ts +8 -0
- package/src/lib/math/operators.ts +171 -0
- package/src/lib/math/tokenizer.ts +477 -0
- package/src/lib/patterns.ts +200 -0
- package/src/lib/session.ts +825 -0
- package/src/lib/think/challenge.ts +323 -0
- package/src/lib/think/complexity.ts +504 -0
- package/src/lib/think/confidence-drift.ts +507 -0
- package/src/lib/think/consistency.ts +347 -0
- package/src/lib/think/guidance.ts +188 -0
- package/src/lib/think/helpers.ts +568 -0
- package/src/lib/think/hypothesis.ts +216 -0
- package/src/lib/think/index.ts +127 -0
- package/src/lib/think/prompts.ts +262 -0
- package/src/lib/think/route.ts +358 -0
- package/src/lib/think/schema.ts +98 -0
- package/src/lib/think/scratchpad-schema.ts +662 -0
- package/src/lib/think/spot-check.ts +961 -0
- package/src/lib/think/types.ts +93 -0
- package/src/lib/think/verification.ts +260 -0
- package/src/lib/tokens.ts +177 -0
- package/src/lib/verification.ts +620 -0
- package/src/prompts/index.ts +10 -0
- package/src/prompts/templates.ts +336 -0
- package/src/resources/index.ts +8 -0
- package/src/resources/sessions.ts +196 -0
- package/src/tools/compress.ts +138 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/scratchpad.ts +2659 -0
- 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
|
+
}
|