Skip to content

Commit 1890ae9

Browse files
committed
doc
1 parent 5a3dd7d commit 1890ae9

7 files changed

Lines changed: 563 additions & 161 deletions

File tree

.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ CLAUDE.md
33

44
doc/
55

6-
TODO.md
7-
REVIEW.md
8-
PLAYGROUND.md
9-
106
build/
117
dist/
128
stage/

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,20 @@
342342
guard against pathological expressions. All recursive calls now track depth
343343
and gracefully return `undefined` if the limit is exceeded.
344344

345+
### Bug Fixes
346+
347+
- **Equation Equivalence in `isEqual()`** (Issue #275): Two equations are now
348+
correctly recognized as equivalent if they have the same solution set:
349+
350+
```javascript
351+
ce.parse('2x+1=0').isEqual(ce.parse('x=-1/2')); // → true
352+
ce.parse('3x+1=0').isEqual(ce.parse('6x+2=0')); // → true
353+
```
354+
355+
The implementation uses a sampling-based approach to check if the ratio of
356+
(LHS₁-RHS₁) to (LHS₂-RHS₂) is a non-zero constant, which indicates equivalent
357+
solution sets.
358+
345359
## 0.33.0 _2026-01-30_
346360

347361
### Bug Fixes

PLAYGROUND.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Playground Test Results
2+
3+
This document summarizes the status of various test snippets from
4+
`test/playground.ts`.
5+
6+
## Summary
7+
8+
| Category | Fixed | Issues Remaining |
9+
| ----------------- | ----- | ---------------- |
10+
| Parsing | 14 | 6 |
11+
| Simplification | 18 | 0 |
12+
| Evaluation | 17 | 1 |
13+
| Solve | 4 | 1 |
14+
| Matrix Operations | 6 | 0 |
15+
| Pattern Matching | 0 | 2 |
16+
| Formatting | 3 | 0 |
17+
| Other | 4 | 1 |
18+
19+
---
20+
21+
## Parsing
22+
23+
### Working Correctly
24+
25+
| Test | Line | Result |
26+
| ----------------------------------------------- | ------- | ------------------------------------------------ |
27+
| `\textcolor{red}{y + 1}` in expression | 6 | Parses with `Annotated` wrapper |
28+
| Sum with multiple bounds `\sum_{n=0,m=4}^{4,8}` | 8 | Correctly parses as multiple `Limits` |
29+
| `f()` and `f(x)` parsing | 99-103 | All variants parse correctly |
30+
| Plus/minus `21\pm1` | 79-80 | Parses as `PlusMinus`, evaluates to `Tuple` |
31+
| Non-canonical parsing `12+(-2)` | 196-197 | Correctly preserves structure |
32+
| `x_\text{a}` subscript | 226 | Parses as `"x_a"` |
33+
| `f'` derivative notation | 613 | Parses as `["Derivative","f"]` |
34+
| `x_{1,2}` subscript with comma | 615-616 | Parses as `Subscript(x, 1 2)` |
35+
| Sum with subscript `\sum_{n,m} k_{n,m}` | 455 | Correctly uses `Subscript`, not `At` |
36+
| Integral LaTeX output | 618-619 | Correct LaTeX: `\int_{0}^{1}\!x^2\, \mathrm{d}x` |
37+
| `\sin(-23\pi/12)` | 499 | Parses and evaluates correctly |
38+
| Error handling for `(`, `(3+x`, `)` | 668-674 | Correct error messages |
39+
| `\operatorname{HorizontalScaling}(3)` | 628-629 | Parses as `["HorizontalScaling", 3]` |
40+
| Knuth's interval `(a..b)` | 731 | Parses as `["Range", "a", "b"]` |
41+
42+
### Issues Remaining
43+
44+
| Test | Line | Expected | Actual | Notes |
45+
| ------------------------------------- | ------- | ---------------------- | ------------------------------------- | ----------------------------------- |
46+
| `\textcolor{red}{=}` | 11-12 | Styled equals | `Error('expected-closing-delimiter')` | Cannot style delimiters |
47+
| Double integral parsing | 27-31 | Nested integrals | Complex nested structure | Parses but N() times out |
48+
| `\mathrm{x+\alpha}` | 188 | Symbol with operators | `"xplusalpha"` (loses plus) | Characters merged |
49+
| `\mathrm{\oplus}` | 189 | Valid symbol | Error: invalid-first-char | Cannot use operators in mathrm |
50+
| `\gamma(2, 1)` | 430 | `Gamma(2, 1)` function | `"EulerGamma" * (2, 1)` | Should be incomplete gamma |
51+
| `\sin\left(x\right.` | 645 | Graceful handling | Multiple errors | Missing matchfix handling |
52+
53+
---
54+
55+
## Simplification
56+
57+
### Working Correctly
58+
59+
| Test | Line | Input | Result |
60+
| ---------------------------- | ------- | ---------------------- | -------------- |
61+
| Arithmetic | 277 | `-1234 - 5678` | `-6912` |
62+
| Sqrt addition | 278 | `2\sqrt{3}+\sqrt{1+2}` | `3sqrt(3)` |
63+
| cos(30°) | 281 | `\cos(30\degree)` | `sqrt(3)/2` |
64+
| Abs of pi | 295-298 | `\|-\pi\|` | `pi` |
65+
| Nested abs | 269 | `\|\|a\| + 3\|` | `\|a\| + 3` |
66+
| Multiply simplify | 517 | `2\times3xxx` | `6x^3` |
67+
| Trig identity | 529 | `1+4\sin(\pi/10)` | `sqrt(5)` |
68+
| Sqrt division | 650-658 | `\sqrt{15}/\sqrt{3}` | `sqrt(5)` |
69+
| `x+x` | 244 | `x+x` | `2x` |
70+
| `e^x e^{-x}` | 301-304 | `e^x e^{-x}` | `1` |
71+
| `\log_4(x^2)` | 307-310 | `\log_4(x^2)` | `2 log_4(x)` |
72+
| `\log_4(x^{7/4})` | 319-322 | `\log_4(x^{7/4})` | `7/4 log_4(x)` |
73+
| `\sin(\infty)` | 313-316 | `\sin(\infty)` | `NaN` |
74+
| `\tanh(\infty)` | 402-404 | `\tanh(\infty)` | `1` |
75+
| `\sqrt{x^2}` with `x > 0` | 257-259 | `x` | `x` |
76+
| `\sqrt[4]{x^4}` with `x > 0` | 257-259 | `x` | `x` |
77+
| `\cos(5\pi+k)` | 588-590 | `-cos(k)` | `-cos(k)` |
78+
| `a \times (c+d)` expand | 561-565 | `ac + ad` | `ac + ad` |
79+
80+
### Notes
81+
82+
- **Distribution/Expansion**: `simplify()` does NOT automatically distribute. Use
83+
`.expand()` to distribute: `a*(c+d)``ac + ad`. This is by design.
84+
- **Assumptions**: Simplifications that depend on assumptions (like `x > 0`) now
85+
work correctly. Use `ce.assume()` before simplifying.
86+
87+
---
88+
89+
## Evaluation
90+
91+
### Working Correctly
92+
93+
| Test | Line | Input | Result |
94+
| ------------------- | ------- | ---------------------------- | --------------------------------- |
95+
| Precision 30 digits | 69-73 | `\pi.N()` | `3.14159265358979323846264338328` |
96+
| Type of fraction | 75 | `3/4` type | `finite_rational` |
97+
| Replace | 58-66 | Note: use `.subs()` instead | Returns `3` (by design) |
98+
| `\sin(\pi^2)` | 181 | Evaluate | `-0.430301217...` |
99+
| Solve `x^2-1=0` | 201-206 | Solutions | `['1', '-1']` |
100+
| Variance functions | 208-210 | Statistics | Correct values |
101+
| Cube root `-1` | 228 | `(-1)^{1/3}` | `-1` |
102+
| Polynomial expand | 431-434 | `(2x^2+3x+1)(2x+1)` | `4x^3 + 8x^2 + 5x + 1` |
103+
| Floating point | 437-438 | `(0+1.1-1.1)(0+1/4-1/4)` | `0` |
104+
| Definite integral | 513 | `\int_0^1 x^2 dx` | `1/3` |
105+
| Bigint sqrt | 505-507 | Large number | `3513640562152.025...` |
106+
| Numerical integral | 44 | `\int_0^1 \sin(x) dx` `.N()` | `0.4598 ± 0.0001` |
107+
| Subscript fn call | 222-223 | `f_\text{a}(5)` with assign | `6` (evaluates correctly) |
108+
| Symbolic factorial | 342 | `(n-1)!` | `(n - 1)!` (stays symbolic) |
109+
110+
### Issues Remaining
111+
112+
| Test | Line | Expected | Actual | Notes |
113+
| -------------------- | ------- | --------------- | ------ | --------------------------------- |
114+
| `.replace()` bug | 61-72 | `2*x + b` | `2` | Single-char symbols auto-wildcard |
115+
116+
**Note:** `.replace({match: 'a', replace: 2})` on `a*x + b` returns `2` instead of `2*x + b`.
117+
The bug is in `parseRulePart` (rules.ts:350) which auto-converts all single-character symbols
118+
to wildcards. So `'a'` becomes `'_a'`, matching ANY expression rather than the literal symbol `a`.
119+
See TODO.md #23.
120+
121+
### Expected Behavior (Not Bugs)
122+
123+
| Test | Line | Behavior | Workaround |
124+
| ------------------------------- | ------- | --------------------------- | -------------------------------- |
125+
| `D(\sin(x), x)` via LaTeX | 52-56 | Parses `D` as user symbol | Use `ce.box(['D', ...])` instead |
126+
| Power `.value` | 229-230 | `undefined` before evaluate | Use `.evaluate().value` instead |
127+
128+
**Notes:**
129+
- In LaTeX, `D` is parsed as a predicate/symbol. For derivatives, use `ce.box(['D', expr, var])`
130+
or LaTeX notation like `\frac{d}{dx}`.
131+
- `.value` returns the numeric value only for already-evaluated expressions. For symbolic
132+
expressions like `Power(2, 3)`, call `.evaluate()` first.
133+
- For simple variable substitution, use `.subs()` not `.replace()`. The `.replace()` method
134+
is for pattern-based rule replacement.
135+
136+
---
137+
138+
## Solve
139+
140+
### Working Correctly
141+
142+
| Test | Line | Equation | Solutions |
143+
| -------------- | ------- | --------- | --------- |
144+
| `2x=\sqrt{5x}` | 156-158 | Quadratic | `5/4, 0` |
145+
| `x^2-1=0` | 201-206 | Quadratic | `1, -1` |
146+
| `5x=0` | 497-500 | Linear | `0` |
147+
| `x=\sqrt{5}` | 502-504 | Identity | `sqrt(5)` |
148+
149+
### Issues Remaining
150+
151+
| Test | Line | Expected | Actual | Notes |
152+
| ------------------------- | ---- | -------- | ------- | ---------------------------------------- |
153+
| `2x+1=0` isEqual `x=-1/2` || `true` | `false` | `isEqual` should recognize equivalent equations |
154+
155+
**Note:** `isEqual` is for mathematical equality (vs `isSame` for structural equality).
156+
For equations, `isEqual` should check if `(LHS1-RHS1)/(LHS2-RHS2)` simplifies to a
157+
non-zero constant, indicating the same solution set. See TODO.md #22.
158+
159+
---
160+
161+
## Matrix Operations
162+
163+
### Working Correctly
164+
165+
| Test | Line | Input | Result |
166+
| ----------------------- | ------- | ---------------------------------- | ------------------- |
167+
| `Shape(A)` | 532-534 | 2x2 numeric matrix | `(2, 2)` |
168+
| `Rank(A)` | 532-534 | 2x2 numeric matrix | `2` |
169+
| `Flatten(A)` | 532-534 | `[[1,2],[3,4]]` | `[1,2,3,4]` |
170+
| `Transpose(A)` | 532-534 | `[[1,2],[3,4]]` | `[[1,3],[2,4]]` |
171+
| `Determinant(A)` | 532-534 | Numeric matrix | `-2` |
172+
| `Determinant(X)` symbolic | 536-549 | `[[a,b],[c,d]]` | `-b*c + a*d` |
173+
174+
### Notes
175+
176+
All matrix operations now work correctly, including symbolic matrices assigned via `ce.assign()`.
177+
178+
---
179+
180+
## Pattern Matching
181+
182+
### Issues Remaining
183+
184+
| Test | Line | Expected | Actual | Notes |
185+
| --------------------- | ------- | ------------------ | ------ | --------------------------- |
186+
| Match with variation | 135-148 | Substitution found | `null` | Match `0` against `_a*x` with `a=0` variation |
187+
| Complex pattern match | 153-165 | Substitution | `null` | Match `2x-√5√x` against complex Add pattern |
188+
189+
**Note:** Pattern matching with `useVariations: true` has known limitations. The system
190+
doesn't fully handle all algebraic variations (like matching `0` as `0*x`).
191+
192+
---
193+
194+
## Formatting / Serialization
195+
196+
### Working Correctly
197+
198+
| Test | Line | Input | Result |
199+
| ------------------------- | ------- | -------------------------------- | ------------------------------ |
200+
| Scientific notation | 40-44 | `1000` | `1\cdot10^{3}` |
201+
| Fraction canonical | 298 | `\frac{2}{-3222233}+\frac{1}{3}` | Uses `Rational` not `Subtract` |
202+
| `1/(2\sqrt{3})` canonical | 617 | Rationalized | `\frac{\sqrt{3}}{6}` |
203+
204+
### Expected Behavior
205+
206+
| Test | Line | Behavior | Notes |
207+
| -------------------- | ---- | --------------- | --------------------------------------- |
208+
| `3\times3` canonical | 59 | Returns `3 * 3` | `.simplify()` returns `9`. Converting to `3^2` would be a separate optimization. |
209+
210+
---
211+
212+
## Other Features
213+
214+
### Working Correctly
215+
216+
| Test | Line | Feature | Result |
217+
| ------------- | ------- | ---------------------------- | --------------------------------- |
218+
| Filter | 573-577 | `Filter([1,2,3,4,5], IsOdd)` | `[1,3,5]` |
219+
| Expand | 467-475 | `4x(3x+2)-5(5x-4)` | `12x^2 - 17x + 20` |
220+
| Negate i | 405-406 | `-i` | `-i` |
221+
| Hold | 232-234 | `Add(1, Hold(2))` | `1 + Hold(2)` |
222+
223+
### Issues Remaining
224+
225+
| Test | Line | Expected | Actual | Notes |
226+
| ------------------------- | ------- | ---------------- | --------------------------------- | ----------------------------- |
227+
| `List(Filter).evaluate()` | 581-583 | Evaluated filter | Filter not evaluated inside List | Nested evaluation issue |
228+
229+
### Expected Behavior
230+
231+
| Test | Line | Behavior | Notes |
232+
| ----------------- | ------- | ------------------ | -------------------------------------------------- |
233+
| Sum with data | 175-186 | `[50, 130]` | Element-wise multiplication is correct; use explicit indexing for dot product |
234+
| `Floor(Cos(n))` | 271-273 | `floor(cos(n))` | `n` is unknown, so expression stays symbolic |
235+
236+
---
237+
238+
## Parsing Edge Cases (Error Handling)
239+
240+
These tests verify error handling for malformed input:
241+
242+
| Input | Line | Status |
243+
| ------------------------- | ---- | ------------------------------------ |
244+
| `x__+1` | 510 | Returns string `"x__"` + 1 (not At) |
245+
| `\operatorname{a?#_!}` | 160 | Error: invalid-symbol |
246+
| `(a, b; c, d, ;; n ,, m)` | 677 | Parses as nested Tuples with Nothing |
247+
| `\operatorname{$invalid}` | 680 | Error for invalid identifier |
248+
249+
---
250+
251+
## Notes
252+
253+
1. **Double/complex integrals**: The playground includes double integrals and
254+
complex integration tests that timeout during numerical evaluation.
255+
256+
2. **Assumptions**: Simplifications that depend on assumptions (like `x > 0`)
257+
now work correctly. Use `ce.assume(ce.parse('x > 0'))` before simplifying.
258+
259+
3. **Matrix operations**: Matrix-related functions (Shape, Rank, Flatten,
260+
Transpose, Determinant, Inverse, Trace) now work correctly for both numeric
261+
and symbolic matrices.
262+
263+
4. **Pattern matching**: The pattern matching system has issues with variations
264+
and complex patterns.
265+
266+
5. **D operator syntax**: The `D` operator must be used via `ce.box(['D', ...])`,
267+
not LaTeX parsing. In LaTeX, `D` is parsed as a user symbol.
268+
269+
6. **Trig periodicity**: Trigonometric functions now reduce arguments by their
270+
period (e.g., `cos(5π + k)` simplifies to `-cos(k)`).

requirements/TODO.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,46 @@ ce.parse('2x').isEqual(ce.parse('x+x')) // Returns: true (correct)
541541

542542
---
543543

544+
### 23. Replace Method Auto-Wildcards Single-Char Symbols
545+
546+
**Problem:** `.replace({match: 'a', replace: 2})` unexpectedly converts `'a'` to a wildcard
547+
`'_a'`, causing it to match ANY expression instead of just the literal symbol `a`.
548+
549+
**Current behavior:**
550+
```typescript
551+
const expr = ce.box(['Add', ['Multiply', 'a', 'x'], 'b']);
552+
expr.replace({match: 'a', replace: 2}, {recursive: true})
553+
// Returns: 2 (wrong!)
554+
// Expected: 2*x + b
555+
```
556+
557+
**Root cause:** In `parseRulePart` (rules.ts:350), all single-character symbols are
558+
auto-converted to wildcards:
559+
```typescript
560+
if (x.symbol && x.symbol.length === 1) return ce.symbol('_' + x.symbol);
561+
```
562+
563+
This makes sense when parsing rule strings like `"a*x -> 2*x"` where `a`, `x` should be
564+
wildcards, but NOT when the user explicitly provides `{match: 'a', replace: 2}` where
565+
they likely want literal matching.
566+
567+
**Solution options:**
568+
1. Only auto-wildcard when parsing a rule string, not when rule is provided as an object
569+
2. Add an option like `{literal: true}` to disable auto-wildcarding
570+
3. Require explicit wildcard syntax `'_a'` and never auto-wildcard
571+
572+
**Workaround:** Use `.subs()` for simple variable substitution:
573+
```typescript
574+
expr.subs({a: 2}) // Returns: 2*x + b (correct)
575+
```
576+
577+
**Files to modify:**
578+
- `src/compute-engine/boxed-expression/rules.ts` - `parseRulePart` function
579+
580+
**Tests:** See `test/playground.ts` lines 61-72 and PLAYGROUND.md Evaluation section
581+
582+
---
583+
544584
## Subscript and Superscript Enhancements
545585

546586
### 16. Pre-Subscript and Pre-Superscript Parsing

0 commit comments

Comments
 (0)