This document describes the overall architecture of the Cortex Compute
Engine — a TypeScript library for symbolic manipulation and numeric
evaluation of mathematical expressions, published as
@cortex-js/compute-engine.
It is the high-level map of the codebase, intended for contributors and
maintainers. For the public API and usage guides, see
README.md and
cortexjs.io/compute-engine. For the
detailed module-boundary and dependency-direction rules, see
docs/architecture/CURRENT-ARCHITECTURE.md.
The Compute Engine parses, manipulates, evaluates, and serializes mathematical expressions. Its capabilities fall into a few categories:
- Parse LaTeX (and ASCIIMath-ish lenient syntax) into a structured representation.
- Canonicalize expressions into a normalized form for consistent comparison and efficient operations.
- Evaluate expressions symbolically or numerically (machine precision or arbitrary precision).
- Simplify, expand, factor, solve, differentiate, and integrate.
- Reason about expressions using assumptions and a type system.
- Compile expressions to executable JavaScript (and GLSL/WGSL/Python).
- Serialize back to LaTeX, MathASCII, or MathJSON.
Everything in the Compute Engine flows between three representations of an
expression, mediated by the ComputeEngine instance:
LaTeX string MathJSON BoxedExpression
"x + \frac{1}{2}" ◄──► ["Add","x",...] ◄──► (boxed, typed, canonical)
│ │ │
latex-syntax/ math-json/ boxed-expression/
(parse/serialize) (interchange format) (the runtime object model)
- LaTeX is the human-facing input/output notation.
- MathJSON is the JSON-based interchange format — plain data, no behavior. It is the serialization boundary and the format consumers store and exchange.
- BoxedExpression is the live, in-memory object model. Boxed expressions carry a type, a definition binding, cached properties, and methods to evaluate, simplify, compare, and serialize themselves. All computation happens on boxed expressions.
The typical lifecycle:
Parse LaTeX ──► MathJSON ──► BoxedExpression (ce.parse)
Canonicalize normalize structure & operands
Evaluate symbolic / numeric result
Simplify apply algebraic rewrite rules
Serialize BoxedExpression ──► MathJSON ──► LaTeX (.json / .latex)
The ComputeEngine (src/compute-engine/index.ts) is the composition root that
owns scopes, symbol/operator definitions, assumptions, numeric configuration,
the standard library, and the LaTeX syntax. It exposes the public API:
parse(), box()/expr(), function(), evaluate(), simplify(),
declare(), assign(), assume(), ask(), and more.
src/
├── compute-engine.ts Full package entry (engine + LaTeX + compilation)
├── core.ts Core engine entry (no LaTeX, no compilation)
├── math-json.ts MathJSON types + utilities entry
├── latex-syntax.ts Standalone LaTeX parser/serializer entry
├── interval.ts Interval-arithmetic entry
├── numerics.ts Numeric primitives entry
├── compile.ts Compilation-targets entry
├── identities.ts Fungrim identities plugin entry (loadIdentities)
├── integration-rules.ts Rubi integration-rules plugin entry
├── cortex.ts Experimental Cortex language (not a published entry)
│
├── math-json/ MathJSON format: types + accessors/guards
│ ├── types.ts MathJsonExpression and its object forms
│ └── utils.ts operator(), operands(), type guards, accessors
│
├── big-decimal/ Arbitrary-precision decimals backed by native bigint
│ ├── big-decimal.ts BigDecimal class: arithmetic, comparison, rounding
│ ├── transcendentals.ts sqrt/exp/ln/sin/cos/atan/… (prototype-merged)
│ └── utils.ts fixed-point bigint primitives, π constant, pow10 cache
│
├── common/ Cross-cutting utilities
│ └── type/ The type system (see "Type system" below)
│
└── compute-engine/ The engine itself
├── index.ts ComputeEngine class (composition root, ~1700 lines)
├── compute-engine.ts Full-package wiring (LatexSyntax factory injection)
├── engine-*.ts Runtime services (one bounded concern per file)
├── types-*.ts Type contracts (kernel generics + concrete wrappers)
├── free-functions.ts Top-level parse/simplify/evaluate/N/… via a global engine
│
├── boxed-expression/ The runtime object model (the heart of the engine)
├── latex-syntax/ LaTeX ↔ MathJSON tokenizer, parser, serializer, dictionary
├── library/ Standard library of operators & constants, by domain
├── symbolic/ Simplification rules, calculus, the Fu trig algorithm
├── rubi/ Rubi rule-based integrator (loader, matcher, driver)
├── numerics/ Numeric algorithms (primes, rationals, special functions)
├── numeric-value/ NumericValue abstraction (exact / machine / bignum)
├── compilation/ Expression → JS/GLSL/WGSL/Python code generation
├── interval/ Interval arithmetic
└── tensor/ Vectors, matrices, multi-dimensional arrays
Outside src/: data/ holds the Rubi and Fungrim rule corpora consumed
by the plugin entries (see "Integration rules" below); benchmarks/ is
the cross-library benchmark and audit harness (see
benchmarks/README.md); docs/reviews/ holds
findings trackers from codebase reviews.
A task-oriented index — where to start for the most common kinds of work
(paths relative to src/compute-engine/ unless noted):
| Task | Start here |
|---|---|
| Parse bug (LaTeX → wrong MathJSON) | latex-syntax/dictionary/definitions-*.ts |
| Serialization bug (expression → wrong LaTeX) | latex-syntax/serializer.ts + the operator's dictionary entry |
| Wrong canonical form | boxed-expression/arithmetic-*.ts, boxed-expression/canonical.ts |
Wrong evaluate()/N() result |
the operator's evaluate handler in library/<domain>.ts, then numerics/ for the kernel |
| Simplification missing or wrong | symbolic/simplify-rules.ts + docs/SIMPLIFY.md |
| Trig simplification | symbolic/fu*.ts, boxed-expression/trigonometry.ts (constructibleValues) |
| Integration (symbolic) | symbolic/antiderivative.ts for built-ins; rubi/ for the rule-based integrator; latex-syntax/dictionary/README.md for the ∫ parse→eval→serialize pipeline |
| Solve | boxed-expression/solve.ts, boxed-expression/solve-linear-system.ts |
| Type-inference bug | library/type-handlers.ts, src/common/type/ |
| Compilation target bug | compilation/<target>.ts; the tree walk is in compilation/base-compiler.ts |
Assumptions / verify() / ask() |
engine-assumptions.ts, assume.ts |
| Step-by-step explanations | boxed-expression/explain.ts, explain-labels.ts |
Within src/compute-engine/, modules are organized into layers with a strict,
acyclic dependency direction (enforced by ESLint import/no-restricted-paths
and a zero-circular-dependency budget checked with madge):
4. Composition root index.ts (ComputeEngine) — composes services, public API
3. Runtime services engine-*.ts — one bounded concern per file
2. Type wrappers types-*.ts, global-types.ts — bind generics to concrete types
1. Kernel type layer types-kernel-*.ts — generic contracts, no engine imports
The ComputeEngine class is deliberately an "API shell and integration point":
business logic lives in the engine-*.ts services (startup, scoping,
declarations, assumptions, numeric configuration, caching, extension contracts,
compilation-target registry, etc.). See
docs/architecture/CURRENT-ARCHITECTURE.md
for the full service inventory and the dependency rules.
src/math-json/ defines the format. A MathJsonExpression is plain JSON in one
of these forms:
- A number —
2.5, or{ num: "3.14159..." }for arbitrary precision / special values ("NaN","+Infinity"). - A symbol —
"x","Pi", or{ sym: "Pi" }. - A string —
{ str: "hello" }. - A function —
["Add", "x", 1], or{ fn: [...] }. The first element is the operator (head); the rest are operands. - A dictionary —
{ dict: { ... } }.
Any object form may carry optional metadata: comment, latex, wikidata,
sourceUrl, and similar. src/math-json/utils.ts provides safe accessors
(operator(), operands(), stringValue()) and type guards. MathJSON has no
behavior — it is the inert boundary between the LaTeX layer, the boxed object
model, and the outside world.
src/compute-engine/boxed-expression/ is the heart of the engine. A
"boxed" expression is a class instance that wraps a piece of math with a
consistent interface for evaluation, comparison, and serialization.
_BoxedExpression (abstract-boxed-expression.ts) is the abstract base; the
concrete subclasses are:
| Class | File | Represents |
|---|---|---|
BoxedNumber |
boxed-number.ts |
Numeric literals (int, rational, radical, float, bignum, complex) |
BoxedSymbol |
boxed-symbol.ts |
Symbols/identifiers, bound to a value definition when canonical |
BoxedFunction |
boxed-function.ts |
Function applications [operator, ...operands], bound to an operator definition |
BoxedString |
boxed-string.ts |
String literals (stored NFC-normalized) |
BoxedDictionary |
boxed-dictionary.ts |
Key/value maps |
BoxedTensor |
boxed-tensor.ts |
Vectors, matrices, n-dimensional arrays |
The shared public surface (on _BoxedExpression) includes the getters/methods
most consumers use: .json, .latex/.toLatex(), .operator, .ops,
.type, .canonical, .structural, .evaluate(), .simplify(), .N(),
.subs(), .match(), the algebra helpers (.add(), .mul(), .pow(), …),
and the three comparison methods .isSame() / .is() / .isEqual() (see
CLAUDE.md for their distinct
semantics).
box.ts is the factory: it takes raw input (a number, string, MathJSON, or
existing boxed expression), chooses the concrete class, and — by default —
canonicalizes the result. Canonicalization normalizes structure so that
mathematically equal expressions share a representation: it flattens associative
operators, orders operands, and folds exact numeric operands. The arithmetic
modules implement the per-operator canonical logic:
arithmetic-add.ts— flatten, drop zeros, fold exact numbers, sort terms.arithmetic-mul-div.ts— group by exponent, combine coefficients (Product).arithmetic-power.ts— exponent rules, root/radical normalization.flatten.ts,negate.ts,order.ts— supporting transforms.
canonical.ts orchestrates partial/selective canonicalization by named form
(Number, Multiply, Add, Power, Divide, Flatten, Order).
Three creation modes exist — canonical (default), structural (bound but not canonicalized), and non-canonical (not bound, usable only for pattern matching/serialization). The distinctions and pitfalls are documented in detail in CLAUDE.md.
Canonicalization applies a small set of algebraic folds to generic (unconstrained) symbols — an operand that carries a free variable stands for a generic value of that variable. These are deliberate conventions, not bugs: they trade exactness at a measure-zero point for a simpler canonical form.
| Input | Canonical form |
|---|---|
x / x, (x + 1)/(x + 1) |
1 |
1 ^ x |
1 |
x / 0 |
~∞ (ComplexInfinity) |
0 / x |
0 |
x / ∞ |
0 |
The cancellation asserts genericity: x/x → 1 treats x as a generic
(hence non-zero) value. This is why the fold survives a later substitution —
ce.parse('\\frac{x}{x}').subs({ x: 0 }) is 1, not NaN, because the .json
is already the literal 1 before the substitution reaches it. To recover the
true value at an excluded point, keep the expression uncanonicalized
({ canonical: false }) and evaluate after substituting.
Constant operands are protected. These folds fire only on operands that carry a free variable. A numerator or denominator that is a constant expression is never collapsed by the generic rules; it is evaluated instead, so a provably-zero constant denominator surfaces the true indeterminate:
0 / (1 − 1)canonicalizes toNaN— the denominator evaluates to0, giving0/0— not to0: the0/x → 0fold does not reach a constant zero.(1 − 1) / 0and0 / 0likewise canonicalize toNaN.(π + 1)/(π + 1)is left as a fraction at canonicalization (a constant, so the identical-operand fold is skipped); it reduces to1only under.simplify(), which first proves the denominator non-zero.
Larger cancellations (x²/(5x²) → 1/5) and the wider policy that governs which
.simplify() rewrites treat an unknown as a generic real (why
ln(x) + ln(y) → ln(xy) fires for an unconstrained x but not for a
declared-complex one) are simplify-level, documented in
docs/SIMPLIFY.md.
The numeric lattice (D10) can name only some of the non-finite values:
non_finite_number is exactly {+∞, −∞}; real/rational/integer and
complex admit ±∞ on top of their finite tower; ~oo (ComplexInfinity)
and NaN are admitted only by the top type number. Since the lattice cannot
distinguish "±∞" from "maybe ~oo" from "maybe NaN" (SYM P2-23, resolved as a
convention, not a lattice extension), every operator type handler follows
these rules:
- Claim
non_finite_numberonly when the value is provably±∞. Examples:Ln(0) = −∞;Round/Ceil/Floor/Truncateof a provably real ±∞;±∞ · (finite reals all provably non-zero);EllipticK(1) = +∞;(+∞)^pfor finite realp > 0;±∞ + (real terms). - When a non-finite value or NaN is merely possible — or the value is
provably
~oo— claimnumber. Do not claimnon_finite_numberspeculatively, do not claim a finite type, and do not claimcomplex(which does not admit~oo/NaN). Examples:x · ∞with a possibly-zerox(0·∞ = NaN);∞/∞,k/0; pole-capable operators at arguments that can land on a pole (Tan(π/2),Csc(0),Gamma(0),Zeta(1),Factorial(−2)— all~oo);√(−∞) = i·∞ = ~oo. - Unknown finiteness follows the generic-point convention; zero-ness must
be proven when a non-finite operand is present. An operand whose
finiteness is unknown (a bare
realsymbol;isFinite === undefined) is treated as a generic (finite) point —Sin(x)claimsfinite_real, and only a provably non-finite operand (isFinite === false) triggers the non-finite analysis. But once an operand is provably non-finite, a claim that depends on another operand being non-zero (e.g.x · ∞ = ±∞, wherex = 0gives NaN) must prove it (viasgn), never assume it. (A possibly-zero denominator with finite operands keepsDivide's documented generic-point behavior — see the handler's comment.)
The value ~oo itself currently reports type complex (the
ComplexInfinity symbol declaration and the numeric-value type getters);
this is a historical placement that the lattice cannot express better without
the deferred ~oo/NaN lattice refinement. Handlers must not rely on it: an
expression that can evaluate to ~oo is typed number per rule 2.
The shared handler implementations (and per-operator dispatch) live in
src/compute-engine/library/type-handlers.ts; the convention is pinned by
test/compute-engine/non-finite-typing.test.ts.
Behavior is attached not to the expression but to its definition, resolved during binding against the current scope:
boxed-value-definition.ts—_BoxedValueDefinition: a symbol's type, value, constancy, andholdUntilevaluation semantics.boxed-operator-definition.ts—_BoxedOperatorDefinition: a function's type signature, algebraic flags (associative,commutative,idempotent,pure,lazy,broadcastable, …), and its handlers (canonical,evaluate,evaluateAsync,compile, comparison, collection).
The standard library (below) is just a large table of such definitions.
BoxedFunction.evaluate() looks up the operator definition and dispatches to its
evaluate handler. simplify.ts drives rule-based simplification with guards
against infinite recursion (deduplication, a step limit, and loop detection —
see CLAUDE.md).
serialize.ts produces MathJSON from a boxed expression. The directory also
holds focused modules for expand.ts, factor.ts, solve.ts /
solve-linear-system.ts, polynomials.ts, trigonometry.ts, pattern matching
(match.ts, pattern-utils.ts), comparison (compare.ts), and the type guards
exported to consumers (type-guards.ts).
Step-by-step explanations. explain.ts / explain-labels.ts implement
expr.explain('simplify' | 'solve' | 'D'), which returns a structured trace
of the operation with human-readable labels (a ~250-entry registry in
explain-labels.ts; the registry lives in boxed-expression/ rather than
symbolic/ for layering reasons). The derivative driver
(symbolic/explain-derivative.ts) self-registers via _setExplainDDriver,
keeping boxed-expression/ free of an upward import into symbolic/.
src/common/type/ is a self-contained type system used to describe and check
the types of mathematical expressions. It models:
- Numeric primitives in a subtype lattice:
integer ⊂ rational ⊂ real ⊂ complex ⊂ number, with finite/non-finite variants and bounded ranges (e.g.integer<5..10>). - Collections:
list,set,tuple,record,dictionary, with shapes (e.g.matrix<number^(2x3)>for a matrix). - Function signatures:
(x: number, number?) -> number, including named, optional, and variadic arguments. - Algebraic types: union (
|), intersection (&), negation (!), plus the special typesany,unknown,nothing,never,error.
Types are written as strings in a small grammar and parsed through
lexer.ts → parser.ts → type-builder.ts into a Type AST; subtype.ts and
reduce.ts implement compatibility checking and simplification; serialize.ts
renders types back to strings. boxed-type.ts provides BoxedType, the
immutable wrapper used throughout the engine and exposed on every
BoxedExpression via .type.
src/common/ also hosts other shared utilities (an interruptible/cancellation
helper, JSON/JSON5, Markdown rendering, grapheme handling, fuzzy string
matching).
src/compute-engine/latex-syntax/ converts between LaTeX and MathJSON,
independently of the engine. The pipeline:
LaTeX string ──tokenizer.ts──► tokens ──parse.ts──► MathJSON
MathJSON ──serializer.ts──► LaTeX string
LatexSyntax (latex-syntax.ts) is the public class, exposing parse() and
serialize(). Parsing is a recursive-descent / precedence-climbing parser
driven by a dictionary of entries (dictionary/) that map LaTeX notation to
MathJSON operators. The dictionary is assembled in default-dictionary.ts from
domain files (definitions-arithmetic.ts, definitions-algebra.ts,
definitions-trigonometry.ts, definitions-sets.ts, …), each contributing
symbol, function, infix, prefix, postfix, matchfix, or environment
entries with precedence. A lenient (non-strict) mode accepts ASCIIMath-style
input such as bare sin(x) and x^(2); see
docs/LENIENT_PARSER.md.
LatexSyntax is an injectable dependency of ComputeEngine, not a static
import. The full package entry wires it in via a factory so ce.parse() works
out of the box; the core entry omits it (LaTeX methods then throw a clear
error). This keeps LaTeX out of bundles that don't need it — see
docs/architecture/CURRENT-ARCHITECTURE.md.
ComputeEngine (src/compute-engine/index.ts) ties everything together. Its
public API groups into:
- Construction:
parse(),box()/expr(),function(),hold(),tuple(), plus numeric helpersbignum(),complex(),chop(). - Operations:
evaluate(),N(),simplify(),expand()/expandAll(),factor(),solve(). - Declarations & assignment:
declare(),declareType(),declareSequence(),assign(). - Reasoning:
assume(),ask(),verify(),forget(). - Compilation:
registerCompilationTarget(),getCompilationTarget(),listCompilationTargets(). - Configuration:
precision(default 21 significant digits;'machine'selects 64-bit float),tolerance(default1e-10),angularUnit(default'rad'),strict(validation depth),timeLimit(default 2000 ms),iterationLimit/recursionLimit(default 1024).
Internally these responsibilities are delegated to focused services
(engine-startup-coordinator.ts, engine-scope.ts, engine-declarations.ts,
engine-assumptions.ts, engine-numeric-configuration.ts,
engine-runtime-state.ts, engine-cache.ts,
engine-compilation-targets.ts, engine-extension-contracts.ts, …).
Scopes & assumptions. Symbol and operator definitions live in lexical scopes
with proper inheritance. Assumptions (e.g. "x > 0") are recorded per scope and
consulted during simplification and three-valued (true/false/undefined)
queries via ask() / verify(). The contract is detailed in the next section.
The assumptions subsystem (engine-assumptions.ts, assume.ts,
boxed-expression/constraint-subject.ts) lets callers state facts about
otherwise-free symbols and query them back. Its public surface is assume(),
ask(), verify(), and forget().
Predicate forms. assume() accepts a BoxedExpression, a MathJSON
expression, or a string (parsed as LaTeX — 'x > 0', '$x > 0$', or
'\pi > 0'; verify() accepts the same three forms). The supported predicates
are membership (Element / NotElement), (dis)equality (Equal / NotEqual),
the inequalities (Less / LessEqual / Greater / GreaterEqual), and their
conjunction (And). The predicate is canonicalized, then stored in a normalized
shape: inequalities as Less/LessEqual against 0 (e.g. x + y > 0 →
Less(Add(Negate(x), Negate(y)), 0)), equalities as Equal(lhs − rhs, 0). A
value assumption (assume(x = 5)) additionally installs a value binding for the
symbol. assume() returns 'ok', 'tautology' (already implied), or
'contradiction' (incompatible with existing facts).
Scoping. Assumptions are recorded in the current lexical scope
(ce.context.assumptions). A child scope sees the parent's facts (they are
copied on push) but discards its own additions on popScope(); a
subsequently-restored scope therefore recovers the parent's original facts. Any
mutation bumps the engine generation counter (ce._generation), which
invalidates the cached rule sets and the FactIndex (below) so stale sign/bound
answers cannot survive a scope change.
Three-valued discharge. verify(P) returns true when P is provable from
the current assumptions, false when its negation is provable, and undefined
when neither can be decided (Kleene semantics). It first evaluates the predicate
(so x < 0 reduces to False under assume(x > 0)), recurses through
And/Or/Not with Kleene combination, and finally consults the assumptions
DB directly (via ask) for opaque multi-symbol facts such as x·y > 0 that the
evaluator cannot reduce. ask(pattern) matches a pattern containing
wildcards (_k, _val, …) against the stored facts and returns the list of
binding substitutions; for a closed (wildcard-free) predicate it degrades to a
verify-style existence check. Recursion between the two (verify → ask → verify) is broken by an _isVerifying re-entrancy flag: while it is set,
ask skips its closed-predicate verify fallback.
Bounds & the FactIndex. Sign and bound queries are answered from a cached
FactIndex (getFactIndex, keyed on the generation counter, the assumptions
map identity, and the fact count). It maps each subject — a bare symbol or a
part extractor of one (Re(z), Im(τ), Abs(q), Argument(z)) — to its
numeric lower/upper bounds with strictness flags. assume() uses it for the
tautology/contradiction checks; the sign getters (isPositive, isNonNegative,
…) and verify() both read the same bounds, so they converge: an assumption
that fixes a sign is reflected identically whether queried through expr.sgn /
isPositive or through verify(Greater(x, 0)). A legacy linear scan remains
only as a fallback for facts the index does not capture.
forget() and provenance. forget(symbol) removes every stored fact that
references the symbol and resets any value the symbol received from an
assumption — declare() / assign() values are left intact. To tell the two
apart, assume-installed value bindings are tracked in a per-scope provenance set
(context.assumptionBindings); forget() (no argument) clears all facts and
undoes exactly those bindings, so a symbol assigned only via assume(x = …)
evaluates back to itself, while a user assign() survives.
Free functions. free-functions.ts exports top-level parse, simplify,
evaluate, N, expand, factor, solve, assign, etc., backed by a
lazily-created shared engine (getDefaultEngine()), so simple use needs no
explicit ComputeEngine setup.
src/compute-engine/library/ is the table of built-in operator and constant
definitions, organized by domain: core, arithmetic, trigonometry,
calculus, polynomials, logic, collections, sets, linear-algebra,
complex, combinatorics, number-theory, statistics, units, and more.
library.ts declares the domains, their requires dependencies, and a
topological sort so dependencies load first. Libraries can be loaded selectively,
and consumers can register their own (validated by the extension contracts).
src/compute-engine/symbolic/ implements rewriting:
simplify-rules.tsis the main rule set, supported by domain-specific simplifiers (simplify-sum.ts,simplify-product.ts,simplify-power.ts,simplify-trig.ts,simplify-log.ts,simplify-logic.ts, …).fu.ts/fu-transforms.ts/fu-cost.tsimplement the Fu et al. algorithm for automated trigonometric simplification, using a cost function to greedily reduce complexity.derivative.tsandantiderivative.tsprovide symbolic calculus;distribute.tshandles distribution.
Rules are pattern/predicate based and must avoid re-entrant .simplify() calls;
the recursion constraints are documented in
CLAUDE.md.
Symbolic capabilities beyond the built-in tables come from two optional,
separately-bundled plugins whose rule corpora live in data/:
- Rubi (
src/compute-engine/rubi/, corpus indata/rubi/, published as…/integration-rules) is a port of the Rubi rule-based integrator: a loader that compiles rule chapters into matchers, a normal-form module, and a driver that applies rules duringIntegrateevaluation. Rules ship in chapter bundles; chapter bundles assume the chapter-1 foundation rules are loaded first — loading, say, the trig chapters without ch1 silently degrades coverage rather than erroring. - Fungrim (
src/identities.ts, corpus indata/fungrim/, published as…/identities) provides curated identities and special values, loadable as simplify/solve rules vialoadIdentities(ce, { topics: […] }). The loader uses only the public engine API, so this bundle shares no engine code with the main entry.
Because these plugins re-bundle engine code, instanceof and
constructor.name checks fail across the host/plugin boundary — use string
checks like e.name === '…' (see
CLAUDE.md).
Numeric values are abstracted so the engine can stay exact when possible and fall back to floating or arbitrary precision when needed.
numeric-value/defines theNumericValueabstraction with three concrete variants:ExactNumericValue—(rational × √radical) + imaginary, keeping integers, rationals, and radicals exact.MachineNumericValue— IEEE-754 64-bit float (with Gaussian-integer imaginary part).BigNumericValue— arbitrary precision viaBigDecimal.
big-decimal/is a custom arbitrary-precision decimal type backed by nativebigint(significand × 10^exponent), with its own transcendental functions. It replaces the formerdecimal.jsdependency insrc/;decimal.jsnow appears only intest/big-decimal/cross-validation and benchmarks.numerics/holds the algorithms: rationals, primes/factorization, special functions (gamma, zeta, Bessel, …), statistics, interval helpers, and numeric integration/extrapolation. Complex numbers use thecomplex-esmpackage.
src/compute-engine/compilation/ turns a boxed expression into source code in a
target language. base-compiler.ts walks the expression tree language-agnostically
and delegates formatting to a LanguageTarget / CompileTarget
(types.ts). Built-in targets:
javascript-target.ts— executable JavaScript (withconstant-folding.ts).glsl-target.ts,wgsl-target.ts,gpu-target.ts— GPU shaders.python-target.ts— Python 3.interval-javascript-target.ts— JavaScript with interval arithmetic.interval-glsl-target.ts— GLSL with interval arithmetic (vec2intervals, outward rounding for float32 soundness); powerscompileExclusionShader()for implicit-curve plotting. Design notes indocs/INTERVAL_GLSL_PLAN.md.
Targets are registered in the engine's compilation-target registry and validated
by the extension contracts. Consumers can register custom targets via
ce.registerCompilationTarget().
The package ships several independently importable bundles (the exports map in
package.json). Each is built by scripts/build.mjs (esbuild) as ESM + UMD,
minified, with .d.ts types in dist/types/:
| Import path | Source | Contents |
|---|---|---|
@cortex-js/compute-engine |
src/compute-engine.ts |
Full: engine + LaTeX + compilation + free functions |
…/core |
src/core.ts |
Engine + free functions, no LaTeX/compilation |
…/math-json |
src/math-json.ts |
MathJSON types + utilities only |
…/latex-syntax |
src/latex-syntax.ts |
Standalone LaTeX parser/serializer |
…/numerics |
src/numerics.ts |
Numeric primitives (BigDecimal, rationals, special functions) |
…/interval |
src/interval.ts |
Interval arithmetic |
…/compile |
src/compile.ts |
Compilation targets |
…/identities |
src/identities.ts |
Fungrim identities loader (rules-as-data plugin) |
…/integration-rules |
src/integration-rules.ts |
Rubi rule-based integration plugin |
The full entry (compute-engine.ts) registers
ComputeEngine._latexSyntaxFactory and a default-engine factory that injects
LatexSyntax; the core entry registers a factory without it. This is the
mechanism that lets new ComputeEngine() support LaTeX when imported from the
full package while keeping LaTeX out of core bundles. (src/cortex.ts is an
experimental Cortex-language entry and is not part of the published exports.)
- Build:
npm run build(dev →/build),npm run build production(minified →/dist). Driven byscripts/build.sh→scripts/build.mjs(esbuild) plustsc --emitDeclarationOnlyfor types. - Type check:
npm run typecheck(run when completing a task). - Test:
npm run test compute-engine/<name>runstest/compute-engine/<name>.test.ts(Jest viaconfig/jest.config.cjs).npm run test snapshotupdates snapshots. - Lint:
npm run lint(ESLint + Prettier). - Dependency check:
npm run check:deps(madge— zero circular dependencies; ESLint also enforces the layering rules). - Docs:
npm run doc(TypeDoc +concat-md) regeneratessrc/api.md. It is generated — do not edit it by hand.
These properties are intentional and enforced; preserve them when contributing:
- Zero circular dependencies in
src/compute-engine(runtime and type-only), checked withmadge. See CLAUDE.md anddocs/architecture/ZERO-CYCLES-PLAN.md. - Layered imports: kernel types → wrappers → services → composition root;
no upward imports. Enforced by ESLint
import/no-restricted-paths. - LaTeX is optional: the engine depends on the structural
ILatexSyntaxinterface, never the concrete class. - Extension points are validated at runtime: custom libraries, compilation
targets, and
compile()options are shape-checked (extension contracts). - Public type surfaces avoid explicit
any. - Don't call
.simplify()from within simplification rules or functions they invoke (infinite-recursion risk).
README.md— installation, quick start, public API examples.CLAUDE.md— detailed conventions: comparison methods, creation modes, simplification/recursion rules, circular-dependency patterns.docs/architecture/CURRENT-ARCHITECTURE.md— module/service inventory, dependency rules, extension contracts.docs/— focused notes:LENIENT_PARSER.md,SIMPLIFY.md,NUMERIC-SERIALIZATION.md, and architecture/refactor plans.BUILD.md— build instructions.