Skip to content

Commit 55418ae

Browse files
arnogclaude
andauthored
Verify pattern matching with repeated wildcards works correctly (#278)
- Verified that the pattern matching system in match.ts correctly handles wildcards appearing multiple times in a pattern - The captureWildcard() function properly ensures all occurrences of a named wildcard (like _x) match the same expression - Added 15 comprehensive tests in patterns.test.ts covering: - Simple repeated wildcards in flat structures - Repeated wildcards in nested function arguments - The specific 1/(x*ln(x)) pattern from TODO #3 - Complex expression matching - Commutative reordering with repeated wildcards - Canonical expression matching - Updated TODO.md to mark task #3 as completed - Updated DONE.md with implementation details - Updated CHANGELOG.md with testing section https://claude.ai/code/session_01VqUF7VSwkY7RP1cA18xjv9 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4188fc6 commit 55418ae

4 files changed

Lines changed: 258 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
- `x - 2√x - 3 = 0` → returns `[9]` (filters out x=1)
1919
- `2x + 3√x - 2 = 0` → returns `[1/4]` (filters out x=4)
2020

21+
### Testing
22+
23+
- **Pattern Matching with Repeated Wildcards**: Added comprehensive tests
24+
verifying that the pattern matching system correctly handles wildcards that
25+
appear multiple times in a pattern. When a named wildcard like `_x` appears
26+
in multiple positions, the matcher correctly ensures all occurrences match
27+
the same expression. This works with:
28+
- Nested function arguments (e.g., `['Multiply', '_x', ['Ln', '_x']]`)
29+
- Multiple nesting levels (3+ levels deep)
30+
- Commutative operators (handles reordering)
31+
- Canonical expressions (from parsed LaTeX)
32+
- Complex sub-expressions (matching entire sub-trees)
33+
2134
### New Features
2235

2336
#### Subscripts and Indexing

requirements/DONE.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,3 +934,76 @@ ce.parse('2x + 3\\sqrt{x} - 2 = 0').solve('x') // → [1/4]
934934
validate against it
935935
- `test/compute-engine/solve.test.ts` - Added 6 new tests in "EXTRANEOUS ROOT
936936
FILTERING FOR SQRT EQUATIONS" describe block
937+
938+
---
939+
940+
### 3. Pattern Matching with Repeated Wildcards ✅
941+
942+
**Status:** Verified working correctly. The pattern matching system properly handles
943+
wildcards that appear multiple times in a pattern.
944+
945+
**Investigated behavior:**
946+
947+
The `captureWildcard()` function in `match.ts` (lines 37-58) correctly handles
948+
repeated wildcards:
949+
950+
1. When a named wildcard (like `_x`) is first encountered, it's captured in the
951+
substitution dictionary
952+
2. When the same wildcard is encountered again, it checks if the new expression
953+
is the same as the previously captured one using `isSame()`
954+
3. If they match, the substitution is preserved; if not, the match fails
955+
956+
**Examples that work correctly:**
957+
958+
```typescript
959+
// Pattern with repeated wildcard
960+
pattern = ['Divide', 1, ['Multiply', '_x', ['Ln', '_x']]]
961+
expr = ['Divide', 1, ['Multiply', 'x', ['Ln', 'x']]]
962+
// Result: { _x: x } ✓
963+
964+
// Pattern with 3 levels of nesting
965+
pattern = ['Add', '_x', ['Multiply', '_x', ['Power', '_x', 2]]]
966+
expr = ['Add', 'x', ['Multiply', 'x', ['Power', 'x', 2]]]
967+
// Result: { _x: x } ✓
968+
969+
// Repeated wildcard with commutative operators
970+
pattern = ['Add', '_x', ['Ln', '_x']]
971+
expr = ['Add', ['Ln', 'x'], 'x'] // order swapped
972+
// Result: { _x: x } ✓ (handles commutative reordering)
973+
974+
// Mismatch correctly detected
975+
pattern = ['Multiply', '_x', ['Ln', '_x']]
976+
expr = ['Multiply', 'x', ['Ln', 'y']] // different variables
977+
// Result: null ✓ (correctly rejects)
978+
979+
// Complex expression matching
980+
pattern = ['Add', '_x', ['Power', '_x', 2]]
981+
expr = ['Add', ['Add', 'a', 1], ['Power', ['Add', 'a', 1], 2]]
982+
// Result: { _x: ['Add', 'a', 1] } ✓ (matches complex sub-expression)
983+
```
984+
985+
**Integration with antiderivative.ts:**
986+
987+
The specific integral `∫ 1/(x·ln(x)) dx = ln|ln(x)|` is handled procedurally in
988+
`antiderivative.ts` (lines 1772-1800) using Case D2, which:
989+
990+
1. Recognizes patterns like `1/(g(x)·h(x))` where `g(x) = d/dx(h(x))`
991+
2. For `1/(x·ln(x))`, identifies that `1/x = d/dx(ln(x))`
992+
3. Returns `ln|h(x)|` = `ln|ln(x)|`
993+
994+
This procedural approach is appropriate because it requires computing derivatives
995+
dynamically, which cannot be expressed as a static pattern matching rule.
996+
997+
**Files verified:**
998+
- `src/compute-engine/boxed-expression/match.ts` - Pattern matching logic
999+
- `src/compute-engine/symbolic/antiderivative.ts` - Integration patterns
1000+
1001+
**Tests added:**
1002+
- `test/compute-engine/patterns.test.ts` - Added 15 new tests in "Repeated
1003+
Wildcards in Nested Contexts" describe block covering:
1004+
- Simple repeated wildcards in flat structures
1005+
- Repeated wildcards in nested function arguments
1006+
- Repeated wildcards with Divide patterns
1007+
- Complex expression matching
1008+
- Commutative reordering with repeated wildcards
1009+
- Canonical expression matching

requirements/TODO.md

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,35 +63,9 @@ output.
6363

6464
---
6565

66-
### 3. Pattern Matching Improvements
66+
### ~~3. Pattern Matching Improvements~~ ✅ COMPLETED
6767

68-
**Problem:** Some integration patterns don't match because the pattern matching
69-
system has limitations with:
70-
71-
- Same wildcard appearing multiple times (should match same expression)
72-
- Matching against canonicalized expressions that have been reordered
73-
- Complex nested patterns
74-
75-
**Example that doesn't work:**
76-
77-
```typescript
78-
// Trying to match 1/(x*ln(x)) where x appears twice
79-
{
80-
match: ['Divide', 1, ['Multiply', '_x', ['Ln', '_x']]],
81-
replace: ['Ln', ['Ln', '_x']],
82-
}
83-
```
84-
85-
**Investigation needed:**
86-
87-
- Review `src/compute-engine/boxed-expression/match.ts`
88-
- Understand how wildcard binding works
89-
- Consider adding "same as" constraints: `_x@1` and `_x@1` must match same expr
90-
91-
**Files:**
92-
93-
- `src/compute-engine/boxed-expression/match.ts`
94-
- `src/compute-engine/symbolic/antiderivative.ts`
68+
See `requirements/DONE.md` for implementation details.
9569

9670
---
9771

test/compute-engine/patterns.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,3 +1481,173 @@ describe('Permutation Matching Optimization', () => {
14811481
});
14821482
});
14831483
});
1484+
1485+
// ============================================================================
1486+
// REPEATED WILDCARDS IN NESTED CONTEXTS (TODO #3)
1487+
// ============================================================================
1488+
1489+
describe('Repeated Wildcards in Nested Contexts', () => {
1490+
// Non-canonical matching helper
1491+
const matchNonCanonical = (pattern, expr) => {
1492+
const boxedPattern = ce.box(pattern, { canonical: false });
1493+
const boxedExpr = ce.box(expr, { canonical: false });
1494+
return boxedExpr.match(boxedPattern);
1495+
};
1496+
1497+
describe('Simple repeated wildcards (flat structure)', () => {
1498+
test('_a appears twice - same value should match', () => {
1499+
const result = matchNonCanonical(
1500+
['Add', '_a', '_a'],
1501+
['Add', 'x', 'x']
1502+
);
1503+
expect(result).not.toBeNull();
1504+
expect(result?._a?.toString()).toBe('x');
1505+
});
1506+
1507+
test('_a appears twice - different values should not match', () => {
1508+
const result = matchNonCanonical(
1509+
['Add', '_a', '_a'],
1510+
['Add', 'x', 'y']
1511+
);
1512+
expect(result).toBeNull();
1513+
});
1514+
});
1515+
1516+
describe('Repeated wildcards in nested function arguments', () => {
1517+
test('_x in direct arg and inside Ln - same value', () => {
1518+
const result = matchNonCanonical(
1519+
['Multiply', '_x', ['Ln', '_x']],
1520+
['Multiply', 'x', ['Ln', 'x']]
1521+
);
1522+
expect(result).not.toBeNull();
1523+
expect(result?._x?.toString()).toBe('x');
1524+
});
1525+
1526+
test('_x in direct arg and inside Ln - different values should not match', () => {
1527+
const result = matchNonCanonical(
1528+
['Multiply', '_x', ['Ln', '_x']],
1529+
['Multiply', 'x', ['Ln', 'y']]
1530+
);
1531+
expect(result).toBeNull();
1532+
});
1533+
1534+
test('_x appears at 3 different nesting levels', () => {
1535+
const result = matchNonCanonical(
1536+
['Add', '_x', ['Multiply', '_x', ['Power', '_x', 2]]],
1537+
['Add', 'x', ['Multiply', 'x', ['Power', 'x', 2]]]
1538+
);
1539+
expect(result).not.toBeNull();
1540+
expect(result?._x?.toString()).toBe('x');
1541+
});
1542+
1543+
test('exp(x)*sin(x) with repeated _x', () => {
1544+
const result = matchNonCanonical(
1545+
['Multiply', ['Exp', '_x'], ['Sin', '_x']],
1546+
['Multiply', ['Exp', 'x'], ['Sin', 'x']]
1547+
);
1548+
expect(result).not.toBeNull();
1549+
expect(result?._x?.toString()).toBe('x');
1550+
});
1551+
});
1552+
1553+
describe('Repeated wildcards with Divide patterns', () => {
1554+
test('1/(x*ln(x)) pattern - the TODO #3 example', () => {
1555+
const result = matchNonCanonical(
1556+
['Divide', 1, ['Multiply', '_x', ['Ln', '_x']]],
1557+
['Divide', 1, ['Multiply', 'x', ['Ln', 'x']]]
1558+
);
1559+
expect(result).not.toBeNull();
1560+
expect(result?._x?.toString()).toBe('x');
1561+
});
1562+
1563+
test('1/(x*ln(x)) with different variables should not match', () => {
1564+
const result = matchNonCanonical(
1565+
['Divide', 1, ['Multiply', '_x', ['Ln', '_x']]],
1566+
['Divide', 1, ['Multiply', 'x', ['Ln', 'y']]]
1567+
);
1568+
expect(result).toBeNull();
1569+
});
1570+
});
1571+
1572+
describe('Repeated wildcards matching complex expressions', () => {
1573+
test('_x should match the same complex expression everywhere', () => {
1574+
const result = matchNonCanonical(
1575+
['Add', '_x', ['Power', '_x', 2]],
1576+
['Add', ['Add', 'a', 1], ['Power', ['Add', 'a', 1], 2]]
1577+
);
1578+
expect(result).not.toBeNull();
1579+
// _x should match ['Add', 'a', 1]
1580+
});
1581+
1582+
test('_x matching complex expr - mismatch should fail', () => {
1583+
const result = matchNonCanonical(
1584+
['Add', '_x', ['Power', '_x', 2]],
1585+
['Add', ['Add', 'a', 1], ['Power', ['Add', 'a', 2], 2]]
1586+
);
1587+
expect(result).toBeNull();
1588+
});
1589+
});
1590+
1591+
describe('Repeated wildcards with commutative reordering', () => {
1592+
test('_x + ln(_x) with Add being commutative', () => {
1593+
const result = matchNonCanonical(
1594+
['Add', '_x', ['Ln', '_x']],
1595+
['Add', ['Ln', 'x'], 'x']
1596+
);
1597+
// Add is commutative, so this should match with _x = x
1598+
expect(result).not.toBeNull();
1599+
expect(result?._x?.toString()).toBe('x');
1600+
});
1601+
1602+
test('_x * sin(_x) with Multiply being commutative', () => {
1603+
const result = matchNonCanonical(
1604+
['Multiply', '_x', ['Sin', '_x']],
1605+
['Multiply', ['Sin', 'x'], 'x']
1606+
);
1607+
expect(result).not.toBeNull();
1608+
expect(result?._x?.toString()).toBe('x');
1609+
});
1610+
});
1611+
1612+
describe('Repeated wildcards with CANONICAL expressions', () => {
1613+
test('1/(x*ln(x)) with canonical expression', () => {
1614+
// Pattern is non-canonical (structural)
1615+
const pattern = ce.box(['Divide', 1, ['Multiply', '_x', ['Ln', '_x']]], {
1616+
canonical: false,
1617+
});
1618+
// Expression is canonical (what you get from parsing)
1619+
const expr = ce.parse('\\frac{1}{x \\ln x}');
1620+
1621+
const result = expr.match(pattern);
1622+
expect(result).not.toBeNull();
1623+
expect(result?._x?.toString()).toBe('x');
1624+
});
1625+
1626+
test('exp(x)*sin(x) canonical expression', () => {
1627+
// e^x becomes Power(ExponentialE, x), which doesn't match Exp(_x)
1628+
// So this pattern needs to account for the canonical form
1629+
const pattern = ce.box(
1630+
['Multiply', ['Power', 'ExponentialE', '_x'], ['Sin', '_x']],
1631+
{ canonical: false }
1632+
);
1633+
const expr = ce.parse('e^x \\sin x');
1634+
1635+
const result = expr.match(pattern);
1636+
expect(result).not.toBeNull();
1637+
expect(result?._x?.toString()).toBe('x');
1638+
});
1639+
1640+
test('Repeated wildcard fails when canonical order differs', () => {
1641+
// Multiply may reorder operands, so [x, ln(x)] might become [ln(x), x]
1642+
// The pattern matching should still work due to commutativity handling
1643+
const pattern = ce.box(['Multiply', '_x', ['Ln', '_x']], {
1644+
canonical: false,
1645+
});
1646+
const expr = ce.parse('x \\ln x');
1647+
1648+
const result = expr.match(pattern);
1649+
expect(result).not.toBeNull();
1650+
expect(result?._x?.toString()).toBe('x');
1651+
});
1652+
});
1653+
});

0 commit comments

Comments
 (0)