Skip to content

Commit 8f9de6e

Browse files
committed
arch: separate LatexSyntax from Compute Engine Core
1 parent 7203d2e commit 8f9de6e

14 files changed

+845
-82
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,39 @@
77
treatment. Non-numeric arguments (e.g., tuples, triples) still trigger
88
function-call behavior.
99

10+
- **LatexSyntax is now an injectable dependency**. The `ComputeEngine`
11+
constructor accepts an optional `latexSyntax` option to inject a `LatexSyntax`
12+
instance. When importing the full package (`@cortex-js/compute-engine`), a
13+
`LatexSyntax` is created automatically — existing code works unchanged. When
14+
importing only `@cortex-js/compute-engine/core`, no `LatexSyntax` is bundled;
15+
`ce.parse()`, `.latex`, and `.toLatex()` will throw unless a `LatexSyntax` is
16+
explicitly provided. MathJSON serialization (`.json`, `.toMathJson()`)
17+
gracefully omits the optional `latex` metadata field when no `LatexSyntax` is
18+
available.
19+
20+
```ts
21+
// Full package — works as before, LatexSyntax auto-created:
22+
import { ComputeEngine } from '@cortex-js/compute-engine';
23+
const ce = new ComputeEngine();
24+
ce.parse('x^2'); // works
25+
26+
// Core-only — explicit injection for LaTeX support:
27+
import { ComputeEngine } from '@cortex-js/compute-engine/core';
28+
import { LatexSyntax } from '@cortex-js/compute-engine/latex-syntax';
29+
const ce = new ComputeEngine({ latexSyntax: new LatexSyntax() });
30+
ce.parse('x^2'); // works
31+
32+
// Core-only — no LaTeX needed:
33+
import { ComputeEngine } from '@cortex-js/compute-engine/core';
34+
const ce = new ComputeEngine();
35+
ce.expr(['Add', 'x', 1]).simplify(); // works
36+
ce.parse('x + 1'); // throws: LatexSyntax not available
37+
```
38+
39+
- **New `ILatexSyntax` interface**: A structural interface for LaTeX
40+
parser/serializers, exposed on `IComputeEngine.latexSyntax`. Allows custom
41+
implementations without depending on the `LatexSyntax` class.
42+
1043
**Package modularization**: The monolithic `@cortex-js/compute-engine` package
1144
can now be imported as seven independently usable sub-paths, enabling smaller
1245
bundles for consumers who only need a subset of functionality.

MIGRATION_GUIDE_0.55.0.md

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ work** — with a few breaking API changes listed below.
1212
| 0.54.x | 0.55.0 |
1313
| --- | --- |
1414
| `ce.box(input)` | `ce.expr(input)` |
15-
| `ce.latexDictionary` | `new LatexSyntax({ dictionary })` |
16-
| `ce.latexDictionary = [...]` | `new LatexSyntax({ dictionary: [...] })` |
15+
| `ce.latexDictionary` | `new LatexSyntax({ dictionary })` passed to constructor |
16+
| `ce.latexDictionary = [...]` | `new ComputeEngine({ latexSyntax: new LatexSyntax({ dictionary: [...] }) })` |
1717
| `ComputeEngine.getLatexDictionary()` | `import { LATEX_DICTIONARY }` |
1818
| `isBoxedExpression(x)` | `isExpression(x)` |
1919
| `isBoxedNumber(x)` | `isNumber(x)` |
@@ -49,10 +49,11 @@ import { expr } from '@cortex-js/compute-engine';
4949
const e = expr(['Add', 'x', 1]);
5050
```
5151

52-
### 2. `ce.latexDictionary` removed
52+
### 2. `ce.latexDictionary` removed — use `LatexSyntax` constructor injection
5353

5454
The `latexDictionary` getter and setter on `ComputeEngine` are removed. LaTeX
55-
dictionaries are now managed by the standalone `LatexSyntax` class.
55+
dictionaries are now managed by the `LatexSyntax` class, which is injected into
56+
`ComputeEngine` via the `latexSyntax` constructor option.
5657

5758
```ts
5859
// Before
@@ -68,8 +69,7 @@ ce.latexDictionary = [
6869
];
6970

