Skip to content

Commit 0814581

Browse files
committed
fix: fixed #258
1 parent e0daafd commit 0814581

3 files changed

Lines changed: 133 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
### Bug Fixes
44

5+
- **([#258](https://github.com/cortex-js/compute-engine/issues/258))
6+
Pattern Matching**: Fixed `BoxedExpression.match()` returning `null` when
7+
matching a `Rational` pattern against expressions like `['Rational', 'x', 2]`.
8+
The issue occurred because symbolic rationals with numeric denominators are
9+
canonicalized to `Multiply` expressions (e.g., `x * 1/2`), which didn't match
10+
`Divide` or `Rational` patterns. The pattern matching now correctly recognizes
11+
these forms.
12+
513
- **([#264](https://github.com/cortex-js/compute-engine/issues/264))
614
Serialization**: Fixed LaTeX serialization of quantified expressions
715
(`ForAll`, `Exists`, `ExistsUnique`, `NotForAll`, `NotExists`). Previously,

src/compute-engine/boxed-expression/match.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,59 @@ function matchOnce(
131131

132132
const operator = pattern.operator;
133133

134+
// Special case: Match a BoxedNumber rational against a Divide pattern
135+
// This allows patterns like ['Divide', '_num', '_den'] to match rationals like 3/2
136+
if (
137+
operator === 'Divide' &&
138+
expr.numericValue !== null &&
139+
!expr.denominator.is(1)
140+
) {
141+
// Create a synthetic Divide expression to match against
142+
const divideExpr = ce.function(
143+
'Divide',
144+
[expr.numerator, expr.denominator],
145+
{ canonical: false, structural: true }
146+
);
147+
return matchArguments(divideExpr, pattern.ops, substitution, options);
148+
}
149+
150+
// Special case: Match Multiply(Rational(1, n), x) against a Divide pattern
151+
// This handles cases like x/2 which is canonicalized as x * (1/2)
152+
if (operator === 'Divide' && expr.operator === 'Multiply') {
153+
const ops = expr.ops!;
154+
for (let i = 0; i < ops.length; i++) {
155+
const op = ops[i];
156+
157+
// Check if op is a rational number with numerator 1 (i.e., 1/n form)
158+
if (
159+
op.numericValue !== null &&
160+
op.numerator.is(1) &&
161+
!op.denominator.is(1)
162+
) {
163+
// Collect all other operands
164+
const others = ops.filter((_, j) => j !== i);
165+
const numerator =
166+
others.length === 1
167+
? others[0]
168+
: ce.function('Multiply', others, { canonical: false });
169+
170+
// Create a synthetic Divide expression to match against
171+
const divideExpr = ce.function(
172+
'Divide',
173+
[numerator, op.denominator],
174+
{ canonical: false, structural: true }
175+
);
176+
const result = matchArguments(
177+
divideExpr,
178+
pattern.ops,
179+
substitution,
180+
options
181+
);
182+
if (result !== null) return result;
183+
}
184+
}
185+
}
186+
134187
if (operator.startsWith('_')) {
135188
//
136189
// 1. The pattern operator is a wildcard

test/compute-engine/patterns.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,78 @@ describe('NON EXACT WILDCARDS', () => {
995995
});
996996
});
997997

998+
// GitHub Issue #258: BoxedExpression.match() with a Rational pattern
999+
describe('RATIONAL PATTERN MATCHING', () => {
1000+
it('should match Rational(3, 2) with Rational pattern', () => {
1001+
// Rational with two integers becomes a BoxedNumber
1002+
const result = match(['Rational', '_num', '_den'], ['Rational', 3, 2]);
1003+
expect(result).toMatchInlineSnapshot(`
1004+
{
1005+
_den: 2,
1006+
_num: 3,
1007+
}
1008+
`);
1009+
});
1010+
1011+
it('should match Rational(x, 2) with Rational pattern', () => {
1012+
// Rational with symbolic numerator is canonicalized as Multiply(x, Rational(1, 2))
1013+
const result = match(['Rational', '_num', '_den'], ['Rational', 'x', 2]);
1014+
expect(result).toMatchInlineSnapshot(`
1015+
{
1016+
_den: 2,
1017+
_num: x,
1018+
}
1019+
`);
1020+
});
1021+
1022+
it('should match Rational(x, 9) with Rational pattern', () => {
1023+
// Rational(x, Power(3, 2)) is canonicalized as Multiply(x, Rational(1, 9))
1024+
const result = match(
1025+
['Rational', '_num', '_den'],
1026+
['Rational', 'x', ['Power', 3, 2]]
1027+
);
1028+
expect(result).toMatchInlineSnapshot(`
1029+
{
1030+
_den: 9,
1031+
_num: x,
1032+
}
1033+
`);
1034+
});
1035+
1036+
it('should match Rational(3, y) with Rational pattern', () => {
1037+
// Rational with symbolic denominator becomes Divide(3, y)
1038+
const result = match(['Rational', '_num', '_den'], ['Rational', 3, 'y']);
1039+
expect(result).toMatchInlineSnapshot(`
1040+
{
1041+
_den: y,
1042+
_num: 3,
1043+
}
1044+
`);
1045+
});
1046+
1047+
it('should match Rational(x, y) with Rational pattern', () => {
1048+
// Rational with two symbols becomes Divide(x, y)
1049+
const result = match(['Rational', '_num', '_den'], ['Rational', 'x', 'y']);
1050+
expect(result).toMatchInlineSnapshot(`
1051+
{
1052+
_den: y,
1053+
_num: x,
1054+
}
1055+
`);
1056+
});
1057+
1058+
it('should match Divide pattern against Multiply with reciprocal', () => {
1059+
// x/2 is canonicalized as Multiply(Rational(1, 2), x)
1060+
const result = match(['Divide', '_num', '_den'], ['Rational', 'x', 2]);
1061+
expect(result).toMatchInlineSnapshot(`
1062+
{
1063+
_den: 2,
1064+
_num: x,
1065+
}
1066+
`);
1067+
});
1068+
});
1069+
9981070
describe('PATTERN VALIDATION', () => {
9991071
// Test validatePattern directly with non-canonical patterns
10001072
// (canonical forms may reorder operands for commutative operators)

0 commit comments

Comments
 (0)