Skip to content

Commit 8de6c60

Browse files
committed
fixed #287
1 parent 20f42dd commit 8de6c60

6 files changed

Lines changed: 94 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
## [Unreleased]
2+
3+
### Numerics
4+
5+
- **\[#287\](https://github.com/cortex-js/compute-engine/issues/287) Improved
6+
precision for large integer products**: Multiplications and additions of large
7+
integers that would previously lose precision (exceeding
8+
`Number.MAX_SAFE_INTEGER`) are now automatically promoted to `BigInt` to
9+
maintain exact results.
10+
11+
### Evaluation
12+
13+
- **Fixed scope leak**: Ensured that evaluation contexts are correctly popped
14+
even when an error or timeout occurs during function evaluation.
15+
- **Improved numerical evaluation performance**: `Sum`, `Product`, and `Divide`
16+
now correctly propagate the `numericApproximation` option to their arguments,
17+
significantly speeding up large numerical calculations by avoiding expensive
18+
exact rational arithmetic.
19+
120
## 0.50.1 _2026-02-11_
221

322
### Compilation

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,12 +1139,16 @@ export class BoxedFunction
11391139
//
11401140
// 6/ Call the `evaluate` handler
11411141
//
1142-
const evalResult = def.evaluate?.(tail, {
1143-
numericApproximation,
1144-
engine: this.engine,
1145-
materialization: materialization,
1146-
});
1147-
if (isScoped) this.engine._popEvalContext();
1142+
let evalResult: Expression | undefined;
1143+
try {
1144+
evalResult = def.evaluate?.(tail, {
1145+
numericApproximation,
1146+
engine: this.engine,
1147+
materialization: materialization,
1148+
});
1149+
} finally {
1150+
if (isScoped) this.engine._popEvalContext();
1151+
}
11481152

11491153
// Fallback to a symbolic result if we could not evaluate
11501154
return evalResult ?? this.engine.function(this._operator, tail);

src/compute-engine/library/arithmetic.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,11 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
323323

324324
return result;
325325
},
326-
evaluate: ([num, den]) => num.div(den),
326+
evaluate: ([num, den], { numericApproximation }) => {
327+
const res = num.div(den);
328+
if (numericApproximation && res.operator !== 'Divide') return res.N();
329+
return res;
330+
},
327331
},
328332

