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,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
|
+
}
|