7071
// After
71-
import { LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine';
72-
// Or: import { LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine/latex-syntax';
72+
import { ComputeEngine, LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine';
7373

7474
const syntax = new LatexSyntax({
7575
dictionary: [
@@ -84,6 +84,11 @@ const syntax = new LatexSyntax({
8484
],
8585
});
8686

87+
// Inject into the engine so ce.parse(), .latex, .toLatex() use it:
88+
const ce = new ComputeEngine({ latexSyntax: syntax });
89+
ce.parse('\\placeholder{x}');
90+
91+
// Or use the LatexSyntax instance directly (no engine needed):
8792
const mathJson = syntax.parse('\\placeholder{x}');
8893
```
8994

@@ -127,7 +132,7 @@ import { isExpression, isNumber } from '@cortex-js/compute-engine';
127132

128133
If you were passing custom library definitions with `latexDictionary` entries
129134
to the `ComputeEngine` constructor, that field is no longer recognized. Define
130-
your LaTeX entries via a `LatexSyntax` instance instead.
135+
your LaTeX entries via a `LatexSyntax` instance and inject it.
131136

132137
```ts
133138
// Before
@@ -140,21 +145,22 @@ const ce = new ComputeEngine({
140145
});
141146

142147
// After
143-
import { LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine';
144-
145-
const ce = new ComputeEngine({
146-
libraries: [{
147-
name: 'mylib',
148-
operators: { MyOp: { ... } },
149-
}],
150-
});
148+
import { ComputeEngine, LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine';
151149

152150
const syntax = new LatexSyntax({
153151
dictionary: [
154152
...LATEX_DICTIONARY,
155153
{ latexTrigger: '\\myop', parse: 'MyOp' },
156154
],
157155
});
156+
157+
const ce = new ComputeEngine({
158+
latexSyntax: syntax,
159+
libraries: [{
160+
name: 'mylib',
161+
operators: { MyOp: { ... } },
162+
}],
163+
});
158164
```
159165

160166
### 6. Compilation registry methods now `@internal`
@@ -220,6 +226,19 @@ const e = ce.expr(['Add', ['Power', 'x', 2], 1]);
220226
e.simplify();
221227
```
222228

229+
To add LaTeX support to a core-only engine, inject a `LatexSyntax`:
230+
231+
```ts
232+
import { ComputeEngine } from '@cortex-js/compute-engine/core';
233+
import { LatexSyntax } from '@cortex-js/compute-engine/latex-syntax';
234+
235+
const ce = new ComputeEngine({ latexSyntax: new LatexSyntax() });
236+
ce.parse('x^2 + 1'); // works
237+
```
238+
239+
Without injection, `ce.parse()`, `.latex`, and `.toLatex()` throw an error.
240+
MathJSON operations (`ce.expr()`, `.json`, `.evaluate()`, etc.) work without it.
241+
223242
### Compilation targets only
224243

225244
```ts
@@ -243,10 +262,19 @@ The following convenience methods were **kept** on `ComputeEngine` and
243262

244263
| Method | Still works? | Standalone alternative |
245264
| --- | --- | --- |
246-
| `ce.parse(latex)` | Yes | `parse(latex)` from `latex-syntax` + `ce.expr()` |
247-
| `expr.latex` | Yes | `serialize(expr.json)` from `latex-syntax` |
248-
| `expr.toLatex()` | Yes | `serialize(expr.json)` from `latex-syntax` |
265+
| `ce.parse(latex)` | Yes (requires `LatexSyntax`) | `parse(latex)` from `latex-syntax` + `ce.expr()` |
266+
| `expr.latex` | Yes (requires `LatexSyntax`) | `serialize(expr.json)` from `latex-syntax` |
267+
| `expr.toLatex()` | Yes (requires `LatexSyntax`) | `serialize(expr.json)` from `latex-syntax` |
249268
| `ce.box(input)` | Yes (deprecated) | `ce.expr(input)` |
250269

251-
These convenience methods use the standalone `LatexSyntax` internally and
252-
will continue to work for the foreseeable future.
270+
These convenience methods delegate to the engine's injected `LatexSyntax`
271+
instance. When using the full package (`@cortex-js/compute-engine`), a
272+
`LatexSyntax` is automatically created — existing code works unchanged. When
273+
using only the core sub-path, pass a `LatexSyntax` via the constructor:
274+
275+
```ts
276+
const ce = new ComputeEngine({ latexSyntax: new LatexSyntax() });
277+
```
278+
279+
The `LatexSyntax` instance is accessible via `ce.latexSyntax` (returns
280+
`undefined` if none is available).

docs/architecture/CURRENT-ARCHITECTURE.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ This document captures the implemented architecture after the recent modularizat
7979
| `engine-runtime-state.ts` | Execution limits (time, iteration, recursion) and verification state |
8080
| `engine-configuration-lifecycle.ts` | Configuration change propagation and reset fan-out |
8181
| `engine-cache.ts` | Expression and rule-set caching with generation-based invalidation |
82-
| `engine-latex-dictionary-state.ts` | LaTeX dictionary indexing and rebuild |
8382

8483
### Scoping & Declarations
8584
| File | Responsibility |
@@ -101,6 +100,38 @@ This document captures the implemented architecture after the recent modularizat
101100
| `engine-compilation-targets.ts` | Registry for named compilation targets (JavaScript, GLSL, etc.) |
102101
| `engine-type-resolver.ts` | Type resolution callback for parser integration |
103102

103+
## LatexSyntax as Injectable Dependency
104+
105+
`LatexSyntax` is an optional dependency of `ComputeEngine`. The engine does not
106+
statically import the `LatexSyntax` class — it only uses the structural
107+
`ILatexSyntax` interface from `types-engine.ts`.
108+
109+
**Injection mechanism:**
110+
111+
1. **Explicit constructor option**: `new ComputeEngine({ latexSyntax: new LatexSyntax() })`.
112+
2. **Static factory** (`ComputeEngine._latexSyntaxFactory`): Registered by the
113+
full entry point (`compute-engine.ts`). Enables lazy creation so `new ComputeEngine()` still works with LaTeX when imported from the full package.
114+
3. **No LatexSyntax**: When imported from the core sub-path without injection,
115+
`ce.parse()`, `.latex`, and `.toLatex()` throw a clear error. MathJSON
116+
serialization gracefully omits the optional `latex` metadata field.
117+
118+
**Key types:**
119+
120+
- `ILatexSyntax` (in `types-engine.ts`) — structural interface with `parse()` and `serialize()` methods.
121+
- `IComputeEngine.latexSyntax` — getter returning the instance (lazily created via factory if available) or `undefined`.
122+
- `IComputeEngine._requireLatexSyntax()` — returns the instance or throws.
123+
124+
**Files involved:**
125+
126+
| File | Role |
127+
|------|------|
128+
| `types-engine.ts` | Defines `ILatexSyntax` interface, declares `latexSyntax` / `_requireLatexSyntax()` on `IComputeEngine` |
129+
| `index.ts` | Stores instance, exposes getter/helper, accepts constructor option, holds static factory |
130+
| `compute-engine.ts` | Registers the factory: `ComputeEngine._latexSyntaxFactory = () => new LatexSyntax()` |
131+
| `abstract-boxed-expression.ts` | `.latex` / `.toLatex()` route through `engine._requireLatexSyntax()` |
132+
| `serialize.ts` | Latex metadata conditional on `ce.latexSyntax` availability |
133+
| `rules.ts` | Rule parsing constructs LatexSyntax via `ce._requireLatexSyntax().constructor` |
134+
104135
## Free Functions & Lazy Global Engine
105136

106137
Top-level free functions (`parse`, `simplify`, `evaluate`, `N`, `assign`) are exported from `index.ts` via `free-functions.ts`. They are backed by a lazily-instantiated global `ComputeEngine` accessible via `getDefaultEngine()`.
@@ -112,7 +143,10 @@ Top-level free functions (`parse`, `simplify`, `evaluate`, `N`, `assign`) are ex
112143
- `assign(id, value)` / `assign({...})` — assign values in the global engine
113144
- `getDefaultEngine()` — access the shared engine instance for configuration
114145

115-
The global engine is created on first call to any free function, using a dynamic `require('./index')` inside `getDefaultEngine()` to avoid circular dependency (since `index.ts` re-exports `free-functions.ts`).
146+
The global engine is created on first call to any free function via a factory
147+
registered by the entry point. The full entry point (`compute-engine.ts`)
148+
registers a factory that includes `LatexSyntax`; the core entry point
149+
(`index.ts`) registers a factory without it.
116150

117151
## Extension Contracts (Runtime Guards)
118152

@@ -130,7 +164,7 @@ Custom libraries (`new ComputeEngine({ libraries: [...] })`):
130164
- `requires` must be an array of library names
131165
- duplicate dependencies in `requires` are rejected
132166
- `definitions` must be object or array of objects
133-
- `latexDictionary` must be an array
167+
- `latexDictionary` must be an array (if present; removed from standard libraries)
134168

135169
Compile option payloads (`compile(expr, options)`):
136170
- validates `to`, `target`, `operators`, `functions`, `vars`, `imports`, `preamble`, `fallback`

docs/plans/2026-02-27-big-decimal-design.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ BigDecimal.precision = 50; // set by engine's setPrecision()
4141
- **Guard digits**: transcendental implementations use `precision + 10`
4242
internally, round result to `precision`.
4343

44+
See `docs/NUMERIC-SERIALIZATION.md` for how precision affects the three output
45+
paths (`.json`, `.latex`, `.toString()`).
46+
4447
## API Surface
4548

4649
### Construction

docs/plans/2026-02-27-modularization-design.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,28 @@ e.json; // → MathJsonExpression
146146
e.toString(); // → ASCII math
147147
```
148148

149+
**`LatexSyntax` is an injectable dependency** (implemented):
150+
151+
`ComputeEngine` accepts an optional `latexSyntax` constructor option. When
152+
importing the full package, a `LatexSyntax` is auto-created via a static
153+
factory. When importing only from `/core`, no `LatexSyntax` is bundled.
154+
155+
- `ce.parse()` — available when `LatexSyntax` is injected, throws otherwise
156+
- `ce.latexSyntax` — getter returning the `ILatexSyntax` instance or `undefined`
157+
- `ce._requireLatexSyntax()` — returns instance or throws with clear message
158+
149159
**Removed from `ComputeEngine`**:
150160

151-
- `ce.parse()` — use `parse()` from latex-syntax + `ce.expr()`
152161
- `ce.latexDictionary` (get/set) — owned by `LatexSyntax`
153162
- `ce.decimalSeparator` — moves to `LatexSyntax` options
154163
- `static getLatexDictionary()` — moves to latex-syntax exports
155164
- `registerCompilationTarget()`, `getCompilationTarget()`,
156165
`listCompilationTargets()`, `unregisterCompilationTarget()` — registry removed
157166
- `_compile()` — replaced by free `compile()` in compile module
158167

159-
**Removed from `BoxedExpression`**:
168+
**Still on `BoxedExpression`** (require injected LatexSyntax):
160169

161-
- `.toLatex()` / `.latex` — use `serialize(expr.json)` from latex-syntax
170+
- `.toLatex()` / `.latex`available when `LatexSyntax` is injected; alternatively use `serialize(expr.json)` from latex-syntax directly
162171
- `.compile()` — use `compile(expr)` from compile module
163172

164173
**Renamed**:
@@ -243,10 +252,10 @@ simplify(expr(parse('x^2 + 2x + 1')));
243252

244253
## Breaking Changes Summary
245254

246-
| Removed | Replacement |
255+
| Changed | Replacement |
247256
| --- | --- |
248-
| `ce.parse(latex)` | `ce.expr(parse(latex))` |
249-
| `expr.toLatex()` / `expr.latex` | `serialize(expr.json)` |
257+
| `ce.parse(latex)` | Still available when `LatexSyntax` is injected; or `ce.expr(parse(latex))` |
258+
| `expr.toLatex()` / `expr.latex` | Still available when `LatexSyntax` is injected; or `serialize(expr.json)` |
250259
| `expr.compile(options)` | `compile(expr, options)` |
251260
| `ce.box(input)` | `ce.expr(input)` |
252261
| `ce.latexDictionary` | `new LatexSyntax({ dictionary })` |
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Precision-Scaling bigGamma / bigGammaln
2+
3+
**Date:** 2026-02-28
4+
**Status:** Approved
5+
6+
## Problem
7+
8+
`bigGamma` and `bigGammaln` in `src/compute-engine/numerics/special-functions.ts`
9+
use fixed Lanczos/Spouge coefficient tables with ~16 digits of precision. At
10+
BigDecimal.precision > 16, the results are dominated by coefficient error:
11+
12+
- Gamma(1) at prec=100: `1.00000000000000000000298...` (should be exactly 1)
13+
- Gamma(5) at prec=100: `24.0000000000000000000564...` (should be exactly 24)
14+
- Gamma(1/2) at prec=100: 16 matching digits vs sqrt(pi)
15+
16+
Meanwhile, all BigDecimal elementary functions (exp, ln, sin, cos, sqrt, atan)
17+
match decimal.js at 499-500 of 500 digits. Gamma is the outlier.
18+
19+
## Approach: Stirling Series + Runtime Bernoulli Numbers
20+
21+
Replace the fixed-coefficient Lanczos approximation with the Stirling asymptotic
22+
series for ln(Γ(z)), using Bernoulli numbers computed at runtime to the needed
23+
precision. This is the same pattern already used by `bigDigamma`.
24+
25+
### Algorithm for bigGammaln(ce, z)
26+
27+
1. **Exact fast paths** (no approximation error):
28+
- Positive integer z: return `ln((z-1)!)` from exact bigint factorial
29+
- Half-integer z = n + 1/2: return `ln((2n)! √π / (4^n n!))` — exact up to √π
30+
31+
2. **Reflection** (existing, unchanged):
32+
For z < 0.5, use `Γ(z) = π / (sin(πz) · Γ(1-z))`
33+
34+
3. **Stirling series** for general z ≥ 0.5:
35+
- Shift z upward by m steps using recurrence:
36+
`lnΓ(z) = lnΓ(z+m) - ln(z·(z+1)·...·(z+m-1))`
37+
- Shift target: `z+m > p·ln(10)/(2π) ≈ 0.37·p` where p = BigDecimal.precision
38+
- Compute via Stirling:
39+
`lnΓ(w) = (w-½)ln(w) - w + ½ln(2π) + Σ_{k=1}^{N} B_{2k} / (2k(2k-1)·w^{2k-1})`
40+
- N ≈ π·w terms (stop when term < 10^{-(p+guard)})
41+
42+
### Algorithm for bigGamma(ce, z)
43+
44+
Thin wrapper:
45+
- Positive integer z ≥ 1: return exact `(z-1)!` as BigDecimal
46+
- Otherwise: `exp(bigGammaln(ce, z))`
47+
48+
### Bernoulli Number Computation
49+
50+
Compute B₂, B₄, ..., B_{2N} as exact rationals `[bigint, bigint]` using the
51+
even-only recurrence:
52+
53+
```
54+
B_{2m} = (2m-1)/(2(2m+1)) - 1/(2m+1) · Σ_{j=1}^{m-1} C(2m+1, 2j) · B_{2j}
55+
```
56+
57+
This is O(N²) bigint operations with no precision loss. The rational table is
58+
converted to BigDecimal at working precision + guard digits when evaluating the
59+
Stirling series.
60+
61+
**Scaling:** For p=100, N ≈ 115 Bernoulli numbers. For p=500, N ≈ 575. The
62+
bigint recurrence runs in under 100ms even for p=500.
63+
64+
### Caching
65+
66+
- `'bernoulli-even-rationals'` in `ce._cache`: the `[bigint, bigint][]` table.
67+
Grows monotonically (exact rationals never need recomputation). Extended if a
68+
higher-precision call needs more terms.
69+
- Remove `'lanczos-7-c'`, `'gamma-p-ln'`, `'gamma-g-ln'` cache entries (dead).
70+
71+
## Files Changed
72+
73+
1. **`src/compute-engine/numerics/special-functions.ts`**
74+
- Replace `bigGamma` and `bigGammaln` implementations
75+
- Add `computeBernoulliEven(n)``[bigint, bigint][]` utility
76+
- Remove `LANCZOS_7_C` and `GAMMA_P_LN` coefficient arrays
77+
78+
2. **No other files change.** Function signatures are identical, so all callers
79+
(`bigBeta`, `bigZeta`, Gamma library entry in `arithmetic.ts`) work unchanged.
80+
81+
## Testing
82+
83+
The existing `test/big-decimal/precision-comparison.test.ts` Gamma section is
84+
the regression test. After the fix, expected results:
85+
86+
| Input | Before (digits) | After (digits, prec=50) | After (digits, prec=100) |
87+
|--------------|-----------------|------------------------|-------------------------|
88+
| Gamma(1) | 0 | 50 (exact) | 100 (exact) |
89+
| Gamma(2) | 0 | 50 (exact) | 100 (exact) |
90+
| Gamma(5) | 1 | 50 (exact) | 100 (exact) |
91+
| Gamma(10) | 4 | 50 (exact) | 100 (exact) |
92+
| Gamma(1/2) | 16 | ~45 | ~95 |
93+
| Gamma(3/2) | 16 | ~45 | ~95 |
94+
95+
## Future Work
96+
97+
- Upgrade `bigDigamma`/`bigTrigamma`/`bigPolygamma` to use the same runtime
98+
Bernoulli table (they currently use the 15-entry `BERNOULLI_2K_STRINGS` array,
99+
limiting them to ~50 digits). Natural follow-up, not part of this change.

0 commit comments

Comments
 (0)