329333
Exp: {
@@ -1787,11 +1791,13 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
17871791

17881792
evaluate: (ops, options) => {
17891793
const ce = options.engine;
1794+
const numericApproximation = options.numericApproximation;
17901795
const result = run(
17911796
reduceBigOp(
17921797
ops[0],
17931798
ops.slice(1),
1794-
(acc: Expression, x) => acc.mul(x.evaluate(options)),
1799+
(acc: Expression, x) =>
1800+
acc.mul(x.evaluate({ numericApproximation })),
17951801
ce.One
17961802
),
17971803
ce._timeRemaining
@@ -1801,16 +1807,18 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
18011807
return undefined; // Return undefined to keep expression symbolic
18021808
}
18031809
// Evaluate the accumulated result to combine numeric factors
1804-
return result?.evaluate() ?? ce.NaN;
1810+
return result?.evaluate({ numericApproximation }) ?? ce.NaN;
18051811
},
18061812

18071813
evaluateAsync: async (ops, options) => {
18081814
const ce = options.engine;
1815+
const numericApproximation = options.numericApproximation;
18091816
const result = await runAsync(
18101817
reduceBigOp(
18111818
ops[0],
18121819
ops.slice(1),
1813-
(acc: Expression, x) => acc.mul(x.evaluate(options)),
1820+
(acc: Expression, x) =>
1821+
acc.mul(x.evaluate({ numericApproximation })),
18141822
ce.One
18151823
),
18161824
ce._timeRemaining,
@@ -1820,7 +1828,7 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
18201828
if (result === NON_ENUMERABLE_DOMAIN) {
18211829
return undefined; // Return undefined to keep expression symbolic
18221830
}
1823-
return result?.evaluate() ?? ce.NaN;
1831+
return result?.evaluate({ numericApproximation }) ?? ce.NaN;
18241832
},
18251833
},
18261834

@@ -1837,12 +1845,13 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
18371845
canonical: ([body, ...bounds], { scope }) =>
18381846
canonicalBigop('Sum', body, bounds, scope),
18391847

1840-
evaluate: ([body, ...indexes], { engine }) => {
1848+
evaluate: ([body, ...indexes], { engine, numericApproximation }) => {
18411849
const result = run(
18421850
reduceBigOp(
18431851
body,
18441852
indexes,
1845-
(acc: Expression, x) => acc.add(x.evaluate()),
1853+
(acc: Expression, x) =>
1854+
acc.add(x.evaluate({ numericApproximation })),
18461855
engine.Zero
18471856
),
18481857
engine._timeRemaining
@@ -1853,15 +1862,16 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
18531862
}
18541863
// Evaluate the accumulated result to combine numeric terms
18551864
// e.g., 3x + 1 + 2 + 3 → 3x + 6
1856-
return result?.evaluate() ?? engine.NaN;
1865+
return result?.evaluate({ numericApproximation }) ?? engine.NaN;
18571866
},
18581867

1859-
evaluateAsync: async (xs, { engine, signal }) => {
1868+
evaluateAsync: async (xs, { engine, signal, numericApproximation }) => {
18601869
const result = await runAsync(
18611870
reduceBigOp(
18621871
xs[0],
18631872
xs.slice(1),
1864-
(acc: Expression, x) => acc.add(x.evaluate()),
1873+
(acc: Expression, x) =>
1874+
acc.add(x.evaluate({ numericApproximation })),
18651875
engine.Zero
18661876
),
18671877
engine._timeRemaining,
@@ -1871,7 +1881,7 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
18711881
if (result === NON_ENUMERABLE_DOMAIN) {
18721882
return undefined; // Return undefined to keep expression symbolic
18731883
}
1874-
return result?.evaluate() ?? engine.NaN;
1884+
return result?.evaluate({ numericApproximation }) ?? engine.NaN;
18751885
},
18761886
},
18771887
},

src/compute-engine/numerics/rationals.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,39 @@ export function add(lhs: Rational, rhs: Rational): Rational {
8787
const bigRhs = [BigInt(rhsNum[0]), BigInt(rhsNum[1])];
8888
return [bigRhs[1] * lhs[0] + bigRhs[0] * lhs[1], bigRhs[1] * lhs[1]];
8989
}
90-
return [rhsNum[1] * lhs[0] + rhsNum[0] * lhs[1], rhsNum[1] * lhs[1]];
90+
91+
const n =
92+
(rhsNum[1] as number) * (lhs[0] as number) +
93+
(rhsNum[0] as number) * (lhs[1] as number);
94+
const d = (rhsNum[1] as number) * (lhs[1] as number);
95+
96+
if (n <= 9007199254740991 && n >= -9007199254740991 && d <= 9007199254740991)
97+
return [n, d];
98+
99+
if (!Number.isFinite(n) || !Number.isFinite(d)) return [n, d];
100+
101+
return [
102+
BigInt(rhsNum[1]) * BigInt(lhs[0]) + BigInt(rhsNum[0]) * BigInt(lhs[1]),
103+
BigInt(rhsNum[1]) * BigInt(lhs[1]),
104+
];
91105
}
92106

93107
export function mul(lhs: Rational, rhs: Rational): Rational {
94-
if (isMachineRational(lhs) && isMachineRational(rhs))
95-
return [lhs[0] * rhs[0], lhs[1] * rhs[1]];
108+
if (isMachineRational(lhs) && isMachineRational(rhs)) {
109+
const n = lhs[0] * rhs[0];
110+
const d = lhs[1] * rhs[1];
111+
if (
112+
n <= 9007199254740991 &&
113+
n >= -9007199254740991 &&
114+
d <= 9007199254740991
115+
)
116+
return [n, d];
117+
118+
if (!Number.isFinite(n) || !Number.isFinite(d)) return [n, d];
119+
120+
return [BigInt(lhs[0]) * BigInt(rhs[0]), BigInt(lhs[1]) * BigInt(rhs[1])];
121+
}
122+
96123
if (isMachineRational(lhs))
97124
return [
98125
BigInt(lhs[0]) * (rhs[0] as bigint),

test/compute-engine/arithmetic.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,10 +710,17 @@ describe('SUM', () => {
710710
it('should compute the sum of a function over an open interval', () =>
711711
expect(
712712
ce
713-
.box(['Sum', ['Divide', 1, 'x'], 'x'])
713+
.box(['Sum', ['Divide', 1, 'x'], ['Tuple', 'x', 1, 100]])
714714
.evaluate()
715715
.toString()
716-
).toMatchInlineSnapshot(`5690887772881993/581432233225878`));
716+
).toMatchInlineSnapshot(
717+
`14466636279520351160221518043104131447711/2788815009188499086581352357412492142272`
718+
));
719+
720+
it('should compute the sum of a function over an open interval numerically', () => {
721+
const result = ce.box(['Sum', ['Divide', 1, 'x'], 'x']).N();
722+
expect(result.re).toBeCloseTo(9.787606036044382);
723+
});
717724

718725
it('should compute the sum of a collection', () =>
719726
expect(

test/playground.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import { expand } from '../src/compute-engine/boxed-expression/expand';
1010
const ce = new ComputeEngine();
1111
const engine = ce;
1212

13-
const cr = compile('sin(x)/x', { to: 'interval-wgsl' });
14-
console.log(cr.code);
13+
ce.declare('g', 'any');
14+
ce.assign('g', ce.parse('\\bot'));
15+
ce.assign('g', ce.parse('x \\mapsto x'));
16+
ce.assign('g', undefined);
17+
ce.assign('g', ce.parse('42')); // Error: Cannot change the operator "g" to a value
1518

1619
// 1. sin(theta)**2 + cos(theta)**2 → 1 — Clean trig identity, but too simple.
1720
// 2. (alpha**2 - beta**2) / (alpha - beta) → didn't simplify. Engine doesn't cancel the

0 commit comments

Comments
 (0)