Compute Engine API Reference
Compute Engine
AngularUnit
type AngularUnit = "rad" | "deg" | "grad" | "turn";
When a unitless value is passed to or returned from a trigonometric function, the angular unit of the value.
| Angular Unit | Description |
|---|---|
rad | radians, 2π radians is a full circle |
deg | degrees, 360 degrees is a full circle |
grad | gradians, 400 gradians is a full circle |
turn | turns, 1 turn is a full circle |
To change the angular unit used by the Compute Engine, use:
ce.angularUnit = 'deg';
AssignValue
type AssignValue = KernelAssignValue<Expression, ExpressionInput, IComputeEngine>;
Assignable value for ce.assign().
ExpressionComputeEngine
Compute engine surface used by expression types.
This interface is augmented by types-engine.ts with the concrete
IComputeEngine members to avoid type-layer circular dependencies.
Deprecated
Use ComputeEngine (the type exported from the package entry
points) or IComputeEngine instead — the three are interchangeable, and
this alias will be removed in a future release.
Extends
ExpressionComputeEngine.latexSyntax
readonly latexSyntax: ILatexSyntax | undefined;
The LatexSyntax instance used for LaTeX parsing/serialization.
undefined when no LatexSyntax was provided to the constructor and
the entry point has no LaTeX support (the core-only bundle).
To add a notation to a running engine, call
ce.latexSyntax.addEntries([...]): later parses and serializations use
the new entries. An engine created without the latexSyntax option
has its own instance, so the change applies to that engine only. An
instance given to several engines with the latexSyntax option is
shared: the change applies to all of them.
ExpressionComputeEngine.latexOptions
latexOptions: Partial<ParseLatexOptions & SerializeLatexOptions>;
Engine-wide LaTeX parse/serialize options (e.g. decimalSeparator).
Merged into every parse() and toLatex() call between LatexSyntax
defaults and per-call overrides.
ExpressionComputeEngine.True
readonly True: Expression;
ExpressionComputeEngine.False
readonly False: Expression;
ExpressionComputeEngine.Pi
readonly Pi: Expression;
ExpressionComputeEngine.E
readonly E: Expression;
ExpressionComputeEngine.Nothing
readonly Nothing: Expression;
ExpressionComputeEngine.Missing
readonly Missing: Expression;
The Missing symbol: an absent value whose position is preserved.
ExpressionComputeEngine.Zero
readonly Zero: Expression;
ExpressionComputeEngine.One
readonly One: Expression;
ExpressionComputeEngine.Half
readonly Half: Expression;
ExpressionComputeEngine.NegativeOne
readonly NegativeOne: Expression;
ExpressionComputeEngine.Two
readonly Two: Expression;
ExpressionComputeEngine.NaN
readonly NaN: Expression;
ExpressionComputeEngine.Indeterminate
readonly Indeterminate: Expression;
The exact answer to an indeterminate form such as 0/0: a number with
no value. Its double value is NaN, but it is a different value from
NaN, which is the result of a floating-point computation that failed.
Its numeric approximation (.N()) is NaN.
ExpressionComputeEngine.PositiveInfinity
readonly PositiveInfinity: Expression;
ExpressionComputeEngine.NegativeInfinity
readonly NegativeInfinity: Expression;
ExpressionComputeEngine.ComplexInfinity
readonly ComplexInfinity: Expression;
ExpressionComputeEngine.context
readonly context: EvalContext;
ExpressionComputeEngine.contextStack
contextStack: readonly EvalContext[];
ExpressionComputeEngine.iterationLimit
iterationLimit: number;
ExpressionComputeEngine.recursionLimit
recursionLimit: number;
ExpressionComputeEngine.maxCollectionSize
maxCollectionSize: number;
ExpressionComputeEngine.bignum
bignum: (a) => BigDecimal;
ExpressionComputeEngine.complex
complex: (a, b?) => Complex;
ExpressionComputeEngine.tolerance
tolerance: number;
ExpressionComputeEngine.angularUnit
angularUnit: AngularUnit;
ExpressionComputeEngine.costFunction
costFunction: (expr) => number;
ExpressionComputeEngine.simplificationRules
simplificationRules: Rule[];
The rules used by .simplify() when no explicit rules option is passed.
Initialized to the built-in simplification rules.
Users can push() additional rules or replace the entire array.
ExpressionComputeEngine.solveRules
solveRules: Rule[];
The rules used by solve() to find roots of univariate expressions.
Each rule matches a normalized equation f(_x) = 0 — the unknown is
the wildcard _x — and replace produces a root expression.
Conditions should reject matches where other wildcards capture _x.
Candidate roots are validated against the original equation, so an
over-eager template degrades to a no-op rather than a wrong answer.
Initialized to the built-in root-finding rules; push() to extend,
assign to replace.
ExpressionComputeEngine.harmonizationRules
harmonizationRules: Rule[];
The rules used by solve() to transform an equation into equivalent,
easier-to-solve forms before root-finding (e.g. ln f(x) → f(x) - 1).
Same conventions and extension pattern as solveRules.
ExpressionComputeEngine.strict
strict: boolean;
ExpressionComputeEngine.jit
jit: "auto" | "off";
Whether the engine may implicitly generate and execute compiled code as
a performance optimization (auto-compiled Map drains, compiled numeric
quadrature/limit kernels). 'auto' (default) attempts implicit
compilation and latches to 'off' engine-wide on the first CSP
EvalError; 'off' never attempts it. Explicit compile() is exempt.
ExpressionComputeEngine.trace
trace: readonly string[];
A list of the function calls to the current evaluation context
ExpressionComputeEngine.effects
get effects(): EffectHandlers
set effects(handlers: EffectHandlerOverrides): void
The host capabilities of this engine: the handlers the library operators
use to reach the host. Print and Input use effects.console.
The registry is an immutable object. Reading returns the registry that a
new evaluation would use. Assigning installs a new registry: the assigned
object is a COMPLETE description — a handler it does not mention returns
to its default, so ce.effects = {} restores every default. A null
handler denies the capability: an operator that needs it evaluates to an
Error("capability-denied", …) value.
const lines: string[] = [];
ce.effects = {
console: { log: (line) => lines.push(line), readLine: () => undefined },
};
Each evaluation (evaluate(), N(), evaluateAsync()) uses the registry
that was installed when it started. An assignment does not change the
handlers of an evaluation that is already running.
For a change that must last for one block of code only, use
withEffects.
ExpressionComputeEngine.precision
get precision(): number
set precision(p: number | "auto" | "machine"): void
ExpressionComputeEngine.checkpoint()
checkpoint(label?): EngineCheckpoint
Take a checkpoint of the engine's state at a quiescent point — between
statements, at any scope depth — so a later restore can rewind
to it. Legal on a freshly constructed engine, which is how a client gets
a cp[0] covering an edit of the first cell, and inside a host-pushed
scope, which is how a notebook takes per-cell checkpoints within a pass.
A checkpoint taken inside a scope dies when that scope pops. Throws a
CheckpointError when the engine is mid-evaluation or mid-pre-pass;
restore additionally requires the same scope stack the
checkpoint was taken on.
####### label?
string
ExpressionComputeEngine.restore()
restore(cp): void
Rewind to cp, invalidating every checkpoint taken after it; cp itself
stays live and can be restored again. Expressions built BEFORE cp stay
valid — their definitions are rewritten in place. Expressions built
during the rewound window are not: cache cell outputs as serialized
artifacts, never as live boxed nodes.
####### cp
ExpressionComputeEngine.discard()
discard(cp): void
Release cp's restore capability. Restoring past a discarded INTERIOR
checkpoint stays possible through any earlier live one; discarding the
OLDEST makes the state before the next-younger one unreachable.
####### cp
ExpressionComputeEngine.declareProtocol()
declareProtocol(name, members): void
Declare a protocol (Appendix A "Host API"). Throws on error, including on re-declaration — the Epsil statement route replaces instead (P5).
####### name
string
####### members
ExpressionComputeEngine.declareProtocolImplementation()
declareProtocolImplementation(
type,
protocol,
impl,
options?): void
Implement protocol for type, declaring the conformance edge if it is
not already registered (Appendix A "Host API").
THROWS on every error — the host channel; the Epsil statement route returns error VALUES instead. A second host implementation of the same (type, protocol) pair throws rather than replacing (P5).
The callbacks are JavaScript functions, so they carry no signature the
engine can check: they are trusted like host-declared operator handlers,
and only member-name coverage, unknown members and a set handler on a
readonly property are validated.
options.where declares a CONDITIONAL conformance: type is then a HEAD
PATTERN naming the variables ('list<T>') and where is the clause SOURCE
that binds them ('where T is Comparable'; the where word may be
omitted). A malformed clause, or a head variable the clause does not bind,
throws.
####### type
string
####### protocol
string
####### impl
####### options?
####### where?
string
ExpressionComputeEngine.conformsTo()
conformsTo(type, protocol): boolean
Whether type conforms to protocol, answered without calling any of
the protocol's members. An unknown protocol answers false.
Inheritance included: a conformance registered for a supertype answers
for its subtypes. A CONDITIONAL conformance (list<T> is P where T is P) recurses, deciding itself against type's own arguments.
type may be a TypeString, parsed the way IComputeEngine.type
parses one.
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
####### protocol
string
ExpressionComputeEngine.withTimeLimit()
withTimeLimit<T>(limit, fn): T
Run fn with at most ms milliseconds (numeric form) or limit.ms
(object form, which also accepts an attribution label). A tighter
enclosing span preempts this limit; use the label and
CancellationError.attribution/spans to tell which limit fired.
⚠️ fn MUST be synchronous. The span is restored in a synchronous
finally, so a Promise-returning (async) callback hands control back
at its first await while the span is still open: work that resumes after
that point runs outside the deadline and is never cancelled (see
docs/TIMEOUT-MODEL.md §6.4). For asynchronous cancellation use
expr.evaluateAsync({ signal }) with an AbortSignal instead.
• T
####### limit
| number
| {
ms: number;
label: string;
}
####### fn
() => T extends Promise<unknown> ? never : T
ExpressionComputeEngine.withStepBudget()
withStepBudget<T>(limit, fn): T
Run fn with at most limit.steps steps of engine work: a hang guard
that fires at the same point on every machine, unlike a wall-clock
limit. A step is one of the engine's cooperative cancellation checks —
an opaque unit, deterministic for one computation on one engine state,
but not a measure of cost and not comparable across engine versions;
tune the budget empirically and keep a withTimeLimit span outside it.
A spent budget throws a CancellationError with cause: 'step-budget'
and the span's label as its attribution.
⚠️ fn MUST be synchronous, as for withTimeLimit.
• T
####### limit
####### steps
number
####### label?
string
####### fn
() => T extends Promise<unknown> ? never : T
ExpressionComputeEngine.withEffects()
withEffects<T>(overrides, fn): T
Run fn with some host capabilities replaced or denied, then put the
previous ones back. Evaluations that START inside fn use the changed
registry.
overrides is applied on top of the registry in effect when
withEffects is called, so calls nest: a capability an inner call does
not mention keeps the handler of the outer call. A null value denies the
capability, even if it has a default handler — this is how to evaluate an
expression that is not trusted:
const result = ce.withEffects({ console: null }, () => expr.evaluate());
The previous registry is put back when fn returns or throws. If fn
returns a promise, it is put back when that promise settles (fulfilled or
rejected), and withEffects returns a promise that settles the same way.
An asynchronous evaluation keeps the registry it started with, so an
evaluation that started BEFORE withEffects was called is not changed by
it. But while the promise of an asynchronous fn is pending, the changed
registry is the installed one: an unrelated evaluation that starts during
that time, from other code, also uses it. Start such evaluations before
calling withEffects, or use a separate engine.
• T
####### overrides
####### fn
() => T
ExpressionComputeEngine.chop()
chop(n)
chop(n): number
####### n
number
chop(n)
chop(n): 0 | BigDecimal
####### n
BigDecimal
chop(n)
chop(n): number | BigDecimal
####### n
number | BigDecimal
ExpressionComputeEngine.expr()
expr(expr, options?): Expression
####### expr
| NumericValue
| ExpressionInput
####### options?
####### form?
####### scope?
Scope
ExpressionComputeEngine.box()
box(expr, options?): Expression
####### expr
| NumericValue
| ExpressionInput
####### options?
####### form?
####### scope?
Scope
Deprecated
Use expr() instead.
ExpressionComputeEngine.rebind()
rebind(expr, options?): Expression
Rebuild expr as if ce.expr(expr.json, { form, scope }) had been
called — every symbol resolves afresh in scope (or the current scope)
— without serializing expr to MathJSON.
ce.expr(expr, { scope }) on an already-boxed expression keeps the
bindings the expression was boxed with; it never re-resolves a symbol.
This is the operation that does. Use it when an expression built under
one set of declarations must be read under another: a body boxed in a
shadow scope, a row re-classified after a declaration changed.
For the canonical and partial forms the MathJSON is built as a DAG — one
array per DISTINCT function node, shared by every parent that reads it
(a leaf contributes its own constant-size MathJSON) — where
expr.json writes a tree, one copy of a shared node per path. That
MathJSON is then boxed by the ordinary route, so the result matches
ce.expr(expr.json, …) by construction, including for an expression
that already holds an Error node. Canonical boxing still visits every
path, as it does for any MathJSON. The raw and structural forms
canonicalize nothing, so each distinct node is rebuilt once and a shared
sub-expression stays shared in the result as well.
form:'canonical'(default),'structural','raw', or a partial form such as['Flatten', 'Order'].scope: the lexical scope the rebuild resolves and declares in.
Verbatim LaTeX and source positions are dropped, as the MathJSON route drops them. A mutable object is rebuilt as its record snapshot, as that route boxes it.
####### expr
####### options?
####### form?
####### scope?
Scope
ExpressionComputeEngine.parse()
parse(latex, options)
parse(latex, options?): Expression
Parse a LaTeX string and return a boxed expression.
This is a convenience method equivalent to ce.expr(parse(latex)),
but uses the engine's symbol definitions for better parsing accuracy.
options.scope RECEIVES the parse's writes: the whole parse runs with
that scope as the current lexical scope, so name resolution (including
the parser's symbol oracle) walks scope → parents, and every
auto-declare and inference lands rooted there. Discarding the scope
discards the writes. Use ce.createScope() to make one that can be read
back.
options.speculative leaves NO trace in the engine's type state: the
parse runs inside a transient scope (auto-declares land there and are
discarded with it), and every ambient symbol whose type is currently
inferred is shadowed in that scope with its current type — so a
narrowing use in latex refines the discarded shadow instead of
persistently narrowing the ambient symbol. Use it for derive-style
parses that only READ the result (its type, structure, or
serialization): the result's bindings refer to the discarded scope, so
do not retain, evaluate, or compare it against later expressions.
Mutually exclusive with scope.
####### latex
string
####### options?
Partial<ParseLatexOptions> & {
form: FormOption;
scope: Scope;
speculative: boolean;
}
parse(latex, options)
parse(latex, options?): Expression | null
####### latex
string | null
####### options?
Partial<ParseLatexOptions> & {
form: FormOption;
scope: Scope;
speculative: boolean;
}
ExpressionComputeEngine.appliedNonFunctions()
appliedNonFunctions(latex): string[]
The symbols that appear in function-application syntax f(…) in latex
but are not defined as functions in the current scope (so they parse as
implicit multiplication or are left unresolved). Scope-aware and
side-effect-free. Intended to flag calls to undefined functions in tools
such as notebooks; intersect with Expression.freeVariables
to drop deliberate multiplication of defined values.
Only parenthesized-group application is detected: a symbol juxtaposed
with a matrix environment (\mathrm{Eigenvalues}\begin{pmatrix}…) is
not reported, since a matrix never reaches the symbol-with-delimiter
juxtaposition analysis.
####### latex
string
ExpressionComputeEngine.function()
function(name, ops, options?): Expression
####### name
string
####### ops
readonly ExpressionInput[]
####### options?
####### metadata?
####### form?
####### scope?
Scope
ExpressionComputeEngine._getCompilationTarget()
_getCompilationTarget(name)
_getCompilationTarget(name):
| JavaScriptCompilationTarget<Expression>
| undefined
####### name
"javascript"
_getCompilationTarget(name)
_getCompilationTarget(name):
| LanguageTarget<Expression, string, unknown, number>
| undefined
####### name
string
ExpressionComputeEngine.number()
number(value, options)
number(value, options?): Expression
Create a complex number from its real part and its imaginary part, each
a JavaScript number or a BigDecimal.
When the engine works above machine precision (ce.precision greater
than 15), a BigDecimal part is kept at the precision it holds: a part
too small or too large for a double (1e-800, 1e800) or with more
than 16 significant digits is not rounded to a double. This is the
lossless alternative to ce.number(ce.complex(re, im)): ce.complex()
returns a Complex object, whose parts are always doubles. At machine
precision, both parts are rounded to doubles: there
{ re: ce.bignum('1e-800'), im: ce.bignum(2) } gives 2i.
When the imaginary part is zero (a number or a BigDecimal), the
result is a real number.
When both parts are integers, the result is the EXACT Gaussian integer,
as ce.number(2) is the exact 2: a part is an integer when it is a
number that is a safe integer, or an integer-valued BigDecimal whose
exponent is at most 10^6 (also at machine precision, and also outside
the double range). When a part has a fraction, is a number past the
safe integers, or is a BigDecimal with a larger exponent (1e2000000),
the result is a float. The same rule applies to a Complex given to
ce.number() or ce.box(): ce.number(new Complex(2, 3)) is the exact
2+3i, ce.number(new Complex(2.5, 3)) is a float.
ce.precision = 30;
ce.number({ re: ce.bignum('1e-800'), im: ce.bignum(2) });
// ➔ a complex number with the real part 1e-800 and the imaginary part 2
ce.number({ re: 1, im: 0 });
// ➔ 1
ce.number({ re: 2, im: 3 }).isExact;
// ➔ true
ce.number({ re: 2.5, im: 3 }).isExact;
// ➔ false
####### value
####### re
number | BigDecimal
####### im
number | BigDecimal
####### options?
####### metadata?
####### canonical?
number(value, options)
number(value, options?): Expression
####### value
| string
| number
| bigint
| MathJsonNumberObject
| BigDecimal
| Rational
| NumericValue
| Complex
####### options?
####### metadata?
####### canonical?
ExpressionComputeEngine.symbol()
symbol(sym, options?): Expression
####### sym
string
####### options?
####### canonical?
####### metadata?
####### autoDeclare?
boolean
ExpressionComputeEngine.character()
character(s, metadata?): Expression
Create a boxed character — one user-perceived character.
s must be exactly one grapheme cluster after NFC normalization; use the
CharacterFrom operator when the content is not known to satisfy that, as
it reports a diagnostic instead.
####### s
string
####### metadata?
ExpressionComputeEngine.error()
error(message, where?): Expression
####### message
string | string[]
####### where?
string
ExpressionComputeEngine.typeError()
typeError(expectedType, actualType, where?): Expression
####### expectedType
####### actualType
| Type
| BoxedType
| undefined
####### where?
ExpressionComputeEngine.tuple()
tuple(elements)
tuple(...elements): Expression
####### elements
...readonly number[]
tuple(elements)
tuple(...elements): Expression
####### elements
...readonly Expression[]
ExpressionComputeEngine.list()
list(values): Expression
A List of numbers, built without boxing each element.
The elements are copied into a frozen array of machine numbers that the
list keeps as its store: count, at, type, isSame and array
answer from it, and the boxed operands are built only if ops is read.
The result is an ordinary canonical List in every other respect.
values may be a number[], a Float64Array or any array-like of
numbers. Its length must be a non-negative safe integer and each
element a JS number; anything else throws a TypeError. -0 is stored
as +0.
Use it to hand a large numeric list to the engine cheaply, and read it
back with expr.array.
####### values
ArrayLike<number>
ExpressionComputeEngine.type()
type(type): BoxedType
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
ExpressionComputeEngine.rules()
rules(rules, options?): BoxedRuleSet
####### rules
Rule | readonly Rule | BoxedRule[] | BoxedRuleSet | null | undefined
####### options?
####### canonical?
boolean
####### purpose?
Default purpose applied to any rule in the set that doesn't carry
its own purpose tag (a per-rule tag takes precedence).
ExpressionComputeEngine.getRuleSet()
getRuleSet(id?): BoxedRuleSet | undefined
####### id?
"harmonization" | "solve-univariate" | "standard-simplification"
ExpressionComputeEngine.pushScope()
pushScope(scope?, name?): void
####### scope?
Scope
####### name?
string
ExpressionComputeEngine.popScope()
popScope(): void
ExpressionComputeEngine.createScope()
createScope(bindings?, parent?): InspectableScope
####### bindings?
Record<string,
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| TaggedValueDefinition
| TaggedOperatorDefinition>
####### parent?
Scope
ExpressionComputeEngine.lookupDefinition()
lookupDefinition(id): BoxedDefinition | undefined
####### id
string
ExpressionComputeEngine.assign()
assign(ids)
assign(ids): IComputeEngine
####### ids
assign(id, value)
assign(id, value): IComputeEngine
####### id
string
####### value
AssignValue
assign(arg1, arg2)
assign(arg1, arg2?): IComputeEngine
####### arg1
string | {}
####### arg2?
AssignValue
ExpressionComputeEngine.declareType()
declareType(name, type, options?): void
####### name
string
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
####### options?
####### alias?
boolean
####### fromStatement?
boolean
####### mint?
boolean
####### typeParams?
ExpressionComputeEngine.declare()
declare(symbols)
declare(symbols): IComputeEngine
####### symbols
declare(id, type, scope)
declare(id, type, scope?): IComputeEngine
####### id
string
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
####### scope?
| Scope
| DeclareOptions & {
extend: false;
}
declare(id, patch, options)
declare(id, patch, options): IComputeEngine
####### id
string
####### patch
####### options
DeclareOptions & {
extend: true;
}
declare(id, def, scope)
declare(id, def, scope?): IComputeEngine
####### id
string
####### def
####### scope?
| Scope
| DeclareOptions & {
extend: false;
}
declare(arg1, arg2, arg3)
declare(arg1, arg2?, arg3?): IComputeEngine
####### arg1
string | {}
####### arg2?
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| Partial<OnlyFirst<ValueDefinition, BaseDefinition & {
holdUntil: "never" | "evaluate" | "N";
type: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferred: boolean;
effectsDeclared: boolean;
value: | ExpressionInput
| ((ce) => Expression | null);
eq: (a) => boolean | undefined;
neq: (a) => boolean | undefined;
cmp: (a) => "<" | ">" | "=" | undefined;
collection: CollectionHandlers;
subscriptEvaluate: (subscript, options) => Expression | undefined;
} & Partial<BaseDefinition> & Partial<OperatorDefinitionFlags> & {
type: OperatorTypeHandlerOnTypes;
signature: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferredSignature: boolean;
sgn: (ops, options) => Sign | undefined;
isPositive: boolean;
isNonNegative: boolean;
isNegative: boolean;
isNonPositive: boolean;
even: (ops, options) => boolean | undefined;
complexity: number;
canonical: (ops, options) => Expression | null;
evaluate: | Expression
| ((ops, options) => Expression | undefined);
evaluateAsync: (ops, options) => Promise<Expression | undefined>;
evalDimension: (args, options) => Expression;
derivative: OperatorDerivative;
compile: OperatorCompileHandler;
eq: (a, b, prover?) => boolean | undefined;
neq: (a, b) => boolean | undefined;
collection: CollectionHandlers;
canEnumerate: (expr) => boolean | undefined;
elementCount: (expr) => number | undefined;
inferOperandTypes: (ops, requirement) =>
| readonly (Type | undefined)[]
| undefined;
}>>
| Partial<OnlyFirst<OperatorDefinition, BaseDefinition & {
holdUntil: "never" | "evaluate" | "N";
type: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferred: boolean;
effectsDeclared: boolean;
value: | ExpressionInput
| ((ce) => Expression | null);
eq: (a) => boolean | undefined;
neq: (a) => boolean | undefined;
cmp: (a) => "<" | ">" | "=" | undefined;
collection: CollectionHandlers;
subscriptEvaluate: (subscript, options) => Expression | undefined;
} & Partial<BaseDefinition> & Partial<OperatorDefinitionFlags> & {
type: OperatorTypeHandlerOnTypes;
signature: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferredSignature: boolean;
sgn: (ops, options) => Sign | undefined;
isPositive: boolean;
isNonNegative: boolean;
isNegative: boolean;
isNonPositive: boolean;
even: (ops, options) => boolean | undefined;
complexity: number;
canonical: (ops, options) => Expression | null;
evaluate: | Expression
| ((ops, options) => Expression | undefined);
evaluateAsync: (ops, options) => Promise<Expression | undefined>;
evalDimension: (args, options) => Expression;
derivative: OperatorDerivative;
compile: OperatorCompileHandler;
eq: (a, b, prover?) => boolean | undefined;
neq: (a, b) => boolean | undefined;
collection: CollectionHandlers;
canEnumerate: (expr) => boolean | undefined;
elementCount: (expr) => number | undefined;
inferOperandTypes: (ops, requirement) =>
| readonly (Type | undefined)[]
| undefined;
}>>
| BoxedOperatorDefinition
| OperatorDefinitionPatch
####### arg3?
Scope | DeclareOptions
ExpressionComputeEngine.loadLibrary()
loadLibrary(library): IComputeEngine
Load a library on an engine that is already constructed. Its
definitions are declared in the global scope, as with ce.declare(),
and its name is recorded (see libraryOf()). Each library in its
requires list must already be loaded.
####### library
ExpressionComputeEngine.libraryOf()
libraryOf(name): string | undefined
The name of the library whose definition name resolves to in the
current scope ('trigonometry' for Sin), or undefined for a name
that no library defines or that a declaration shadows.
####### name
string
ExpressionComputeEngine.assume()
assume(predicate): AssumeResult
####### predicate
string | Expression
ExpressionComputeEngine.declareSequence()
declareSequence(name, def): IComputeEngine
Declare a sequence with a recurrence relation.
####### name
string
####### def
Example
// Fibonacci sequence
ce.declareSequence('F', {
base: { 0: 0, 1: 1 },
recurrence: 'F_{n-1} + F_{n-2}',
});
ce.parse('F_{10}').evaluate(); // → 55
ExpressionComputeEngine.getSequenceStatus()
getSequenceStatus(name): SequenceStatus
Get the status of a sequence definition.
####### name
string
Example
ce.parse('F_0 := 0').evaluate();
ce.getSequenceStatus('F');
// → { status: 'pending', hasBase: true, hasRecurrence: false, baseIndices: [0] }
ExpressionComputeEngine.getSequence()
getSequence(name): SequenceInfo | undefined
Get information about a defined sequence.
Returns undefined if the symbol is not a sequence.
####### name
string
ExpressionComputeEngine.listSequences()
listSequences(): string[]
List all defined sequences. Returns an array of sequence names.
ExpressionComputeEngine.isSequence()
isSequence(name): boolean
Check if a symbol is a defined sequence.
####### name
string
ExpressionComputeEngine.clearSequenceCache()
clearSequenceCache(name?): void
Clear the memoization cache for a sequence. If no name is provided, clears caches for all sequences.
####### name?
string
ExpressionComputeEngine.getSequenceCache()
getSequenceCache(name):
| Map<string | number, Expression>
| undefined
Get the memoization cache for a sequence.
Returns a Map of index → value, or undefined if not a sequence or memoization is disabled.
For single-index sequences, keys are numbers. For multi-index sequences, keys are comma-separated strings (e.g., '5,2').
####### name
string
ExpressionComputeEngine.getSequenceTerms()
getSequenceTerms(
name,
start,
end,
step?): Expression[] | undefined
Generate a list of sequence terms from start to end (inclusive).
####### name
string
The sequence name
####### start
number
Starting index (inclusive)
####### end
number
Ending index (inclusive)
####### step?
number
Step size (default: 1)
Example
ce.declareSequence('F', { base: { 0: 0, 1: 1 }, recurrence: 'F_{n-1} + F_{n-2}' });
ce.getSequenceTerms('F', 0, 10);
// → [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
ExpressionComputeEngine.lookupOEIS()
lookupOEIS(terms, options?): Promise<OEISSequenceInfo[]>
Look up sequences in OEIS by their terms.
####### terms
(number | Expression)[]
Array of sequence terms to search for
####### options?
Optional configuration (timeout, maxResults)
Example
const results = await ce.lookupOEIS([0, 1, 1, 2, 3, 5, 8, 13]);
// → [{ id: 'A000045', name: 'Fibonacci numbers', ... }]
ExpressionComputeEngine.checkSequenceOEIS()
checkSequenceOEIS(name, count?, options?): Promise<{
matches: OEISSequenceInfo[];
terms: number[];
}>
Check if a defined sequence matches an OEIS sequence.
####### name
string
Name of the defined sequence
####### count?
number
Number of terms to check (default: 10)
####### options?
Optional configuration
Example
ce.declareSequence('F', { base: { 0: 0, 1: 1 }, recurrence: 'F_{n-1} + F_{n-2}' });
const result = await ce.checkSequenceOEIS('F', 10);
// → { matches: [{ id: 'A000045', name: 'Fibonacci numbers', ... }], terms: [0, 1, 1, ...] }
ExpressionComputeEngine.interpret()
interpret(expr, options?): Promise<InterpretResult>
Interpret a notational expression, then propose OEIS-attributed closed
forms for it (the async v4 of the Interpret ladder).
result.expression is exactly what the synchronous Interpret head
returns (a Sum/Product, or the input unchanged); result.candidates
are OEIS-attributed closed forms, each verified to reproduce every
extracted sample exactly. This is the only interpretation path that
performs a network lookup. Too few samples, being offline, a timeout, or an
empty result all yield an empty candidate list rather than a rejection.
####### expr
The (typically inert, continuation-bearing) expression
####### options?
OEIS request options (timeout, maxResults)
Example
const { expression, candidates } = await ce.interpret(
ce.parse('1 + 3 + 6 + 10 + \\cdots + n')
);
ExpressionComputeEngine.operatorInfo()
operatorInfo(head): OperatorInfo | undefined
Introspect a registered operator head.
Returns undefined if no definition is registered in this engine.
Otherwise returns { kind, signature? } where kind is 'function'
when the operator has an evaluate or collection handler, and
'opaque' when it is declared as a typed-but-opaque node (e.g.,
Triangle, Sphere).
Use this to classify heads encountered in parsed MathJSON without maintaining a parallel list of "known" operators.
####### head
string
ExpressionComputeEngine.normalizeIdentifier()
normalizeIdentifier(latex): string
Convert a LaTeX identifier string to its canonical MathJSON name without declaring the symbol in the engine scope.
Examples:
'R_{3}'→'R_3''\\theta_x'→'theta_x''\\alpha'→'alpha''1 + 2'→''(not an identifier)
Use this instead of ce.parse(latex).symbol when you need the canonical
name without the side-effect of auto-declaring the symbol.
####### latex
string
ExpressionComputeEngine.symbolInfo()
symbolInfo(name): SymbolInfo | undefined
Return introspection metadata for a symbol (value definition) in the current scope chain.
kind: 'constant'when the symbol is a CE-registered constant (e.g.Pi,True,ExponentialE).kind: 'variable'for declared but non-constant value symbols (e.g. afterce.declare('a', 'real')).
Returns undefined for unknown names and for names that resolve to
operator/function definitions (use operatorInfo() for those — the
two methods are non-overlapping).
####### name
string
ExpressionComputeEngine.searchDefinitions()
searchDefinitions(query, options?): DefinitionSearchResult[]
Reverse library search: map plain-text concept keywords to a ranked list of matching identifiers in the current scope chain (standard library plus any user declarations).
The query is a string (tokenized on whitespace) or an array of strings; tokens are OR-ed — a definition matches when any token matches — and definitions matching more tokens, or matching them more exactly, rank higher.
Every returned id resolves via ce.lookupDefinition(id); chain that
call for full detail.
####### query
string | string[]
####### options?
####### limit?
number
ExpressionComputeEngine.suggestOperatorName()
suggestOperatorName(name): string | undefined
Given a name that is not a known operator, return the closest known
operator name — a "did you mean" suggestion — or undefined when nothing
is close enough. Powers the Epsil unknown-function diagnostic.
Matching is conservative and applied in priority order (first match wins): case-insensitive exact match, singular/plural, Damerau–Levenshtein distance (≤ 2 for names of length ≥ 6, ≤ 1 for length 5, never for shorter names), then a prefix match against exactly one operator. Ties prefer the candidate sharing the longest prefix with the query.
ce.suggestOperatorName('Quartile'); // → 'Quartiles'
ce.suggestOperatorName('foo'); // → undefined
####### name
string
ExpressionComputeEngine.functionProperties()
functionProperties(name): FunctionProperties | undefined
Return the known analytic properties of an operator — poles, zeros, branch
points/cuts, residues, holomorphic/meromorphic domains — drawn from the
Fungrim-derived metadata store, or undefined if none are recorded.
ce.functionProperties('Gamma')?.poles?.toString(); // 'NonPositiveIntegers'
The set-valued accessors (poles, zeros, ...) return a boxed set for the
unconditional record of that kind; parametric / conditional records (e.g.
residues that depend on parameters) are available via entries.
####### name
string
ExpressionComputeEngine.contourIntegrate()
contourIntegrate(integrand, variable, contour): ContourIntegralResult
Integrate over a circle, simple polygon, or the entire real line by the
residue theorem. Real-line contours also accept an explicit principal value.
Returns pole classifications, residues, their sum, and the integral.
Unsupported or undecidable inputs have no value; boundary poles have
status pole-on-contour. See ContourInput for contour forms.
####### integrand
####### variable
string
####### contour
Boxed Expression
SimplifyOptions
type SimplifyOptions = {
rules: null | Rule | ReadonlyArray<BoxedRule | Rule> | BoxedRuleSet;
costFunction: (expr) => number;
strategy: "default" | "fu" | "trig";
};
Options for Expression.simplify()
ExplainOptions
type ExplainOptions = SimplifyOptions & {
verbosity: ExplainVerbosity;
variable: string | string[];
order: number;
};
Options for Expression.explain()
In addition to the SimplifyOptions (honored when explaining a
'simplify' operation, so that explain('simplify', options).result
matches simplify(options)):
verbosity:'default'returns the curated step chain (bookkeeping steps filtered out);'all'returns the raw, uncurated chain.variable: the unknown, for the'solve'and'D'operations. For a system of equations (explain('solve')on aList/And), pass the unknowns as an array, in order.order: for the'D'operation only, the order of the derivative to explain (the n-th derivative with respect tovariable). Defaults to1. Ignored when the receiver is already aD(…)expression (which encodes its own differentiation sequence).
EvaluateOptions
type EvaluateOptions = KernelEvaluateOptions;
Options for evaluating boxed expressions.
This is the compute-engine-specialized form of the generic kernel type.
IntervalBounds
type IntervalBounds = {
lower: Expression;
lowerStrict: boolean;
upper: Expression;
upperStrict: boolean;
};
Lower and upper bounds for a symbol extracted from a domain restriction.
lowerStrict/upperStrict are true for strict (<, >) bounds and
false (or undefined) for non-strict (≤, ≥) bounds.
NumberLiteralInterface
Narrowed interface for number literal expressions.
Obtained via isNumber().
NumberLiteralInterface.numericValue
readonly numericValue: number | NumericValue;
NumberLiteralInterface.isExact
readonly isExact: boolean;
NumberLiteralInterface.isComplex
readonly isComplex: boolean;
True if the imaginary part of this number is not zero.
Unlike a test of im !== 0, this is true for an imaginary part too
small or too large for a double (the exact 10^{-800}·i), because it is
read from the numeric value itself, not from its double projection
im.
NumberLiteralInterface.isNumberLiteral
readonly isNumberLiteral: true;
SymbolInterface
Narrowed interface for symbol expressions.
Obtained via isSymbol().
SymbolInterface.symbol
readonly symbol: string;
FunctionInterface
Narrowed interface for function expressions.
Obtained via isFunction().
FunctionInterface.isFunctionExpression
readonly isFunctionExpression: true;
FunctionInterface.ops
readonly ops: readonly Expression[];
FunctionInterface.nops
readonly nops: number;
FunctionInterface._numericStore
readonly _numericStore: readonly number[] | undefined;
Internal. The numeric store of a List built by ce.list(): its
elements as frozen machine numbers, from which the operands are boxed on
the first read of ops. undefined for every other function
expression. A walker that only looks for symbols or effects skips a node
with a store instead of reading ops, which would box every element.
The public view is array.
FunctionInterface._numericStoreFloats
readonly _numericStoreFloats: boolean;
Internal. Are the integer-valued elements of the numeric store floats?
true when a float computation produced the store (2·L with the
element 0.5 holds the float 1): the operand at that position is
then a float, not engine.number(store[i]). false for ce.list()
and when there is no store.
FunctionInterface._machineFloats
readonly _machineFloats: boolean | undefined;
Internal. The exactness of the integer-valued elements of a List of
machine numbers: false exact, true floats, undefined when the
list is not a list of machine numbers or mixes both kinds. See
BoxedFunction._machineFloats.
FunctionInterface.op1
readonly op1: Expression;
FunctionInterface.op2
readonly op2: Expression;
FunctionInterface.op3
readonly op3: Expression;
FunctionInterface._isLiteralData()
_isLiteralData(): boolean
Internal. Is this node written-out DATA: a canonical List or Tuple
bound to the standard library whose every element is a number literal,
or such a List or Tuple in turn? Such a node holds no symbol and
evaluates to itself. The answer is computed once per node.
StringInterface
Narrowed interface for string expressions.
Obtained via isString().
StringInterface.string
readonly string: string;
StringInterface.buffer
readonly buffer: Uint8Array;
The UTF-8 encoding of the string, as a byte buffer.
StringInterface.unicodeScalars
readonly unicodeScalars: number[];
The Unicode scalar values (code points) of the string.
CharacterInterface
Narrowed interface for a character expression — one NFC-normalized grapheme cluster (UAX #29).
Obtained via isCharacter().
string holds the cluster's content and is deliberately spelled the same as
StringInterface.string, so a consumer that only needs the text (the
String interpolation join, StringJoin) can read either kind through one
property without first deciding which it has.
CharacterInterface.string
readonly string: string;
The content of the character: exactly one grapheme cluster.
CharacterInterface.unicodeScalars
readonly unicodeScalars: number[];
The Unicode scalar values (code points) of the cluster.
TensorInterface
Narrowed interface for tensor expressions.
Obtained via isTensor().
TensorInterface.shape
readonly shape: number[];
TensorInterface.rank
readonly rank: number;
CollectionInterface
Narrowed interface for collection expressions.
Obtained via isCollection().
Extended by
CollectionInterface.isCollection
readonly isCollection: true;
CollectionInterface.count
readonly count: number | undefined;
CollectionInterface.isFiniteCollection
readonly isFiniteCollection: boolean | undefined;
CollectionInterface.isEmptyCollection
readonly isEmptyCollection: boolean | undefined;
CollectionInterface.isEnumerableCollection
readonly isEnumerableCollection: boolean | undefined;
CollectionInterface.each()
each(): Generator<Expression>
CollectionInterface.subsetOf()
subsetOf(other, strict): boolean | undefined
####### other
####### strict
boolean
IndexedCollectionInterface
Narrowed interface for indexed collection expressions (lists, vectors, matrices, tuples).
Obtained via isIndexedCollection().
Extends
IndexedCollectionInterface.isIndexedCollection
readonly isIndexedCollection: true;
IndexedCollectionInterface.indexWhere()
indexWhere(predicate): number | undefined
####### predicate
(element) => boolean
ExpressionInput
type ExpressionInput =
| number
| bigint
| boolean
| string
| BigNum
| Complex
| MathJsonNumberObject
| MathJsonStringObject
| MathJsonSymbolObject
| MathJsonFunctionObject
| MathJsonDictionaryObject
| readonly [MathJsonSymbol, ...ExpressionInput[]]
| MathJsonExpression
| Expression;
An expression input is a MathJSON expression which can include some engine expression terms.
This is convenient when creating new expressions from portions
of an existing Expression while avoiding unboxing and reboxing.
ObjectInterface
Narrowed interface for object expressions — the engine's one mutable value kind (a reference to a record whose stored fields can be changed in place).
Obtained via isObject(). The instance IS the heap record: host reference
identity of the expression is object identity, so every comparison tier
(isSame, isEqual, isIdenticallyEqual) answers a === b for objects,
and no code path may clone, rebuild or re-box one.
The members below are engine-internal (they are how the property-access operators and the serialization walk reach the slots); user code reads and writes fields through the language's property syntax, not through these.
Design: docs/TYPE-SYSTEM.md;
semantics: docs/TYPE_SYSTEM_ROADMAP.md Appendix B.
ObjectInterface.typeName
readonly typeName: string;
The name of the nominal type this object was constructed with. The
resolved type itself is pinned on the instance and returned by .type;
this is the name that rides serialization (the Object provenance head
and CircularReference markers).
ReplaceOptions
type ReplaceOptions = {
recursive: boolean;
once: boolean;
useVariations: boolean;
matchPermutations: boolean;
iterationLimit: number;
canonical: CanonicalOptions;
form: FormOption;
direction: "left-right" | "right-left";
};
Options for Expression.replace().
CanonicalForm
type CanonicalForm =
| "InvisibleOperator"
| "Number"
| "Multiply"
| "Add"
| "Power"
| "Divide"
| "Flatten"
| "Order";
Canonical normalization transforms.
CanonicalOptions
type CanonicalOptions =
| boolean
| CanonicalForm
| CanonicalForm[];
FormOption
type FormOption =
| "canonical"
| "structural"
| "raw"
| CanonicalForm
| CanonicalForm[];
Controls how expressions are created.
Metadata
type Metadata = {
latex: string;
wikidata: string;
sourceOffsets: [number, number];
};
Metadata that can be associated with a MathJSON expression.
Pattern Matching
Substitution
type Substitution<T> = KernelSubstitution<T>;
A substitution describes the values of the wildcards in a pattern so that the pattern is equal to a target expression.
A substitution can also be considered a more constrained version of a
rule whose match is always a symbol.
Type Parameters
• T = ExpressionInput
BoxedSubstitution
type BoxedSubstitution<T> = KernelBoxedSubstitution<T>;
Type Parameters
• T = Expression
PatternMatchOptions
type PatternMatchOptions<T> = KernelPatternMatchOptions<T>;
Control how a pattern is matched to an expression.
Type Parameters
• T = Expression
Rules
RuleReplaceFunction
type RuleReplaceFunction = KernelRuleReplaceFunction<Expression>;
Rule replacement callback specialized to boxed expressions.
RuleConditionFunction
type RuleConditionFunction = KernelRuleConditionFunction<Expression, IComputeEngine>;
Rule condition callback with access to the compute engine.
Rule
type Rule = KernelRule<Expression, ExpressionInput, IComputeEngine>;
Rule declaration specialized to boxed expression and compute engine types.
RulePurpose
type RulePurpose = "simplify" | "transform" | "expand";
The purpose of a rule determines how its result is treated by the simplification cost policy:
'simplify': the result must pass the cost gate (the default; today's behavior — results that grow the expression are discarded).'transform': a mathematically-preferred rewrite; exempt from the cost gate (accepted bysimplify()even if structurally larger).'expand': growth-by-design (series, argument expansion); skipped bysimplify(), but reachable viaexpr.replace()and future expand APIs.
ExplainOperation
type ExplainOperation = "simplify" | "solve" | "D" | "Integrate";
The operation that an Explanation traces. See expr.explain().
ExplainVerbosity
type ExplainVerbosity = "default" | "all";
How much of the raw rule trace expr.explain() returns:
'default': curated — bookkeeping steps (driver-internal markers) and no-op steps are filtered out.'all': the raw, uncurated chain (for rule authors and debugging).
Assumptions
ExpressionMapInterface
type ExpressionMapInterface<U> = KernelExpressionMapInterface<U, Expression>;
Map-like interface keyed by boxed expressions.
Type Parameters
• U
Assumption
type Assumption = KernelAssumption<Expression, IComputeEngine>;
Assumption predicates bound to this compute engine.
FactSubject
type FactSubject = KernelFactSubject<BoxedValueDefinition>;
One subject of an assumption, specialized to this engine/runtime model.
FactRecord
type FactRecord = KernelFactRecord<BoxedValueDefinition>;
One assertion recorded by assume(), specialized to this engine/runtime
model. The assumptions store maps a normalized fact to a list of these.
AssumeResult
type AssumeResult =
| "internal-error"
| "not-a-predicate"
| "contradiction"
| "tautology"
| "ok";
Compiling
CompiledType
type CompiledType = boolean | number | string | object;
JSSource
type JSSource = string;
CompiledExpression
type CompiledExpression = {
evaluate: (scope) => number | Expression;
};
OperatorCompileContext
type OperatorCompileContext = {
language: string;
typeOf: (expr) => Type;
};
The context passed to a custom operator OperatorCompileHandler. A curated, stable subset of the internal compilation target: enough to emit target-specific source without exposing the full internal machinery.
OperatorCompileHandler
type OperatorCompileHandler = (args, compile, context) => string | undefined;
A custom compilation handler for an operator, set on an
OperatorDefinition. It mirrors a built-in compiled-function handler:
it receives the (canonical) operands, a compile callback to lower a
sub-expression to target source, and the compilation context (branch on
context.language). It returns target source, or undefined (or
an empty string) to fall back to the target's default compilation of this
operator (a null returned from untyped JavaScript is tolerated and
treated the same).
Takes precedence over the target's built-in operator/function mapping and
broadcast lowering, so it can override how a built-in operator compiles
(e.g. a custom-tolerance GCD, or a re-mapped Add/Multiply/Power/
relational operator). It does NOT override the structural / control-flow
heads (Sequence, Sum, Product, Function, Declare, Assign,
Return, Break, Continue, Loop, Comprehension, If, Which,
When, Match, Block), which have their own bespoke lowering; a handler
declared on one of those heads is ignored.
ce.declare('MyGcd', {
signature: '(number, number) -> number',
compile: (args, compile, { language }) =>
language === 'javascript'
? `_gcd(${compile(args[0])}, ${compile(args[1])})`
: undefined,
});
Definitions
EvaluateHandlerOptions
type EvaluateHandlerOptions = Partial<EvaluateOptions> & {
engine: ComputeEngine;
expression: Expression;
precision: number;
effects: EffectHandlers;
};
The options argument passed to an evaluate / evaluateAsync handler.
EvaluateHandlerOptions.expression?
optional expression?: Expression;
The canonical expression node being evaluated.
Its ops are the raw operands: canonical and bound, but
pre-numericization — the same objects the type handler sees. The
handler's first parameter, by contrast, holds the evaluated operands,
which under numericApproximation have already been turned into floats.
That makes this the handler's only access to the operands' exactness. For
example Power reads the exact rational p/q of its exponent from
expression.op2 to decide the branch of a negative base — under .N()
the exponent it receives as an operand is a double, from which p/q
can only be guessed.
expression.ops[i] is NOT in general the provenance of ops[i]. The
evaluated operands come from holdMap, which reindexes them: it FLATTENS
an associative operator (f(a, f(b, c)) arrives as three operands, one
more than the node has), it UNWRAPS ReleaseHold (so expression.ops[i]
is the wrapper, not what was evaluated), and it DROPS an operand whose
evaluation yields nothing. The correspondence holds only for a
non-associative operator with no ReleaseHold and no dropped operand — so
a handler that indexes into expression.ops must treat
expression.ops.length !== ops.length as "no provenance" and fall back to
what it can compute from the evaluated operands alone.
On a lazy: true operator there is no contrast to draw: holdMap
returns the operands unchanged, so the handler's first parameter is raw
and held too — and, on the box/parse routes, not even canonicalized (see
the lazy-operator trap in CLAUDE.md: such a handler must canonicalize
each held operand it consumes).
Read-only: do not mutate it, and do not assume it is present (a handler invoked outside the evaluation driver may not receive one).
EvaluateHandlerOptions.precision?
optional precision?: number;
The number of significant digits the caller asked for, when
numericApproximation is true. undefined when
numericApproximation is false: an exact evaluation has no precision.
- Inside
N(x, p), it ispfor the evaluation ofxand of every expression evaluated inside it, also whenpis lower than the precision of the engine (thenNcomputes at the precision of the engine and rounds the result topdigits). - Otherwise (
x.N(),N(x),evaluate({ numericApproximation: true })) it isce.precision, the precision of the engine.
A handler can use it to compute to the requested number of digits
without reading ce.precision. Note that N(x, p) with a p greater
than the precision of the engine also sets ce.precision to p, and
leaves it there after the call.
EvaluateHandlerOptions.effects
effects: EffectHandlers;
The host capabilities of THIS evaluation: the ce.effects registry as it
was when the evaluation started. A handler that reaches a host capability
reads it from here, never from ce.effects, so that a change of registry
made while the evaluation runs — or made for a different, concurrent
asynchronous evaluation — has no effect on it.
A handler may use a capability only if its operator declares the
corresponding effect label: options.effects.console requires console
in the signature. A null handler is a denial: return
ce.error(['capability-denied', '<capability>']).
ValueDefinition
type ValueDefinition = BaseDefinition & {
holdUntil: "never" | "evaluate" | "N";
type: | Type
| TypeString
| BoxedType;
inferred: boolean;
effectsDeclared: boolean;
value: | LatexString
| ExpressionInput
| ((ce) => Expression | null);
eq: (a) => boolean | undefined;
neq: (a) => boolean | undefined;
cmp: (a) => "=" | ">" | "<" | undefined;
collection: CollectionHandlers;
subscriptEvaluate: (subscript, options) => Expression | undefined;
};
A bound symbol (i.e. one with an associated definition) has either a type (e.g. ∀ x ∈ ℝ), a value (x = 5) or both (π: value = 3.14... type = 'real').
ValueDefinition.inferred
inferred: boolean;
If true, the type is inferred, and could be adjusted later as more information becomes available or if the symbol is explicitly declared.
ValueDefinition.effectsDeclared
effectsDeclared: boolean;
Annotation provenance on the EFFECTS axis of a function-typed
declaration (docs/EFFECTS-MODEL.md, "Annotation provenance") — the
effects-axis analog of inferred.
True when the author STATED the arrow's effects: a non-empty specifier
((number) scope -> number), or the pure keyword — which denotes the
same empty set a bare arrow does, so the type alone cannot tell them
apart. A bare arrow leaves effects on the inferred track: assigning a
body re-stamps them freely. A stated set is a CONTRACT: every assigned
body must satisfy inferred ⊆ declared.
Set by ce.declare() from the parsed declaration; not normally written
by hand.
ValueDefinition.value
value:
| LatexString
| ExpressionInput
| ((ce) => Expression | null);
value can be a JS function since for some constants, such as
Pi, the actual value depends on the precision setting of the
ComputeEngine and possible other environment settings
ValueDefinition.subscriptEvaluate?
optional subscriptEvaluate?: (subscript, options) => Expression | undefined;
Custom evaluation handler for subscripted expressions of this symbol.
Called when evaluating Subscript(symbol, index).
subscript
The subscript expression (already evaluated)
options
Contains the compute engine and evaluation options
####### engine
ComputeEngine
####### numericApproximation?
boolean
SequenceDefinition
Definition for a sequence declared with ce.declareSequence().
A sequence is defined by base cases and a recurrence relation.
Example
// Fibonacci sequence
ce.declareSequence('F', {
base: { 0: 0, 1: 1 },
recurrence: 'F_{n-1} + F_{n-2}',
});
ce.parse('F_{10}').evaluate(); // → 55
SequenceDefinition.variable?
optional variable?: string;
Index variable name for single-index sequences, default 'n'.
For multi-index sequences, use variables instead.
SequenceDefinition.variables?
optional variables?: string[];
Index variable names for multi-index sequences.
Example: ['n', 'k'] for Pascal's triangle P\_{n,k}
If provided, this takes precedence over variable.
SequenceDefinition.base
base: Record<number | string, number | Expression>;
Base cases as index → value mapping.
For single-index sequences, use numeric keys:
base: { 0: 0, 1: 1 } // F_0 = 0, F_1 = 1
For multi-index sequences, use comma-separated string keys:
base: {
'0,0': 1, // Exact: P_{0,0} = 1
'n,0': 1, // Pattern: P_{n,0} = 1 for all n
'n,n': 1, // Pattern: P_{n,n} = 1 (diagonal)
}
Pattern keys use variable names to match any value. When the same variable appears multiple times (e.g., 'n,n'), the indices must be equal.
SequenceDefinition.recurrence
recurrence: string | Expression;
Recurrence relation as LaTeX string or Expression
SequenceDefinition.memoize?
optional memoize?: boolean;
Whether to memoize computed values (default: true)
SequenceDefinition.domain?
optional domain?:
| {
min: number;
max: number;
}
| Record<string, {
min: number;
max: number;
}>;
Valid index domain constraints.
For single-index sequences:
domain: { min: 0, max: 100 }
For multi-index sequences, use per-variable constraints:
domain: { n: { min: 0 }, k: { min: 0 } }
SequenceDefinition.constraints?
optional constraints?: string | Expression;
Constraint expression for multi-index sequences. The expression should evaluate to a boolean/numeric value. If it evaluates to false or 0, the subscript is considered out of domain.
Example: 'k <= n' for Pascal's triangle (only valid when k ≤ n)
SequenceStatus
Status of a sequence definition.
SequenceStatus.status
status: "complete" | "pending" | "not-a-sequence";
Status of the sequence:
- 'complete': Both base case(s) and recurrence defined
- 'pending': Waiting for base case(s) or recurrence
- 'not-a-sequence': Symbol is not a sequence
SequenceStatus.baseIndices
baseIndices: (string | number)[];
Keys of defined base cases. For single-index: numeric indices (e.g., [0, 1]) For multi-index: string keys including patterns (e.g., ['0,0', 'n,0', 'n,n'])
SequenceStatus.variable?
optional variable?: string;
Index variable name if recurrence is defined (single-index)
SequenceStatus.variables?
optional variables?: string[];
Index variable names if recurrence is defined (multi-index)
SequenceInfo
Information about a defined sequence for introspection.
SequenceInfo.variable?
optional variable?: string;
Index variable name for single-index sequences (e.g., "n")
SequenceInfo.variables?
optional variables?: string[];
Index variable names for multi-index sequences (e.g., ["n", "k"])
SequenceInfo.baseIndices
baseIndices: (string | number)[];
Base case keys. For single-index: numeric indices For multi-index: string keys including patterns
SequenceInfo.domain
domain:
| {
min: number;
max: number;
}
| Record<string, {
min: number;
max: number;
}>;
Domain constraints.
For single-index: { min?, max? }
For multi-index: per-variable constraints
Tri
type Tri = boolean | undefined;
A three-valued fact about an operand: true (provably yes), false
(provably no), undefined (not decidable from what the descriptor knows).
OperandFacts
type OperandFacts = {
finite: Tri;
sgn: Sign;
closed: Tri;
collection: Tri;
finiteCollection: Tri;
indexed: Tri;
shape: readonly number[];
elementType: Type;
};
The facts a type handler in the 'types' shape may read about one
operand, beside the operand's type. Every fact is derived from pure
sources — the operand's type, a literal's value, a symbol's held value or
recorded assumptions, structural reads — never by canonicalizing,
declaring, or evaluating anything.
The set is deliberately minimal: a fact earns a field only when the
operand's TYPE cannot carry it. Anything the type proves is read off
OperandDescriptor.type directly — an error operand's type IS 'error'
(so there is no valid field), and a literal's value, sign, and
finiteness normally travel in its value-carrying type. Each field below
merges the type channel with the pure value channel, so a handler reads
ONE place and never re-derives the combination; the doc of each field
names the residue that justifies it.
OperandStructure
type OperandStructure =
| {
kind: "symbol";
name: string;
system: boolean;
inferred: boolean;
local: boolean;
}
| {
kind: "string";
text: string;
}
| {
kind: "number";
tier: Type;
literal: 0 | 1;
rational: readonly [bigint, bigint];
}
| {
kind: "application";
head: string;
children: ReadonlyArray<OperandDescriptor>;
}
| {
kind: "function-literal";
parameters: ReadonlyArray<{
name: string;
annotated: Type;
rest: boolean;
}>;
body: OperandStructure;
}
| {
kind: "tuple";
arity: number;
elements: ReadonlyArray<OperandDescriptor>;
}
| {
kind: "list-literal";
shape: readonly number[];
elements: ReadonlyArray<OperandDescriptor>;
};
An inert, expression-free structural view of an operand, for type
handlers in the 'types' shape that need more than the operand's type
(is it a symbol? a string literal? an application of which operator?).
Children appear as descriptors, so a handler can recurse without ever
holding an expression.
Type Declaration
{
kind: "symbol";
name: string;
system: boolean;
inferred: boolean;
local: boolean;
}
OperandStructure.system?
optional system?: boolean;
Present (true) when the symbol resolves to the definition the
engine's SYSTEM scope binds under this name — the library constant
or operator, not a user or local declaration that shadows it. The
ring arms of At and Subscript read it: Integers[k] is a
quotient-ring adjunction only for the library Integers.
OperandStructure.inferred?
optional inferred?: boolean;
Present (true) when the symbol's recorded type was INFERRED
(subject to revision) rather than declared — the fact the
Multiply and List-fold handlers consult when deciding how much
to trust an operand's type. Lives on the structure node, not in
OperandFacts: it is a property of this symbol, not of a type.
OperandStructure.local?
optional local?: boolean;
Present (true) when the symbol is a block-local binding a Block
hoisted for a let or a block-introducing assignment, still waiting
for the statement that gives it a value. The List-fold handler
reads it to keep the generic-symbol fold (an unknown bare symbol is
a number) for FREE symbols only: such a local is not a generic
value, its type is simply not known yet.
{
kind: "string";
text: string;
}
{
kind: "number";
tier: Type;
literal: 0 | 1;
rational: readonly [bigint, bigint];
}
OperandStructure.tier
tier: Type;
The tier the literal contributes to a COMPOSITE type: integer,
rational, real, complex, imaginary, nan, infinity, or
the signed pair +oo | -oo. A literal's handler-visible type
(type on the descriptor) carries its value or an enclosing range;
a composite built from the literal — a tuple's component, a list's
element, a record's field — is a stored contract and carries the
tier instead, so a container handler reads it here and never
builds the literal type only to widen it away. Read off the value
(numberLiteralTierType, boxed-expression/literal-tier.ts).
OperandStructure.rational?
optional rational?: readonly [bigint, bigint];
The literal's exact value as a REDUCED fraction, when it is a
rational with no radical part: [numerator, denominator], the
denominator positive. A literal's handler-visible type carries only
an outward-rounded range for a rational no double represents
exactly, so a handler that needs the exact terms (the parity of a
power's exponent denominator decides real against complex) reads
them here. Absent for a float, a complex, a radical, or a
non-finite literal.
{
kind: "application";
head: string;
children: ReadonlyArray<OperandDescriptor>;
}
{
kind: "function-literal";
parameters: ReadonlyArray<{
name: string;
annotated: Type;
rest: boolean;
}>;
body: OperandStructure;
}
OperandStructure.parameters
parameters: ReadonlyArray<{
name: string;
annotated: Type;
rest: boolean;
}>;
One entry per parameter operand, in order. rest marks the REST
parameter ((a, ...rest) => …): it is always the last entry, it
takes no annotation, and it binds a tuple of every argument from its
own position onwards rather than occupying one positional slot. A
consumer reading arity must therefore treat the entries before it as
the required count and admit any number after.
{
kind: "tuple";
arity: number;
elements: ReadonlyArray<OperandDescriptor>;
}
OperandStructure.elements
elements: ReadonlyArray<OperandDescriptor>;
One descriptor per component, in order.
{
kind: "list-literal";
shape: readonly number[];
elements: ReadonlyArray<OperandDescriptor>;
}
OperandStructure.elements
elements: ReadonlyArray<OperandDescriptor>;
One descriptor per top-level element, in order. A nested row is
itself a list-literal structure, reachable through its
descriptor's structureOf().
OperandDescriptor
type OperandDescriptor = {
type: Type;
facts: OperandFacts;
structureOf: () => OperandStructure | undefined;
};
What a type handler in the 'types' shape receives in place of an
operand expression: the operand's handler-visible type (a number
literal's value-carrying type included), a set of three-valued facts,
and an optional on-demand structural view. Descriptors carry no
expression, so a handler cannot canonicalize, declare, or evaluate its
operands while deriving a type — which is the point of the shape: type
derivation must not modify engine state.
Built by describe() (from a real operand) and describeType() (from a
type alone) in boxed-expression/operand-descriptor.ts; the design is
docs/plans/2026-08-22-type-handlers-on-types.md §5.1.
ReadonlyDefinitionView
type ReadonlyDefinitionView = {
value: Readonly<BoxedValueDefinition>;
operator: Readonly<BoxedOperatorDefinition>;
};
The definition view a 'types'-shape type handler gets from
PureEngineView.lookupDefinition: the tagged value/operator halves with
every own property readonly. The shallow Readonly is compile-time
protection against the direct field writes a type handler must never
perform (def.operator.signature = …); the runtime purity guard remains
the dynamic enforcement for anything the type system cannot see.
PureEngineView
The read-only slice of the engine available to a type handler in the
'types' shape: enough to parse and resolve types, and to look up a
definition — none of the mutating surface (declare, assign, box,
parse, evaluate), and the definition lookup answers a read-only view
(ReadonlyDefinitionView). The full ComputeEngine satisfies this
interface structurally, so the restriction is compile-time only; the
runtime purity guard (CE_TYPE_PURITY_GUARD, always on under test) is
what enforces it dynamically.
PureEngineView._typeResolver
readonly _typeResolver: TypeResolver;
PureEngineView.tolerance
readonly tolerance: number;
The engine's numeric tolerance, a read-only configuration value a
membership handler consults (Element declines rather than refute a
near-match inside it).
PureEngineView._protocolRegistry
readonly _protocolRegistry: Readonly<Record<string, object>>;
The protocol registry, keyed by protocol name. Its records are typed
opaquely here because the record type lives in types-engine.ts,
which imports this file; the protocol readers in engine-protocols.ts
(protocolOfName, protocolMemberSignature,
protocolPropertyTypeOfReceiver) take this view and know the records'
real shape. Read-only from a handler.
PureEngineView.type()
type(type): BoxedType
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
PureEngineView.lookupDefinition()
lookupDefinition(id): ReadonlyDefinitionView | undefined
####### id
string
TypeHandlerContext
type TypeHandlerContext = {
engine: PureEngineView;
derive: (operator, operands) => Type | undefined;
};
The context argument of a type handler in the 'types' shape.
derive(operator, operands) is the recursive entry point a handler needs
to type an application it does not have in hand — the body of a mapping
literal over the source's element type, say. It runs the named operator's
own type handler on the given descriptors, falling back to the declared
(instantiated) signature result, and reads nothing but definitions and
types (deriveApplicationType, boxed-expression/derive-application-type.ts).
It answers undefined for an unknown operator.
OperatorTypeHandlerOnTypes
type OperatorTypeHandlerOnTypes = (operands, context) => BoxedType | undefined;
The type handler of an operator definition: a function of operand
DESCRIPTORS. Such a handler never sees an operand expression, so
deriving a type cannot declare, canonicalize, or evaluate anything — the
state-purity contract of
docs/plans/2026-08-22-type-handlers-on-types.md. Under test, and with
CE_TYPE_PURITY_GUARD set elsewhere, a handler that writes engine state
throws.
Return a BoxedType (for example context.engine.type('real')), or
undefined to use the declared signature. Numeric literal cargo in a
boxed result is widened at the application boundary; intentional ranges
remain intact. Built-ins use BoxedType.forResult() to share that work.
OperatorDerivativeHandler
type OperatorDerivativeHandler = (ops, options) => Expression | undefined;
A handler that gives one partial derivative of an operator.
opsare the arguments of the application being differentiated, for example[x^2, y]forF(x^2, y). They are canonical, not evaluated.options.argumentis the 0-based index of the argument with respect to which the partial derivative is requested.
Return the partial derivative ∂F/∂(argument number options.argument)
evaluated AT ops, for example 2·x^2 for the first partial of
F(u, v) = u^2 + v applied to [x^2, y]. Do not multiply by the
derivative of the argument: the chain rule is applied by the caller.
Return undefined when the partial derivative is not known. That
partial then stays symbolic, as Apply(Derivative(F, 1, 0), x^2, y).
Do not call .simplify() on the result: the derivative is computed
inside the simplification of other expressions, and calling .simplify()
there can recurse without end.
OperatorDerivative
type OperatorDerivative =
| ReadonlyArray<ExpressionInput | Expression>
| OperatorDerivativeHandler;
The value of the derivative key of an operator definition. It has one
of two forms:
-
An array with one entry for each argument. Entry
iis a function literal (["Function", body, ...parameters], as MathJSON or as an expression) with one parameter for each argument of the operator. It gives the partial derivative with respect to argumenti, as a function of all the arguments. ForF(x, y) = x^2·y:derivative: [['Function', ['Multiply', 2, 'x', 'y'], 'x', 'y'], // ∂F/∂x['Function', ['Power', 'x', 2], 'x', 'y'], // ∂F/∂y]The array is used only for an application with as many arguments as the array has entries, and an entry is used only if its literal has one parameter for each argument. In any other case the partial derivatives stay symbolic.
-
A handler, see OperatorDerivativeHandler. Use it when the number of arguments varies, or when a partial derivative needs code.
In both forms the derivative of F(g₁, …, gₙ) with respect to v is
given by the chain rule: Σᵢ ∂ᵢF(g₁, …, gₙ) · ∂gᵢ/∂v. A partial
derivative is requested only for an argument that depends on v.
BaseDefinition
Metadata common to both symbols and functions.
BaseDefinition.description
description: string | string[];
If a string, a short description, about one line long.
Otherwise, a list of strings, each string a paragraph.
May contain Markdown.
BaseDefinition.keywords?
optional keywords?: string[];
Search keywords (synonyms, alternate names) used by
ce.searchDefinitions(). Not shown in documentation.
BaseDefinition.examples
examples: string | string[];
A list of examples of how to use this symbol or operator.
Each example is one line of Epsil source — Rationalize(1.75) — that
evaluates on a fresh engine to a value worth showing. A trailing //
comment is allowed and is replaced by the value the example evaluates
to when the standard-library page is generated
(scripts/build-library-docs.ts); that page executes every example, so
one that stops evaluating fails the documentation build.
BaseDefinition.wikidata
wikidata: string;
A short string representing an entry in a wikibase.
For example "Q167" is the wikidata entry
for the Pi constant.
BaseDefinition.isConstant?
readonly optional isConstant?: boolean;
If true, the value or type of the definition cannot be changed
SymbolDefinition
type SymbolDefinition = OneOf<[ValueDefinition, OperatorDefinition]>;
A table mapping symbols to their definition.
Symbols should be valid MathJSON symbols. In addition, the following rules are recommended:
- Use only latin letters, digits and
-:/[a-zA-Z0-9-]+/ - The first character should be a letter:
/^[a-zA-Z]/ - Functions and symbols exported from a library should start with an uppercase letter
/^[A-Z]/
PartialSymbolDefinition
type PartialSymbolDefinition<T> = T extends unknown ? Partial<T> : never;
Partial distributed over the SymbolDefinition union (a plain
Partial<A | B> would merge the arms into one loose object type).
Type Parameters
• T = SymbolDefinition
SymbolDefinitionInput
type SymbolDefinitionInput =
| PartialSymbolDefinition
| BoxedOperatorDefinition;
A definition as ce.declare() accepts it: a partial definition, or the
boxed operator definition read back from expr.operatorDefinition.
Admitting the boxed definition is what lets an operator be re-declared from its existing definition with some handlers replaced:
const sqrt = ce.expr('Sqrt').operatorDefinition!;
ce.declare('Sqrt', {
...sqrt,
evaluate: (ops, options) => sqrt.evaluate!(ops, options),
});
LibraryDefinition
A library bundles symbol/operator definitions and declares dependencies on
other libraries. It carries no LaTeX dictionary entries: to parse or
serialize a new notation, pass a dictionary to the LatexSyntax given with
the latexSyntax constructor option, or add entries to a running engine
with ce.latexSyntax.addEntries().
To load a custom library on a running engine, use ce.loadLibrary().
Use with the libraries constructor option to load standard or custom
libraries:
const ce = new ComputeEngine({
libraries: ['core', 'arithmetic', {
name: 'custom',
requires: ['arithmetic'],
definitions: { G: { value: 6.674e-11, type: 'real', isConstant: true } },
}],
});
LibraryDefinition.requires?
optional requires?: string[];
Libraries that must be loaded before this one
LibraryDefinition.definitions?
optional definitions?: Readonly<{}> | Readonly<{}>[];
Symbol and operator definitions
BaseCollectionHandlers
These handlers are the primitive operations that can be performed on all collections, indexed or not.
Definitions
BaseCollectionHandlers.iterator
iterator: (collection) =>
| Iterator<Expression, undefined, any>
| undefined;
Return an iterator that iterates over the elements of the collection.
The order in which the elements are returned is not defined. Requesting two iterators on the same collection may return the elements in a different order.
Other
BaseCollectionHandlers.count
count: (collection) => number | undefined;
Return the number of elements in the collection.
An empty collection has a count of 0.
BaseCollectionHandlers.isEmpty?
optional isEmpty?: (collection) => boolean | undefined;
Optional flag to quickly check if the collection is empty, without having to count exactly how may elements it has (useful for lazy evaluation).
BaseCollectionHandlers.isFinite?
optional isFinite?: (collection) => boolean | undefined;
Optional flag to quickly check if the collection is finite, without having to count exactly how many elements it has (useful for lazy evaluation).
BaseCollectionHandlers.isEnumerable?
optional isEnumerable?: (collection) => boolean | undefined;
Optional predicate answering whether iterator() will actually produce
this collection's elements — the cheap way to tell an EMPTY collection
from one that merely cannot be walked, which are otherwise
indistinguishable (both yield nothing).
Return false when the elements have no computable value in the current
state: symbolic bounds (Range(a, b), Linspace(a, 1, 3), a symbolic
repeat count), or a source that is itself not enumerable. Return
undefined only when it cannot be decided without evaluating.
Implementations must be O(1) and must NOT consult count, isEmpty or
isFinite on themselves, nor walk the collection: a wrapper answers by
reading its source's isEnumerableCollection, so a chain costs one call
per level. (Reading the emptiness facets instead is exponential in the
chain depth — each read re-enters the next isEmpty down.)
Default when the handler is ABSENT: true (an operator with a
collection block can enumerate its elements). A handler that IS declared
owns all three states — returning undefined from it means "cannot tell
cheaply" and does not fall back to the default.
BaseCollectionHandlers.isCollection?
optional isCollection?: (collection) => boolean;
Optional predicate for operators whose collection-ness depends on their
operands, e.g. When(value, cond), which is a collection exactly when
value is one.
Returning false reports the expression as a scalar, as if it had no
collection handlers at all.
Default: true (an operator with a collection block is a collection).
BaseCollectionHandlers.isLazy?
optional isLazy?: (collection) => boolean;
Return true if the collection is lazy, false otherwise.
If the collection is lazy, it means that the elements are not
computed until they are needed, for example when iterating over the
collection.
Default: false. A collection is eager unless its definition says
otherwise: the elements of a List are already materialized operands,
so nothing is deferred. Lazy collections such as Range or Map declare
this handler to opt in.
BaseCollectionHandlers.elementMemo?
optional elementMemo?: boolean;
Opt this operator's instances into per-instance element memoization: a
complete walk of an unmodified instance is served from a cached prefix
on subsequent walks (boxed-expression/collection-element-memo.ts).
Set it on lazy operators that evaluate a function per element (Map,
Filter, Tabulate, …), where re-deriving an element is expensive.
Leave it off structural reindexers (Take, Reverse, Zip, …), which
re-serve their source's elements cheaply — when the source is itself a
flagged instance, the source's own memo already absorbs the cost.
Default: false
BaseCollectionHandlers.contains?
optional contains?: (collection, target) => boolean | undefined;
Return true if the target expression is in the collection,
false otherwise.
Return undefined if the membership cannot be determined.
BaseCollectionHandlers.subsetOf?
optional subsetOf?: (collection, other, strict) => boolean | undefined;
Return true if all the elements of collection are in other — that
is, collection ⊆ other. The RECEIVER is the candidate subset, matching
the public Expression.subsetOf(other, strict) method that dispatches
here. Both collection and other are collections.
If strict is true, the subset must be strict, that is, other must have
an element that collection does not.
Return undefined if the subset relation cannot be determined. A handler
that cannot see far enough to answer must return undefined rather than
false: false is read as a proof that the relation does NOT hold.
BaseCollectionHandlers.eltsgn?
optional eltsgn?: (collection) => Sign | undefined;
Return the sign of all the elements of the collection.
BaseCollectionHandlers.elttype?
optional elttype?: (collection) => Type | undefined;
Return the widest type of all the elements in the collection
IndexedCollectionHandlers
These additional collection handlers are applicable to indexed collections only.
The elements of an indexed collection can be accessed by index, and the order of the elements is defined.
IndexedCollectionHandlers.at
at: (collection, index) => Expression | undefined;
Return the element at the specified index.
The first element is at(1), the last element is at(-1).
If the index is <0, return the element at index count() + index + 1.
The index can also be a string, for example for records. There is no
handler that enumerates the valid string keys: a handler that accepts
them decides which ones it recognizes, and returns undefined for the
rest.
If the index is invalid, return undefined.
IndexedCollectionHandlers.indexWhere
indexWhere: (collection, predicate) => number | undefined;
Return the index of the first element that matches the predicate.
If no element matches the predicate, return undefined.
CollectionHandlers
type CollectionHandlers = BaseCollectionHandlers & Partial<IndexedCollectionHandlers>;
The collection handlers are the primitive operations that can be performed on collections, such as lists, sets, tuples, etc...
TaggedValueDefinition
type TaggedValueDefinition = {
value: BoxedValueDefinition;
};
The definition for a value, represented as a tagged object literal.
TaggedOperatorDefinition
type TaggedOperatorDefinition = {
operator: BoxedOperatorDefinition;
};
The definition for an operator, represented as a tagged object literal.
BoxedDefinition
type BoxedDefinition =
| TaggedValueDefinition
| TaggedOperatorDefinition;
A definition can be either a value or an operator.
It is collected in a tagged object literal, instead of being a simple union type, so that the type of the definition can be changed while keeping references to the definition in bound expressions.
TypeProvenanceEntry
type TypeProvenanceEntry = {
type: BoxedType;
kind: "declared" | "auto-declared" | "inferred" | "value-derived";
axis: "type" | "effects";
cause: Expression;
epoch: number;
span: {
start: number;
end: number;
};
};
One recorded write to a definition's type (or an operator definition's signature): the type the write installed, the mechanism that installed it, and — for writes triggered by canonicalizing an expression — that expression.
Provenance can never live on Type/BoxedType objects themselves: parsed
types are interned, deep-frozen, and shared across engines (the
TYPE_CACHE in common/type/parse.ts), so two occurrences of boolean
are the same object. The history therefore lives on the per-engine
definition, next to inferredType.
Design: docs/TYPE-SYSTEM.md, phase 1.
BoxedBaseDefinition
Extends
Partial<BaseDefinition>
Extended by
BoxedBaseDefinition.examples?
optional examples?: string[];
The usage examples of the definition. A definition may give a single string; the boxed definition always stores a list.
BoxedBaseDefinition.collection?
optional collection?: CollectionHandlers;
If this is the definition of a collection, the set of primitive operations that can be performed on this collection (counting the number of elements, enumerating it, etc...).
BoxedValueDefinition
Extends
BoxedValueDefinition.holdUntil
holdUntil: "never" | "evaluate" | "N";
If the symbol has a value, it is held as indicated in the table below. A green checkmark indicate that the symbol is substituted.
| Operation | "never" | "evaluate" | "N" |
|---|---|---|---|
canonical() | (X) | ||
evaluate() | (X) | (X) | |
"N()" | (X) | (X) | (X) |
Some examples:
ImaginaryUnithasholdUntil: 'never': it is substituted during canonicalizationxhasholdUntil: 'evaluate'(variables)PihasholdUntil: 'N'(special numeric constant)
Default: evaluate
BoxedValueDefinition.value
value: Expression | undefined;
The current value of the symbol: the value an assume(x = …) puts in
force for the current context if there is one, else the stored value.
For constants, this is immutable.
BoxedValueDefinition.isSelfReferential
readonly isSelfReferential: boolean;
True if the current value refers to the symbol itself (a degenerate
self-referential binding, e.g. a := a + 1 over an unbound a). Such a
binding forms a cycle: resolving the value would re-resolve the symbol
forever. When set, the symbol is treated as unbound during resolution so
that evaluate()/.N()/collection queries stay symbolic instead of
overflowing the stack. Computed once when the value is assigned.
BoxedValueDefinition.eq?
optional eq?: (a) => boolean | undefined;
BoxedValueDefinition.neq?
optional neq?: (a) => boolean | undefined;
BoxedValueDefinition.cmp?
optional cmp?: (a) => "<" | ">" | "=" | undefined;
BoxedValueDefinition.inferredType
inferredType: boolean;
True if the type has been inferred. An inferred type can be updated as more information becomes available.
A type that is not inferred, but has been set explicitly, cannot be updated.
BoxedValueDefinition.effectsDeclared
effectsDeclared: boolean;
Annotation provenance on the EFFECTS axis — the effects-axis analog of
inferredType (docs/EFFECTS-MODEL.md, "Annotation provenance").
True when the declaration STATED the arrow's effects (a non-empty
specifier, or the pure keyword). False for a bare arrow, which leaves
effects on the inferred track: an assigned body's inferred effects are
accepted and re-stamped, never checked against the declaration.
BoxedValueDefinition.type
type: BoxedType;
The type known in the CURRENT state: declaredType narrowed by
everything the assumptions in force prove about this definition. Reading
it is what makes a fact visible; nothing derived from it may be STORED
(see declaredType). Writing it reports a type-write state
event, so cached results that read this type are computed again.
BoxedValueDefinition.subscriptEvaluate?
optional subscriptEvaluate?: (subscript, options) => Expression | undefined;
Custom evaluation handler for subscripted expressions of this symbol.
Called when evaluating Subscript(symbol, index).
BoxedValueDefinition.dispose()
dispose(): void
Release resources owned by this definition when its scope is disposed.
BindingSite
type BindingSite = {
path: readonly number[];
type: TypeString;
clauseLocal: boolean;
};
A located binding site: where, inside an operator expression, one of that operator's bound variables sits, and how to declare it.
BindingSiteSelector
type BindingSiteSelector = (ops, phase) => readonly BindingSite[];
Locate an operator's binding sites among its operands.
Used as the value of the scoped flag of OperatorDefinitionFlags to
declare that an operator is a binder: the framework mints the operator's
scope, declares each site's symbol in it before the canonical handler
runs, and rebinds the sites (and same-named occurrences elsewhere in the
expression) to that scope afterwards. This is what makes the parse,
ce.box() and ce.function() routes agree about which binding a bound
variable denotes.
phase: 'pre' runs on the RAW operands, before the canonical handler; it
may return fewer sites than 'post' — return nothing rather than guess.
phase: 'post' runs on the handler's RESULT operands and is authoritative.
BroadcastExemption
type BroadcastExemption =
| "tensors"
| "tuples"
| "collection-result"
| "evaluated-operands"
| "whole-collection-compare"
| "single-collection-join";
A shape of operand (or result) whose broadcast handling an operator's own handlers provide, exempting it from the generic broadcast machinery. See OperatorDefinitionFlags.broadcastExemptions for the meaning of each label.
OperatorDefinitionFlags
type OperatorDefinitionFlags = {
lazy: boolean;
scoped: boolean | BindingSiteSelector;
broadcastable: boolean;
broadcastExemptions: ReadonlyArray<BroadcastExemption>;
threadsConditionals: boolean | number[];
inspectsErrors: boolean;
selectsOperands: boolean;
namedArgumentsRequired: boolean;
missingBehavior: "reject" | "propagate" | "handle";
missingStrip: "all" | number[];
nanBehavior: | "reject"
| "propagate"
| "handle"
| ReadonlyArray<"reject" | "propagate" | "handle" | undefined>;
partiality: "total" | "may-marker";
definedWhen: (ops) => boolean | undefined;
requires: (ops) => boolean | undefined;
associative: boolean;
commutative: boolean;
commutativeOrder: ((a, b) => number) | undefined;
commutativeMatch: boolean;
idempotent: boolean;
involution: boolean;
pure: boolean;
effects: EffectSet | undefined;
effectsDeclared: boolean;
frameProtocol: "seed" | undefined;
invokes: boolean | {};
discharges: {} | undefined;
holdClass: "evaluate" | "quote" | "release";
drawsRandom: boolean;
readsRandomFrame: boolean;
};
An operator definition can have some flags to indicate specific properties of the operator.
LambdaDefinition
type LambdaDefinition = {
parameters: ReadonlyArray<{
name: string;
type: Type | undefined;
}>;
body: Expression;
};
A traversable, public view of a user-defined function literal
(f(x) := …, x ↦ …, or ce.assign('f', lambda)): its parameters and
its body as a boxed expression. Returned by
BoxedOperatorDefinition.lambda.
BoxedOperatorDefinition
The definition includes information specific about an operator, such as handlers to canonicalize or evaluate a function expression with this operator.
Extends
BoxedOperatorDefinition.scoped
scoped: boolean;
Normalized from the declaration's scoped flag: true when the operator
creates a lexical scope, whether it was declared true or as a
binding-site selector.
BoxedOperatorDefinition.bindingSites?
optional bindingSites?: BindingSiteSelector;
The binding-site selector of the declaration's scoped flag, when one
was given. undefined for scoped: true (a scope with no syntactic
bound variables) and for an unscoped operator.
BoxedOperatorDefinition.complexity
complexity: number;
BoxedOperatorDefinition.inferredSignature
inferredSignature: boolean;
If true, the signature was inferred from usage and may be modified as more information becomes available.
BoxedOperatorDefinition.signature
signature: BoxedType;
The type of the arguments and return value of this function
BoxedOperatorDefinition.resolvedMissingBehavior
readonly resolvedMissingBehavior: "reject" | "propagate" | "handle" | "pass-through";
The resolved missing-value behavior (§3.A of the missing-value typing
design): the declared missingBehavior flag of OperatorDefinitionFlags
when present, otherwise
'propagate' for a declared all-numeric signature and 'pass-through'
for everything else. Recomputed from the current signature — never cached
across a signature mutation.
BoxedOperatorDefinition.enforcesParameterAnnotations
readonly enforcesParameterAnnotations: boolean;
True for a function literal whose parameter annotations are enforced at
a call (at least one annotated parameter). Such a function admits an
argument typed missing | T at boxing, and answers an
incompatible-type error when the value is absent at a parameter whose
annotation has no missing member.
BoxedOperatorDefinition.resolvedPartiality
readonly resolvedPartiality: "total" | "may-marker" | "defined-when";
The resolved partiality of the declaration (Contract B): the
declared partiality, 'defined-when' when a definedWhen predicate
is declared, and the sound 'may-marker' default when nothing is.
BoxedOperatorDefinition.isUserFunctionDefinition
readonly isUserFunctionDefinition: boolean;
True for a USER-DEFINED callable — a lambda, or an unscoped strict
multi-clause definition. The sanctioned opt-out of the Contract B
machinery: its own application machinery owns every exceptional
operand, and the higher-order conservative floor
(docs/ERROR-MODEL.md §4) caps what a consumer may assume about it
at may-marker with unknown NaN behavior.
BoxedOperatorDefinition.invokesNone
readonly invokesNone: boolean;
True when NO operand position invokes — the cheap operator-level pre-gate for the latent half of the projection rule.
BoxedOperatorDefinition.lambda
readonly lambda: LambdaDefinition | undefined;
If this operator definition was created from a user-defined function
literal (f(x) := …, x ↦ …, ce.assign('f', lambda)), a structured
view of it for traversal and classification: the parameters and the body
as a boxed expression. undefined for built-in operators.
The return shape and per-argument types are also available via signature; this accessor additionally exposes the body so a consumer can resolve a function reference structurally — without re-parsing or textually inlining its source.
BoxedOperatorDefinition.type?
optional type?: OperatorTypeHandlerOnTypes;
If present, this handler determines the result type more precisely than the signature, from the operand DESCRIPTORS (types, facts and structure — never the operand expressions).
BoxedOperatorDefinition.sgn?
optional sgn?: (ops, options) => Sign | undefined;
If present, this handler can be used to determine the sign of the return value of the function, based on the sign and type of its arguments.
The arguments themselves should not be evaluated, only their types and sign should be used.
This can be used in some case for example to determine when certain simplifications are valid.
The handler MUST be a pure function of the operands: no evaluation
(.evaluate(), .N() — including indirectly, through helpers that
numericize a bound or probe a collection element), no canonicalization
of new expressions, no declarations. The type path dispatches sgn
handlers while deriving an application's type (the sgn operand fact),
so a handler that changes engine state invalidates the very caches the
derivation is filling. Audit record: open item O7 of
docs/plans/2026-08-22-type-handlers-on-types.md.
BoxedOperatorDefinition.eq?
optional eq?: (a, b, prover?) => boolean | undefined;
See OperatorDefinition.eq for the meaning of prover.
BoxedOperatorDefinition.neq?
optional neq?: (a, b) => boolean | undefined;
BoxedOperatorDefinition.canEnumerate?
optional canEnumerate?: (expr) => boolean | undefined;
The eager producer's enumerability precondition — see the
canEnumerate contract on OperatorDefinition.
BoxedOperatorDefinition.elementCount?
optional elementCount?: (expr) => number | undefined;
The eager producer's element count — see the elementCount contract on
OperatorDefinition.
BoxedOperatorDefinition.inferOperandTypes?
optional inferOperandTypes?: (ops, requirement) =>
| readonly (Type | undefined)[]
| undefined;
Use-driven element inference — see the inferOperandTypes contract on
OperatorDefinition.
BoxedOperatorDefinition.canonical?
optional canonical?: (ops, options) => Expression | null;
BoxedOperatorDefinition.evaluate?
optional evaluate?: (ops, options) => Expression | undefined;
BoxedOperatorDefinition.evaluateAsync?
optional evaluateAsync?: (ops, options) => Promise<Expression | undefined>;
BoxedOperatorDefinition.evalDimension?
optional evalDimension?: (ops, options) => Expression;
BoxedOperatorDefinition.derivative?
optional derivative?: OperatorDerivative;
The partial derivatives of the operator, as given by the derivative
key of its definition. See OperatorDerivative.
BoxedOperatorDefinition.compile?
optional compile?: OperatorCompileHandler;
BoxedOperatorDefinition.stripsMissingAt()
stripsMissingAt(i): boolean
True if a missing arm is stripped from parameter position i before
validation (§3.A). Only propagate/handle operators strip; missingStrip
selects the positions.
####### i
number
BoxedOperatorDefinition.threadsConditionalsAt()
threadsConditionalsAt(i): boolean
True if the threadsConditionals flag of
OperatorDefinitionFlags selects operand position i: a
conditional value (When, Which) there moves out of the application at
evaluation. A broadcastable operator threads every position whatever this
answers.
####### i
number
BoxedOperatorDefinition.resolvedNanBehaviorAt()
resolvedNanBehaviorAt(i, armSignature?): "reject" | "propagate" | "handle" | "inert"
The resolved NaN policy for parameter position i (Contract B,
docs/ERROR-MODEL.md §4). For a user-defined callable the answer is
always 'inert' — the higher-order conservative floor, absolute even
over an explicit declaration (see isUserFunctionDefinition).
Otherwise an explicit nanBehavior declaration wins.
Otherwise, while the slot's declared carrier admits nan (bare
number, an inferred signature, a union with nan) the answer is
'inert' — NaN is an ordinary domain member and the handler owns
it. For a precise carrier that excludes nan, the derived default is
'propagate' when the carrier is a subtype of complex that is not a
subtype of integer and the result type is numeric, 'reject'
otherwise. Recomputed from the current signature — never cached.
####### i
number
####### armSignature?
The resolved overload arm to derive from, when the caller has one:
per-arm carriers give per-arm derived policies. Explicit
nanBehavior declarations remain operator-level.
BoxedOperatorDefinition.contractBResultAdjustment()
contractBResultAdjustment(ops, armSignature?):
| "none"
| "is-nan"
| "widen-nan"
| "widen-nan-cells"
| "widen-marker"
| "is-marker"
The Contract B adjustment to a derived application result type for
these arguments (docs/ERROR-MODEL.md §4): 'is-marker' when the
declared definedWhen is provably false — the value IS the codomain
marker (§2 rule 4: NaN for a numeric codomain, Missing for a
settled non-numeric one); 'widen-marker' when a DECLARED partiality
is undischarged (definedWhen undecided, or an explicit
may-marker) — every cell of the result gains its marker arm;
'is-nan' when a scalar argument in a propagate slot is a proven
NaN — the value of the whole application is NaN, whatever the
codomain's shape; 'widen-nan' when such an argument may be NaN —
the application gains a top-level | nan arm; 'widen-nan-cells'
when the NaN evidence rides in the cells of a broadcast lift — the
lifted result's numeric cells gain | nan; 'none' otherwise. The
omitted may-marker default contributes no arm (see
the implementation note in boxed-operator-definition.ts).
####### ops
readonly Expression[]
####### armSignature?
The resolved overload arm, when the caller has one — the NaN evidence derives per-slot policies from its carriers.
BoxedOperatorDefinition.invokesAt()
invokesAt(i): boolean
True if operand position i may INVOKE a function-valued operand — the
per-position reader for the invokes flag of OperatorDefinitionFlags.
Missing
map indices default to true. Every consumer of the metadata goes
through this accessor (or invokesNone), never the raw field.
####### i
number
DeclareOptions
type DeclareOptions = {
scope: Scope;
extend: boolean;
};
The options of ce.declare(id, def, options).
scope: the scope the declaration is installed in. The default is the current lexical scope.extend: whentrue,defis a PATCH applied to the operator definition currently visible forid, not a new definition. See OperatorDefinitionPatch.
OperatorDefinitionPatch
type OperatorDefinitionPatch = OperatorDefinition & {
addSignature: | Type
| TypeString;
};
The patch ce.declare(id, patch, { extend: true }) applies to the operator
definition currently visible for id.
The engine builds a NEW definition from the fields of the visible one and
the fields of the patch, and installs it in the target scope. A field the
patch does not name keeps its value. A field the patch names replaces the
old value: when two extensions give the same handler (two evaluate
handlers, for example), the later one wins. The visible definition is not
changed, so an expression boxed before the extension keeps using it.
signaturereplaces the signature.addSignatureadds an overload: the new signature is the intersectionold & addSignature. A call that matches both arms resolves to the old arm first.
In both cases the new signature must be a subtype of the old one, so that
every call that was valid stays valid. For example (value, value*) -> set
can replace (set<any>, value*) -> set, but (any*) -> any cannot replace
(value+) -> value. This does not apply when the old signature was only
inferred (inferredSignature is true: an operator declared without a
signature, or a function whose signature is inferred from its body),
since such a signature is not a contract.
Extending a standard-library operator without replacing its evaluate,
canonical, compile or derivative handler (for example, to add a
description or a wider signature) keeps it a library operator: D,
compilation and the numeric evaluation of the library still apply to it.
An extension that replaces one of these handlers is treated as a user
definition with the same name, as a plain ce.declare() is.
EqHandlers
These handlers compare two expressions.
If only one of the handlers is provided, the other is derived from it.
Having both may be useful if comparing non-equality is faster than equality.
EqHandlers.eq
eq: (a, b) => boolean | undefined;
EqHandlers.neq
neq: (a, b) => boolean | undefined;
Hold
type Hold = "none" | "all" | "first" | "rest" | "last" | "most";
Host Capabilities
ConsoleHandler
The host console, as the engine sees it: the implementation behind the
console effect label. The Print operator calls log; the Input
operator calls readLine.
ConsoleHandler.log()
log(line): void
Write one line of text. The line has no trailing newline; the handler adds the line break its output medium needs.
####### line
string
ConsoleHandler.readLine()
readLine(prompt?): string | null | undefined
Read one line of text, synchronously. prompt, when given, is displayed
before the read.
The three results are distinct:
- a string: the line, without its trailing newline;
null: end of input, or the user canceled the read —Inputevaluates toNothing;undefined: this host has no interactive input —Inputstays unevaluated.
####### prompt?
string
EntropyHandler
The unseeded source of randomness of the host: the implementation behind
the entropy effect label. RandomExpression draws from it, and so does
every random operator (Random, Shuffle, RandomChoice, …) when it is
evaluated OUTSIDE a WithRandomSeed frame — inside a frame the draws come
from the seeded, deterministic stream and this handler is not consulted.
EffectHandlers
The host capabilities of an engine: one handler for each capability the
library operators can reach. This is the value of ce.effects.
A handler is either an implementation or null. null is a denial:
an operator that needs the capability evaluates to an
Error("capability-denied", …) value instead of reaching the host.
The object is immutable. To change a handler, install a new registry:
assign ce.effects, or call ce.withEffects() for a change that lasts for
one callback.
Only console and entropy have a handler today, because the console
operators and the random operators are the only library operators that
reach a host capability. The other capability labels of the effect system
(network, fs_read, fs_write, time, environment) get a handler when
the first operator that needs one is added: a handler that no operator
reads would accept an override and silently do nothing.
EffectHandlers.console
readonly console: ConsoleHandler | null;
EffectHandlers.entropy
readonly entropy: EntropyHandler | null;
EffectHandlerOverrides
type EffectHandlerOverrides = { readonly [K in keyof EffectHandlers]?: EffectHandlers[K] };
A partial change to the host capabilities, for ce.withEffects() and the
ce.effects setter. For each capability:
- an implementation replaces the current handler;
nulldenies the capability, even when the default handler exists;- an absent key, or
undefined, keeps the current handler.
Latex Parsing and Serialization
LatexToken
type LatexToken = string | "<{>" | "<}>" | "<space>" | "<$>" | "<$$>";
A LatexToken is a token as returned by Parser.peek.
It can be one of the indicated tokens, or a string that starts with a `` for LaTeX commands, or a LaTeX character which includes digits, letters and punctuation.
LatexString
type LatexString = string;
A LatexString is a regular string of LaTeX, for example:
\frac{\pi}{2}
Delimiter
type Delimiter =
| "."
| ")"
| "("
| "]"
| "["
| "{"
| "}"
| "<"
| ">"
| "|"
| "||"
| "\lceil"
| "\rceil"
| "\lfloor"
| "\rfloor"
| "\llbracket"
| "\rrbracket";
Open and close delimiters that can be used with MatchfixEntry
record to define new LaTeX dictionary entries.
DelimiterScale
type DelimiterScale = "normal" | "scaled" | "big" | "none";
LibraryCategory
type LibraryCategory =
| "arithmetic"
| "calculus"
| "collections"
| "colors"
| "control-structures"
| "combinatorics"
| "core"
| "linear-algebra"
| "logic"
| "number-theory"
| "other"
| "physics"
| "polynomials"
| "relop"
| "statistics"
| "trigonometry"
| "units";
Precedence
type Precedence = number;
The precedence of an operator is a number that indicates the order in which operators are applied.
For example, in 1 + 2 * 3, the * operator has a higher precedence
than the + operator, so it is applied first.
The precedence ranges from 0 to 1000. The larger the number, the higher the precedence, the more "binding" the operator is.
Operator Precedence Table
| Precedence | Operators | Description |
|---|---|---|
| 880 | \lnot \neg ++ -- + - (prefix) | Prefix/postfix unary |
| 810 | ! ' !! ''' | Factorial, prime (postfix) |
| 800 | _ (subscript) | Subscript |
| 780 | \degree \prime | Degree, prime symbols |
| 740 | \% | Percent |
| 720 | / (inline division) | Inline division |
| 700 | ^ \overset \underset | Exponentiation, over/underscript |
| 650 | (invisible multiply) \cdot | Implicit multiplication |
| 600 | \div \frac | Division |
| 390 | \times * / | Multiplication |
| 350 | \cup \cap | Set union/intersection |
| 275 | + - (infix) | Addition, subtraction |
| 270 | \to \rightarrow \mapsto | Arrows |
| 265 | \setminus \smallsetminus : (range) | Set difference, range |
| 260 | := | Assignment |
| 255 | \ne | Not equal |
| 250 | \not\approxeq | Not approximately equal |
| 247 | \approx | Approximately |
| 245-246 | = < > \lt \gt \nless \ngtr | Equality, comparison |
| 241-244 | \le \leq \ge \geq >= | Less/greater or equal |
| 240 | \in \notin \subset \supset ... | Set membership/relations |
| 235 | \land \wedge \& | Logical AND |
| 232 | \veebar \barwedge (Xor, Nand, Nor) | Logical XOR, NAND, NOR |
| 230 | \lor \vee \parallel | Logical OR |
| 220 | \implies \Rightarrow \vdash \models | Implication, entailment |
| 219 | \iff \Leftrightarrow \equiv | Equivalence |
| 200 | \forall \exists \exists! | Quantifiers |
| 160 | \mid \vert (set builder) | Set builder notation |
| 19-20 | , ; \ldots | Sequence separators |
Key Relationships
- Comparisons bind tighter than logic:
x = 1 \lor y = 2parses as(x = 1) \lor (y = 2), notx = (1 \lor y) = 2 - AND binds tighter than OR:
a \land b \lor cparses as(a \land b) \lor c - Logic operators bind tighter than implication:
a \lor b \implies cparses as(a \lor b) \implies c
Some constants are defined below for common precedence values.
Note: MathML defines some operator precedence, but it has some issues and inconsistencies. However, whenever possible we adopted the MathML precedence.
The JavaScript operator precedence is documented here.
Terminator
type Terminator = {
minPrec: Precedence;
condition: (parser) => boolean;
};
This indicates a condition under which parsing should stop:
- an operator of a precedence higher than specified has been encountered
- the last token has been reached
- or if a condition is provided, the condition returns true
ParseHandler
type ParseHandler =
| ExpressionParseHandler
| SymbolParseHandler
| FunctionParseHandler
| EnvironmentParseHandler
| PostfixParseHandler
| InfixParseHandler
| MatchfixParseHandler;
Custom parsing handler.
When this handler is invoked the parser points right after the LaTeX fragment that triggered it.
Tokens can be consumed with parser.nextToken() and other parser methods
such as parser.parseGroup(), parser.parseOptionalGroup(), etc...
If it was in an infix or postfix context, lhs will represent the
left-hand side argument. In a prefix or matchfix context, lhs is null.
In a superfix (^) or subfix (_) context (that is if the first token of
the trigger is ^ or _), lhs is ["Superscript", lhs, rhs]
and ["Subscript", lhs, rhs], respectively.
The handler should return null if the tokens could not be parsed
(didn't match the syntax that was expected), or the matching expression
otherwise.
If the tokens were parsed but should be ignored, the handler should
return Nothing.
ExpressionParseHandler
type ExpressionParseHandler = (parser, until?) => MathJsonExpression | null;
PrefixParseHandler
type PrefixParseHandler = (parser, until?) => MathJsonExpression | null;
SymbolParseHandler
type SymbolParseHandler = (parser, until?) => MathJsonExpression | null;
FunctionParseHandler
type FunctionParseHandler = (parser, until?) => MathJsonExpression | null;
EnvironmentParseHandler
type EnvironmentParseHandler = (parser, until?) => MathJsonExpression | null;
PostfixParseHandler
type PostfixParseHandler = (parser, lhs, until?) => MathJsonExpression | null;
InfixParseHandler
type InfixParseHandler = (parser, lhs, until) => MathJsonExpression | null;
MatchfixParseHandler
type MatchfixParseHandler = (parser, body) => MathJsonExpression | null;
LatexArgumentType
type LatexArgumentType =
| "{expression}"
| "[expression]"
| "{text}"
| "[text]"
| "{unit}"
| "[unit]"
| "{glue}"
| "[glue]"
| "{string}"
| "[string]"
| "{color}"
| "[color]";
Trigger
type Trigger = {
latexTrigger: LatexString | LatexToken[];
symbolTrigger: MathJsonSymbol;
};
A trigger is the set of tokens that will make an entry in the
LaTeX dictionary eligible to parse the stream and generate an expression.
If the trigger matches, the parse handler is called, if available.
The trigger can be specified either as a LaTeX string (latexTrigger) or
as an symbol (symbolTrigger). A symbol match several
LaTeX expressions that are equivalent, for example \operatorname{gcd} or
\mathbin{gcd}, match the "gcd" symbol
matchfix operators use openTrigger and closeTrigger instead.
BaseEntry
type BaseEntry = {
name: MathJsonSymbol;
serialize: LatexString | SerializeHandler;
standaloneSymbol: boolean;
};
Maps a string of LaTeX tokens to a function or symbol and vice-versa.
DefaultEntry
type DefaultEntry = BaseEntry & Trigger & {
parse: | MathJsonExpression
| ExpressionParseHandler;
};
ExpressionEntry
type ExpressionEntry = BaseEntry & Trigger & {
kind: "expression";
parse: | MathJsonExpression
| ExpressionParseHandler;
precedence: Precedence;
};
MatchfixEntry
type MatchfixEntry = BaseEntry & {
kind: "matchfix";
openTrigger: Delimiter | LatexToken[];
closeTrigger: Delimiter | LatexToken[];
parse: MatchfixParseHandler;
};
MatchfixEntry.openTrigger
openTrigger: Delimiter | LatexToken[];
If kind is 'matchfix': the openTrigger and closeTrigger
properties are required.
MatchfixEntry.parse?
optional parse?: MatchfixParseHandler;
When invoked, the parser is pointing after the close delimiter. The argument of the handler is the body, i.e. the content between the open delimiter and the close delimiter.
InfixEntry
type InfixEntry = BaseEntry & Trigger & {
kind: "infix";
associativity: "right" | "left" | "none" | "any";
precedence: Precedence;
parse: string | InfixParseHandler;
};
InfixEntry.kind
kind: "infix";
Infix position, with an operand before and an operand after: a ⊛ b.
Example: +, \times.
InfixEntry.associativity?
optional associativity?: "right" | "left" | "none" | "any";
-
none: a ? b ? c -> syntax error -
any: a + b + c -> +(a, b, c) -
left: a / b / c -> /(/(a, b), c) -
right: a = b = c -> =(a, =(b, c)) -
any-associative operators have an unlimited number of arguments -
left,rightornoneassociative operators have two arguments
PostfixEntry
type PostfixEntry = BaseEntry & Trigger & {
kind: "postfix";
precedence: Precedence;
parse: string | PostfixParseHandler;
};
PostfixEntry.kind
kind: "postfix";
Postfix position, with an operand before: a ⊛
Example: !.
PrefixEntry
type PrefixEntry = BaseEntry & Trigger & {
kind: "prefix";
precedence: Precedence;
parse: string | PrefixParseHandler;
};
PrefixEntry.kind
kind: "prefix";
Prefix position, with an operand after: ⊛ a
Example: -, \not.
EnvironmentEntry
type EnvironmentEntry = BaseEntry & {
kind: "environment";
parse: EnvironmentParseHandler;
symbolTrigger: MathJsonSymbol;
};
A LaTeX dictionary entry for an environment, that is a LaTeX
construct using \begin{...}...\end{...}.
SymbolEntry
type SymbolEntry = BaseEntry & Trigger & {
kind: "symbol";
precedence: Precedence;
parse: | MathJsonExpression
| SymbolParseHandler;
};
SymbolEntry.precedence?
optional precedence?: Precedence;
Used for appropriate wrapping (i.e. when to surround it with parens)
FunctionEntry
type FunctionEntry = BaseEntry & Trigger & {
kind: "function";
parse: | MathJsonExpression
| FunctionParseHandler;
arguments: "enclosure" | "implicit";
};
A function is a symbol followed by:
- some postfix operators such as
\prime - an optional list of arguments in an enclosure (parentheses)
For more complex situations, for example implicit arguments or
inverse functions postfix (i.e. ^-1), use a custom parse handler with a
entry of kind expression.
FunctionEntry.arguments?
optional arguments?: "enclosure" | "implicit";
How arguments are parsed:
'enclosure'(default): arguments must be enclosed in parentheses, e.g.\max(a, b).'implicit': arguments can be provided with or without parentheses, e.g.\det Ais parsed as\det(A). Bare arguments are parsed at multiplication precedence, so\det 2A + 1is parsed as\det(2A) + 1.
LatexDictionaryEntry
type LatexDictionaryEntry = OneOf<[
| ExpressionEntry
| MatchfixEntry
| InfixEntry
| PostfixEntry
| PrefixEntry
| SymbolEntry
| FunctionEntry
| EnvironmentEntry
| DefaultEntry]>;
A dictionary entry is a record that maps a LaTeX token or string of tokens ( a trigger) to a MathJSON expression or to a parsing handler.
To define custom LaTeX parsing and serialization, pass a LatexSyntax
instance to the ComputeEngine constructor via the latexSyntax option.
The LatexSyntax dictionary option replaces the default dictionary, so
start from LATEX_DICTIONARY and append your entries:
import { ComputeEngine, LatexSyntax, LATEX_DICTIONARY } from '@cortex-js/compute-engine';
const ce = new ComputeEngine({
latexSyntax: new LatexSyntax({ dictionary: [...LATEX_DICTIONARY, myEntry] }),
});
SymbolResolution
type SymbolResolution = {
type: | BoxedType
| TypeString;
subscriptEvaluate: boolean;
};
What the ambient environment knows about a declared symbol, as reported by the ParseLatexOptions.resolveSymbol handler.
Declaration is signaled by the presence of this record (the handler
returns undefined for an undeclared symbol), so a declared symbol whose
type is not known is { type: 'unknown' } — there is no way to report a
type for an undeclared symbol.
ParseLatexOptions
type ParseLatexOptions = NumberFormat & {
strict: boolean;
skipSpace: boolean;
parseNumbers: "auto" | "rational" | "decimal" | "never";
resolveSymbol: (symbol) => SymbolResolution | undefined;
resolveApplication: (context) => "apply" | "multiply" | undefined;
parseUnexpectedToken: (lhs, parser) => MathJsonExpression | null;
preserveLatex: boolean;
diagnostics: boolean;
onAmbiguity: "report" | "error";
quantifierScope: "tight" | "loose";
timeDerivativeVariable: string;
tolerance: number;
};
The LaTeX parsing options can be used with the ce.parse() method.
ParseLatexOptions.strict
strict: boolean;
Controls the strictness of LaTeX parsing:
true: Strict LaTeX syntax required (e.g.,\sin{x},x^{n+1})false: Accept relaxed Math-ASCII/Typst-like syntax in addition to LaTeX (e.g.,sin(x),x^(n+1))
Default: true
ParseLatexOptions.skipSpace
skipSpace: boolean;
If true, ignore space characters in math mode.
Default: true
ParseLatexOptions.parseNumbers
parseNumbers: "auto" | "rational" | "decimal" | "never";
When parsing a decimal number, e.g. 3.1415:
"auto"or"decimal": if a decimal number, parse it as an approximate decimal number with a whole part and a fractional part"rational": if a decimal number, parse it as an exact rational number with a numerator and a denominator. If not a decimal number, parse it as a regular number."never": do not parse numbers, instead return each token making up the number (minus sign, digits, decimal marker, etc...).
Note: if the number includes repeating digits (e.g. 1.33(333)),
it will be parsed as a decimal number even if this setting is "rational".
Default: "auto"
ParseLatexOptions.resolveSymbol?
optional resolveSymbol?: (symbol) => SymbolResolution | undefined;
The symbol oracle: invoked when the parser needs to know what the ambient environment knows about a symbol.
Return undefined if the symbol is undeclared, or a SymbolResolution
record if it is declared. Declaration is the presence of the record —
a symbol declared with an unknown type is still declared (return
{ type: 'unknown' } for it), which is distinct from returning
undefined.
Lexical bindings and explicit engine declarations take precedence. This
handler supplies facts for names without an authoritative declaration;
speculative types inferred from earlier uses do not suppress it.
Answers are cached by name for a single parse. Use resolveApplication
for notation choices that depend on an occurrence's syntax or position.
Supplied facts belong to the resulting expression: they are retained for deferred canonicalization, including per-call handlers, without declaring symbols in the caller's scope. Changing a handler later does not change the meaning of an already parsed expression.
The symbol argument is a valid symbol.
ParseLatexOptions.resolveApplication?
optional resolveApplication?: (context) => "apply" | "multiply" | undefined;
Interpret an unresolved symbol followed by parentheses.
Called after structural parsing for heads without an authoritative type.
Explicit declarations, external symbol facts and lexical parameters take
precedence. Return undefined to retain the usual notation heuristics.
Return apply or multiply to commit an occurrence's reading without
declaring its head. The decision is retained in raw MathJSON and survives
later canonicalization, including when this handler is supplied per-call.
This is a pure syntax policy, not a definition recognizer: a host that uses
= for definitions should discover headers and declare them before parsing
bodies. The hook is not called for bare juxtaposition or square brackets.
ParseLatexOptions.parseUnexpectedToken
parseUnexpectedToken: (lhs, parser) => MathJsonExpression | null;
This handler is invoked when the parser encounters an unexpected token.
The lhs argument is the left-hand side of the token, if any.
The handler can access the unexpected token with parser.peek. If
it is a token that should be recognized, the handler can consume it
by calling parser.nextToken().
The handler should return an expression or null if the token is not
recognized.
ParseLatexOptions.preserveLatex
preserveLatex: boolean;
If true, the expression will be decorated with the LaTeX fragments corresponding to each elements of the expression.
The top-level expression, that is the one returned by parse(), will
include the verbatim LaTeX input that was parsed. The sub-expressions
may contain a slightly different LaTeX, for example with consecutive spaces
replaced by one, with comments removed and with some low-level LaTeX
commands replaced, for example \egroup and \bgroup.
Default: false
ParseLatexOptions.diagnostics?
optional diagnostics?: boolean;
If true, collect opt-in parse-time diagnostics (see ParseDiagnostic)
flagging charitable parse decisions — undeclared symbols, application-like
juxtaposition read as multiply, discarded % comments, and trailing noise
dropped by recovery. In non-strict mode, also: letter runs read as a
product (ambiguous-letter-run), an implicit product read as the whole
denominator of a / (ambiguous-denominator), digits
separated by white space read as one number (ambiguous-digit-groups), a
symbol directly followed by .digits read as a product
(ambiguous-letter-decimal), and a prefix ± or two signs in a row
(ambiguous-sign).
This flag only takes effect through
ComputeEngine.parse, which
wires up the collector and attaches the resulting array to the top-level
parsed expression's parseDiagnostics property. On the standalone
LatexSyntax.parse() entry point the flag is a silent no-op (that entry
returns plain MathJSON with nowhere to attach diagnostics).
This is purely additive: enabling it never changes the parse output.
Default: false
ParseLatexOptions.onAmbiguity?
optional onAmbiguity?: "report" | "error";
What the lenient grammar (strict: false) does with a reading that has
a second common reading, that is, with each diagnostic whose code starts
with ambiguous- (see ParseDiagnostic):
"report": keep the reading. The diagnostic is reported whendiagnosticsistrue."error": put anErrornode in place of the smallest expression that holds the source span of the diagnostic. The error code is the diagnostic code, and the error holds the source text of the span:["Error", "'ambiguous-sign'", ["LatexString", "'--'"]]. This works withoutdiagnostics: true, and for every code that starts withambiguous-.
The parser records the source span of the expressions it builds. When
no recorded expression holds the span of a diagnostic, for example
because a later step rebuilt that part of the result, the Error node
replaces the whole result. When two diagnostics select nested
expressions, the Error node of the outer expression is kept.
In strict mode (strict: true) this option has no effect: the strict
grammar reports no ambiguous-* diagnostic.
Default: "report"
ParseLatexOptions.quantifierScope
quantifierScope: "tight" | "loose";
Controls how quantifier scope is determined when parsing expressions
like \forall x. P(x) \rightarrow Q(x).
-
"tight": The quantifier binds only to the immediately following well-formed formula, stopping at logical connectives (\rightarrow,\implies,\land,\lor, etc.). This follows standard First-Order Logic conventions. Use explicit parentheses for wider scope:\forall x. (P(x) \rightarrow Q(x)). -
"loose": The quantifier scope extends to the end of the expression or until a lower-precedence operator is encountered.
Default: "tight"
Example
// With "tight" (default):
// \forall x. P(x) \rightarrow Q(x)
// parses as: (∀x. P(x)) → Q(x)
// With "loose":
// \forall x. P(x) \rightarrow Q(x)
// parses as: ∀x. (P(x) → Q(x))
ParseLatexOptions.timeDerivativeVariable
timeDerivativeVariable: string;
The variable used for time derivatives in Newton notation
(\dot{x}, \ddot{x}, etc.).
When parsing \dot{x}, it will be interpreted as ["D", "x", timeDerivativeVariable].
Default: "t"
ParseLatexOptions.tolerance
tolerance: number;
The tolerance used when validating inferred range steps from sampled
elements (e.g. [0, 0.1, 0.2, \ldots, 1]). Two consecutive differences
are considered equal when they differ by less than this value.
Populated automatically from ce.tolerance by ce.parse().
Default: ce.tolerance (typically 1e-7)
Parser
An instance of Parser is provided to the parse handlers of custom
LaTeX dictionary entries.
Parser.options
readonly options: Readonly<ParseLatexOptions>;
Parser.inQuantifierScope
readonly inQuantifierScope: boolean;
True if currently parsing inside a quantifier body (ForAll, Exists, etc.)
Parser.atEnd
readonly atEnd: boolean;
True if the last token has been reached.
Consider also atTerminator().
Parser.atBoundary
Parser.resolveSymbol()
resolveSymbol(id):
| {
type: BoxedType;
subscriptEvaluate: boolean;
inferred: boolean;
}
| undefined
The single symbol oracle: everything the parser knows about id.
Merges (in priority order) parser-local bindings — sum indices, Block/
Function parameters, tracked in the parser's symbol table — over the
ParseLatexOptions.resolveSymbol handler (which ce.parse() wires
to consult explicit engine declarations before external handlers).
Returns undefined if id is undeclared. A declared symbol always gets
a record — declaration presence is the !== undefined check, distinct
from type knowledge: a symbol declared with an unknown type still
resolves (with type.isUnknown true).
####### id
string
Parser.isFunctionTriggerName()
isFunctionTriggerName(name): boolean
Whether name is claimed by a kind: 'function' dictionary entry's
symbolTrigger (log, lcm, var, …). Such a name owns its call
syntax — including any subscript, which its parser may bind as an
argument (\operatorname{log}_2(x) is Log(x, 2)) — so subscript
absorption must not fold name_sub into a plain symbol and preempt the
function reading.
####### name
string
Parser.pushSymbolTable()
pushSymbolTable(): void
Parser.popSymbolTable()
popSymbolTable(): void
Parser.enterQuantifierScope()
enterQuantifierScope(): void
Enter a quantifier scope for parsing the body of ForAll, Exists, etc.
Parser.atTerminator()
atTerminator(t): boolean
Return true if the terminator condition is met or if the last token has been reached.
####### t
Terminator | undefined
Parser.latex()
latex(start, end?): string
Return a string representation of the expression
between start and end (default: the whole expression)
####### start
number
####### end?
number
Parser.error()
error(code, fromToken): MathJsonExpression
Return an error expression with the specified code and arguments.
The returned Error expression includes sourceOffsets metadata with
zero-based, end-exclusive offsets into the serialized LaTeX. Missing-operand
errors use a collapsed (zero-width) range at the position where the operand
was expected.
####### code
string | [string, ...MathJsonExpression[]]
####### fromToken
number
Parser.sourceOffsets()
sourceOffsets(startToken, endToken?): [number, number]
Return source offsets for a token range, as zero-based, end-exclusive
character offsets into the serialized LaTeX (tokensToString). For input
that round-trips unchanged (e.g. editor-generated LaTeX), these match the
original input string.
####### startToken
number
####### endToken?
number
Parser.skipSpace()
skipSpace(): boolean
If there are any space, advance the index until a non-space is encountered
Parser.skipVisualSpace()
skipVisualSpace(): void
Skip over "visual space" which
includes space tokens, empty groups {}, and commands such as \, and \!
Parser.match()
match(token): boolean
If the next token matches the target advance and return true. Otherwise return false
####### token
string
Parser.matchAll()
matchAll(tokens): boolean
Return true if the next tokens match the argument, an array of tokens, or null otherwise
####### tokens
string[]
Parser.matchAny()
matchAny(tokens): string
Return the next token if it matches any of the token in the argument or null otherwise
####### tokens
string[]
Parser.parseChar()
parseChar(): string | null
If the next token is a character, return it and advance the index
This includes plain characters (e.g. 'a', '+'...), characters
defined in hex (^^ and ^^^^), the \char and \unicode command.
Parser.parseGroup()
parseGroup(): MathJsonExpression | null
Parse an expression in a LaTeX group enclosed in curly brackets {}.
These are often used as arguments to LaTeX commands, for example
\frac{1}{2}.
Return null if none was found
Return Nothing if an empty group {} was found
Parser.parseToken()
parseToken(): MathJsonExpression | null
Some LaTeX commands (but not all) can accept arguments as single
tokens (i.e. without braces), for example ^2, \sqrt3 or \frac12
This argument will usually be a single token, but can be a sequence of
tokens (e.g. \sqrt\frac12 or \sqrt\operatorname{speed}).
The following tokens are excluded from consideration in order to fail
early when encountering a likely syntax error, for example x^(2)
instead of x^{2}. With ( in the list of excluded tokens, the
match will fail and the error can be recovered.
The excluded tokens include !"#$%&(),/;:?@[]|~", \left, \bigl, etc...
Parser.parseOptionalGroup()
parseOptionalGroup(): MathJsonExpression | null
Parse an expression enclosed in a LaTeX optional group enclosed in square brackets [].
Return null if none was found.
Parser.parseEnclosure()
parseEnclosure(): MathJsonExpression | null
Parse an enclosure (open paren/close paren, etc..) and return the expression inside the enclosure
Parser.parseStringGroup()
parseStringGroup(optional?, rawTokens?): string | null
Some LaTeX commands have arguments that are not interpreted as
expressions, but as strings. For example, \begin{array}{ccc} (both
array and ccc are strings), \color{red} or \operatorname{lim sup}.
If the next token is the start of a group ({), return the content
of the group as a string. This may include white space, and it may need
to be trimmed at the start and end of the string.
LaTeX commands are typically not allowed inside a string group (for example,
\alpha would result in an error), but we do not enforce this.
If optional is true, this should be an optional group in square brackets
otherwise it is a regular group in braces.
If rawTokens is provided, the raw (un-normalized) tokens of the group
content are appended to it — useful when the same content must be matched
verbatim later (the returned string normalizes commands such as \alpha
to unicode, which is lossy).
####### optional?
boolean
####### rawTokens?
string[]
Parser.parseSymbol()
parseSymbol(until?): MathJsonExpression | null
A symbol can be:
- a single-letter symbol:
x - a single LaTeX command:
\pi - a multi-letter symbol:
\operatorname{speed}
####### until?
Partial<Terminator>
Parser.parseTabular()
parseTabular():
| MathJsonExpression[][]
| null
Parse an expression in a tabular format, where rows are separated by \\
and columns by &.
Return rows of sparse columns: empty rows are indicated with Nothing,
and empty cells are also indicated with Nothing.
Parser.parseArguments()
parseArguments(kind?, until?):
| readonly MathJsonExpression[]
| null
Parse an argument list, for example: (12, x+1) or \left(x\right)
- 'enclosure' : will look for arguments inside an enclosure (an open/close fence) (default)
- 'implicit': either an expression inside a pair of
(), or just a primary (i.e. we interpret\cos x + 1as\cos(x) + 1)
Return an array of expressions, one for each argument, or null if no
argument was found.
####### kind?
"enclosure" | "implicit"
####### until?
Parser.parseBraceArguments()
parseBraceArguments():
| readonly MathJsonExpression[]
| null
Parse one or more {...} groups as an argument list, exactly as if
they were a parenthesized argument list: \gcd{a,b} ≡ \gcd(a,b),
and consecutive groups are successive arguments (\mod{x}{2} ≡
\mod(x,2), the TeX multi-argument-macro habit). A group whose
content is a comma sequence contributes each element as an argument.
An empty group is not an argument ({} is spacing/grouping
decoration), and no group at all returns null.
Used as a fallback after parseArguments('enclosure') for
dictionary-registered function heads, where the writer's intent is
unambiguous even though the braces render invisibly.
Parser.parsePostfixOperator()
parsePostfixOperator(lhs, until?): MathJsonExpression | null
Parse a postfix operator, such as ' or !.
Prefix, infix and matchfix operators are handled by parseExpression()
####### lhs
MathJsonExpression | null
####### until?
Partial<Terminator>
Parser.parseExpression()
parseExpression(until?): MathJsonExpression | null
Parse an expression:
<expression> ::=
| <primary> ( <infix-op> <expression> )?
| <prefix-op> <expression>
<primary> :=
(<number> | <symbol> | <function-call> | <matchfix-expr>)
(<subsup> | <postfix-operator>)*
<matchfix-expr> :=
<matchfix-op-open> <expression> <matchfix-op-close>
<function-call> ::=
| <function><matchfix-op-group-open><expression>[',' <expression>]<matchfix-op-group-close>
This is the top-level parsing entry point.
Stop when an operator of precedence less than until.minPrec
or the sequence of tokens until.tokens is encountered
until is { minPrec:0 } by default.
####### until?
Partial<Terminator>
Parser.addBoundary()
addBoundary(boundary): void
Boundaries are used to detect the end of an expression.
They are used for unusual syntactic constructs, for example
\int \sin x dx where the dx is not an argument to the \sin
function, but a boundary of the integral.
They are also useful when handling syntax errors and recovery.
For example, \begin{bmatrix} 1 & 2 { \end{bmatrix} has an
extraneous {, but the parser will attempt to recover and continue
parsing when it encounters the \end{bmatrix} boundary.
####### boundary
string[]
Parser.removeBoundary()
removeBoundary(): void
Parser.matchBoundary()
matchBoundary(): boolean
Parser.boundaryError()
boundaryError(msg): MathJsonExpression
####### msg
string | [string, ...MathJsonExpression[]]
RootStyle
type RootStyle = "radical" | "quotient" | "solidus";
How to serialize a root, i.e. \sqrt{x}, x^{1/2} or x^\frac12.
FractionStyle
type FractionStyle =
| "quotient"
| "block-quotient"
| "inline-quotient"
| "inline-solidus"
| "nice-solidus"
| "reciprocal"
| "factor";
How to serialize a fraction.
LogicStyle
type LogicStyle = "word" | "boolean" | "uppercase-word" | "punctuation";
How to serialize the logic operators.
NumericSetStyle
type NumericSetStyle = "compact" | "regular" | "interval" | "set-builder";
How to serialize a numeric set, i.e. \R^*, \R \setminus \lbrace 0\rbrace.
IndexStyle
type IndexStyle = "subscript" | "bracket";
How to serialize collection indexing (the At operator).
StyleOption
type StyleOption<T> = T | ((expr, level) => T);
A serialization style option: either a constant, or a function of the expression and of its nesting level.
Type Parameters
• T extends string
SerializeLatexOptions
type SerializeLatexOptions = NumberSerializationFormat & {
prettify: boolean;
materialization: boolean | number | [number, number];
exponentialE: LatexString;
invisibleMultiply: LatexString;
invisiblePlus: LatexString;
multiply: LatexString;
missingSymbol: LatexString;
keywordStyle: "text" | "keyword" | "operatorname";
applyFunctionStyle: StyleOption<DelimiterScale>;
groupStyle: StyleOption<DelimiterScale>;
rootStyle: StyleOption<RootStyle>;
fractionStyle: StyleOption<FractionStyle>;
logicStyle: StyleOption<LogicStyle>;
powerStyle: StyleOption<PowerStyle>;
numericSetStyle: StyleOption<NumericSetStyle>;
indexStyle: StyleOption<IndexStyle>;
dotNotation: boolean;
dmsFormat: boolean;
angleNormalization: "none" | "0...360" | "-180...180";
};
The LaTeX serialization options can used with the expr.toLatex() method.
SerializeLatexOptions.prettify
prettify: boolean;
If true, prettify the LaTeX output.
For example, render \frac{a}{b}\frac{c}{d} as \frac{ac}{bd}
SerializeLatexOptions.materialization
materialization: boolean | number | [number, number];
Controls the materialization of the lazy collections.
- If
true, lazy collections are materialized, i.e. it is rendered as a LaTeX expression with all its elements. - If
false, the expression is not materialized, i.e. it is rendered as a LaTeX command with its arguments. - If a number is provided, it is the maximum number of elements that will be materialized.
- If a pair of numbers is provided, it is the number of elements of the head and the tail that will be materialized, respectively.
SerializeLatexOptions.exponentialE?
optional exponentialE?: LatexString;
LaTeX used to render the constant ExponentialE, the counterpart of
imaginaryUnit. Use e or \mathrm{e} to match the glyph used for
the imaginary unit.
Serialization only: \exponentialE, \mathrm{e} and \operatorname{e}
are always read as the constant.
Default
\exponentialE
SerializeLatexOptions.invisibleMultiply
invisibleMultiply: LatexString;
LaTeX string used to render an invisible multiply, e.g. in '2x'.
If empty, both operands are concatenated, i.e. 2x.
Use \cdot to insert a \cdot operator between them, i.e. 2 \cdot x.
Empty by default.
SerializeLatexOptions.invisiblePlus
invisiblePlus: LatexString;
LaTeX string used to render mixed numbers e.g. '1 3/4'.
Leave it empty to join the main number and the fraction, i.e. render it
as 1\frac{3}{4}.
Use + to insert an explicit + operator between them,
i.e. 1+\frac{3}{4}
Empty by default.
SerializeLatexOptions.multiply
multiply: LatexString;
LaTeX string used to render an explicit multiply operator.
For example, \times, \cdot, etc...
Default: \times
SerializeLatexOptions.missingSymbol
missingSymbol: LatexString;
Serialize the expression ["Error", "'missing'"], with this LaTeX string
SerializeLatexOptions.keywordStyle
keywordStyle: "text" | "keyword" | "operatorname";
How to serialize keyword constructs (if/then/else, for, where,
and, or, the quantifiers, …).
'text'(default):\text{if },\text{ then }, … — the conventional spelling. Spacing is encoded manually inside the braces.'keyword':\keyword{if},\keyword{then}, … — a math-mode command whose renderer applies symmetric keyword spacing. Requires the rendering environment to define\keyword.'operatorname':\operatorname{if}, … — operator-name spacing.
All three spellings parse back to the same expression.
Default
'text'
SerializeLatexOptions.indexStyle
indexStyle: StyleOption<IndexStyle>;
Notation used to serialize collection indexing (the At operator), e.g.
["At", v, 1].
'bracket'(default):v[1],M[i,j]— programming-style indexing, which always round-trips back toAteven when the collection symbol is not declared.'subscript':v_1,M_{i,j}— conventional mathematical notation, symmetric with how subscript indexing of anindexed_collectionparses; only round-trips when the base is declared as a collection.
SerializeLatexOptions.dotNotation
dotNotation: boolean;
When true, member-access heads serialize to dot notation:
First(p)→p.xSecond(p)→p.yThird(p)→p.zReal(z)→z.\operatorname{real}Imaginary(z)→z.\operatorname{imag}Length(L)→L.\operatorname{count}Sum(L)→L.\operatorname{total}Max(L)→L.\maxMin(L)→L.\min
When false (default), the standard function-call form is used.
Only applies to arity-1 forms. Multi-operand forms (e.g. Sum with
an index tuple) keep their standard serialization even when this is true.
Serializer-only. This flag has no effect on parsing. All input
forms continue to parse as before regardless of the flag (e.g. |L|,
\operatorname{count}(L), and L.\operatorname{count} all parse to
["Length", L] whether dotNotation is on or off). The flag only
decides which form the serializer emits.
Set engine-wide via ce.latexOptions.dotNotation = true, or per-call
via expr.toLatex({ dotNotation: true }).
Default: false
SerializeLatexOptions.dmsFormat?
optional dmsFormat?: boolean;
When true, serialize angle quantities in degrees-minutes-seconds format. When false (default), use decimal degrees.
Default
false
Example
const ce = new ComputeEngine();
const angle = ce.expr(['Quantity', 9.5, 'deg']);
// DMS format
angle.latex({ dmsFormat: true }); // "9°30'"
// Decimal format (default)
angle.latex({ dmsFormat: false }); // "9.5°"
// Full DMS notation
ce.expr(['Quantity', 9.504166, 'deg'])
.latex({ dmsFormat: true }); // "9°30'15\""
SerializeLatexOptions.angleNormalization?
optional angleNormalization?: "none" | "0...360" | "-180...180";
Normalize angles to a specific range during serialization. Useful for geographic coordinates and rotations.
Default
'none'
Example
const ce = new ComputeEngine();
// No normalization (show exact value)
ce.expr(['Degrees', 370])
.latex({ angleNormalization: 'none' }); // "370°"
// Normalize to [0, 360) - useful for bearings
ce.expr(['Degrees', 370])
.latex({ angleNormalization: '0...360' }); // "10°"
ce.expr(['Degrees', -45])
.latex({ angleNormalization: '0...360' }); // "315°"
// Normalize to [-180, 180] - useful for longitude
ce.expr(['Degrees', 190])
.latex({ angleNormalization: '-180...180' }); // "-170°"
// Combine with DMS format
ce.expr(['Degrees', 370])
.latex({
dmsFormat: true,
angleNormalization: '0...360'
}); // "10°0'0\""
ResolvedSerializeLatexOptions
type ResolvedSerializeLatexOptions = Omit<SerializeLatexOptions,
| "applyFunctionStyle"
| "groupStyle"
| "rootStyle"
| "fractionStyle"
| "logicStyle"
| "powerStyle"
| "numericSetStyle"
| "indexStyle"
| "readsAsPointList"
| "readsAsCardinality"> & {
readsAsPointList: ((operands) => boolean | undefined) | undefined;
readsAsCardinality: ((operand) => boolean | undefined) | undefined;
applyFunctionStyle: (expr, level) => DelimiterScale;
groupStyle: (expr, level) => DelimiterScale;
rootStyle: (expr, level) => RootStyle;
fractionStyle: (expr, level) => FractionStyle;
logicStyle: (expr, level) => LogicStyle;
powerStyle: (expr, level) => PowerStyle;
numericSetStyle: (expr, level) => NumericSetStyle;
indexStyle: (expr, level) => IndexStyle;
};
The serialization options as seen by the serializer: the style options
have been normalized from their constant form (e.g. rootStyle: 'solidus')
to their function form.
Serializer
An instance of Serializer is provided to the serialize handlers of custom
LaTeX dictionary entries.
Serializer.options
readonly options: Required<ResolvedSerializeLatexOptions>;
Serializer.dictionary
readonly dictionary: SerializerDictionary;
Serializer.level
level: number;
"depth" of the expression:
- 0 for the root
- 1 for a subexpression of the root
- 2 for subexpressions of the subexpressions of the root
- etc...
This allows the serialized LaTeX to vary depending on the depth of the expression.
For example use \Bigl( for the top level, and \bigl( or ( for others.
Serializer.wrap
wrap: (expr, prec?) => string;
Add a group fence around the expression if it is
an operator of precedence less than or equal to prec.
Serializer.groupStyle
groupStyle: (expr, level) => DelimiterScale;
Serializer.rootStyle
rootStyle: (expr, level) => "radical" | "quotient" | "solidus";
Serializer.fractionStyle
fractionStyle: (expr, level) =>
| "quotient"
| "block-quotient"
| "inline-quotient"
| "inline-solidus"
| "nice-solidus"
| "reciprocal"
| "factor";
Serializer.logicStyle
logicStyle: (expr, level) => "boolean" | "word" | "uppercase-word" | "punctuation";
Serializer.powerStyle
powerStyle: (expr, level) => "quotient" | "solidus" | "root";
Serializer.numericSetStyle
numericSetStyle: (expr, level) => "compact" | "regular" | "interval" | "set-builder";
Serializer.indexStyle
indexStyle: (expr, level) => "subscript" | "bracket";
Serializer.serializeFunction()
serializeFunction(expr, def?): string
####### expr
####### def?
SerializerDictionaryEntry
Serializer.wrapString()
wrapString(s, style, delimiters?): string
Output s surrounded by delimiters.
If delimiters is not specified, use ()
####### s
string
####### style
####### delimiters?
string
Serializer.wrapArguments()
wrapArguments(expr): string
A string with the arguments of expr fenced appropriately and separated by commas.
####### expr
Serializer.wrapShort()
wrapShort(expr): string
Add a group fence around the expression if it is short (not a function)
####### expr
| MathJsonExpression
| null
| undefined
Serializer.wrapPowerBase()
wrapPowerBase(expr): string
Like wrapShort, but for a base directly under a ^ (the base of a
Power/Square, or of a Root written in exponent form: the solidus
or quotient root style), where a nested power or a
postfix Factorial also needs a fence.
####### expr
| MathJsonExpression
| null
| undefined
SerializeHandler
type SerializeHandler = (serializer, expr) => string;
The serialize handler of a custom LaTeX dictionary entry can be
a function of this type.
ParseDiagnostic
type ParseDiagnostic = {
code: string;
start: number;
end: number;
detail: Record<string, unknown>;
};
An opt-in parse-time diagnostic, collected when a LaTeX string is parsed
with ce.parse(latex, { diagnostics: true }) and exposed on the top-level
result via BoxedExpression.parseDiagnostics.
Diagnostics flag charitable parse decisions that are usually errors in
machine-generated LaTeX (LLM output, OCR): a name read as multiplication
where the source looked like a function application, a reference to an
undeclared symbol, an unescaped % that discarded input, or trailing noise
silently dropped by error recovery. They are additive metadata — enabling
them never changes the parse output.
Codes (code, an open enum)
-
"undeclared-symbol"— a parsed symbol reference resolves to no declaration (neither a parser-local binding such as a sum index, nor a definition in the engine scope).detail: { name, type }wheretypeis the string form of the resolved type ("unknown"). Fires at every reference site, including plain variables likex. -
"juxtaposition-as-multiply"— a symbol immediately followed by a delimited group(…)or a matrix environment was read as multiplication rather than function application.detail: { name, declaredAs }withdeclaredAsone of"unknown" | "value" | "function".nameis the source symbol even when it was lexed as a unit (\mathrm{N}(2)) or segmented into a letter run (divisors(60)→"divisors"). When the symbol was read as a unit,detailadditionally carrieslexedAs: "unit". -
"ambiguous-letter-run"— non-strict mode only: a run of two or more letters that is not a known word was read as a product of its parts (eps→e·p·s,sinx→s·i·n·x,xpi→x·π).detail: { run, parts }withrunthe letters as written andpartsthe MathJSON symbols it was read as. The diagnostic span is the run. Not emitted for explicit products such asa*b*c, nor for a run read as one name (a bare function name, a spelled-out Greek letter, a letter run before a parenthesis), nor for a differentialdand one letter that is the numerator or denominator of a differential quotient (dy/dx,\frac{dy}{dx}). Also emitted when an unbraced superscript or subscript takes only the first letter of a run (e^xy→e^x·y,x^ab→x^a·b): the span is then the whole run, andpartsis the script and the rest of the run as written. -
"comment-discarded"— an unescaped%discarded the rest of a line.detail: { discardedLength }. -
"recovered"— trailing tokens skipped/coerced by non-strict error recovery that do not otherwise surface as anErrornode.detailmay include the skipped fragment as{ skipped }. -
"ambiguous-denominator"— non-strict mode only: the denominator of a/or÷is an implicit product, which binds tighter than/.1/2xis read as1/(2x), not(1/2)x. The span covers the denominator. A differential denominator (dy/dx) is not reported. -
"ambiguous-digit-groups"— non-strict mode only: white space between digits was read as part of one number (2 3→ 23,1 000→ 1000,3 .5→ 3.5).detail: { digits }. Visual space commands (1\,000) and the{,}separator are not reported. -
"ambiguous-letter-decimal"— non-strict mode only: a symbol is directly followed by.digits(x.5), read as the productx \cdot 0.5.detail: { name }. The span starts at the.. -
"ambiguous-sign"— non-strict mode only: a prefix±(also spelled\pm,\plusmnor+-) with no left operand, read as a measurement with a nominal value of 0 where a person often means two values (x = ±1is read asMeasurement(0, 1), andy = +-\sqrt{x}asMeasurement(0, √x)), a prefix∓(also spelled\mpor-+) with no left operand, read asMinusPlus(0, …)(x = ∓1and-+xare read asMinusPlus(0, 1)andMinusPlus(0, x)), or two signs in a row (--x,x - -y,a + -b, anda -+ b, which is read asMinusPlus(a, b)).detail: { signs }, the signs as written with no white space. The span covers the signs.a +- bwith no white space is read asMeasurement(a, b)and is not reported. -
Non-strict mode only, codes for a reading that has a second common reading (the reading does not change):
"ambiguous-exponent-end"— where an unbraced exponent ends:e^2pi(e^2·π),e^i pi,e^x/2,x^1/2.detail: { exponent }. The span is from the base to the end of the operand after the exponent:e^2pi."ambiguous-implicit-subscript"— a letter followed by digits is a subscript (x2→x_2), and a digit subscript ends before a letter (x_1y→x_1·y).detail: { base?, subscript }."ambiguous-name-digits"— letters and digits that are not a library function, before a parenthesis:atan3(y)→arctan(3y).detail: { name }."ambiguous-function-argument"— a bare function name with an argument of more than one factor and no parentheses (sin x y→sin(xy)), orlogand a number after white space (log 2 x→log_2(x)), or an argument with no parentheses that starts with+(ln+1→ln(1)).detail: { function }."ambiguous-function-subscript"— a bare function name other thanlogwith a subscript, read as the strict grammar reads it:ln_3(x)→Log(x, 3),tan_1x→Apply(Subscript(Tan, 1), x). A person can mean a name such astan_1.detail: { name, subscript }."ambiguous-function-without-parentheses"— a symbol declared as a function followed by an operand:f x→f·x.detail: { name }."ambiguous-name-then-number"— a name, white space, a number:x 2→x·2.detail: { name }."ambiguous-delta"—ΔorDeltafollowed by a letter:Δx→Δ·x."ambiguous-constant-name"— a library constant alone on the left of=(e = 1.6e-19,pi = 3.14), or followed by a parenthesized group on the left of=(pi(x) = x→π·x = x, where a person can mean the definition of a functionpi).detail: { name }. Only an=at the top level of the line is reported: the index of\sum_{i=1}^nis not."ambiguous-log-base"—logwith two arguments in parentheses:log(x, 2)isLog(x, 2), the base second, and other tools put the base first. Also the namelg, which is the base-10 logarithm and, in computer science, the base-2 logarithm.detail: { name }."ambiguous-engine-operator"— a one-letter library operator written as a plain letter before a parenthesis, read as a call of the operator:N(x)(numeric evaluation),D(x)(derivative). A person usually means a function of their own.\operatorname{N}(x)is not reported.detail: { name }."ambiguous-lookalike-letter"— a Greek letter that looks like a Latin letter (Α,Ρ,ο).detail: { letter }."ambiguous-unknown-character"— a character that is not math, read as a string:y = ж.detail: { text }."ambiguous-radical"— the extent of√without braces or parentheses:√2π→√2·π,√x²→(√x)²,3√8→3·√8. The span ends after the operand that follows the radicand."ambiguous-absolute-value"— bars that pair two ways:|x|y|z|."ambiguous-equation-number"— a parenthesized number or letter at the end of the line, after white space, read as a factor (y = x^2 (2),x = 4 (m)). The span is the group."ambiguous-group-product"— a parenthesized name followed by a parenthesized group with a comma, read as a product ((x)(1,2))."ambiguous-factorial"—!=directly after an operand, read as≠(5!=120). The span is the!=."ambiguous-arrow"—<-, read as< -(x <- 2)."ambiguous-equal-chain"— more than one=in a chain (x = x = x). The span is from the first to the last=."ambiguous-element"—in,\inor∈whose left operand is an equation (y = x in [0,1]). The span is the operator."ambiguous-interval"— afterin,\in,∈or\notin, a bracket pair[a, b]or(a, b), or a range[a..b], followed by an operator, so the pair is not read as an interval:M in [0,1]^2→Element(M, Power(List(0, 1), 2)). The span is the bracket pair and the operator after it, with the operand of a^or a/([0,1]^2).M in [0,1]is not reported."ambiguous-range"— a range with two..(1..10..2), a range with one..next to an operation (1..5/2), or...directly followed by a digit after a decimal number (.5...5, which can be.5..and.5)."ambiguous-percent"— a%after a number (y = 50%), which starts a comment. The span is the number and the%, in original-input coordinates."ambiguous-comma"— a comma outside every bracket (1,5)."ambiguous-list-label"— a list label read as math:1. y = x,x = 1. 5,(1) y = x,a) y = x, or a line that is only1.,(1),(i)or[1]. A letter label is one ofatoh, and only when more follows it: a line that is only(x)is not reported."ambiguous-number-notation"—1_000or0x10.detail: { notation },"digit-grouping"or"hexadecimal"."ambiguous-date"— digit groups joined by-or/that can be a date, a phone number or a range (2026-10-15,9/30/2026,555-1234,7-11).3/4,2-1and two groups in parentheses (x = (1-10)) are not reported.
Their spans use the normalized-LaTeX convention below, except
ambiguous-percent.
Span convention (start/end)
Spans for undeclared-symbol, juxtaposition-as-multiply and every
ambiguous-* code except ambiguous-percent are offsets into CE's
normalized LaTeX (the
re-serialized token stream), which matches the original input only when
the input round-trips unchanged.
comment-discarded is the exception: because the comment is precisely what
was stripped before tokenization, its span is in original-input
coordinates. recovered spans are a best-effort original-input range (equal
to normalized coordinates for the comment-free trailing noise that recovery
handles). Per the ratified spec, spans are informational; policy should key
on code + detail.
Numerics
ExactNumericValueData
type ExactNumericValueData = {
rational: Rational;
radical: number;
imRational: Rational;
imRadical: number;
};
The value is equal to rational * sqrt(radical) + imRational * sqrt(imRadical) * i
Representable set (enforced by ExactNumericValue): one radical times a
Gaussian rational, √r·(p + q·i):
- real values:
rational * sqrt(radical)(imaginary part 0); - pure-imaginary values: the real part is 0 (e.g.
√2·i); - both parts non-zero:
radicalandimRadicalare equal (e.g.2+3i,1/2-5i/3,√2 + √2·i).
A value needing two different radicals on a non-zero real AND a non-zero
imaginary component (e.g. 1 + √2·i) is NOT representable exactly.
NumericValueData
type NumericValueData = {
re: BigDecimal | number;
im: BigDecimal | number;
};
NumericValueFactory
type NumericValueFactory = (data) => NumericValue;
abstract NumericValue
new NumericValue()
new NumericValue(): NumericValue
NumericValue.im
im: number;
The imaginary part of this numeric value, as the double nearest to it.
Can be negative, zero or positive.
This is a PROJECTION for computations in doubles. It is 0 when the
true imaginary part is too small for a double (10^{-800}) and
±Infinity when it is too large (10^{800}). So do not use it to decide
whether the value is complex (use isComplex), nor whether the
imaginary part is finite or an integer.
NumericValue.type
NumericValue.isExact
True if numeric value is the product of a rational and the square root of an integer.
This includes: 3/4√5, -2, √2, etc...
But it doesn't include 0.5, 3.141592, etc...
NumericValue.asExact
If isExact(), returns an ExactNumericValue, otherwise returns undefined.
NumericValue.bignumRe
bignum version of .re, if available
NumericValue.isComplex
True if the imaginary part of this numeric value is not zero.
This is read from a representation that holds the imaginary part without
loss (the exact rational of an ExactNumericValue), so it is true for an
imaginary part whose double projection im is 0, such as the exact
10^{-800}·i. Use it, not im !== 0, to decide whether a value is
complex.
A value with a NaN imaginary part is NaN; do not rely on isComplex to
detect it.
NumericValue.bignumIm
NumericValue.numerator
NumericValue.denominator
NumericValue.isNaN
NumericValue.isPositiveInfinity
NumericValue.isNegativeInfinity
NumericValue.isComplexInfinity
NumericValue.isZero
NumericValue.isOne
NumericValue.isNegativeOne
NumericValue.isZeroWithTolerance()
isZeroWithTolerance(_tolerance): boolean
####### _tolerance
number | BigDecimal
NumericValue.neg()
abstract neg(): NumericValue
NumericValue.inv()
abstract inv(): NumericValue
NumericValue.pow()
abstract pow(n): NumericValue
####### n
| number
| NumericValue
| {
re: number;
im: number;
}
NumericValue.sqrt()
abstract sqrt(): NumericValue
NumericValue.abs()
abstract abs(): NumericValue
NumericValue.exp()
abstract exp(): NumericValue
NumericValue.floor()
abstract floor(): NumericValue
NumericValue.ceil()
abstract ceil(): NumericValue
NumericValue.round()
abstract round(): NumericValue
NumericValue.valueOf()
valueOf(): string | number
Object.valueOf(): returns a primitive value, preferably a JavaScript number over a string, even if at the expense of precision
NumericValue.[toPrimitive]()
toPrimitive: string | number | null
Object.toPrimitive()
####### hint
"string" | "number" | "default"
NumericValue.print()
print(): void
Rational
type Rational =
| [SmallInteger, SmallInteger]
| [bigint, bigint];
A rational number is a number that can be expressed as the quotient or fraction p/q of two integers, a numerator p and a non-zero denominator q.
A rational can either be represented as a pair of small integers or a pair of big integers.
BigNum
type BigNum = BigDecimal;
Sign
type Sign =
| "zero"
| "positive"
| "negative"
| "non-negative"
| "non-positive"
| "not-zero"
| "unsigned";
OEIS
OEISSequenceInfo
Result from an OEIS lookup operation.
OEISSequenceInfo.formula?
optional formula?: string;
Formula or recurrence (if available) — the first formula line
OEISSequenceInfo.formulas?
optional formulas?: string[];
All free-text formula lines, as returned by OEIS (if available)
OEISOptions
Options for OEIS operations.
OEISOptions.maxResults?
optional maxResults?: number;
Maximum number of results to return for lookups (default: 5)
OEISCandidate
An OEIS-attributed closed-form proposal produced by ce.interpret().
The expression has been verified to reproduce every extracted sample
exactly. Attribution (id, name, url, formula) is mandatory: OEIS data
is CC BY-NC, so a candidate must always carry a link back to its source.
OEISCandidate.expression
expression: Expression;
The parsed and sample-verified closed-form expression.
OEISCandidate.formula
formula: string;
The free-text OEIS formula line the expression was parsed from.
InterpretResult
Result of ce.interpret(): the sync-recognized form of the input (the same
value the Interpret head returns), plus any OEIS-attributed candidates.
InterpretResult.expression
expression: Expression;
The recognized expression, or the input unchanged when nothing fired.
InterpretResult.candidates
candidates: OEISCandidate[];
Verified, OEIS-attributed closed-form proposals (possibly empty).
Other
FunctionPropertyRecord
A single analytic-property record for an operator. The MathJSON fields are
raw (as translated from Fungrim); box them with ce.expr to query.
FunctionPropertyRecord.property
readonly property: string;
One of Poles, Zeros, BranchPoints, BranchCuts, Residue,
EssentialSingularities, IsHolomorphic, IsMeromorphic,
AnalyticContinuation, Solutions, ComplexZeroMultiplicity.
FunctionPropertyRecord.var
readonly var: string | null;
The distinguished variable the property is stated in (e.g. z).
FunctionPropertyRecord.argIndex
readonly argIndex: number | null;
Index of var among the operator's arguments, or null when there is no
single argument position (parametric / composite).
FunctionPropertyRecord.expr
readonly expr: ExpressionInput | null;
FunctionPropertyRecord.domain
readonly domain: ExpressionInput | null;
FunctionPropertyRecord.point
readonly point: ExpressionInput | null;
FunctionPropertyRecord.condition
readonly condition: ExpressionInput | null;
FunctionPropertyRecord.value
readonly value: ExpressionInput | null;
FunctionPropertyRecord.assumptions
readonly assumptions: ExpressionInput | null;
FunctionProperties
Queryable analytic properties of an operator, returned by
ce.functionProperties(name). The set-valued accessors return a boxed set
(e.g. NonPositiveIntegers) for the unconditional record of that kind, or
undefined when no such record exists. Parametric / conditional records
(e.g. residues that depend on parameters) are available via entries.
FunctionProperties.operator
readonly operator: string;
FunctionProperties.entries
readonly entries: readonly FunctionPropertyRecord[];
All analytic-property records for this operator.
FunctionProperties.poles
readonly poles: Expression | undefined;
FunctionProperties.zeros
readonly zeros: Expression | undefined;
FunctionProperties.branchPoints
readonly branchPoints: Expression | undefined;
FunctionProperties.branchCuts
readonly branchCuts: Expression | undefined;
FunctionProperties.essentialSingularities
readonly essentialSingularities: Expression | undefined;
FunctionProperties.holomorphicDomain
readonly holomorphicDomain: Expression | undefined;
The domain on which the function is holomorphic.
FunctionProperties.isMeromorphic
readonly isMeromorphic: boolean | undefined;
Whether the function is meromorphic, when the corpus records it.
SymbolTable
type SymbolTable = {
parent: SymbolTable | null;
ids: {};
};
newSymbolIds()
function newSymbolIds(): {}
A prototype-free SymbolTable.ids map — see the note there.
ApplicationContext
type ApplicationContext = {
head: MathJsonSymbol;
arguments: ReadonlyArray<MathJsonExpression>;
sourceOffsets: readonly [number, number];
headSourceOffsets: readonly [number, number];
ancestors: ReadonlyArray<{
operator: MathJsonSymbol;
operandIndex: number;
}>;
};
A syntactically ambiguous symbol followed by parentheses.
The arguments and ancestry describe the parsed structure, not LaTeX tokens. Source offsets are half-open UTF-16 offsets in normalized LaTeX, as for parse diagnostics. Ancestry runs from the outermost expression to the nearest parent, with one-based operand indices; it includes written Delimiters.
ContourOrientation
type ContourOrientation = "counterclockwise" | "clockwise";
Positive orientation is counterclockwise.
Contour
type Contour =
| {
kind: "real-line";
principalValue: boolean;
}
| {
kind: "circle";
center: ExpressionInput;
radius: ExpressionInput;
orientation: ContourOrientation;
}
| {
kind: "polygon";
vertices: readonly ExpressionInput[];
orientation: ContourOrientation;
}
| {
kind: "rectangle";
lowerLeft: ExpressionInput;
upperRight: ExpressionInput;
orientation: ContourOrientation;
};
A closed contour, or a real line completed by a controlled semicircle. Polygon vertices are complex numbers in traversal order. An explicit polygon orientation overrides that order.
Type Declaration
{
kind: "real-line";
principalValue: boolean;
}
Contour.kind
kind: "real-line";
The real axis from -infinity to +infinity, closed in a half-plane after the large-arc contribution has been established.
{
kind: "circle";
center: ExpressionInput;
radius: ExpressionInput;
orientation: ContourOrientation;
}
{
kind: "polygon";
vertices: readonly ExpressionInput[];
orientation: ContourOrientation;
}
{
kind: "rectangle";
lowerLeft: ExpressionInput;
upperRight: ExpressionInput;
orientation: ContourOrientation;
}
ContourInput
type ContourInput = Contour | ExpressionInput;
Accepted MathJSON forms are CircleContour(center, radius, orientation?), PolygonContour(List(vertices...), orientation?), and RectangleContour(lowerLeft, upperRight, orientation?), and RealLineContour(principalValue?). The orientation is +1 (counterclockwise) or -1 (clockwise); principalValue is True or False. Circle equations Equal(Abs(z - center), radius) are also accepted.
NormalizedContour
type NormalizedContour =
| {
kind: "real-line";
principalValue: boolean;
orientation: ContourOrientation;
}
| {
kind: "circle";
center: Expression;
radius: Expression;
orientation: ContourOrientation;
}
| {
kind: "polygon";
vertices: readonly Expression[];
orientation: ContourOrientation;
};
ContourPole
type ContourPole = {
point: Expression;
kind: "pole" | "essential" | "removable" | "undetermined";
location: "inside" | "outside" | "boundary" | "undetermined";
enclosed: boolean | undefined;
order: number;
residue: Expression;
leadingCoefficient: Expression;
};
ContourIntegralResult
type ContourIntegralResult = {
method: "residue-theorem";
status: | "success"
| "pole-on-contour"
| "invalid-contour"
| "unsupported"
| "undetermined";
reason: string;
contour: NormalizedContour;
polesComplete: boolean;
poleScope: "global" | "contour";
poles: readonly ContourPole[];
divergence: "positive-infinity" | "negative-infinity" | "no-value" | "undetermined";
residueSum: Expression;
value: Expression;
realIntegral: {
closure: "upper" | "lower";
projection: "none" | "real" | "imaginary";
principalValue: boolean;
largeArcContribution: Expression;
};
};
Intermediate results of symbolic contour integration. No partial sum is exposed as an integral: value and residueSum exist only on success. poles includes essential singularities and removable denominator zeros, explicitly marked as such. The pole-on-contour status also covers an essential singularity on the integration path. polesComplete means candidate discovery is complete in poleScope, not that every candidate's order or residue has been determined.
OperatorDefinition
type OperatorDefinition = Partial<BaseDefinition> & Partial<OperatorDefinitionFlags> & {
type: OperatorTypeHandlerOnTypes;
signature: | Type
| TypeString
| BoxedType;
inferredSignature: boolean;
sgn: (ops, options) => Sign | undefined;
isPositive: boolean;
isNonNegative: boolean;
isNegative: boolean;
isNonPositive: boolean;
even: (ops, options) => boolean | undefined;
complexity: number;
canonical: (ops, options) => Expression | null;
evaluate: | ((ops, options) => Expression | undefined)
| Expression;
evaluateAsync: (ops, options) => Promise<Expression | undefined>;
evalDimension: (args, options) => Expression;
derivative: OperatorDerivative;
compile: OperatorCompileHandler;
eq: (a, b, prover?) => boolean | undefined;
neq: (a, b) => boolean | undefined;
collection: CollectionHandlers;
canEnumerate: (expr) => boolean | undefined;
elementCount: (expr) => number | undefined;
inferOperandTypes: (ops, requirement) =>
| ReadonlyArray<Type | undefined>
| undefined;
};
OperatorDefinition.type?
optional type?: OperatorTypeHandlerOnTypes;
The type of the result (return type) as a function of the operand DESCRIPTORS — their types, facts and structure, never the operand expressions. See OperatorTypeHandlerOnTypes.
Should be a subtype of the type indicated by the signature: for a
signature (number) -> real the result may be real or integer,
never complex.
OperatorDefinition.signature?
optional signature?:
| Type
| TypeString
| BoxedType;
The function signature, describing the type of the arguments and the return type.
If a type handler is provided, the return type of the function should
be a subtype of the return type in the signature.
OperatorDefinition.inferredSignature?
optional inferredSignature?: boolean;
If true, the signature is a starting point to be refined, not a
contract: assigning a function literal to this operator narrows the
signature from the literal's body, and calls type from the narrowed
signature.
Declaring a signature normally pins it (inferredSignature: false),
which is what you want for a fixed API. Set this to true to vouch
that a name is an operator — so f(x) parses as an application rather
than a multiplication — while leaving its types to be inferred from the
body assigned later:
ce.declare('q', { signature: '(unknown) -> unknown', inferredSignature: true });
ce.assign('q', ce.parse('t \\mapsto 2t+1'));
// signature is now `(unknown) -> number`, so `q(x) < y` types
// `boolean` and compiles, while `q(L) < y` over a list `L` still types
// `list<boolean>` and fails closed.
A declaration that omits signature entirely behaves the same way.
OperatorDefinition.sgn?
optional sgn?: (ops, options) => Sign | undefined;
Return the sign of the function expression.
If the sign cannot be determined, return undefined.
When determining the sign, only literal values and the values of symbols, if they are literals, should be considered.
Do not evaluate the arguments.
However, the type and sign of the arguments can be used to determine the sign.
The handler must be a pure function of the operands — the type path
dispatches it while deriving an application's type. See the purity
contract on OperatorDefinition.sgn.
OperatorDefinition.isPositive?
readonly optional isPositive?: boolean;
The value of this expression is > 0, same as isGreater(0)
OperatorDefinition.isNonNegative?
readonly optional isNonNegative?: boolean;
The value of this expression is >= 0, same as isGreaterEqual(0)
OperatorDefinition.isNegative?
readonly optional isNegative?: boolean;
The value of this expression is < 0, same as isLess(0)
OperatorDefinition.isNonPositive?
readonly optional isNonPositive?: boolean;
The value of this expression is <= 0, same as isLessEqual(0)
OperatorDefinition.even?
optional even?: (ops, options) => boolean | undefined;
Return true if the function expression is even, false if it is odd
and undefined if it is neither (for example if it is not a number,
or if it is a complex number).
OperatorDefinition.complexity?
optional complexity?: number;
A number used to order arguments.
Argument with higher complexity are placed after arguments with lower complexity when ordered canonically in commutative functions.
- Additive functions: 1000-1999
- Multiplicative functions: 2000-2999
- Root and power functions: 3000-3999
- Log functions: 4000-4999
- Trigonometric functions: 5000-5999
- Hypertrigonometric functions: 6000-6999
- Special functions (factorial, Gamma, ...): 7000-7999
- Collections: 8000-8999
- Inert and styling: 9000-9999
- Logic: 10000-10999
- Relational: 11000-11999
Default: 100,000
OperatorDefinition.canonical?
optional canonical?: (ops, options) => Expression | null;
Return the canonical form of the expression with the arguments args.
The arguments (args) may not be in canonical form. If necessary, they
can be put in canonical form.
This handler should validate the type and number of the arguments (arity).
If a required argument is missing, it should be indicated with a
["Error", "'missing"] expression. If more arguments than expected
are present, this should be indicated with an
["Error", "'unexpected-argument'"] error expression
If the type of an argument is not compatible, it should be indicated
with an incompatible-type error.
["Sequence"] expressions are not folded and need to be handled
explicitly.
If the function is associative, idempotent or an involution, this handler should account for it. Notably, if it is commutative, the arguments should be sorted in canonical order.
Values of symbols should not be substituted, unless they have
a holdUntil attribute of "never".
The handler should not consider the value or any assumptions about any
of the arguments that are symbols or functions (i.e. arg.is(0),
arg.isInteger, etc...) since those may change over time.
The result of the handler should be a canonical expression.
If the arguments do not match, they should be replaced with an
appropriate ["Error"] expression. If the expression cannot be put in
canonical form, the handler should return null.
OperatorDefinition.evaluate?
optional evaluate?:
| ((ops, options) => Expression | undefined)
| Expression;
Evaluate a function expression.
When the handler is invoked, the arguments have been evaluated, except
if the lazy option is set to true.
It is not necessary to further simplify or evaluate the arguments.
If performing numerical calculations and options.numericalApproximation
is false return an exact numeric value, for example return a rational
number or a square root, rather than a floating point approximation.
Use ce.number() to create the numeric value.
If the expression cannot be evaluated, due to the values, types, or
assumptions about its arguments, return undefined or
an ["Error"] expression.
OperatorDefinition.evaluateAsync?
optional evaluateAsync?: (ops, options) => Promise<Expression | undefined>;
An asynchronous version of evaluate.
OperatorDefinition.evalDimension?
optional evalDimension?: (args, options) => Expression;
Experimental
Dimensional analysis
OperatorDefinition.derivative?
optional derivative?: OperatorDerivative;
The partial derivatives of this operator, used by D, Derivative
and the prime notation (f'(x)). See OperatorDerivative for
the two accepted forms.
Without this key, the derivative of an application of an operator
whose evaluate handler does not give a formula stays symbolic:
D(Sq(x), x) is Apply(Derivative(Sq, 1), x). With it, the
derivative is computed with the chain rule:
ce.declare('Sq', {
signature: '(number) -> number',
evaluate: ([x]) => (isNumber(x) ? x.mul(x) : undefined),
derivative: [['Function', ['Multiply', 2, 'x'], 'x']],
});
ce.parse('\\frac{d}{dx} \\operatorname{Sq}(x^2)').evaluate();
// ➔ 4x^3 (that is, Sq'(x^2) · 2x = 2x^2 · 2x)
When the definition also has an evaluate handler that is a function
literal, this key has precedence: the derivative is computed from
this key, not by differentiating the body of the literal.
An operator of the standard library keeps its own derivative rule. A
definition that shadows a standard library name (a user definition
of Sinh, for example) is a different operator, and its derivative
key is used.
OperatorDefinition.compile?
optional compile?: OperatorCompileHandler;
A custom compilation handler for this operator: emit target-language
source for a call to this operator. Takes precedence over the target's
built-in operator/function mapping and its broadcast lowering, so it can
override how a built-in operator compiles (e.g. a custom-tolerance GCD,
or a re-mapped Add/Multiply/Power/relational operator).
It does NOT override the structural / control-flow heads, which have
their own bespoke lowering: Sequence, Sum, Product, Function,
Declare, Assign, Return, Break, Continue, Loop,
Comprehension, If, When, Match, Block. A handler
declared on one of those heads is ignored.
Exception: Which IS overridable (it has no binding structure — its
operands are plain condition/value pairs a handler can compile through
the callback it is given). To customize how Which compiles while
keeping its stock evaluation semantics, attach the handler to the
engine's own definition rather than re-declaring the operator (a
re-declaration replaces the stock evaluate/canonical handlers):
const def = ce.lookupDefinition('Which');
if (def && 'operator' in def) def.operator.compile = myWhichHandler;
The override is per-engine (each ComputeEngine builds its own
standard-library definitions), and the decline contract applies: a
handler returning undefined falls back to the built-in Which
lowering, coercion and frame-protocol wrapping included.
Attaching in place is the supported route for EVERY operator the
engine already defines, not only Which. Three things follow from
re-declaring instead, and all three are silent:
-
A re-declaration REPLACES the stock
evaluate/canonicalhandlers. Spreading the captured definition (ce.declare(op, {...orig, compile})) is an attempt to carry them across by hand and is not equivalent — attaching to the definitionlookupDefinitionreturns keeps them by construction, with nothing to carry. -
A re-declaration also replaces the definition's EFFECTS declaration, and that is what decides whether a compiled
Sum/Productover a body mentioning the operator keeps its NaN early exit — theif (acc !== acc) return NaN;emitted between terms, valid because NaN absorbs+and*, so once the accumulator is NaN no later term can change the answer. An operator definition is GRANTED purity, so a re-declaration that states no effects keeps the exit. One that states any effects refuses it, since skipping terms would skip the effects too — and the lever is the effect SET, not thepurekeyword:pureis a derived reading ofeffects, soeffects: ['random']or an effect-annotated signature loses the exit exactly aspure: falsedoes, whileeffects: []keeps it exactly as an unspecified definition does. For this exit, carrying acompilehandler costs nothing by itself, whether it supplies source or declines for the target at hand: the gate reads the definition's declared effects, not who supplied the code. The one shape it cannot catch is a handler emitting effectful source under a definition that states no effects.The exit this governs is the one the scalar
Sum/Productlowering emits throughBaseCompiler.isEmissionSkippable. An element-wise (collection-valued) body carries a separate, UNCONDITIONAL latch of the same spelling, emitted so that a length mismatch collapsing the fold to a scalar NaN cannot be broadcast back over the next term's shape. That latch does not consult the declared effects, so declaring effects does not buy back the later iterations of an element-wise body. -
Call-sharing is the one cost a handler still pays for being on a re-declared definition, and the declared effects do not govern it. A
compilehandler the engine did not install is a live-source splice the CSE harvest cannot analyse, so every node under that head is refused as a candidate and every callee body mentioning it is refused with it. A self-recursive body loses the binding that made its repeated self-call linear and compiles exponentially — measured ×4 per two levels ofR(i,x,y) = R(i-1,x,y) + 0.5·S(x,y,R(i-1,x,y)). Declaringpure: trueon the re-declaration does NOT restore sharing. Attaching in place is exempt, because the definition is still the engine's own.
The evaluate side is NOT symmetric with the decline contract above:
returning undefined from an evaluate handler leaves the expression
unevaluated rather than falling back, so a handler that means to
delegate must call the captured original explicitly.
Return undefined (or an empty string) to fall back to the
default compilation (a null returned from untyped JavaScript is
tolerated and treated the same). See OperatorCompileHandler.
OperatorDefinition.eq?
optional eq?: (a, b, prover?) => boolean | undefined;
Custom equality handler.
prover indicates the tier of the caller: false for the cheap
arithmetic tier (eq() / .isEqual()), true for the prover tier
(eqIdentical() / .isIdenticallyEqual()), and undefined when the
caller does not distinguish (e.g. cmp()). A handler that does
prover-tier work (sampling, expand/simplify, identity questions in the
free variables) must decline — return undefined — when
prover === false.
OperatorDefinition.canEnumerate?
optional canEnumerate?: (expr) => boolean | undefined;
For an operator that RETURNS a collection but has no collection
handlers (an EAGER producer — Characters, Divisors, Eigenvalues,
…): can evaluate() produce the collection's elements in the current
state?
This is the operator's own decline test — the guard at the top of its
evaluate handler — exposed so the enumerability facet
(isEnumerableCollection) can answer without evaluating. Contract
(see docs/COLLECTIONS-MODEL.md):
- MUST be O(1), evaluation-free and side-effect free. An impure producer answers from its operands' facets, consuming no draws.
falsemeans evaluation WOULD decline — callers stay inert without paying for the evaluation.trueis a hard promise that evaluation produces the collection. An operator whose success is not cheaply decidable (Solve,FindRoot) must returnundefined, nevertrue.- The operand seen here is the CANONICAL operand, not the evaluated
one. An unevaluated compound operand (
Divisors(n + 1)) whose value cannot be read cheaply must yieldundefined(undecidable), notfalse— only a definitively unavailable operand (a valueless symbol, a literal of the wrong kind) yieldsfalse. SeecanEnumerateOperand(collection-utils.ts) for the shared tri-state resolution.
Ignored (never consulted) when the definition has collection
handlers — those own enumerability via collection.isEnumerable.
OperatorDefinition.elementCount?
optional elementCount?: (expr) => number | undefined;
For an operator that RETURNS a collection but has no collection
handlers (an EAGER producer — Sort, Chunk, Ordering, …): how many
elements would evaluate() produce?
The count twin of canEnumerate, and the honest replacement for
the broadcast count fallback: count reads the operands' agreed length
only for a broadcastable operator, where agreement IS the semantics
(docs/BROADCAST-MODEL.md). A reshaping operator's length is its own
business, so it must say so here or report undefined.
Contract, mirroring canEnumerate:
- MUST be O(1), evaluation-free and side-effect free. An impure producer
(
RandomShuffle) answers from its operands' facets, consuming ZERO draws. - The operands seen here are the CANONICAL ones. Anything not cheaply
knowable — a non-literal shape argument, an unknown source length —
must report
undefined(decline), never a guess. - A returned number is a hard promise: it must equal
expr.evaluate().count. When evaluation would DECLINE (an infinite or unknown-length source), reportundefined— a count nobody can walk is worse than no count (Tycho item-169 ruling).
Consulted only when the definition has no collection.count handler —
a declared count owns the answer, including its undefined.
OperatorDefinition.inferOperandTypes?
optional inferOperandTypes?: (ops, requirement) =>
| ReadonlyArray<Type | undefined>
| undefined;
Use-driven element inference. Called when a type REQUIREMENT reaches
an application of this operator: its result is an operand of a typed
parameter (k(xs[1]) with k: (integer) -> integer requires
integer) or of an arithmetic operator, which requires a scalar
numeric result (xs[1] + 1 requires real). The handler answers the
type each OPERAND must have for the result to satisfy the requirement:
one entry per operand, undefined where that operand learns nothing,
or undefined for the whole call to decline.
The engine writes each entry onto the operand through the ordinary
inference path, so only an operand whose type is inferred (or still
unknown) moves, a declared type never does, and an operand that is
itself an application forwards to its own operator's handler
(m[1][2] + 1 reaches m). A widen never reaches the handler: it
carries a result possibility, not a constraint on the operands.
Only a VALUE requirement reaches the handler: never any, unknown,
value, nothing, an absence marker alone, or a function type. An
absence arm (real | missing) is stripped before the call.
At answers dictionary<r> | indexed_collection<r> for its base and
First/Second/Third/Last answer indexed_collection<r>. The
scalar reading is written on purpose: xs[1] + 1 requires number
of the element, exactly as x + 1 infers a bare x as number.
Design and rulings: docs/INFERENCE_ROADMAP.md §5.
SymbolDefinitions
type SymbolDefinitions = Readonly<{}>;
ILatexSyntax
Minimal interface for a LaTeX parser/serializer.
Structurally compatible with LatexSyntax without importing it.
ILatexSyntax.parse()
parse(latex, options?): MathJsonExpression | null
####### latex
string
####### options?
Partial<ParseLatexOptions>
ILatexSyntax.serialize()
serialize(expr, options?): string
####### expr
####### options?
Record<string, unknown>
ILatexSyntax.getNamedTriggers()?
optional getNamedTriggers(): readonly {
name: string;
triggers: string[];
}[]
Named dictionary entries with their LaTeX trigger strings, for reverse
library search (ce.searchDefinitions()). Optional: MathJSON-only
builds and minimal injected syntaxes may not implement it.
ILatexSyntax.addEntries()?
optional addEntries(entries): void
Add LaTeX dictionary entries. The next parse or serialization uses
them. Optional: LatexSyntax implements it, a minimal injected syntax
may not. If the same instance is used by several engines, the entries
apply to all of them. See LatexSyntax.addEntries().
####### entries
readonly Partial<OnlyFirst<
| ExpressionEntry
| MatchfixEntry
| InfixEntry
| PostfixEntry
| PrefixEntry
| SymbolEntry
| FunctionEntry
| EnvironmentEntry
| DefaultEntry, {} &
| ExpressionEntry
| MatchfixEntry
| InfixEntry
| PostfixEntry
| PrefixEntry
| SymbolEntry
| FunctionEntry
| EnvironmentEntry
| DefaultEntry>>[]
OperatorInfo
type OperatorInfo = {
kind: "function" | "opaque";
signature: BoxedType;
canEvaluate: boolean;
};
SymbolInfo
type SymbolInfo = {
kind: "constant" | "variable";
type: BoxedType;
};
DefinitionSearchResult
type DefinitionSearchResult = {
id: MathJsonSymbol;
kind: "function" | "opaque" | "constant" | "variable";
};
One result of ce.searchDefinitions().
IntegrationProvider
type IntegrationProvider = (integrand, variable, trace?) => Expression | null;
A symbolic-integration provider: given an integrand and the integration
variable, returns a closed-form antiderivative (an expression in variable),
or null when it cannot integrate it. See IComputeEngine._integrationProvider.
When an optional trace accumulator is passed (by expr.explain('Integrate')),
the provider appends a curated, whole-state step chain describing how the
antiderivative was found. The argument is backward-compatible: the plain
Integrate evaluator calls the provider with two arguments and never traces.
ProtocolMember
type ProtocolMember =
| {
kind: "function";
signature: string;
}
| {
kind: "readonly" | "readwrite";
type: string;
};
One requirement of a protocol. A function member's signature is stored
VERBATIM, with Self unsubstituted: Self is a textual substitution token
(ruling P12), never a type the registry can resolve.
InferenceWriteEvent
type InferenceWriteEvent = {
name: string;
binding: BoxedDefinition;
target: | BoxedValueDefinition
| BoxedOperatorDefinition;
valueDef: BoxedValueDefinition;
from: BoxedType;
to: BoxedType;
kind: "inferred";
};
One write of inference evidence onto a definition, as delivered to
IComputeEngine._noteInferenceWrite — the single emission point whose
subscribers are the provenance history, the fresh-inference set, and the
narrowing sink. See docs/TYPE-SYSTEM.md
(phase 1).
InferenceCauseContext
type InferenceCauseContext = {
operator: string;
ops: ReadonlyArray<ExpressionInput>;
expr: Expression;
};
The ambient canonicalization context recorded as the cause of provenance
entries: the operator expression being canonicalized when an inference
write fires. Kept as the operator name + the operand array as the
canonicalizer received it (possibly raw MathJSON — canonicalization has
not run yet); expr is the non-canonical materialization, built lazily on
the first write that records it (writes are rare — building an expression
per canonicalization would not be).
JSImplementation
type JSImplementation = {
host: ProtocolHostHandler;
};
A HOST (JavaScript) implementation of a protocol member. A callback carries no type information the engine can read, so — like a host-declared operator handler — it is TRUSTED: only member-name coverage is checked, never its signature. Boxed in a wrapper so it stays distinguishable from an Epsil function literal (design P10).
ProtocolHostHandler
type ProtocolHostHandler = (...args) => unknown;
A host callback implementing one protocol member. Its arguments arrive as boxed engine values (the receiver first, per P1); its result is boxed by the engine. The engine cannot type-check it — that is what "trusted" means here.
ConformanceRecord
type ConformanceRecord = {
target: Type;
targetKey: string;
where: TypeParameter[];
impl: Record<string, Expression | JSImplementation>;
_authored: Record<string, Expression | JSImplementation>;
_implOrigin: {
batch: number;
block: Expression;
};
pending: boolean;
_pendingReason: string;
declaredByStatement: boolean;
};
One conformance edge: "this target type conforms to this protocol". Conformances are add-only (monotone); only their implementations replace.
SumConformanceRecord
type SumConformanceRecord = {
sum: string;
impl: Record<string, Expression | JSImplementation>;
block: Expression;
};
A whole-SUM conformance, as the author wrote it: type shape is Area { … }
where shape is a sum type (user ruling of 2026-09-22).
The statement itself registers one ordinary edge per variant — a sum names a
transparent alias of its variants, and an alias cannot conform — so this
record is bookkeeping, not an edge: it is what lets a variant the sum gains
in a LATER batch receive the same implementation block. The block is kept as
the author wrote it, BEFORE Self is bound: each variant's edge substitutes
Self with its own target, so the substituted block of one variant is the
wrong body for another.
ProtocolRecord
type ProtocolRecord = {
name: string;
members: Record<string, ProtocolMember>;
conformances: ConformanceRecord[];
declaredByStatement: boolean;
_sumConformances: SumConformanceRecord[];
_declOrigin: DeclarationOrigin;
};
A protocol declaration and every conformance registered against it.
ProtocolMembersInput
type ProtocolMembersInput = {
functions: Record<string, string>;
readonly: Record<string, string>;
readwrite: Record<string, string>;
};
The host-API shape of a protocol's requirements. A flat
Record<string, string> cannot represent properties, hence the three
buckets (Appendix A "Host API").
ProtocolImplementationInput
type ProtocolImplementationInput = {
functions: Record<string, ProtocolHostHandler>;
getters: Record<string, ProtocolHostHandler>;
setters: Record<string, ProtocolHostHandler>;
};
The host-API shape of an IMPLEMENTATION block. Property handlers are given
under their surface names (getters.hash), not under the internal
__get__hash mangling (Appendix A "Properties": the mangling is an
implementation detail, not part of the public surface).
EngineCheckpoint
A handle on a saved engine state, from IComputeEngine.checkpoint.
Deliberately opaque: id is for logging and live is the only state a
client can act on. Declared here rather than in checkpoint.ts because it
is part of the engine's public type surface — and because importing it from
the implementation would make this file depend on it, closing a cycle
through the sequence registry.
EngineCheckpoint.id
readonly id: number;
EngineCheckpoint.live
readonly live: boolean;
False once invalidated — by a restore to an EARLIER checkpoint, by
discard(), or by popping a scope this checkpoint was taken inside
(the pop disposes the scope's bindings, so there is no world left to
restore). A dead checkpoint can never be restored again.
IComputeEngine
Extended by
IComputeEngine.latexSyntax
readonly latexSyntax: ILatexSyntax | undefined;
The LatexSyntax instance used for LaTeX parsing/serialization.
undefined when no LatexSyntax was provided to the constructor and
the entry point has no LaTeX support (the core-only bundle).
To add a notation to a running engine, call
ce.latexSyntax.addEntries([...]): later parses and serializations use
the new entries. An engine created without the latexSyntax option
has its own instance, so the change applies to that engine only. An
instance given to several engines with the latexSyntax option is
shared: the change applies to all of them.
IComputeEngine.latexOptions
latexOptions: Partial<ParseLatexOptions & SerializeLatexOptions>;
Engine-wide LaTeX parse/serialize options (e.g. decimalSeparator).
Merged into every parse() and toLatex() call between LatexSyntax
defaults and per-call overrides.
IComputeEngine.True
readonly True: Expression;
IComputeEngine.False
readonly False: Expression;
IComputeEngine.Pi
readonly Pi: Expression;
IComputeEngine.E
readonly E: Expression;
IComputeEngine.Nothing
readonly Nothing: Expression;
IComputeEngine.Missing
readonly Missing: Expression;
The Missing symbol: an absent value whose position is preserved.
IComputeEngine.Zero
readonly Zero: Expression;
IComputeEngine.One
readonly One: Expression;
IComputeEngine.Half
readonly Half: Expression;
IComputeEngine.NegativeOne
readonly NegativeOne: Expression;
IComputeEngine.Two
readonly Two: Expression;
IComputeEngine.NaN
readonly NaN: Expression;
IComputeEngine.Indeterminate
readonly Indeterminate: Expression;
The exact answer to an indeterminate form such as 0/0: a number with
no value. Its double value is NaN, but it is a different value from
NaN, which is the result of a floating-point computation that failed.
Its numeric approximation (.N()) is NaN.
IComputeEngine.PositiveInfinity
readonly PositiveInfinity: Expression;
IComputeEngine.NegativeInfinity
readonly NegativeInfinity: Expression;
IComputeEngine.ComplexInfinity
readonly ComplexInfinity: Expression;
IComputeEngine.context
readonly context: EvalContext;
IComputeEngine.contextStack
contextStack: readonly EvalContext[];
IComputeEngine.iterationLimit
iterationLimit: number;
IComputeEngine.recursionLimit
recursionLimit: number;
IComputeEngine.maxCollectionSize
maxCollectionSize: number;
IComputeEngine.bignum
bignum: (a) => BigDecimal;
IComputeEngine.complex
complex: (a, b?) => Complex;
IComputeEngine.tolerance
tolerance: number;
IComputeEngine.angularUnit
angularUnit: AngularUnit;
IComputeEngine.costFunction
costFunction: (expr) => number;
IComputeEngine.simplificationRules
simplificationRules: Rule[];
The rules used by .simplify() when no explicit rules option is passed.
Initialized to the built-in simplification rules.
Users can push() additional rules or replace the entire array.
IComputeEngine.solveRules
solveRules: Rule[];
The rules used by solve() to find roots of univariate expressions.
Each rule matches a normalized equation f(_x) = 0 — the unknown is
the wildcard _x — and replace produces a root expression.
Conditions should reject matches where other wildcards capture _x.
Candidate roots are validated against the original equation, so an
over-eager template degrades to a no-op rather than a wrong answer.
Initialized to the built-in root-finding rules; push() to extend,
assign to replace.
IComputeEngine.harmonizationRules
harmonizationRules: Rule[];
The rules used by solve() to transform an equation into equivalent,
easier-to-solve forms before root-finding (e.g. ln f(x) → f(x) - 1).
Same conventions and extension pattern as solveRules.
IComputeEngine.strict
strict: boolean;
IComputeEngine.jit
jit: "auto" | "off";
Whether the engine may implicitly generate and execute compiled code as
a performance optimization (auto-compiled Map drains, compiled numeric
quadrature/limit kernels). 'auto' (default) attempts implicit
compilation and latches to 'off' engine-wide on the first CSP
EvalError; 'off' never attempts it. Explicit compile() is exempt.
IComputeEngine.trace
trace: readonly string[];
A list of the function calls to the current evaluation context
IComputeEngine.effects
get effects(): EffectHandlers
set effects(handlers: EffectHandlerOverrides): void
The host capabilities of this engine: the handlers the library operators
use to reach the host. Print and Input use effects.console.
The registry is an immutable object. Reading returns the registry that a
new evaluation would use. Assigning installs a new registry: the assigned
object is a COMPLETE description — a handler it does not mention returns
to its default, so ce.effects = {} restores every default. A null
handler denies the capability: an operator that needs it evaluates to an
Error("capability-denied", …) value.
const lines: string[] = [];
ce.effects = {
console: { log: (line) => lines.push(line), readLine: () => undefined },
};
Each evaluation (evaluate(), N(), evaluateAsync()) uses the registry
that was installed when it started. An assignment does not change the
handlers of an evaluation that is already running.
For a change that must last for one block of code only, use
withEffects.
IComputeEngine.precision
get precision(): number
set precision(p: number | "auto" | "machine"): void
IComputeEngine.checkpoint()
checkpoint(label?): EngineCheckpoint
Take a checkpoint of the engine's state at a quiescent point — between
statements, at any scope depth — so a later restore can rewind
to it. Legal on a freshly constructed engine, which is how a client gets
a cp[0] covering an edit of the first cell, and inside a host-pushed
scope, which is how a notebook takes per-cell checkpoints within a pass.
A checkpoint taken inside a scope dies when that scope pops. Throws a
CheckpointError when the engine is mid-evaluation or mid-pre-pass;
restore additionally requires the same scope stack the
checkpoint was taken on.
####### label?
string
IComputeEngine.restore()
restore(cp): void
Rewind to cp, invalidating every checkpoint taken after it; cp itself
stays live and can be restored again. Expressions built BEFORE cp stay
valid — their definitions are rewritten in place. Expressions built
during the rewound window are not: cache cell outputs as serialized
artifacts, never as live boxed nodes.
####### cp
IComputeEngine.discard()
discard(cp): void
Release cp's restore capability. Restoring past a discarded INTERIOR
checkpoint stays possible through any earlier live one; discarding the
OLDEST makes the state before the next-younger one unreachable.
####### cp
IComputeEngine.declareProtocol()
declareProtocol(name, members): void
Declare a protocol (Appendix A "Host API"). Throws on error, including on re-declaration — the Epsil statement route replaces instead (P5).
####### name
string
####### members
IComputeEngine.declareProtocolImplementation()
declareProtocolImplementation(
type,
protocol,
impl,
options?): void
Implement protocol for type, declaring the conformance edge if it is
not already registered (Appendix A "Host API").
THROWS on every error — the host channel; the Epsil statement route returns error VALUES instead. A second host implementation of the same (type, protocol) pair throws rather than replacing (P5).
The callbacks are JavaScript functions, so they carry no signature the
engine can check: they are trusted like host-declared operator handlers,
and only member-name coverage, unknown members and a set handler on a
readonly property are validated.
options.where declares a CONDITIONAL conformance: type is then a HEAD
PATTERN naming the variables ('list<T>') and where is the clause SOURCE
that binds them ('where T is Comparable'; the where word may be
omitted). A malformed clause, or a head variable the clause does not bind,
throws.
####### type
string
####### protocol
string
####### impl
####### options?
####### where?
string
IComputeEngine.conformsTo()
conformsTo(type, protocol): boolean
Whether type conforms to protocol, answered without calling any of
the protocol's members. An unknown protocol answers false.
Inheritance included: a conformance registered for a supertype answers
for its subtypes. A CONDITIONAL conformance (list<T> is P where T is P) recurses, deciding itself against type's own arguments.
type may be a TypeString, parsed the way IComputeEngine.type
parses one.
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
####### protocol
string
IComputeEngine.withTimeLimit()
withTimeLimit<T>(limit, fn): T
Run fn with at most ms milliseconds (numeric form) or limit.ms
(object form, which also accepts an attribution label). A tighter
enclosing span preempts this limit; use the label and
CancellationError.attribution/spans to tell which limit fired.
⚠️ fn MUST be synchronous. The span is restored in a synchronous
finally, so a Promise-returning (async) callback hands control back
at its first await while the span is still open: work that resumes after
that point runs outside the deadline and is never cancelled (see
docs/TIMEOUT-MODEL.md §6.4). For asynchronous cancellation use
expr.evaluateAsync({ signal }) with an AbortSignal instead.
• T
####### limit
| number
| {
ms: number;
label: string;
}
####### fn
() => T extends Promise<unknown> ? never : T
IComputeEngine.withStepBudget()
withStepBudget<T>(limit, fn): T
Run fn with at most limit.steps steps of engine work: a hang guard
that fires at the same point on every machine, unlike a wall-clock
limit. A step is one of the engine's cooperative cancellation checks —
an opaque unit, deterministic for one computation on one engine state,
but not a measure of cost and not comparable across engine versions;
tune the budget empirically and keep a withTimeLimit span outside it.
A spent budget throws a CancellationError with cause: 'step-budget'
and the span's label as its attribution.
⚠️ fn MUST be synchronous, as for withTimeLimit.
• T
####### limit
####### steps
number
####### label?
string
####### fn
() => T extends Promise<unknown> ? never : T
IComputeEngine.withEffects()
withEffects<T>(overrides, fn): T
Run fn with some host capabilities replaced or denied, then put the
previous ones back. Evaluations that START inside fn use the changed
registry.
overrides is applied on top of the registry in effect when
withEffects is called, so calls nest: a capability an inner call does
not mention keeps the handler of the outer call. A null value denies the
capability, even if it has a default handler — this is how to evaluate an
expression that is not trusted:
const result = ce.withEffects({ console: null }, () => expr.evaluate());
The previous registry is put back when fn returns or throws. If fn
returns a promise, it is put back when that promise settles (fulfilled or
rejected), and withEffects returns a promise that settles the same way.
An asynchronous evaluation keeps the registry it started with, so an
evaluation that started BEFORE withEffects was called is not changed by
it. But while the promise of an asynchronous fn is pending, the changed
registry is the installed one: an unrelated evaluation that starts during
that time, from other code, also uses it. Start such evaluations before
calling withEffects, or use a separate engine.
• T
####### overrides
####### fn
() => T
IComputeEngine.chop()
chop(n)
chop(n): number
####### n
number
chop(n)
chop(n): 0 | BigDecimal
####### n
BigDecimal
chop(n)
chop(n): number | BigDecimal
####### n
number | BigDecimal
IComputeEngine.expr()
expr(expr, options?): Expression
####### expr
| NumericValue
| ExpressionInput
####### options?
####### form?
####### scope?
Scope
IComputeEngine.box()
box(expr, options?): Expression
####### expr
| NumericValue
| ExpressionInput
####### options?
####### form?
####### scope?
Scope
Deprecated
Use expr() instead.
IComputeEngine.rebind()
rebind(expr, options?): Expression
Rebuild expr as if ce.expr(expr.json, { form, scope }) had been
called — every symbol resolves afresh in scope (or the current scope)
— without serializing expr to MathJSON.
ce.expr(expr, { scope }) on an already-boxed expression keeps the
bindings the expression was boxed with; it never re-resolves a symbol.
This is the operation that does. Use it when an expression built under
one set of declarations must be read under another: a body boxed in a
shadow scope, a row re-classified after a declaration changed.
For the canonical and partial forms the MathJSON is built as a DAG — one
array per DISTINCT function node, shared by every parent that reads it
(a leaf contributes its own constant-size MathJSON) — where
expr.json writes a tree, one copy of a shared node per path. That
MathJSON is then boxed by the ordinary route, so the result matches
ce.expr(expr.json, …) by construction, including for an expression
that already holds an Error node. Canonical boxing still visits every
path, as it does for any MathJSON. The raw and structural forms
canonicalize nothing, so each distinct node is rebuilt once and a shared
sub-expression stays shared in the result as well.
form:'canonical'(default),'structural','raw', or a partial form such as['Flatten', 'Order'].scope: the lexical scope the rebuild resolves and declares in.
Verbatim LaTeX and source positions are dropped, as the MathJSON route drops them. A mutable object is rebuilt as its record snapshot, as that route boxes it.
####### expr
####### options?
####### form?
####### scope?
Scope
IComputeEngine.parse()
parse(latex, options)
parse(latex, options?): Expression
Parse a LaTeX string and return a boxed expression.
This is a convenience method equivalent to ce.expr(parse(latex)),
but uses the engine's symbol definitions for better parsing accuracy.
options.scope RECEIVES the parse's writes: the whole parse runs with
that scope as the current lexical scope, so name resolution (including
the parser's symbol oracle) walks scope → parents, and every
auto-declare and inference lands rooted there. Discarding the scope
discards the writes. Use ce.createScope() to make one that can be read
back.
options.speculative leaves NO trace in the engine's type state: the
parse runs inside a transient scope (auto-declares land there and are
discarded with it), and every ambient symbol whose type is currently
inferred is shadowed in that scope with its current type — so a
narrowing use in latex refines the discarded shadow instead of
persistently narrowing the ambient symbol. Use it for derive-style
parses that only READ the result (its type, structure, or
serialization): the result's bindings refer to the discarded scope, so
do not retain, evaluate, or compare it against later expressions.
Mutually exclusive with scope.
####### latex
string
####### options?
Partial<ParseLatexOptions> & {
form: FormOption;
scope: Scope;
speculative: boolean;
}
parse(latex, options)
parse(latex, options?): Expression | null
####### latex
string | null
####### options?
Partial<ParseLatexOptions> & {
form: FormOption;
scope: Scope;
speculative: boolean;
}
IComputeEngine.appliedNonFunctions()
appliedNonFunctions(latex): string[]
The symbols that appear in function-application syntax f(…) in latex
but are not defined as functions in the current scope (so they parse as
implicit multiplication or are left unresolved). Scope-aware and
side-effect-free. Intended to flag calls to undefined functions in tools
such as notebooks; intersect with Expression.freeVariables
to drop deliberate multiplication of defined values.
Only parenthesized-group application is detected: a symbol juxtaposed
with a matrix environment (\mathrm{Eigenvalues}\begin{pmatrix}…) is
not reported, since a matrix never reaches the symbol-with-delimiter
juxtaposition analysis.
####### latex
string
IComputeEngine.function()
function(name, ops, options?): Expression
####### name
string
####### ops
readonly ExpressionInput[]
####### options?
####### metadata?
####### form?
####### scope?
Scope
IComputeEngine._getCompilationTarget()
_getCompilationTarget(name)
_getCompilationTarget(name):
| JavaScriptCompilationTarget<Expression>
| undefined
####### name
"javascript"
_getCompilationTarget(name)
_getCompilationTarget(name):
| LanguageTarget<Expression, string, unknown, number>
| undefined
####### name
string
IComputeEngine.number()
number(value, options)
number(value, options?): Expression
Create a complex number from its real part and its imaginary part, each
a JavaScript number or a BigDecimal.
When the engine works above machine precision (ce.precision greater
than 15), a BigDecimal part is kept at the precision it holds: a part
too small or too large for a double (1e-800, 1e800) or with more
than 16 significant digits is not rounded to a double. This is the
lossless alternative to ce.number(ce.complex(re, im)): ce.complex()
returns a Complex object, whose parts are always doubles. At machine
precision, both parts are rounded to doubles: there
{ re: ce.bignum('1e-800'), im: ce.bignum(2) } gives 2i.
When the imaginary part is zero (a number or a BigDecimal), the
result is a real number.
When both parts are integers, the result is the EXACT Gaussian integer,
as ce.number(2) is the exact 2: a part is an integer when it is a
number that is a safe integer, or an integer-valued BigDecimal whose
exponent is at most 10^6 (also at machine precision, and also outside
the double range). When a part has a fraction, is a number past the
safe integers, or is a BigDecimal with a larger exponent (1e2000000),
the result is a float. The same rule applies to a Complex given to
ce.number() or ce.box(): ce.number(new Complex(2, 3)) is the exact
2+3i, ce.number(new Complex(2.5, 3)) is a float.
ce.precision = 30;
ce.number({ re: ce.bignum('1e-800'), im: ce.bignum(2) });
// ➔ a complex number with the real part 1e-800 and the imaginary part 2
ce.number({ re: 1, im: 0 });
// ➔ 1
ce.number({ re: 2, im: 3 }).isExact;
// ➔ true
ce.number({ re: 2.5, im: 3 }).isExact;
// ➔ false
####### value
####### re
number | BigDecimal
####### im
number | BigDecimal
####### options?
####### metadata?
####### canonical?
number(value, options)
number(value, options?): Expression
####### value
| string
| number
| bigint
| MathJsonNumberObject
| BigDecimal
| Rational
| NumericValue
| Complex
####### options?
####### metadata?
####### canonical?
IComputeEngine.symbol()
symbol(sym, options?): Expression
####### sym
string
####### options?
####### canonical?
####### metadata?
####### autoDeclare?
boolean
IComputeEngine.character()
character(s, metadata?): Expression
Create a boxed character — one user-perceived character.
s must be exactly one grapheme cluster after NFC normalization; use the
CharacterFrom operator when the content is not known to satisfy that, as
it reports a diagnostic instead.
####### s
string
####### metadata?
IComputeEngine.error()
error(message, where?): Expression
####### message
string | string[]
####### where?
string
IComputeEngine.typeError()
typeError(expectedType, actualType, where?): Expression
####### expectedType
####### actualType
| Type
| BoxedType
| undefined
####### where?
IComputeEngine.tuple()
tuple(elements)
tuple(...elements): Expression
####### elements
...readonly number[]
tuple(elements)
tuple(...elements): Expression
####### elements
...readonly Expression[]
IComputeEngine.list()
list(values): Expression
A List of numbers, built without boxing each element.
The elements are copied into a frozen array of machine numbers that the
list keeps as its store: count, at, type, isSame and array
answer from it, and the boxed operands are built only if ops is read.
The result is an ordinary canonical List in every other respect.
values may be a number[], a Float64Array or any array-like of
numbers. Its length must be a non-negative safe integer and each
element a JS number; anything else throws a TypeError. -0 is stored
as +0.
Use it to hand a large numeric list to the engine cheaply, and read it
back with expr.array.
####### values
ArrayLike<number>
IComputeEngine.type()
type(type): BoxedType
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
IComputeEngine.rules()
rules(rules, options?): BoxedRuleSet
####### rules
Rule | readonly Rule | BoxedRule[] | BoxedRuleSet | null | undefined
####### options?
####### canonical?
boolean
####### purpose?
Default purpose applied to any rule in the set that doesn't carry
its own purpose tag (a per-rule tag takes precedence).
IComputeEngine.getRuleSet()
getRuleSet(id?): BoxedRuleSet | undefined
####### id?
"harmonization" | "solve-univariate" | "standard-simplification"
IComputeEngine.popScope()
popScope(): void
IComputeEngine.createScope()
createScope(bindings?, parent?): InspectableScope
####### bindings?
Record<string,
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| TaggedValueDefinition
| TaggedOperatorDefinition>
####### parent?
Scope
IComputeEngine.assign()
assign(ids)
assign(ids): IComputeEngine
####### ids
assign(id, value)
assign(id, value): IComputeEngine
####### id
string
####### value
AssignValue
assign(arg1, arg2)
assign(arg1, arg2?): IComputeEngine
####### arg1
string | {}
####### arg2?
AssignValue
IComputeEngine.declareType()
declareType(name, type, options?): void
####### name
string
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
####### options?
####### alias?
boolean
####### fromStatement?
boolean
####### mint?
boolean
####### typeParams?
IComputeEngine.declare()
declare(symbols)
declare(symbols): IComputeEngine
####### symbols
declare(id, type, scope)
declare(id, type, scope?): IComputeEngine
####### id
string
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
####### scope?
| Scope
| DeclareOptions & {
extend: false;
}
declare(id, patch, options)
declare(id, patch, options): IComputeEngine
####### id
string
####### patch
####### options
DeclareOptions & {
extend: true;
}
declare(id, def, scope)
declare(id, def, scope?): IComputeEngine
####### id
string
####### def
####### scope?
| Scope
| DeclareOptions & {
extend: false;
}
declare(arg1, arg2, arg3)
declare(arg1, arg2?, arg3?): IComputeEngine
####### arg1
string | {}
####### arg2?
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| Partial<OnlyFirst<ValueDefinition, BaseDefinition & {
holdUntil: "never" | "evaluate" | "N";
type: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferred: boolean;
effectsDeclared: boolean;
value: | ExpressionInput
| ((ce) => Expression | null);
eq: (a) => boolean | undefined;
neq: (a) => boolean | undefined;
cmp: (a) => "<" | ">" | "=" | undefined;
collection: CollectionHandlers;
subscriptEvaluate: (subscript, options) => Expression | undefined;
} & Partial<BaseDefinition> & Partial<OperatorDefinitionFlags> & {
type: OperatorTypeHandlerOnTypes;
signature: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferredSignature: boolean;
sgn: (ops, options) => Sign | undefined;
isPositive: boolean;
isNonNegative: boolean;
isNegative: boolean;
isNonPositive: boolean;
even: (ops, options) => boolean | undefined;
complexity: number;
canonical: (ops, options) => Expression | null;
evaluate: | Expression
| ((ops, options) => Expression | undefined);
evaluateAsync: (ops, options) => Promise<Expression | undefined>;
evalDimension: (args, options) => Expression;
derivative: OperatorDerivative;
compile: OperatorCompileHandler;
eq: (a, b, prover?) => boolean | undefined;
neq: (a, b) => boolean | undefined;
collection: CollectionHandlers;
canEnumerate: (expr) => boolean | undefined;
elementCount: (expr) => number | undefined;
inferOperandTypes: (ops, requirement) =>
| readonly (Type | undefined)[]
| undefined;
}>>
| Partial<OnlyFirst<OperatorDefinition, BaseDefinition & {
holdUntil: "never" | "evaluate" | "N";
type: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferred: boolean;
effectsDeclared: boolean;
value: | ExpressionInput
| ((ce) => Expression | null);
eq: (a) => boolean | undefined;
neq: (a) => boolean | undefined;
cmp: (a) => "<" | ">" | "=" | undefined;
collection: CollectionHandlers;
subscriptEvaluate: (subscript, options) => Expression | undefined;
} & Partial<BaseDefinition> & Partial<OperatorDefinitionFlags> & {
type: OperatorTypeHandlerOnTypes;
signature: | string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType;
inferredSignature: boolean;
sgn: (ops, options) => Sign | undefined;
isPositive: boolean;
isNonNegative: boolean;
isNegative: boolean;
isNonPositive: boolean;
even: (ops, options) => boolean | undefined;
complexity: number;
canonical: (ops, options) => Expression | null;
evaluate: | Expression
| ((ops, options) => Expression | undefined);
evaluateAsync: (ops, options) => Promise<Expression | undefined>;
evalDimension: (args, options) => Expression;
derivative: OperatorDerivative;
compile: OperatorCompileHandler;
eq: (a, b, prover?) => boolean | undefined;
neq: (a, b) => boolean | undefined;
collection: CollectionHandlers;
canEnumerate: (expr) => boolean | undefined;
elementCount: (expr) => number | undefined;
inferOperandTypes: (ops, requirement) =>
| readonly (Type | undefined)[]
| undefined;
}>>
| BoxedOperatorDefinition
| OperatorDefinitionPatch
####### arg3?
Scope | DeclareOptions
IComputeEngine.loadLibrary()
loadLibrary(library): IComputeEngine
Load a library on an engine that is already constructed. Its
definitions are declared in the global scope, as with ce.declare(),
and its name is recorded (see libraryOf()). Each library in its
requires list must already be loaded.
####### library
IComputeEngine.libraryOf()
libraryOf(name): string | undefined
The name of the library whose definition name resolves to in the
current scope ('trigonometry' for Sin), or undefined for a name
that no library defines or that a declaration shadows.
####### name
string
IComputeEngine.declareSequence()
declareSequence(name, def): IComputeEngine
Declare a sequence with a recurrence relation.
####### name
string
####### def
Example
// Fibonacci sequence
ce.declareSequence('F', {
base: { 0: 0, 1: 1 },
recurrence: 'F_{n-1} + F_{n-2}',
});
ce.parse('F_{10}').evaluate(); // → 55
IComputeEngine.getSequenceStatus()
getSequenceStatus(name): SequenceStatus
Get the status of a sequence definition.
####### name
string
Example
ce.parse('F_0 := 0').evaluate();
ce.getSequenceStatus('F');
// → { status: 'pending', hasBase: true, hasRecurrence: false, baseIndices: [0] }
IComputeEngine.getSequence()
getSequence(name): SequenceInfo | undefined
Get information about a defined sequence.
Returns undefined if the symbol is not a sequence.
####### name
string
IComputeEngine.listSequences()
listSequences(): string[]
List all defined sequences. Returns an array of sequence names.
IComputeEngine.isSequence()
isSequence(name): boolean
Check if a symbol is a defined sequence.
####### name
string
IComputeEngine.clearSequenceCache()
clearSequenceCache(name?): void
Clear the memoization cache for a sequence. If no name is provided, clears caches for all sequences.
####### name?
string
IComputeEngine.getSequenceCache()
getSequenceCache(name):
| Map<string | number, Expression>
| undefined
Get the memoization cache for a sequence.
Returns a Map of index → value, or undefined if not a sequence or memoization is disabled.
For single-index sequences, keys are numbers. For multi-index sequences, keys are comma-separated strings (e.g., '5,2').
####### name
string
IComputeEngine.getSequenceTerms()
getSequenceTerms(
name,
start,
end,
step?): Expression[] | undefined
Generate a list of sequence terms from start to end (inclusive).
####### name
string
The sequence name
####### start
number
Starting index (inclusive)
####### end
number
Ending index (inclusive)
####### step?
number
Step size (default: 1)
Example
ce.declareSequence('F', { base: { 0: 0, 1: 1 }, recurrence: 'F_{n-1} + F_{n-2}' });
ce.getSequenceTerms('F', 0, 10);
// → [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
IComputeEngine.lookupOEIS()
lookupOEIS(terms, options?): Promise<OEISSequenceInfo[]>
Look up sequences in OEIS by their terms.
####### terms
(number | Expression)[]
Array of sequence terms to search for
####### options?
Optional configuration (timeout, maxResults)
Example
const results = await ce.lookupOEIS([0, 1, 1, 2, 3, 5, 8, 13]);
// → [{ id: 'A000045', name: 'Fibonacci numbers', ... }]
IComputeEngine.checkSequenceOEIS()
checkSequenceOEIS(name, count?, options?): Promise<{
matches: OEISSequenceInfo[];
terms: number[];
}>
Check if a defined sequence matches an OEIS sequence.
####### name
string
Name of the defined sequence
####### count?
number
Number of terms to check (default: 10)
####### options?
Optional configuration
Example
ce.declareSequence('F', { base: { 0: 0, 1: 1 }, recurrence: 'F_{n-1} + F_{n-2}' });
const result = await ce.checkSequenceOEIS('F', 10);
// → { matches: [{ id: 'A000045', name: 'Fibonacci numbers', ... }], terms: [0, 1, 1, ...] }
IComputeEngine.interpret()
interpret(expr, options?): Promise<InterpretResult>
Interpret a notational expression, then propose OEIS-attributed closed
forms for it (the async v4 of the Interpret ladder).
result.expression is exactly what the synchronous Interpret head
returns (a Sum/Product, or the input unchanged); result.candidates
are OEIS-attributed closed forms, each verified to reproduce every
extracted sample exactly. This is the only interpretation path that
performs a network lookup. Too few samples, being offline, a timeout, or an
empty result all yield an empty candidate list rather than a rejection.
####### expr
The (typically inert, continuation-bearing) expression
####### options?
OEIS request options (timeout, maxResults)
Example
const { expression, candidates } = await ce.interpret(
ce.parse('1 + 3 + 6 + 10 + \\cdots + n')
);
IComputeEngine.operatorInfo()
operatorInfo(head): OperatorInfo | undefined
Introspect a registered operator head.
Returns undefined if no definition is registered in this engine.
Otherwise returns { kind, signature? } where kind is 'function'
when the operator has an evaluate or collection handler, and
'opaque' when it is declared as a typed-but-opaque node (e.g.,
Triangle, Sphere).
Use this to classify heads encountered in parsed MathJSON without maintaining a parallel list of "known" operators.
####### head
string
IComputeEngine.normalizeIdentifier()
normalizeIdentifier(latex): string
Convert a LaTeX identifier string to its canonical MathJSON name without declaring the symbol in the engine scope.
Examples:
'R_{3}'→'R_3''\\theta_x'→'theta_x''\\alpha'→'alpha''1 + 2'→''(not an identifier)
Use this instead of ce.parse(latex).symbol when you need the canonical
name without the side-effect of auto-declaring the symbol.
####### latex
string
IComputeEngine.symbolInfo()
symbolInfo(name): SymbolInfo | undefined
Return introspection metadata for a symbol (value definition) in the current scope chain.
kind: 'constant'when the symbol is a CE-registered constant (e.g.Pi,True,ExponentialE).kind: 'variable'for declared but non-constant value symbols (e.g. afterce.declare('a', 'real')).
Returns undefined for unknown names and for names that resolve to
operator/function definitions (use operatorInfo() for those — the
two methods are non-overlapping).
####### name
string
IComputeEngine.searchDefinitions()
searchDefinitions(query, options?): DefinitionSearchResult[]
Reverse library search: map plain-text concept keywords to a ranked list of matching identifiers in the current scope chain (standard library plus any user declarations).
The query is a string (tokenized on whitespace) or an array of strings; tokens are OR-ed — a definition matches when any token matches — and definitions matching more tokens, or matching them more exactly, rank higher.
Every returned id resolves via ce.lookupDefinition(id); chain that
call for full detail.
####### query
string | string[]
####### options?
####### limit?
number
IComputeEngine.suggestOperatorName()
suggestOperatorName(name): string | undefined
Given a name that is not a known operator, return the closest known
operator name — a "did you mean" suggestion — or undefined when nothing
is close enough. Powers the Epsil unknown-function diagnostic.
Matching is conservative and applied in priority order (first match wins): case-insensitive exact match, singular/plural, Damerau–Levenshtein distance (≤ 2 for names of length ≥ 6, ≤ 1 for length 5, never for shorter names), then a prefix match against exactly one operator. Ties prefer the candidate sharing the longest prefix with the query.
ce.suggestOperatorName('Quartile'); // → 'Quartiles'
ce.suggestOperatorName('foo'); // → undefined
####### name
string
IComputeEngine.functionProperties()
functionProperties(name): FunctionProperties | undefined
Return the known analytic properties of an operator — poles, zeros, branch
points/cuts, residues, holomorphic/meromorphic domains — drawn from the
Fungrim-derived metadata store, or undefined if none are recorded.
ce.functionProperties('Gamma')?.poles?.toString(); // 'NonPositiveIntegers'
The set-valued accessors (poles, zeros, ...) return a boxed set for the
unconditional record of that kind; parametric / conditional records (e.g.
residues that depend on parameters) are available via entries.
####### name
string
IComputeEngine.contourIntegrate()
contourIntegrate(integrand, variable, contour): ContourIntegralResult
Integrate over a circle, simple polygon, or the entire real line by the
residue theorem. Real-line contours also accept an explicit principal value.
Returns pole classifications, residues, their sum, and the integral.
Unsupported or undecidable inputs have no value; boundary poles have
status pole-on-contour. See ContourInput for contour forms.
####### integrand
####### variable
string
####### contour
ExplainStep
type ExplainStep = KernelExplainStep<Expression>;
One step of an Explanation. See expr.explain().
Explanation
type Explanation = KernelExplanation<Expression>;
A structured step-by-step explanation. See expr.explain().
BoxedRule
type BoxedRule = KernelBoxedRule<Expression, IComputeEngine>;
A boxed/normalized rule form.
BoxedRuleSet
type BoxedRuleSet = KernelBoxedRuleSet<Expression, IComputeEngine>;
Collection of boxed rules.
InspectableScope
type InspectableScope = KernelInspectableScope<BoxedDefinition>;
A caller-owned, readable lexical scope — the product of
ce.createScope(). Specialized to boxed definitions.
ScopeDeclaration
type ScopeDeclaration = KernelScopeDeclaration<BoxedDefinition>;
One entry of an InspectableScope harvest.
ScopeNarrowing
type ScopeNarrowing = KernelScopeNarrowing<BoxedDefinition>;
One outer-definition narrowing observed by an InspectableScope.
EvalContext
type EvalContext = KernelEvalContext<Expression, BoxedDefinition, BoxedValueDefinition>;
Evaluation context specialized to this engine/runtime model.
Expression
Function Expression
Expression.operator
readonly operator: string;
The name of the operator of the expression.
For example, the name of the operator of ["Add", 2, 3] is "Add".
A string literal has a "String" operator.
A symbol has a "Symbol" operator.
A number has a "Number", "Real", "Rational" or "Integer" operator; amongst some others.
Practically speaking, for fully canonical and valid expressions, all of these are likely to
collapse to "Number".
Latex Parsing and Serialization
Expression.parseDiagnostics?
optional parseDiagnostics?: readonly ParseDiagnostic[];
Parse-time diagnostics collected when the expression was produced by
ce.parse(latex, { diagnostics: true }).
This property is present (as a possibly-empty, frozen array) only on
the top-level expression returned by a diagnostics: true parse; it is
undefined everywhere else (sub-expressions, and any expression parsed
without the flag). Diagnostics are purely additive metadata: enabling them
never changes the parse output.
See ParseDiagnostic for the code enumeration and span conventions.
Numeric Expression
Expression.isEven
readonly isEven: boolean | undefined;
If the value of this expression is not an integer return undefined.
Expression.isOdd
readonly isOdd: boolean | undefined;
If the value of this expression is not an integer return undefined.
Expression.re
readonly re: number;
Return the real part of the value of this expression, if a number.
Otherwise, return NaN (not a number).
Expression.im
readonly im: number;
If value of this expression is a number, return the imaginary part of the value. If the value is a real number, the imaginary part is 0.
Otherwise, return NaN (not a number).
Expression.bignumRe
readonly bignumRe: BigDecimal | undefined;
If the value of this expression is a number, return the real part of the
value as a BigNum.
If the value is not available as a bignum return undefined. That is,
the value is not upconverted to a bignum.
To get the real value either as a bignum or a number, use
expr.bignumRe ?? expr.re.
When using this pattern, the value is returned as a bignum if available,
otherwise as a number or NaN if the value is not a number.
Expression.bignumIm
readonly bignumIm: BigDecimal | undefined;
If the value of this expression is a number, return the imaginary part as
a BigNum.
It may be 0 if the number is real.
If the value of the expression is not a number or the value is not
available as a bignum return undefined. That is, the value is not
upconverted to a bignum.
To get the imaginary value either as a bignum or a number, use
expr.bignumIm ?? expr.im.
When using this pattern, the value is returned as a bignum if available, otherwise as a number or NaN if the value is not a number.
Expression.sgn
readonly sgn: Sign | undefined;
Return the sign of the expression.
Note that complex numbers have no natural ordering, so if the value is an
imaginary number (a complex number with a non-zero imaginary part),
this.sgn will return unsigned.
If a symbol, this does take assumptions into account, that is this.sgn
will return positive if the symbol is assumed to be positive
using ce.assume().
Non-canonical expressions return undefined.
Expression.isPositive
readonly isPositive: boolean | undefined;
The value of this expression is > 0, same as isGreaterEqual(0)
Expression.isNonNegative
readonly isNonNegative: boolean | undefined;
The value of this expression is >= 0, same as isGreaterEqual(0)
Expression.isNegative
readonly isNegative: boolean | undefined;
The value of this expression is < 0, same as isLess(0)
Expression.isNonPositive
readonly isNonPositive: boolean | undefined;
The value of this expression is <= 0, same as isLessEqual(0)
Expression.isNaN
readonly isNaN: boolean | undefined;
If true, the value of this expression is a number with no value: either
NaN or Indeterminate.
NaN ("Not a Number", from the floating point format standard IEEE-754)
is the result of a floating-point computation with no value, such as
0.0/0.0, and the marker of an absent numeric operand. Indeterminate
is the result of an exact form with no value, such as 0/0. Both report
isNaN === true; isIndeterminate tells them apart.
Note that if isNaN is true, isNumber is also true (yes, NaN is a
number).
Expression.isIndeterminate
readonly isIndeterminate: boolean;
If true, this expression is the Indeterminate number literal
(ce.Indeterminate): the exact answer to an indeterminate form such as
0/0, a number with no value.
Its double value is NaN, so isNaN is also true. It differs from the
NaN literal, which is the result of a floating-point computation that
failed: the two are different values (isSame is false between them).
A numeric approximation (.N()) of Indeterminate is NaN.
false for every other expression, including an unevaluated expression
whose value would be Indeterminate.
Expression.isInfinity
readonly isInfinity: boolean | undefined;
The numeric value of this expression is ±Infinity or ComplexInfinity.
Expression.isFinite
readonly isFinite: boolean | undefined;
This expression is a number, but not ±Infinity, ComplexInfinity or
NaN
Other
Expression.hash
readonly hash: number;
A structural hash of this expression, suitable as an in-memory bucketing or cache key with a deep compare on hit.
The contract:
-
Invariant: if
a.isSame(b)istrue, thena.hash === b.hash. The hash is the structural tier's companion — a pure function of the canonical tree. A symbol's assigned value never affects it. -
Stability: deterministic within a release — the same canonical tree yields the same hash across engine instances and processes, as it is computed from structure and strings only, with no engine state. Not stable across releases: the hash function may change in any release, so a cache keyed on it must not outlive the engine build. Never persist it.
-
Collisions: a 32-bit-class, bucketing-grade hash. Distinct expressions may share a hash; always verify a hash hit with
isSame()(or another structural compare) before treating two expressions as identical. -
Bound variables: folds bound-variable names (binding-identity, not alpha-equivalence), matching
isSame():Sum(i, i in 1..n)andSum(j, j in 1..n)hash differently, just as they are notisSame(). This clause co-evolves withisSame()semantics.
Expression.digest
readonly digest: string;
A 128-bit digest of this expression's serialized structure — the
MathJSON .json writes — as 32 hexadecimal characters: an in-memory
cache key that needs no compare on hit, computed without writing the
MathJSON out.
It replaces JSON.stringify(expr.json) as a key, and keys the same
way: two expressions digest alike exactly when their MathJSON is the
same tree (a collision between two distinct trees is not expected in
practice — 128 bits from a non-cryptographic mixer, on the engine's own
serializations), with two deliberate exceptions where the digest is
coarser than the text — a dictionary's entry order does not enter it,
and a character digests like the one-cluster string with the same
content, as the two are the same value.
It is therefore not an isSame key, in both directions, and
hash remains the isSame companion:
-
two symbols of the same name digest alike whatever they are bound to (a symbol serializes as its name), even when
isSame— which reads binding identity — says they differ; -
the exact rational
1/2and the float0.5areisSamebut serialize apart, and digest apart. -
Cost: memoized per node, so it is linear in the DISTINCT nodes of the expression — except at and above a mutable object, whose record snapshot is read fresh like
.json, so a store to it is seen by every node that contains it.JSON.stringify(expr.json)is linear in the PATHS, which on a value that shares its sub-expressions is exponential in the depth. -
Stability: deterministic within a release, across engine instances and processes. Not stable across releases and not cryptographic: never persist it, never use it to authenticate.
-
Bound variables: folds bound-variable names, as the MathJSON does.
Expression.engine
readonly engine: ExpressionComputeEngine;
The Compute Engine instance associated with this expression provides a context in which to interpret it, such as definition of symbols and functions.
Expression.toMathJson()
toMathJson(options?): MathJsonExpression
Serialize to a MathJSON expression with specified options.
Use { fractionalDigits: 'auto' } to round arbitrary-precision
numbers to ce.precision significant digits. The default
('max') emits all available digits with no rounding.
####### options?
Readonly<Partial<JsonSerializationOptions>>
Expression.json
readonly json: MathJsonExpression;
MathJSON representation of this expression.
This representation always use shorthands when possible. Metadata is not included.
Numbers are converted to JavaScript numbers and may lose precision.
The expression is represented exactly and no sugaring is applied. For
example, ["Power", "x", 2] is not represented as ["Square", "x"].
For more control over the serialization, use expr.toMathJson().
Note that lazy collections are not eagerly evaluated.
For arbitrary-precision numbers, the full raw BigDecimal value is
emitted with no rounding (same as toJSON()). This preserves data
fidelity for round-tripping but may include trailing digits beyond
ce.precision that are not meaningful. Use
toMathJson({ fractionalDigits: 'auto' }) for rounded output.
Applicable to canonical and non-canonical expressions.
Expression.latex
readonly latex: string;
Return a LaTeX representation of this expression.
This is a convenience getter that delegates to the standalone
serialize() function from the latex-syntax module.
Numeric values are rounded to ce.precision significant digits.
Noise digits from precision-bounded operations (division,
transcendentals) are not displayed.
Expression.toLatex()
toLatex(options?): string
Return a LaTeX representation of this expression with custom serialization options.
Numeric values are rounded to ce.precision significant digits.
####### options?
Record<string, any>
Expression.print()
print(): void
Output to the console a string representation of the expression.
Note that lazy collections are eagerly evaluated when printed.
Expression.verbatimLatex?
optional verbatimLatex?: string;
If the expression was constructed from a LaTeX string, the verbatim LaTeX string it was parsed from.
Expression.sourceOffsets?
optional sourceOffsets?: [number, number];
Source offsets in the original source string, when available.
Expression.isCanonical
If true, this expression is in a canonical form.
Expression.isStructural
If true, this expression is in a structural form.
The structural form of an expression is used when applying rules to
an expression. For example, a rational number is represented as a
function expression instead of a Expression object.
Expression.canonical
Return the canonical form of this expression.
If a function expression or symbol, they are first bound with a definition in the current scope.
When determining the canonical form the following operator definition flags are applied:
associative: $ f(a, f(b), c) \longrightarrow f(a, b, c) $idempotent: $ f(f(a)) \longrightarrow f(a) $involution: $ f(f(a)) \longrightarrow a $commutative: sort the arguments.
If this expression is already canonical, the value of canonical is
this.
The arguments of a canonical function expression may not all be
canonical, for example in the ["Declare", "i", 2] expression,
i is not canonical since it is used only as the name of a symbol, not
as a (potentially) existing symbol.
Partially canonical expressions, such as those produced through
CanonicalForm, also yield an expression which is marked as canonical.
This means that, likewise for partially canonical expressions, the
canonical property will return the self-same expression (and
'isCanonical' will also be true).
Expression.structural
Return the structural form of this expression.
Some expressions, such as rational numbers, are represented with
a Expression object. In some cases, for example when doing a
structural comparison of two expressions, it is useful to have a
structural representation of the expression where the rational numbers
is represented by a function expression instead.
If there is a structural representation of the expression, return it,
otherwise return this.
Expression.isValid
readonly isValid: boolean;
false if this expression or any of its subexpressions is an ["Error"]
expression.
The check is deep: an ["Error"] anywhere in the expression tree
invalidates the whole expression. This includes the elements of a list,
vector or matrix — (1,2) + [3,4] broadcasts to a list of
incompatible-type errors, and that list is invalid — and operands held
unevaluated, such as the body of a ["Hold"].
Applicable to canonical and non-canonical expressions. For non-canonical expression, this may indicate a syntax error while parsing LaTeX. For canonical expression, this may indicate argument type mismatch, or missing or unexpected arguments.
This is a check for well-formedness, not for meaningfulness: it
answers "is this expression free of errors?", not "will it evaluate to a
useful value?". An expression is still valid when it contains free
symbols (x + 1), calls an undeclared function, or evaluates to NaN or
±∞ (0/0 and 1/0 are both valid). Conversely, a valid expression may
still fail to evaluate for reasons this property does not report.
Use it as an admission gate — check isValid before compiling,
plotting, or otherwise consuming an expression built from untrusted or
user-supplied input, since malformed input surfaces as an ["Error"]
expression rather than as a thrown exception. To find out what is
wrong, walk the expression for ["Error"] subexpressions; each carries
an error code and the offending operand.
Expression.isPure
readonly isPure: boolean;
If true, evaluating this expression has no side-effects (does not change the state of the Compute Engine).
If false, evaluating this expression may change the state of the Compute Engine or it may return a different value each time it is evaluated, even if the state of the Compute Engine is the same.
As an example, the ["Add", 2, 3] function expression is pure, but
the ["Random"] function expression is not pure.
For a function expression to be pure, the function itself (its operator) must be pure, and all of its arguments must be pure too.
A pure function expression may return a different value each time it is
evaluated if its arguments are not constant. For example, the
["Add", "x", 1] function expression is pure, but it is not
constant, because x is not constant.
Applicable to canonical expressions only
Since Stage 2 of the effects model this is a view of the runtime
effect channel: "no impurity label in effectsOf(expr)" (see
boxed-expression/effects-of.ts).
Expression.effects
readonly effects:
| "any"
| readonly EffectLabel[]
| undefined;
The effects of evaluating this expression: undefined when there are
none (the expression is pure), 'any' when the effects are not known, or
the effect labels in alphabetical order.
This is the set isPure summarizes — isPure is "no impurity label in
here" — but it says which effects, so a consumer can act on them: a
scope write invalidates a memo, random means the value will differ on
re-evaluation, network means evaluating may be slow or may fail.
It reports what evaluating this expression does, not what the value it produces can do if you later invoke it. A symbol bound to a drawing function has no effects — evaluating it just yields the function — while its type carries the draw:
ce.assign('rf', ce.box(['Function', ['Random'], 'x']));
ce.box('rf').effects; // ➔ undefined (producing the value)
ce.box('rf').isPure; // ➔ true
ce.box('rf').type.effects; // ➔ ['random'] (invoking it)
ce.box(['Map', xs, 'rf']).effects; // ➔ ['random'] (Map invokes it)
Numbers, strings, symbols and dictionaries have no effects. See
docs/EFFECTS-MODEL.md ("Projection and discharge") for how an
application's effects are computed from its operator and operands.
Expression.isConstant
readonly isConstant: boolean;
True if evaluating this expression always returns the same value.
If true and a function expression, implies that it is pure and that all of its arguments are constant.
Number literals, symbols with constant values, and pure numeric functions with constant arguments are all constant, i.e.:
42is constantPiis constant["Divide", "Pi", 2]is constantxis not constant, unless declared with a constant flag.["Add", "x", 2]is either constant only ifxis constant.
Expression.errors
readonly errors: readonly Expression[];
All the ["Error"] subexpressions.
If an expression includes an error, the expression is also an error.
In that case, the this.isValid property is false.
Applicable to canonical and non-canonical expressions.
Expression.getSubexpressions()
getSubexpressions(operator): readonly Expression[]
All the subexpressions matching the named operator, recursively.
Example:
const expr = ce.parse('a + b * c + d');
const subexpressions = expr.getSubexpressions('Add');
// -> `[['Add', 'a', 'b'], ['Add', 'c', 'd']]`
Applicable to canonical and non-canonical expressions.
####### operator
string
Expression.subexpressions
readonly subexpressions: readonly Expression[];
All the subexpressions in this expression, recursively
Example:
const expr = ce.parse('a + b * c + d');
const subexpressions = expr.subexpressions;
// -> `[['Add', 'a', 'b'], ['Add', 'c', 'd'], 'a', 'b', 'c', 'd']`
Applicable to canonical and non-canonical expressions.
Expression.symbols
readonly symbols: readonly string[];
All the symbols in the expression, recursively, including bound variables (e.g., summation/product index variables).
Use unknowns or freeVariables to get only the symbols that are free (not bound by a scoping construct).
const expr = ce.parse('a + b * c + d');
const symbols = expr.symbols;
// -> ['a', 'b', 'c', 'd']
Applicable to canonical and non-canonical expressions.
Expression.unknowns
readonly unknowns: readonly string[];
All the symbols used in the expression that do not have a value associated with them, i.e. they are declared but not defined.
Expression.freeVariables
readonly freeVariables: readonly string[];
The free variables of the expression: symbols that are not constants, not operators, not bound to a value, and not locally scoped (e.g., summation/product index variables are excluded).
This is an alias for unknowns.
Expression.defines
readonly defines: readonly string[];
The symbols defined by this expression: the target of a top-level
["Assign", …] or ["Declare", …] (e.g. a in a := 3, f in
f(x) := …), recursing through ["Block", …] sequences. Empty for
expressions that define nothing.
Complements freeVariables (the symbols an expression
references). A tool that builds a dependency graph keyed on cells —
e.g. a notebook — can use defines for the out-edges and
references for the in-edges.
Applicable to canonical and non-canonical expressions.
Expression.referencedFunctions
readonly referencedFunctions: readonly string[];
The user functions applied in this expression: the operator head of
a function application when that head is a user-definable symbol — f in
f(x), g in g(x) := f(x) + 1. Built-in operators (Add, Sin, …),
constants, and names bound to a value or by an enclosing scope (function
parameters, summation indices, Block locals) are excluded — the same
predicate freeVariables applies to ordinary symbols.
Operator heads are not symbols of the expression, so they appear in neither symbols nor freeVariables. This accessor recovers the function-call dependency edges those views miss.
Applicable to canonical and non-canonical expressions.
Expression.references
readonly references: readonly string[];
The symbols this expression references but does not itself define: the union of freeVariables (referenced values) and referencedFunctions (applied user functions), minus defines.
This is the complete in-edge set for a dependency graph keyed on cells
(e.g. a notebook): pair it with defines for the out-edges.
Subtracting defines drops self-references, so a recursive
g(x) := g(x - 1) reports no dependency on itself.
const cell = ce.parse('g(x) := f(x) + a', { canonical: false });
cell.defines; // -> ['g']
cell.references; // -> ['a', 'f']
Applicable to canonical and non-canonical expressions.
Expression.toNumericValue()
toNumericValue(): [NumericValue, Expression]
Attempt to factor a numeric coefficient c and a rest out of a
canonical expression such that rest.mul(c) is equal to this.
Attempts to make rest a positive value (i.e. pulls out negative sign).
['Multiply', 2, 'x', 3, 'a']
-> [NumericValue(6), ['Multiply', 'x', 'a']]
['Divide', ['Multiply', 2, 'x'], ['Multiply', 3, 'y', 'a']]
-> [NumericValue({rational: [2, 3]}), ['Divide', 'x', ['Multiply, 'y', 'a']]]
Expression.numerator
Return this expression expressed as a numerator.
Expression.denominator
Return this expression expressed as a denominator.
Expression.numeratorDenominator
Return this expression expressed as a numerator and denominator.
Expression.toRational()
toRational(): [number, number] | null
Return the value of this expression as a pair of integer numerator and
denominator, or null if the expression is not a rational number.
- For a
BoxedNumberwith an exact rational value, extracts from the numeric representation. - For an integer, returns
[n, 1]. - For a
DivideorRationalfunction with integer operands, returns[num, den]. - For everything else, returns
null.
The returned rational is always in lowest terms.
ce.parse('\\frac{6}{4}').toRational() // [3, 2]
ce.parse('7').toRational() // [7, 1]
ce.parse('x + 1').toRational() // null
ce.number(1.5).toRational() // null (machine float)
Expression.factors()
factors(): readonly Expression[]
Return the multiplicative factors of this expression as a flat array.
This is a structural decomposition — it does not perform algebraic
factoring (use ce.function('Factor', [expr]) for that).
Multiply(a, b, c)returns[a, b, c]Negate(x)returns[-1, ...x.factors()]- Anything else returns
[expr]
ce.parse('2xyz').factors() // [2, x, y, z]
ce.parse('-3x').factors() // [-1, 3, x]
ce.parse('x + 1').factors() // [x + 1]
Expression.polynomialCoefficients()
polynomialCoefficients(variable?): readonly Expression[] | undefined
Return the coefficients of this expression as a polynomial in variable,
in descending order of degree. Returns undefined if the expression is
not a polynomial in the given variable.
If variable is omitted, auto-detects when the expression has exactly
one unknown. Returns undefined if there are zero or multiple unknowns.
ce.parse('x^2 + 2x + 1').polynomialCoefficients('x') // [1, 2, 1]
ce.parse('x^3 + 2x + 1').polynomialCoefficients('x') // [1, 0, 2, 1]
ce.parse('sin(x)').polynomialCoefficients('x') // undefined
ce.parse('x^2 + 5').polynomialCoefficients() // [1, 0, 5]
Subsumes isPolynomial:
const isPolynomial = expr.polynomialCoefficients('x') !== undefined;
Subsumes polynomialDegree:
const degree = expr.polynomialCoefficients('x')?.length - 1;
When variable is an array, the expression must be polynomial in ALL
listed variables. Coefficients are decomposed by the first variable;
remaining variables appear as symbolic coefficients.
ce.parse('x^2*y + 3x + y^2').polynomialCoefficients(['x', 'y'])
// → [y, 3, y²] (coefficients of x², x¹, x⁰)
####### variable?
string | string[]
Expression.polynomialRoots()
polynomialRoots(variable?): readonly Expression[] | undefined
Return the roots of this expression treated as a polynomial in variable.
Returns undefined if the expression is not a polynomial in the given
variable. Returns an empty array if no roots can be found.
If variable is omitted, auto-detects when the expression has exactly
one unknown.
ce.parse('x^2 - 5x + 6').polynomialRoots('x') // [2, 3]
ce.parse('x^2 + 1').polynomialRoots('x') // [] (no real roots)
ce.parse('sin(x)').polynomialRoots('x') // undefined
####### variable?
string
Expression.isScoped
readonly isScoped: boolean;
If true, the expression has its own local scope that can be used for local variables and arguments. Only true if the expression is a function expression.
Expression.localScope
If this expression has a local scope, return it.
Expression.subs()
subs(sub, options?): Expression
Replace all the symbols in the expression as indicated.
Note the same effect can be achieved with this.replace(), but
using this.subs() is more efficient and simpler, but limited
to replacing symbols.
The free symbols of the result are bound in the CURRENT scope, not in the scope the receiver was built in. A node that owns a local scope keeps that scope, so a binder's bound variables go on denoting the binder's own bindings — including for a binder nested inside another one, whose scope chain would otherwise no longer reach the outer binder's index.
If options.canonical is not set, the result is canonical if this
is canonical.
Applicable to canonical and non-canonical expressions.
If this is a function, an empty substitution is given, and the computed value of canonical
does not differ from that of this expr.: then a call this method is analagous to requesting a
clone.
####### sub
Substitution<ExpressionInput>
####### options?
####### canonical?
Expression.map()
map(fn, options?): Expression
Recursively replace all the subexpressions in the expression as indicated.
To remove a subexpression, return an empty ["Sequence"] expression.
The canonical option is applied to each function subexpression after
the substitution is applied.
If no options.canonical is set, the result is canonical if this
is canonical.
Default: { canonical: this.isCanonical, recursive: true }
Applicable to canonical and non-canonical expressions.
####### fn
(expr) => Expression
####### options?
####### canonical
####### recursive?
boolean
Expression.replace()
replace(rules, options?): Expression | null
Transform the expression by applying one or more replacement rules:
-
If the expression matches the
matchpattern and theconditionpredicate is true, replace it with thereplacepattern. -
If no rules apply, return
null.
The form option controls the form of replacements. The deprecated
canonical option is also accepted for backward compatibility; only one
of the two may be specified.
When neither form nor canonical is specified, the form of each
replacement is determined as follows:
- the form of the replacement produced by the rule, if it has a
non-
'raw'form; - otherwise, the form of the expression being replaced;
- otherwise, the replacement is left in its raw form.
While the form applies directly to replaced sub-expressions only, a
non-'raw' form also propagates 'opportunistically' up the expression
tree: an expression whose operands all share a form after replacement
assumes that form as well. (Specifying form: 'raw' disables this
propagation.)
Applicable to input expressions of any form.
To match a specific symbol (not a wildcard pattern), the match must be
a Expression (e.g., { match: ce.expr('x'), replace: ... }).
For simple symbol substitution, consider using subs() instead.
####### rules
BoxedRuleSet | Rule | Rule[]
####### options?
Partial<ReplaceOptions>
Expression.has()
has(v): boolean
True if the expression includes a symbol v or a function operator v.
Applicable to canonical and non-canonical expressions.
####### v
string | string[]
Expression.match()
match(pattern, options?): BoxedSubstitution<Expression> | null
If this expression matches pattern, return a substitution that makes
pattern equal to this. Otherwise return null.
If pattern includes wildcards (symbols that start
with _), the substitution will include a prop for each matching named
wildcard.
If this expression matches pattern but there are no named wildcards,
return the empty substitution, {}.
pattern can be:
- A string (LaTeX): single-character symbols are auto-converted to
wildcards (e.g.,
'ax^2+bx+c'treatsa,b,cas wildcards). Results use unprefixed keys ({a: 3}not{_a: 3}) and self-matches are filtered out.useVariationsandmatchMissingTermsdefault totrue. Unprefixed keys are accepted insubstitution. - A MathJSON array (e.g.,
['Add', '_a', '_b']): boxed automatically. - A BoxedExpression: used directly.
Read more about patterns and rules.
Applicable to canonical and non-canonical expressions.
####### pattern
####### options?
PatternMatchOptions<Expression>
Expression.wikidata
readonly wikidata: string | undefined;
Wikidata identifier.
If not a canonical expression, return undefined.
Expression.description
readonly description: string[] | undefined;
An optional short description if a symbol or function expression.
May include markdown. Each string is a paragraph.
If not a canonical expression, return undefined.
Expression.url
readonly url: string | undefined;
An optional URL pointing to more information about the symbol or function operator.
If not a canonical expression, return undefined.
Expression.complexity
readonly complexity: number | undefined;
Expressions with a higher complexity score are sorted first in commutative functions
If not a canonical expression, return undefined.
Expression.baseDefinition
readonly baseDefinition: BoxedBaseDefinition | undefined;
For symbols and functions, a definition associated with the
expression. this.baseDefinition is the base class of symbol and function
definition.
If not a canonical expression, return undefined.
Expression.operatorDefinition
readonly operatorDefinition: BoxedOperatorDefinition | undefined;
For function expressions, the definition of the operator associated with
the expression. For symbols, the definition of the symbol if it is an
operator, for example "Sin".
If not a canonical expression or not a function expression,
its value is undefined.
Expression.valueDefinition
readonly valueDefinition: BoxedValueDefinition | undefined;
For symbols, a definition associated with the expression, if it is not an operator.
If not a canonical expression, or not a value, its value is undefined.
Expression.simplify()
simplify(options?): Expression
Return a simpler form of this expression.
A series of rewriting rules are applied repeatedly, until no more rules apply.
The values assigned to symbols and the assumptions about symbols may be
used, for example expr.isInteger or expr.isPositive.
No calculations involving decimal numbers (numbers that are not integers) are performed but exact calculations may be performed, for example:
\sin(\frac{\pi}{4}) \longrightarrow \frac{\sqrt{2}}{2}.
The result is canonical.
To manipulate symbolically non-canonical expressions, use expr.replace().
####### options?
Partial<SimplifyOptions>
Expression.explain()
explain(operation?, options?): Explanation
Return a structured, step-by-step explanation of an operation applied to this expression: the textbook chain expression → step (with a reason) → … → result.
The operation defaults to 'simplify'. The explanation runs the same
engine code as the plain method: explain('simplify').result is the
same value simplify() returns.
For 'solve', the receiver is a univariate equation (or an expression
f, read as f = 0); the unknown is inferred or passed via
options.variable. Step values are equations — the state after each
phase (2x+1=5 → 2x-4=0 → x=2), including candidate roots and
rejected extraneous candidates — and result is a List of the same
roots solve() returns. Systems of equations are not supported yet.
For 'D', steps are whole-expression states in traversal order: each
textbook rule (sum, product, quotient, power, chain, …) first appears
with its unresolved sub-derivatives as inert D(…) terms, which
resolve step by step. The variable is inferred when unambiguous, or
passed via options.variable; result matches evaluating
D(expr, variable).
Each step carries the expression state after the step, a stable machine
id (the key for localization and custom copy) and a default English
description. The initial property is the canonical form of this
expression — canonicalization happens before the first step is recorded
and is not traced.
By default the step chain is curated: internal bookkeeping steps are
filtered out. Pass verbosity: 'all' to get the raw chain (for
debugging and rule authoring).
####### operation?
####### options?
ExplainOptions
Expression.toSignedFunction()
toSignedFunction(): Expression | undefined
For a relation expression (Equal, Less, Greater, LessEqual,
GreaterEqual, NotEqual), return the "signed function" form
useful for implicit-surface rendering and region classification:
Equal(a, b)→a - b(zero on the surface)Less(a, b)/LessEqual(a, b)→a - b(negative when relation holds)Greater(a, b)/GreaterEqual(a, b)→b - a(negative when relation holds)NotEqual(a, b)→a - b(caller checks ≠ 0)
For non-relation expressions, returns undefined.
Strictness (strict vs non-strict inequality) and direction (less vs
greater) are encoded in the original expr.operator, not in the
returned expression. Callers handling 3D implicit rendering use
expr.operator for the boundary policy and the signed function for
the interior/exterior classification.
Notes:
- CE canonical form normalizes
GreaterEqual(a, b)toLessEqual(b, a)(and similarlyGreatertoLess). Callers usingtoSignedFunction()on canonicalized parsed expressions will seeLessEqual/Lessrather thanGreaterEqual/Greater. The signed-function semantics are preserved through the normalization. TheGreaterEqual/Greaterbranches handle non-canonical expressions constructed viace.expr(['GreaterEqual', ...]). - For chained relations with more than two operands (e.g.
Less(a, b, c)froma < b < c), only the first pair is used. The result is the signed function for the first sub-relation only; 3D implicit rendering rarely uses chained relations, but if it does, callers should decompose first.
Expression.getInterval()
getInterval(symbol): IntervalBounds | undefined
For an expression representing a domain restriction (a When whose
condition is a comparison or And of comparisons over symbol, or
a bare comparison expression), return the lower/upper bounds for
symbol. Returns undefined if no bounds can be extracted.
Supported shapes:
- Bare comparisons:
a < x,x < b, etc. - Chained comparisons:
a < x < b(parsed asLess(a, x, b)) And(c1, c2, ...)where eachciis a supported shapeWhen(e, cond)— operates oncondMultiply(f, When(...), ...)— the Desmos parse shape forf(x)\{a < x < b\}; bounds from eachWhenfactor are merged
lowerStrict/upperStrict are true for strict (<, >) bounds
and false for non-strict (≤, ≥).
Returns undefined for unsupported shapes (e.g. equations, non-linear
constraints, comparisons over multiple symbols, disjunctions).
####### symbol
string
Expression.evaluate()
evaluate(options?): Expression
Return the value of the canonical form of this expression.
A pure expression always returns the same value (provided that it remains constant / values of sub-expressions or symbols do not change), and has no side effects.
Evaluating an impure expression may return a varying value, and may have some side effects such as adjusting symbol assumptions.
To perform approximate calculations, use expr.N() instead,
or call with options.numericApproximation to true.
It is possible that the result of expr.evaluate() may be the same as
expr.simplify().
The result is in canonical form.
Time and recursion limits: if the evaluation runs inside an enclosing
ComputeEngine.withTimeLimit
span and exceeds its deadline, spends an enclosing
ComputeEngine.withStepBudget
budget, or exceeds the recursion limit, a CancellationError is thrown
(its cause is 'timeout', 'step-budget' or
'recursion-depth-exceeded'). Catch it to distinguish
an interrupted evaluation from a symbolic (inert) result.
####### options?
Partial<EvaluateOptions>
Expression.evaluateAsync()
evaluateAsync(options?): Promise<Expression>
Asynchronous version of evaluate().
The options argument can include a signal property, which is an
AbortSignal object. If the signal is aborted, a CancellationError is thrown.
####### options?
Partial<EvaluateOptions>
Expression.N()
N(): Expression
Return a numeric approximation of the canonical form of this expression.
Any necessary calculations, including on decimal numbers (non-integers), are performed.
The calculations are performed according to the
precision property of the ComputeEngine.
To only perform exact calculations, use this.evaluate() instead.
If the function is not numeric, the result of this.N() is the same as
this.evaluate().
The result is in canonical form.
Note on typing (SYMBOLIC P2-24, by design): N() produces a float
literal, so its type can widen relative to the exact input's — e.g.
1/3 has type rational while (1/3).N() has type
real. The result type reflects the representation produced,
not the mathematical value's tightest type.
Expression.solve()
solve(vars?):
| readonly Expression[]
| Record<string, Expression>
| Record<string, Expression>[]
| null
If this is an equation, solve the equation for the variables in vars.
Otherwise, solve the equation this = 0 for the variables in vars.
For univariate equations, returns an array of solutions (roots). For a
trigonometric equation, the array holds the principal roots, which
represent the periodic families of roots. Returns null when the solver
cannot solve the equation, and also when it can find only a part of the
roots: when the unknown is in a function that the solver cannot invert
and that is not periodic ((x - 1)·BesselJ(0, x) = 0), or in a factor of
a product that gives no root and is not shown to have none
((x - 2)(x + e^x) = 0).
For systems of linear equations (List of Equal expressions), returns
an object mapping variable names to their values.
For non-linear polynomial systems (like xy=6, x+y=5), returns an array
of solution objects (multiple solutions possible).
// Univariate equation
const expr = ce.parse("x^2 + 2*x + 1 = 0");
console.log(expr.solve("x")); // Returns array of roots
// System of linear equations
const system = ce.parse("\\begin{cases}x+y=70\\\\2x-4y=80\\end{cases}");
console.log(system.solve(["x", "y"])); // Returns { x: 60, y: 10 }
// Non-linear polynomial system (product + sum)
const nonlinear = ce.parse("\\begin{cases}xy=6\\\\x+y=5\\end{cases}");
console.log(nonlinear.solve(["x", "y"])); // Returns [{ x: 2, y: 3 }, { x: 3, y: 2 }]
####### vars?
| string
| Iterable<string, any, any>
| Expression
| Iterable<Expression, any, any>
Expression.value
get value(): Expression | undefined
set value(value:
| number[]
| ExpressionInput
| OnlyFirst<{
re: number;
im: number;
}, {
re: number;
im: number;
} & {
num: number;
denom: number;
} & Expression>
| OnlyFirst<{
num: number;
denom: number;
}, {
re: number;
im: number;
} & {
num: number;
denom: number;
} & Expression>
| OnlyFirst<Expression, {
re: number;
im: number;
} & {
num: number;
denom: number;
} & Expression>
| undefined): void
If this expression is a number literal, a string literal or a function literal, return the expression.
If the expression is a symbol, return the value of the symbol.
Otherwise, the expression is a symbolic expression, including an unknown
symbol, i.e. a symbol with no value, return undefined.
If the expression is a symbol, set the value of the symbol.
Will throw a runtime error if either not a symbol, or a symbol with the
constant flag set to true.
Setting the value of a symbol results in the forgetting of all assumptions about it in the current scope.
Expression.isCollection
isCollection: boolean;
Is true if the expression is a collection.
When isCollection is true, the expression:
- has an
each()method that returns a generator over the elements of the collection. - has a
sizeproperty that returns the number of elements in the collection. - has a
contains(other)method that returnstrueif theotherexpression is in the collection.
isCollection is a CAPABILITY, type.matches('collection<any>') is a SHAPE
This is the single most common source of collection-handling bugs in the engine, so it is worth stating precisely. The two predicates answer different questions and neither implies the other:
isCollection— "can I enumerate this now?" It isfalsefor a symbol declaredlist<number>/vector<2>that has not been assigned yet, and for an application whose head returns a collection (L(1)underL: (number) -> vector<2>): both are collection-shaped, but there is nothing to walk.type.matches('collection<any>')— "is this operand collection-shaped?" It istruefor those valueless cases, andfalsefor a materialized collection whose type is top (unknown/any), whichisCollectionreportstrue.
A shape test must spell the <any> FAMILY TOP, never the bare name:
since the bare-synonym ruling (2026-08-17) bare collection is the
values-only collection<unknown>, so list<any>, list<nothing> and
list<integer|missing> — all collection-shaped — do NOT match it.
(COLLECTION_SHAPE_TYPE and friends in common/type/primitive.ts are
the same tops as Type constants, for isSubtype call sites.)
Pick by the question you are actually asking:
- About to call
each(),contains(),at(), or readcount— that is a capability question. UseisCollection. - Deciding whether an operand takes the SCALAR path or the
collection/broadcast path — that is a shape question. Test
isCollection || type.matches('collection<any>'), or the operand class alone withisValuelessCollectionTyped()(collection-utils.ts).
Getting this wrong has a characteristic signature: the operator takes its
scalar path for an operand that is not a scalar and commits an answer that
the SAME expression contradicts once the symbol is assigned. A 2026-08-15
audit of all 95 isCollection sites found seven operator families doing
exactly that — Sum(L) answering L, Union(L, Set(1)) collapsing L
into a single element, SetMinus INVERTING a membership answer,
Mean(L) committing NaN, Which throwing on a list<boolean>
condition. Pinned in
test/compute-engine/valueless-collection-typed-operand.test.ts, which is
the place to add a case if you touch this.
A third predicate covers a distinct case: isPossiblyCollectionTyped()
(collection-utils.ts) is for an operand that MIGHT become a collection
at runtime — a top-typed application, or a broadcastable<T> — where the
honest answer is that the shape is not statically visible at all.
One more operand class answers a confident false to BOTH isCollection
and type.matches('collection<any>') while still being able to hold a
collection: a union of a scalar branch and a collection branch — a
valueless u: number | list<number>, and the 2u lifted over it, since a
broadcast over such an operand carries the union through rather than
claiming a definite list. The scalar branch defeats the match, so a gate
that must decline for a MAYBE-collection has to ask one of the two
union predicates in collection-utils.ts as well:
unionMayHoldACollection() for an ENUMERATION gate (a big op folding its
body — tuple, string and fixed-shape branches enumerate too), or
scalarOrCollectionUnionBranches() for a BROADCAST gate.
Expression.array
readonly array: readonly number[] | undefined;
The elements of a List as plain machine numbers, or undefined.
Defined for a List whose every element is a machine number: an
integer, a finite double, an infinity or NaN. A list built by
ce.list() answers its own frozen array without boxing an element; an
ordinary list answers a frozen array computed once from its elements.
An element that is not a machine number — an exact rational such as
1/3, a radical, a bignum with more digits than a double holds, a
complex number, a symbol, a nested list — makes the answer undefined:
the value is never approximated. Evaluate with .N() first to get the
floats of an exact list.
The array is frozen. It may be passed as is into a compiled function's argument bag.
:category: Collections
Expression.isMachineNumeric
readonly isMachineNumeric: boolean;
Does array reproduce this expression, exactness included?
For a List: true when array is defined and ce.list(expr.array)
is this list element for element, as the interpreter computes with it.
A list built by ce.list() answers true in constant time. An
ordinary list answers true when every element is a float with a
fraction part or an exact integer a double holds. It answers false
when some element is a float with an integer value, such as 2.0:
ce.list() boxes the double 2 as the exact integer 2. It answers
false too when some element is an exact non-integer such as the
rational 1/2: array admits it, since a
double holds 0.5 with no rounding, but re-boxing 0.5 gives a float,
which computes as one (0.5 / 3 is 0.1666… where 1/2 ÷ 3 is
1/6). A consumer that must keep exact values exact takes array only
when this is true.
For a number: true when the number is a float with a fraction part
or past the safe integers, an exact integer a double holds, NaN or an
infinity; false for a float with a safe-integer value (2.0), an
exact non-integer, a radical or a complex number.
false for every other expression.
:category: Collections
Expression.isIndexedCollection
isIndexedCollection: boolean;
Is true if this is an indexed collection, such as a list, a vector,
a matrix, a tuple, etc...
The elements of an indexed collection can be accessed by a one-based index.
When isIndexedCollection is true, the expression:
- has an
each(),size()andcontains(rhs)methods as for a collection. - has an
at(index: number)method that returns the element at the specified index. - has an
indexWhere(predicate: (element: Expression) => boolean)method that returns the index of the first element that matches the predicate.
Expression.isLazyCollection
isLazyCollection: boolean;
False if not a collection, or if the elements of the collection are not computed lazily.
The elements of a lazy collection are computed on demand, when
iterating over the collection using each().
Use ListFrom and related functions to create eager collections from
lazy collections.
Expression.each()
each(): Generator<Expression>
If this is a collection, return an iterator over the elements of the collection.
const expr = ce.parse('[1, 2, 3, 4]');
for (const e of expr.each()) {
console.log(e);
}
Expression.contains()
contains(rhs): boolean | undefined
If this is a collection, return true if the rhs expression is in the
collection.
Return undefined if the membership cannot be determined without
iterating over the collection.
####### rhs
Expression.subsetOf()
subsetOf(other, strict): boolean | undefined
Check if this collection is a subset of another collection, i.e.
this ⊆ other.
Returns undefined when the relation cannot be determined — including
when this expression is not (yet) a collection.
####### other
The other collection to check against.
####### strict
boolean
If true, the subset relation is strict (i.e., proper subset).
Expression.count
If this is a collection, return the number of elements in the collection.
Only top-level elements are counted: for a nested collection (e.g. a
matrix represented as a list of lists), this is the number of immediate
elements (the rows), not the total number of scalar entries. For example
the count of [[2, 3, 4], [6, 7, 9]] is 2, not 6. This is consistent
with each() and at(), which iterate over and index the same elements.
If the collection is infinite, return Infinity.
If the number of elements cannot be determined, return undefined, for
example, if the collection is lazy and not finite and the size cannot
be determined without iterating over the collection.
Expression.isFiniteCollection
isFiniteCollection: boolean | undefined;
If this is a finite collection, return true.
Expression.isEmptyCollection
isEmptyCollection: boolean | undefined;
If this is an empty collection, return true.
An empty collection has a size of 0.
Expression.isEnumerableCollection
isEnumerableCollection: boolean | undefined;
Whether each() yields this collection's actual elements.
This is the cheap predicate that separates the two reasons each() can
produce nothing:
true: the elements are enumerable, so a walk that yields nothing means the collection is empty.false: the elements cannot be produced in the current state, so a walk that yields nothing means nothing at all. For exampleRange(a, b)with free variables,Repeat(3, n)with a symbolic count, or a wrapper over such a source (Take(Range(a, b), 2)).undefined: undecidable without evaluating — an eager collection operator (Characters(s),UnicodeScalars(s)) has no collection handlers until it is evaluated, yeteach()walks it through the materialize-then-iterate path.
An arithmetic broadcast (x + [1, 2], Sin(1..99) — a
broadcastable operator whose collection-ness is a lift over its
operands) answers from its participants: true when they agree on a
length, evaluation is draw-free, and every collection-typed participant
is itself enumerable; false when a participant is definitively
unwalkable (a valueless symbol, an application of an UNBOUND head —
x + Total([1, 2]) with Total undeclared binds vacuously and can
never produce elements); undefined when the lengths disagree or an
impure participant (RandomShuffle(xs) + 1) makes per-index reads
unable to promise draw coherence — there each() still walks, but
at() declines. Impurity confined to a scalar (lifted) operand does
not demote the answer: [1, 2] + RandomInteger(1, 10) is true — its
evaluation distributes structurally without consuming randomness, and
both each() and at() serve the unevaluated elements.
Independent of count: a collection can know its size and still not be
enumerable (Linspace(a, 1, 3) has a count of 3 and no computable
elements), and an enumerable collection can have an unknown size.
Answered structurally, without evaluating: O(1) for a leaf, O(depth) for a chain of wrappers.
Expression.at()
at(index): Expression | undefined
If this is an indexed collection, return the element at the specified index. The first element is at index 1.
If the index is negative, return the element at index size() + index + 1.
The last element is at index -1.
####### index
number
Expression.get()
get(key): Expression | undefined
If this is a keyed collection (map, record, tuple), return the value of the corresponding key.
If key is a Expression, it should be a string.
####### key
string | Expression
Expression.indexWhere()
indexWhere(predicate): number | undefined
If this is an indexed collection, return the index of the first element that matches the predicate.
####### predicate
(element) => boolean
Primitive Methods
Expression.valueOf()
valueOf(): string | number | boolean | number[] | number[][] | number[][][]
Return a JavaScript primitive value for the expression, based on
Object.valueOf().
This method is intended to make it easier to work with JavaScript primitives, for example when mixing JavaScript computations with symbolic computations from the Compute Engine.
If the expression is a machine number, a bignum, or a rational
that can be converted to a machine number, return a JavaScript number.
This conversion may result in a loss of precision.
If the expression is the symbol "True" or the symbol "False",
return true or false, respectively.
If the expression is a symbol with a numeric value, return the numeric value of the symbol.
If the expression is a string literal, return the string value.
If the expression is a tensor (list of number or multidimensional array or matrix), return an array of numbers, or an array of arrays of numbers, or an array of arrays of arrays of numbers.
If the expression is a function expression return a string representation of the expression.
Expression.[toPrimitive]()
toPrimitive: string | number | null
Similar toexpr.valueOf() but includes a hint.
####### hint
"string" | "number" | "default"
Expression.toString()
toString(): string
Return an ASCIIMath representation of the expression. This string is suitable to be output to the console for debugging, for example.
Based on Object.toString().
To get a LaTeX representation of the expression, use expr.latex.
Note that lazy collections are eagerly evaluated.
Used when coercing a Expression to a String.
For arbitrary-precision numbers (BigNumericValue), the output is
rounded to BigDecimal.precision significant digits. Digits beyond the
working precision are noise from precision-bounded operations (division,
transcendentals) and are not displayed. Machine-precision numbers use
their native Number.toString().
Expression.toJSON()
toJSON(): MathJsonExpression
Used by JSON.stringify() to serialize this object to JSON.
Method version of expr.json.
Based on Object.toJSON().
Note that lazy collections are not eagerly evaluated.
The output preserves the full raw BigDecimal value with no rounding,
ensuring lossless round-tripping via ce.expr(expr.json). Digits beyond
ce.precision may be present but are not guaranteed to be accurate.
Use toMathJson({ fractionalDigits: 'auto' }) for precision-rounded
MathJSON output.
Expression.is()
is(other, tolerance?): boolean
Smart equality check: structural first, then numeric evaluation fallback.
Symmetric: a.is(b) always equals b.is(a).
First tries an exact structural check (same as isSame()). If that fails
and the expression is constant (no free variables), evaluates numerically
and compares within engine.tolerance.
For literal numbers compared to primitives (number, bigint), behaves
identically to isSame() — no tolerance is applied. Tolerance only
applies to expressions that require evaluation (e.g., \\sin(\\pi)).
ce.parse('\\cos(\\frac{\\pi}{2})').is(0) // true — evaluates, within tolerance
ce.number(1e-17).is(0) // false — literal, no tolerance
ce.parse('x + 1').is(1) // false — has free variables
ce.parse('\\pi').is(3.14, 0.01) // true — within custom tolerance
After the structural check, attempts to expand both sides (distributing
products, applying the multinomial theorem, etc.) and re-checks
structural equality. This catches equivalences like (x+1)^2 vs
x^2+2x+1 even when the expression has free variables.
####### other
string | number | bigint | boolean | Expression
####### tolerance?
number
If provided, overrides engine.tolerance for the
numeric comparison. Has no effect when the comparison is structural
(i.e., when isSame() succeeds or the expression has free variables).
Relational Operator
Expression.isSame()
isSame(rhs): boolean
Fast exact structural/symbolic equality check.
Returns true if the expression is structurally identical to rhs.
For symbols with value bindings, follows the binding (e.g., if one = 1,
then ce.symbol('one').isSame(1) is true).
Accepts JavaScript primitives: number, bigint, boolean, string.
Does not evaluate expressions — purely structural.
ce.parse('1+x', {form: 'raw'}).isSame(ce.parse('x+1', {form: 'raw'})) is false.
See expr.is() for a smart check with numeric evaluation fallback,
expr.isEqual() for value equality, and expr.isIdenticallyEqual() to
prove an identity in the free variables.
Applicable to canonical and non-canonical expressions.
####### rhs
string | number | bigint | boolean | Expression
Expression.isLess()
isLess(other): boolean | undefined
The value of both expressions are compared.
If the expressions cannot be compared, return undefined
####### other
number | Expression
Expression.isLessEqual()
isLessEqual(other): boolean | undefined
The value of both expressions are compared.
If the expressions cannot be compared, return undefined
####### other
number | Expression
Expression.isGreater()
isGreater(other): boolean | undefined
The value of both expressions are compared.
If the expressions cannot be compared, return undefined
####### other
number | Expression
Expression.isGreaterEqual()
isGreaterEqual(other): boolean | undefined
The value of both expressions are compared.
If the expressions cannot be compared, return undefined
####### other
number | Expression
Expression.isEqual()
isEqual(other): boolean | undefined
Mathematical equality (strong equality), that is the value
of this expression and the value of other are numerically equal.
An expression without free variables is evaluated numerically, and the
two values are compared. Numbers whose difference is less than
engine.tolerance are considered equal. This tolerance is set when the
engine.precision is changed to be such that the last two digits are
ignored.
Evaluating the expressions may be expensive. Other options to consider to compare two expressions include:
expr.isSame(other)for a fast exact structural comparison (no evaluation)expr.is(other)for a smart check that tries structural first, then numeric evaluation fallback for constant expressionsexpr.isIdenticallyEqual(other)to prove an identity in the free variables, such as(x+1)^2vsx^2+2x+1
Examples
let expr = ce.parse('2 + 2');
console.log(expr.isEqual(4)); // true
console.log(expr.isSame(4)); // false (structural only)
console.log(expr.is(4)); // true (evaluates, within tolerance)
expr = ce.parse('4');
console.log(expr.isEqual(4)); // true
console.log(expr.isSame(4)); // true
console.log(expr.is(4)); // true
Free variables — "truth under constraints" semantics. When either
expression has free variables, equality means "could these be equal
under the current (and possible) constraints?". The result is true
when the two canonical forms are structurally the same (x+1 vs 1+x),
and a fact in the assumptions database (ce.assume(...)) can decide it
either way: after ce.assume(ce.parse('x = 4')), x+1 vs 5 is
true and x vs 3 is false. Anything else is undefined, never a
definitive false — including x vs 2, which an assumption could
make true.
No identity proof is attempted: (x+1)^2 vs x^2+2x+1, and even
x+x vs 2x, are undefined. Use expr.isIdenticallyEqual() to prove
that two expressions are equal for every value of their free variables.
####### other
number | Expression
Expression.isIdenticallyEqual()
isIdenticallyEqual(other): boolean | undefined
Identity of this expression and other in all their free variables,
that is sin(x)^2 + cos(x)^2 ≡ 1. This is the deepest — and most
expensive — of the three equality tiers:
| Method | Semantics |
|---|---|
expr.isSame(other) | Structural: syntactic equality of the canonical forms. Always decidable, no evaluation. |
expr.isEqual(other) | Arithmetic: the values are equal (within engine.tolerance). |
expr.isIdenticallyEqual(other) | Identity: the two expressions are equal for every value of their free variables. |
Three-valued: true when identity could be established, false when the
expressions are provably different, and undefined when neither could be
determined. In particular, expressions that merely disagree at sampled
points (x+1 vs x+2) are undefined, not false: an assumption could
still constrain them equal.
Identity is established by stochastic sampling (evaluating both
expressions at random points), falling back to a symbolic
expand-and-simplify proof. A true obtained from sampling alone is
therefore a very strong indication, but not a formal proof
(Richardson's theorem
makes a complete decision procedure impossible).
This is the API counterpart of the IdenticallyEqual operator (\equiv
in LaTeX).
####### other
number | Expression
Tensor Expression
Expression.shape
readonly shape: number[];
The shape describes the axes of the expression, where each axis represent a way to index the elements of the expression.
When the expression is a scalar (number), the shape is [].
When the expression is a vector of length n, the shape is [n].
When the expression is a n by m matrix, the shape is [n, m].
Expression.rank
readonly rank: number;
The rank refers to the number of dimensions (or axes) of the expression.
Return 0 for a scalar, 1 for a vector, 2 for a matrix, > 2 for a multidimensional matrix.
The rank is equivalent to the length of expr.shape
There are several definitions of rank in the literature. For example, the row rank of a matrix is the number of linearly independent rows. The rank can also refer to the number of non-zero singular values of a matrix.
Type Properties
Expression.type
get type(): BoxedType
set type(type:
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType): void
The type of the value of this expression.
If a symbol the type of the value of the symbol.
If a function expression, the type of the value of the function (the result type).
If a symbol with a "function" type (a function literal), returns the
signature.
If not valid, return "error".
If the type is not known, return "unknown".
Expression.isNumber
readonly isNumber: boolean | undefined;
true if the value of this expression is a number.
Note that in a fateful twist of cosmic irony, NaN ("Not a Number")
is a number.
If isNumber is true, this indicates that evaluating the expression
will return a number.
This does not indicate that the expression is a number literal. To check
if the expression is a number literal, use expr.isNumberLiteral.
For example, the expression ["Add", 1, "x"] is a number if "x" is a
number and expr.isNumber is true, but isNumberLiteral is false.
Expression.isInteger
readonly isInteger: boolean | undefined;
The value of this expression is an element of the set ℤ: ...,-2, -1, 0, 1, 2...
Note that ±∞ and NaN are not integers.
Expression.isRational
readonly isRational: boolean | undefined;
The value of this expression is an element of the set ℚ, p/q with p ∈ ℕ, q ∈ ℤ ⃰ q >= 1
Note that every integer is also a rational.
This is equivalent to this.type === "rational" || this.type === "integer"
Note that ±∞ and NaN are not rationals.
Expression.isExtendedReal
readonly isExtendedReal: boolean | undefined;
The value of this expression is on the extended real line: a finite
real number, or one of the two signed infinities +∞ and -∞.
The unsigned complex infinity ~∞ is not on the extended real line,
and neither is NaN; both answer false. A number with a non-zero
imaginary part answers false.
Use this predicate for a gate that must also hold at ±∞ — sign
reasoning, the 1/±∞ = 0 fold, a claim that a result is a signed
infinity. For a finite real, test this.type.matches("real")
instead: the bare type name real denotes the finite reals.
Expression.isFunction
readonly isFunction: boolean | undefined;
The value of this expression is a function.
This is equivalent to this.type.matches("function"), that is, the
expression evaluates to a function (for example a symbol bound to a
function literal, or a function literal itself).
This is distinct from isFunctionExpression, which is a structural
property (true when the expression is a function application node such
as ["Add", 1, 2]).
Value Properties
Expression.constantValue
readonly constantValue: string | number | boolean | object | undefined;
If this expression is constant (see isConstant), return its value,
otherwise undefined.
DictionaryInterface
Interface for dictionary-like structures.
Use isDictionary() to check if an expression is a dictionary.
DictionaryInterface.keys
DictionaryInterface.entries
DictionaryInterface.values
SemiBoxedExpression
type SemiBoxedExpression = ExpressionInput;
Deprecated
Use ExpressionInput instead.
Serialization
NumberFormat
type NumberFormat = {
positiveInfinity: LatexString;
negativeInfinity: LatexString;
notANumber: LatexString;
imaginaryUnit: LatexString;
decimalSeparator: LatexString;
digitGroupSeparator: | LatexString
| [LatexString, LatexString];
digitGroup: "lakh" | number | [number | "lakh", number];
exponentProduct: LatexString;
beginExponentMarker: LatexString;
endExponentMarker: LatexString;
truncationMarker: LatexString;
repeatingDecimal: "auto" | "vinculum" | "dots" | "parentheses" | "arc" | "none";
};
These options control how numbers are parsed and serialized.
NumberSerializationFormat
type NumberSerializationFormat = NumberFormat & {
digits: DisplayDigits;
fractionalDigits: "auto" | "max" | number;
notation: "auto" | "engineering" | "scientific" | "adaptiveScientific";
avoidExponentsInRange: undefined | null | [number, number];
};
NumberSerializationFormat.digits?
optional digits?: DisplayDigits;
Controls how many digits a number is displayed with. See DisplayDigits.
When serializing via .toLatex({ digits }), rounding is applied at the
MathJSON (kernel) layer; the LaTeX layer only lays out the already-rounded
digits.
NumberSerializationFormat.fractionalDigits
fractionalDigits: "auto" | "max" | number;
The maximum number of significant digits in serialized numbers.
"max": all availabe digits are serialized."auto": use the same precision as the compute engine.
Default: "auto"
Deprecated
Use digits instead.
DisplayDigits
type DisplayDigits =
| "auto"
| "max"
| {
significant: number;
}
| {
fractional: number;
};
Controls how many digits a number is displayed with when serialized.
This is a display/formatting concern only: it does not change the stored value, nor the precision used for computation.
"auto": round to the engine's working precision (ce.precision). This is the default used by the.latexgetter."max": display all stored digits, with no rounding. This is the default used by.json/toMathJson().{ significant: n }: round inexact values tonsignificant figures. Exact values (integers, rationals, radicals) are displayed in full — this is a no-op on them. Truncation only: trailing zeros are not padded (2.0stays2).{ fractional: n }: displayndigits after the decimal point, usingtoFixedsemantics (may pad with trailing zeros, e.g.2→2.00).
Rounding is orthogonal to notation: it never switches a number to
scientific/exponential notation as a side effect. Fixed-vs-scientific is
controlled by the notation / avoidExponentsInRange options.
JsonSerializationOptions
type JsonSerializationOptions = {
prettify: boolean;
inferredAnnotations: boolean;
exclude: string[];
shorthands: ("all" | "number" | "symbol" | "function" | "string" | "dictionary")[];
metadata: ("all" | "wikidata" | "latex" | "sourceOffsets")[];
repeatingDecimal: boolean;
digits: DisplayDigits;
fractionalDigits: "auto" | "max" | number;
};
Options to control serialization to MathJSON when using
Expression.toMathJson().
Tensors
DataTypeMap
type DataTypeMap = {
float64: number;
float32: number;
int32: number;
uint8: number;
complex128: Complex;
complex64: Complex;
bool: boolean;
expression: Expression;
};
Map of TensorDataType to JavaScript type.
TensorData
A record representing the type, shape and data of a tensor.
Extended by
TensorData.dtype
dtype: DT;
TensorData.shape
shape: number[];
TensorData.rank?
optional rank?: number;
TensorData.data
data: DataTypeMap[DT][];
TensorField
TensorField.one
readonly one: T;
TensorField.zero
readonly zero: T;
TensorField.nan
readonly nan: T;
TensorField.cast()
cast(x, dtype)
cast(x, dtype): number | undefined
####### x
T
####### dtype
"float64"
cast(x, dtype)
cast(x, dtype): number | undefined
####### x
T
####### dtype
"float32"
cast(x, dtype)
cast(x, dtype): number | undefined
####### x
T
####### dtype
"int32"
cast(x, dtype)
cast(x, dtype): number | undefined
####### x
T
####### dtype
"uint8"
cast(x, dtype)
cast(x, dtype): Complex | undefined
####### x
T
####### dtype
"complex128"
cast(x, dtype)
cast(x, dtype): Complex | undefined
####### x
T
####### dtype
"complex64"
cast(x, dtype)
cast(x, dtype): boolean | undefined
####### x
T
####### dtype
"bool"
cast(x, dtype)
cast(x, dtype): Expression | undefined
####### x
T
####### dtype
"expression"
cast(x, dtype)
cast(x, dtype): number[] | undefined
####### x
T[]
####### dtype
"float64"
cast(x, dtype)
cast(x, dtype): number[] | undefined
####### x
T[]
####### dtype
"float32"
cast(x, dtype)
cast(x, dtype): number[] | undefined
####### x
T[]
####### dtype
"int32"
cast(x, dtype)
cast(x, dtype): number[] | undefined
####### x
T[]
####### dtype
"uint8"
cast(x, dtype)
cast(x, dtype): Complex[] | undefined
####### x
T[]
####### dtype
"complex128"
cast(x, dtype)
cast(x, dtype): Complex[] | undefined
####### x
T[]
####### dtype
"complex64"
cast(x, dtype)
cast(x, dtype): boolean[] | undefined
####### x
T[]
####### dtype
"bool"
cast(x, dtype)
cast(x, dtype): Expression[] | undefined
####### x
T[]
####### dtype
"expression"
cast(x, dtype)
cast(x, dtype):
| number
| boolean
| number[]
| Expression
| Complex
| Expression[]
| Complex[]
| boolean[]
| undefined
####### x
T | T[]
####### dtype
keyof DataTypeMap
Tensor
Extends
TensorData<DT>
Tensor.dtype
dtype: DT;
Tensor.shape
shape: number[];
Tensor.rank
rank: number;
Tensor.data
data: DataTypeMap[DT][];
Tensor.field
readonly field: TensorField<DataTypeMap[DT]>;
Tensor.expression
readonly expression: Expression;
Tensor.array
readonly array: NestedArray<DataTypeMap[DT]>;
Tensor.isSquare
readonly isSquare: boolean;
Tensor.isSymmetric
readonly isSymmetric: boolean;
Tensor.isSkewSymmetric
readonly isSkewSymmetric: boolean;
Tensor.isDiagonal
readonly isDiagonal: boolean;
Tensor.isUpperTriangular
readonly isUpperTriangular: boolean;
Tensor.isLowerTriangular
readonly isLowerTriangular: boolean;
Tensor.isTriangular
readonly isTriangular: boolean;
Tensor.isIdentity
readonly isIdentity: boolean;
Tensor.isZero
readonly isZero: boolean;
Tensor.diagonal()
diagonal(axis1?, axis2?): DataTypeMap[DT][] | undefined
####### axis1?
number
####### axis2?
number
Tensor.trace()
trace(axis1?, axis2?):
| Tensor<DT>
| DataTypeMap[DT]
| undefined
####### axis1?
number
####### axis2?
number
Tensor.flatten()
flatten(): DataTypeMap[DT][]
Tensor.transpose()
transpose(axis1?, axis2?): Tensor<DT> | undefined
####### axis1?
number
####### axis2?
number
Tensor.conjugateTranspose()
conjugateTranspose(axis1?, axis2?): Tensor<DT> | undefined
####### axis1?
number
####### axis2?
number
Tensor.determinant()
determinant(): DataTypeMap[DT] | undefined
Tensor.inverse()
inverse(): Tensor<DT> | undefined
Tensor.pseudoInverse()
pseudoInverse(): Tensor<DT> | undefined
Tensor.adjugateMatrix()
adjugateMatrix(): Tensor<DT> | undefined
Tensor.minor()
minor(axis1, axis2): DataTypeMap[DT] | undefined
####### axis1
number
####### axis2
number
Tensor.map1()
map1(fn, scalar): Tensor<DT>
####### fn
(lhs, rhs) => DataTypeMap[DT]
####### scalar
DataTypeMap[DT]
Type
BoxedType
new BoxedType()
new BoxedType(type, typeResolver?): BoxedType
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
####### typeResolver?
BoxedType.unknown
static unknown: BoxedType;
BoxedType.number
static number: BoxedType;
BoxedType.signed_infinity
static signed_infinity: BoxedType;
BoxedType.infinity
static infinity: BoxedType;
BoxedType.nan
static nan: BoxedType;
BoxedType.complex
static complex: BoxedType;
BoxedType.real
static real: BoxedType;
BoxedType.integer
static integer: BoxedType;
BoxedType.string
static string: BoxedType;
BoxedType.character
static character: BoxedType;
BoxedType.dictionary
static dictionary: BoxedType;
BoxedType.setNumber
static setNumber: BoxedType;
BoxedType.setComplex
static setComplex: BoxedType;
BoxedType.setImaginary
static setImaginary: BoxedType;
BoxedType.setReal
static setReal: BoxedType;
BoxedType.setRational
static setRational: BoxedType;
BoxedType.setInteger
static setInteger: BoxedType;
BoxedType.type
readonly type: Type;
BoxedType.isPolymorphic
readonly isPolymorphic: boolean;
True when this type is a polytype: a signature carrying a where
clause, or an overload set with at least one such arm.
Computed ONCE, here, at construction: every per-call dispatch check (argument validation, result typing) reads this boolean and is O(1) — it must never become a tree walk. Polytypes are legal only as signatures, so the computation itself is a shallow field test.
BoxedType.facts
Lazily shared facts of this type, independent of any expression.
BoxedType.typeResolver
The resolver this type was created with, so a DERIVED boxed type (a projection of this one) can be built without losing the ability to name a user-declared type.
BoxedType.unionMembers
The members of a union type, each boxed, or [this] for any other type.
Lets a consumer reason arm-by-arm without reading the raw Type AST.
Note that a union may be nested inside a parameter (list<A | B>), which
this does not reach — couldMatch() handles that case directly and is
usually what an arm walk was reaching for.
BoxedType.effects
The latent effects on this type's arrow: what fires if a value of this
type is invoked. undefined when the type is not callable, or when its
arrow states nothing (the inferred track); [] when it states pure;
'any' for "unknown effects"; otherwise the labels, alphabetically
sorted.
This is how an operator asks "what happens if I call this operand?" —
op.type.effects, which resolves through symbol bindings because .type
does. It is the invoking half of the effects model; the producing
half — what evaluating an expression does — is expr.effects.
For an overload set (an intersection of signatures) the answer is the union of the arms': an overload with one effect-bearing arm is not pure.
ce.type('(real) random -> real').effects; // ➔ ['random']
ce.type('(real) pure -> real').effects; // ➔ []
ce.type('(real) -> real').effects; // ➔ undefined
ce.type('number').effects; // ➔ undefined
BoxedType.isUnknown
BoxedType.from()
static from(type, resolver?): BoxedType
Box an ordinary type, sharing immutable type values within a resolver.
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
####### resolver?
BoxedType.forResult()
forResult(type, resolver)
static forResult(type, resolver?): undefined
A type-handler result: widen numeric literal cargo to its stored tier, retaining intentional ranges and resolver context. Undefined declines. Normalization is shared only for immutable input types.
####### type
undefined
####### resolver?
forResult(type, resolver)
static forResult(type, resolver?): BoxedType
A type-handler result: widen numeric literal cargo to its stored tier, retaining intentional ranges and resolver context. Undefined declines. Normalization is shared only for immutable input types.
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
####### resolver?
forResult(type, resolver)
static forResult(type, resolver?): BoxedType | undefined
A type-handler result: widen numeric literal cargo to its stored tier, retaining intentional ranges and resolver context. Undefined declines. Normalization is shared only for immutable input types.
####### type
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
| undefined
####### resolver?
BoxedType.matches()
matches(other): boolean
True when every value of this type is an other.
A polymorphic PATTERN is a consistent existential (D12): the pattern's
variables are solved against the subject and the match holds iff a
consistent instantiation exists — so
ce.type('(number) -> number').matches('(T) -> T where T') is true,
the probe users actually mean. couldMatch deliberately answers false
on the same row (D6's bound-reading, contravariant any); the two
predicates diverge by design.
A polymorphic SUBJECT is the isSubtype story: rule 1 against a ground
pattern (instantiate-and-check), rule 3 (α-equivalence) against a
polymorphic one.
####### other
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
BoxedType.is()
is(other): boolean
####### other
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
BoxedType.isDisjointFrom()
isDisjointFrom(other): boolean
True when no value can inhabit both this type and other.
Use this — not !matches() — to decide whether two types are unrelated.
matches() answers "is this a subtype of other", so two types that
share values without either containing the other (integer | string vs
integer | boolean) fail matches() in both directions.
Conservative in the safe direction: when disjointness cannot be
established the answer is false ("they may overlap"), never a false
claim of disjointness. So !a.isDisjointFrom(b) reads as possible
overlap, and unknown overlaps everything.
Throws if other is a string that is not a valid type.
####### other
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
BoxedType.couldMatch()
couldMatch(other): boolean
True when some value inhabits both this type and other — "could a
value of this type be an other?".
This is the predicate for classifying a value by shape ("might this be a
point, a point list, a matrix"). Prefer it to matches(), which answers
"is EVERY value of this type an other" and so reports false for a
union whose members include exactly the shape asked about:
const t = ce.type('tuple<number, number> | list<tuple<number, number>>');
t.matches('list<tuple<number, number>>'); // false
t.couldMatch('list<tuple<number, number>>'); // true
Unions are distributed at every depth, so a union nested inside a
parameter is handled too — list<integer | tuple<number, number>> could
be a list<tuple<number, number>>, witness [(1,2)].
Symmetric, and decisive for the composite shapes it models: a
tuple<number, number> could not be a list<tuple<number, number>>, and
list<integer> could not be a list<string>. Shapes it does not model
fall back to assignability in either direction, so the answer is never
narrower than matches() — with one deliberate exception: never is
uninhabited, so nothing could be a never.
unknown could be anything. Consumers that treat an inconclusive type as
"no" must check isUnknown themselves.
Throws if other is a string that is not a valid type.
####### other
| string
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference
| BoxedType
BoxedType.toString()
toString(): string
BoxedType.toJSON()
toJSON(): string
BoxedType.valueOf()
valueOf(): string
MathJSON
MathJsonAttributes
type MathJsonAttributes = {
comment: string;
documentation: string;
latex: string;
wikidata: string;
wikibase: string;
openmathSymbol: string;
openmathCd: string;
sourceUrl: string;
sourceContent: string;
sourceOffsets: [number, number];
};
The following properties can be added to any MathJSON expression to provide additional information about the expression.
MathJsonSymbol
type MathJsonSymbol = string;
MathJsonNumberObject
type MathJsonNumberObject = {
num: "NaN" | "-Infinity" | "+Infinity" | string;
} & MathJsonAttributes;
A MathJSON numeric quantity.
The num string is made of:
- an optional
-minus sign - a string of decimal digits
- an optional fraction part (a
.decimal marker followed by decimal digits) - an optional repeating decimal pattern: a string of digits enclosed in parentheses
- an optional exponent part (a
eorEexponent marker followed by an optional-minus sign, followed by a string of digits)
It can also consist of the string NaN, -Infinity or +Infinity to
represent these respective values.
A MathJSON number may contain more digits or an exponent with a greater range than can be represented in an IEEE 64-bit floating-point.
For example:
-12.340.234e-561.(3)123456789123456789.123(4567)e999
MathJsonSymbolObject
type MathJsonSymbolObject = {
sym: MathJsonSymbol;
} & MathJsonAttributes;
MathJsonStringObject
type MathJsonStringObject = {
str: string;
} & MathJsonAttributes;
MathJsonFunctionObject
type MathJsonFunctionObject = {
fn: [MathJsonSymbol, ...MathJsonExpression[]];
} & MathJsonAttributes;
DictionaryValue
type DictionaryValue =
| boolean
| number
| string
| ExpressionObject
| ReadonlyArray<DictionaryValue>;
MathJsonDictionaryObject
type MathJsonDictionaryObject = {
dict: Record<string, DictionaryValue>;
} & MathJsonAttributes;
ExpressionObject
type ExpressionObject =
| MathJsonNumberObject
| MathJsonStringObject
| MathJsonSymbolObject
| MathJsonFunctionObject
| MathJsonDictionaryObject;
MathJsonExpression
type MathJsonExpression =
| ExpressionObject
| number
| MathJsonSymbol
| string
| readonly [MathJsonSymbol, ...MathJsonExpression[]];
A MathJSON expression is a recursive data structure.
The leaf nodes of an expression are numbers, strings and symbols. The dictionary and function nodes can contain expressions themselves.
Type
PrimitiveType
type PrimitiveType =
| NumericPrimitiveType
| "collection"
| "indexed_collection"
| "list"
| "range"
| "set"
| "dictionary"
| "record"
| "object"
| "tuple"
| "value"
| "scalar"
| "function"
| "symbol"
| "boolean"
| "string"
| "character"
| "regexp"
| "color"
| "type"
| "expression"
| "unknown"
| "error"
| "nothing"
| "missing"
| "never"
| "any";
A primitive type is a simple type that represents a concrete value.
-
any: the top typeexpressionerror: an invalid value, such as["Error", "missing"]nothing: the type of theNothingsymbol, the unit typemissing: the type of theMissingsymbol, the unit type of an absent-but-positioned value (Juliamissing, RNA)never: the bottom typeunknown: a value whose type is not known
-
expression:- a symbolic expression, such as
["Add", "x", 1] <value>symbol: a symbol, such asx.function: a function literal such as["Function", ["Add", "x", 1], "x"].
- a symbolic expression, such as
-
valuescalar<number>boolean: a boolean value:TrueorFalse.character: exactly one user-perceived character (grapheme cluster).
collectionset: a collection of unique expressions, e.g.set<string>.record: a collection of specific key-value pairs, e.g.record{x: number, y: boolean}.dictionary: a collection of arbitrary key-value pairs e.g.dictionary<string, number>.indexed_collection: collections whose elements can be accessed by a numeric indexlist: a collection of expressions, possibly recursive, with optional dimensions, e.g.[number],[boolean^32],[number^(2x3)]. Used to represent a vector, a matrix or a tensor when the type of its elements is a numbertuple: a fixed-size collection of named or unnamed elements, e.g.tuple<number, boolean>,tuple<x: number, y: boolean>.string: a string of characters, i.e. an indexed collection ofcharacter. A sibling oflist<character>, not a subtype.
NumericPrimitiveType
type NumericPrimitiveType =
| "number"
| "complex"
| "imaginary"
| "real"
| "rational"
| "integer"
| "infinity"
| "nan";
The numeric tree is FINITE BY DEFAULT and DISJOINT: every numeric VALUE is a
finite number, a number of infinite magnitude, or the not-a-number marker,
and no value is two of those — number = complex ⊔ infinity ⊔ nan as a
partition of the values. Every bare name below complex contains only
finite values. A bare real result type is therefore a promise of
finiteness, and the extended real line is spelled
real | signed_infinity — signed_infinity being the named union of
the two signed-infinity value types (equivalently real | +oo | -oo),
so it excludes the unsigned ~∞ that infinity would bring in. That
spelling is shared as the frozen EXTENDED_REAL_TYPE constant in
common/type/primitive.ts; use it rather than rebuilding the union.
The partition is a statement about values, NOT one the SUBTYPE RELATION
closes over. isSubtype('complex | infinity | nan', 'number') is true, but
the converse isSubtype('number', 'complex | infinity | nan') is FALSE: a
union is a supertype only of types below one of its members, and number is
above all three rather than inside any one of them. Deciding the converse
needs covering-union machinery that the type checker does not have. So do
not use a three-way union as a stand-in for number in a signature, and do
not read the ⊔ above as a subtyping identity.
number: any numeric value — a finite number, a number of infinite magnitude, or the not-a-number marker.complex: a FINITE complex number =imaginary+real.imaginary: a finite complex number with a real part of 0 (pure imaginary).real: a finite real number (imaginary part 0) =rationalplus the finite irrationals.rational: a finite rational number (includes the integers).integer: a finite whole number.infinity: a number of infinite magnitude, of any direction — the signed+∞and−∞, the unsigned complex infinity~∞, and mixed directed values such as∞ + i. Disjoint fromcomplex: an infinity is not a finite number.nan: the not-a-number marker. Its only supertype isnumber, so it is disjoint fromcomplex,infinityand every type below them.
The SIGNED pair +∞/−∞ has no one-word name: spell it +oo | -oo
(the union of the two value types) where the sign-aware guarantee
matters — the 1/±∞ = 0 folds, the sign-reading gates — and infinity
where any infinite value is acceptable. The former one-word name
non_finite_number was retired 2026-08-31 (ruling L5 executed): the
name was misleading — ~∞ and ∞ + i are non-finite numbers, yet
neither was a member.
RETIRED SPELLINGS. The five names that prefixed a tier with finite_ are
no longer members of this union. Each denoted exactly the same set of values
as one of the bare names above, because every bare name under number is
finite: the four per-tier spellings each meant their own tier, and the
widest of them meant complex ("any finite number" IS the finite complex
type). The type PARSER still accepts all five as input aliases for one
release cycle and normalizes each to the name it denotes
(RETIRED_NUMERIC_ALIASES in parser.ts), but an alias never reaches a
Type node and is never serialized back out.
NamedElement
type NamedElement = {
name: string;
type: Type;
};
EffectLabel
type EffectLabel =
| "console"
| "entropy"
| "environment"
| "fs_read"
| "fs_write"
| "network"
| "random"
| "scope"
| "state"
| "time";
An effect label: a member of a closed, engine-versioned enumeration.
Each label carries fixed metadata (impurity, observation vs action, frame
kind, handler-backed); consumers key on that metadata, never on the label
name. See docs/EFFECTS-MODEL.md.
The labels bear no implication relations to each other: the order on effect
sets is plain powerset inclusion, so the singletons are pairwise
incomparable (in particular fs_write does not imply fs_read).
EffectSet
type EffectSet = "any" | EffectLabel[];
The effect set carried by a signature's arrow.
'any'is the distinguished top: "unknown effects". Under union it absorbs, and no finite bound admits it.- Otherwise a duplicate-free, alphabetically sorted list of labels, possibly empty.
An absent (undefined) effects field and [] denote the same set, ∅:
every semantic operation — subtyping, pure, the label predicates, union,
matches() — treats them identically. They differ only in serialization
(ruled 2026-08-01): absent is an empty specifier slot (effects were never
stated, and stay on the inferred track), while [] is the author's pure
and serializes back as pure, so an explicit purity contract survives a
parse → serialize → re-declare round trip.
Build one with normalizeEffectSet() (inference: an empty result collapses
to undefined) or normalizeStatedEffectSet() (a stated set: an empty
result stays []).
TypeVariable
type TypeVariable = {
kind: "variable";
name: string;
value: true;
};
A universally quantified type variable (rank-1).
Only legal inside a function signature; declared and scoped by its arm's
where clause (the typeParams field of FunctionSignature). A variable is
atomic and opaque: it is never reduced, distributed or collapsed, and it
is substituted away by instantiation at a call site.
TypeVariance
type TypeVariance = "in" | "out" | "inout";
How a parameterized NOMINAL type relates two of its applications
(docs/TYPE-SYSTEM.md).
Declared inside a type-parameter clause (type tree<out T> = …); the words
are contextual there and are never reserved. Only a nominal declaration
carries one — a transparent alias has no declaration-level variance, and a
where clause never does.
TypeParameter
type TypeParameter = {
name: string;
kind: "value";
bound: Type;
variance: TypeVariance;
protocols: string[];
};
One entry of a signature's where clause, or of a declared type's
type-parameter clause: the variable's name and its optional declared upper
bound.
The bound must be ground (no type variables) — validated when the
declared type is boxed. An unbounded variable's implicit bound is any.
TypeParamsOption
type TypeParamsOption =
| string
| ReadonlyArray<
| string
| {
name: string;
bound: Type | TypeString;
variance: TypeVariance;
kind: "value";
}>;
The typeParams option of a generic type declaration — an ALIAS
(ce.declareType('Pair', 'tuple<T, T>', { alias: true, typeParams: ['T'] }))
or a parameterized NOMINAL type
(ce.declareType('tree', '…', { typeParams: [{ name: 'T', variance: 'out' }] })).
Either clause TEXT ('T, U: number', also accepted one entry at a time) or
pre-built parameters whose bound may be a type string. Every TEXT spelling
goes through the shared clause parser (parseTypeParameterClause); the
object-array form is validated directly by normalizeDeclaredTypeParams
(same rules: reserved names, duplicates, ground bounds).
FunctionSignature
type FunctionSignature = {
kind: "signature";
args: NamedElement[];
optArgs: NamedElement[];
variadicArg: NamedElement;
variadicMin: 0 | 1;
effects: EffectSet;
typeParams: TypeParameter[];
result: Type;
};
AlgebraicType
type AlgebraicType = {
kind: "union" | "intersection";
types: Type[];
};
NegationType
type NegationType = {
kind: "negation";
type: Type;
};
ValueType
type ValueType = {
kind: "value";
value: any;
};
RecordType
type RecordType = {
kind: "record";
elements: Record<string, Type>;
};
A record is a collection of key-value pairs.
The keys are strings. The set of keys is fixed.
For a record type to be a subtype of another record type, it must contain every key required by the other type, and all their types must match (width subtyping). It may contain additional keys.
ObjectType
type ObjectType = {
kind: "object";
elements: Record<string, Type>;
};
The stored-field layout of an object type — the engine's one mutable value kind.
Structurally this looks like RecordType, and the two are read the same way (an ordered map from field name to field type), but they behave in opposite ways, and the difference is deliberate:
- An object type is nominal. This shape is only ever the definition
(
def) of a declared TypeReference:type Person = object{…}. Two object types with identical layouts are unrelated, because a store through one view would break the other's declared field types (write1.5into anobject{count: integer}viewed asobject{count: number}). The nominal reference is what supplies that opacity; this shape only carries the layout. - Every field is a read/write position, so a field type is invariant:
two object layouts relate only when every field type is mutually equal,
and a type variable occurring in a field verifies only as
inout.
The bare primitive 'object' means "any object" and is the one common
bound every declared object type is a subtype of. It sits BESIDE record
in the lattice and is disjoint from it — sibling categories, one
immutable/structural, one mutable/nominal — and is deliberately not a
collection.
Spec: docs/TYPE_SYSTEM_ROADMAP.md Appendix B, "Declaring an object type",
"No subtyping between object types", "Generic object types" (ruling B13),
and the lattice bullet of "The rest of the system" (ruling B6).
DictionaryType
type DictionaryType = {
kind: "dictionary";
values: Type;
};
A dictionary is a collection of key-value pairs.
The keys are strings. The set of keys is also not defined as part of the type and can be modified at runtime.
A dictionary is suitable for use as cache or data storage.
CollectionType
type CollectionType = {
kind: "collection" | "indexed_collection";
elements: Type;
};
CollectionType is a generic collection of elements of a certain type.
- Indexed collections: List, Tuple
- Non-indexed: Set, Record, Dictionary
ListType
type ListType = {
kind: "list";
elements: Type;
dimensions: number[];
dimensionVariables: readonly (string | undefined)[];
};
The elements of a list can be accessed by their one-based index.
All elements of a list have the same type, but it can be a broad type,
up to any.
The same element can be present in the list more than once.
A list can be multi-dimensional. For example, a list of integers with dimensions 2x3x4 is a 3D tensor with 2 layers, 3 rows and 4 columns.
SymbolType
type SymbolType = {
kind: "symbol";
name: string;
};
ExpressionType
type ExpressionType = {
kind: "expression";
operator: string;
};
NumericType
type NumericType = {
kind: "numeric";
type: NumericPrimitiveType;
lower: number;
upper: number;
lowerOpen: boolean;
upperOpen: boolean;
};
SetType
type SetType = {
kind: "set";
elements: Type;
};
Each element of a set is unique (is not present in the set more than once). The elements of a set are not indexed.
BroadcastableType
type BroadcastableType = {
kind: "broadcastable";
elements: Type;
};
A broadcastable<T> is either a T, or an indexed collection of T
applied element-wise (runtime broadcasting). It is the static type of an
arithmetic result whose operand's collection-ness is not statically visible.
A T (and any subtype of T) is a subtype of broadcastable<T>, and so is
any indexed collection whose elements are subtypes of T. It is not a
subtype of T (it may be a collection) nor of list<T> (it may be a
scalar). See subtype.ts for the full relation.
TupleType
type TupleType = {
kind: "tuple";
elements: NamedElement[];
};
The elements of a tuple are indexed and may be named or unnamed. If one element is named, all elements must be named.
TypeReference
type TypeReference = {
kind: "reference";
name: string;
alias: boolean;
def: Type | undefined;
typeParams: TypeParameter[];
args: Type[];
_varianceState: "deferred" | "verified";
_varianceBlockedOn: string[];
_sumOf: string;
_sumVariants: {
name: string;
typeParams: string[];
}[];
_declOrigin: DeclarationOrigin;
};
Nominal typing
DeclarationOrigin
type DeclarationOrigin = {
batch: number;
statementId: unknown;
firstRange: [number, number];
};
Which compilation unit and which declaring statement a registry record came
from — the runtime half of the redefinition discipline
(docs/TYPE-SYSTEM.md, "Mechanics").
A second declaration of a name with the SAME batch and a DIFFERENT
statementId is a within-unit redefinition and is refused; the same
statementId re-registering is the same statement declaring itself again
(one statement registers up to three times per batch — the static pre-pass
canonicalizes it, then the evaluation loop canonicalizes and evaluates it)
and is accepted.
statementId is an opaque IDENTITY token, compared with !== and never
inspected: the raw (uncanonicalized) name operand the Declare* handlers
thread from their canonical handler into their evaluate handler. It is typed
unknown so this engine-free module needs no expression type.
Type
type Type =
| PrimitiveType
| AlgebraicType
| NegationType
| CollectionType
| ListType
| SetType
| BroadcastableType
| RecordType
| ObjectType
| DictionaryType
| TupleType
| SymbolType
| ExpressionType
| NumericType
| NumericPrimitiveType
| FunctionSignature
| ValueType
| TypeVariable
| TypeReference;
TypeString
type TypeString = string;
The type of a boxed expression indicates the kind of expression it is and the value it represents.
The type is represented either by a primitive type (e.g. number, complex, collection, etc.), or a compound type (e.g. tuple, function signature, etc.).
Types are described using the following BNF grammar:
<type> ::= <union_type> | "(" <type> ")"
<union_type> ::= <intersection_type> (" | " <intersection_type>)*
<intersection_type> ::= <primary_type> (" & " <primary_type>)*
<primary_type> ::= <primitive>
| <tuple_type>
| <signature>
| <list_type>
| <set>
| <broadcastable>
| <collection>
| <type_reference>
(A reference to a user-declared type. The optional argument list applies a
GENERIC type alias (`Pair<integer>`); it is expanded eagerly into the
substituted alias body when the type is built, so an applied reference never
appears in a `Type`. The authoritative grammar lives with the parser in
`./parser.ts`.)
<type_reference> ::= ( "type" )? <identifier> ( "<" <type> ("," <type>)* ">" )?
<primitive> ::= "any" | "unknown" | <value-type> | <symbolic-type> | <numeric-type>
<numeric-type> ::= "number" | "complex" | "imaginary" | "real" | "rational" | "integer"
<value-type> ::= "value" | <numeric-type> | "collection" | "boolean" | "string"
<symbolic-type> ::= "expression" | "function" | "symbol"
<tuple_type> ::= "tuple<" (<name> <type> "," <named_tuple_elements>*) ">"
| "tuple<" (<type> "," <unnamed_tuple_elements>*) ">" |
| "tuple<" <tuple_elements> ">"
<tuple_elements> ::= <unnamed_tuple_elements> | <named_tuple_elements>
<unnamed_tuple_elements> ::= <type> ("," <type>)*
<named_tuple_elements> ::= <name> <type> ("," <name> <type>)*
<signature> ::= <arguments> (" " <effects>)? " -> " <type>
<effects> ::= "pure" | "any" | <effect-label> (" " <effect-label>)*
(`pure` is the STATED empty set: the same set as an empty slot, and the
spelling that round-trips through serialization. See {@link EffectSet}.)
<effect-label> ::= "console" | "entropy" | "environment" | "fs_read"
| "fs_write" | "network" | "random" | "scope" | "time"
<arguments> ::= "()"
| "(" <argument-list> ")"
<argument> ::= <type>
| <name> <type>
<rest_argument> ::= "..." <type>
| <name> "..." <type>
<optional_argument> ::= <argument> "?"
<optional_arguments> ::= <optional_argument> ("," <optional_argument>)*
<required_arguments> ::= <argument> ("," <argument>)*
<argument-list> ::= <required_arguments> ("," <rest_argument>)?
| <required_arguments> <optional_arguments>?
| <optional_arguments>?
| <rest_argument>
<list_type> ::= "list<" <type> <dimensions>? ">"
| "vector<" (<type> <dimensions>? | <dimensions>) ">"
| "matrix<" (<type> <dimensions>? | <dimensions>) ">"
| "tensor<" <type> ">"
Note: there is no `[type]` bracket shorthand; a list is always written with
one of the `list`/`vector`/`matrix`/`tensor` heads. The authoritative
grammar lives with the parser in `./parser.ts`.
<dimensions> ::= "^" <dimension>
| "^(" <dimension> ("x" <dimension>)* ")"
<dimension> ::= <positive-integer_literal> | <identifier>
An identifier in a length slot is a DIMENSION VARIABLE, declared by the
enclosing `where` clause (`(a: vector<real^N>, b: vector<real^N>) -> real
where N`) or by the type-parameter clause of the type being declared
(`type permutation<N> = list<integer^N>`). The leading-length spellings
`vector<3>` and `matrix<2x3>` take literals, or an `x`-joined group of
two or more dimensions (`matrix<MxN>`); a bare identifier there is an
element type (`vector<T>`).
(The `callback<…>` constructor of Design D was RETIRED by Design E
(`docs/TYPE-SYSTEM.md`): callback
slots are ordinary arrow types, admitted by COMPATIBILITY rather than
subtyping. The spelling now fails to parse, with a migration hint.)
<set> ::= "set<" <type> ">"
<broadcastable> ::= "broadcastable" ( "<" <type> ">" )?
<collection> ::= ( "collection" | "indexed_collection" ) ( "<" <type> ">" )?
<name> ::= <identifier> ":"
<identifier> ::= [a-zA-Z_][a-zA-Z0-9_]*
<positive-integer_literal> ::= [1-9][0-9]*
Examples of types strings:
"number"-- a simple type primitive"(number, boolean)"-- a tuple type"(x: number, y:boolean)"-- a named tuple/record type. Either all arguments are named, or none are"collection<any>"-- an arbitrary collection type, with no length or element type restrictions"collection<integer>"-- a collection type where all the elements are integers"collection<(number, boolean)>"-- a collection of tuples"collection<(value:number, seen:boolean)>"-- a collection of named tuples"vector<boolean^32>"-- a list type with a fixed size of 32 elements"matrix<integer^(2x3)>"-- an integer matrix of 2 columns and 3 rows"list<integer^(2x3x4)>"-- a tensor of dimensions 2x3x4"number -> number"-- a signature with a single argument"(x: number, number) -> number"-- a signature with a named argument"(number, y:number?) -> number"-- a signature with an optional named argument (can have several optional arguments, at the end)"(number, number+) -> number"-- a signature with a rest argument (can have only one, and no optional arguments if there is a rest argument)."() -> number"-- a signature with an empty argument list"(number) random -> number"-- a signature that may draw from the seeded random stream"(number) random scope -> number"-- a signature with two effect labels"(number) any -> number"-- a signature with unknown effects"number | boolean"-- a union type"(x: number) & (y: number)"-- an intersection type"number | ((x: number) & (y: number))"-- a union type with an intersection type"(number -> number) | number"-- a union type with a signature and a primitive type
TypeCompatibility
type TypeCompatibility = "covariant" | "contravariant" | "bivariant" | "invariant";
TypeResolver
type TypeResolver = {
get names: string[];
forward: (name) => TypeReference | undefined;
resolve: (name) => TypeReference | undefined;
conformsTo: (type, protocol) => boolean;
};
A type resolver should return a definition for a given type name.
COMPLEX_INFINITY_VALUE
const COMPLEX_INFINITY_VALUE: Readonly<{
complexInfinity: true;
}>;
The value carried by the type of the unsigned complex infinity ~oo, which
has no JavaScript number to stand for it: Infinity and -Infinity are the
signed pair, and NaN is a different value altogether.
A value-literal type holds an arbitrary runtime value (see ValueType), so this frozen tagged object is that value. Test for it with
isComplexInfinityValue, which reads the TAG: a Type node can be
rebuilt or re-frozen on its way through the parser and the reducers, so
object identity is not a reliable test.
INDETERMINATE_VALUE
const INDETERMINATE_VALUE: Readonly<{
indeterminate: true;
}>;
The value carried by the type of the Indeterminate literal: the exact
answer to an indeterminate form (such as 0/0), a number with no value.
Its double value is NaN, like the IEEE NaN literal, but the two are
DIFFERENT values: NaN is the result of a floating-point failure and
Indeterminate the result of an exact computation. A JavaScript NaN
cannot tell them apart, so this frozen tagged object is the value of the
Indeterminate value type. It widens to nan, like the NaN value
type. Test for it with isIndeterminateValue, which reads the
TAG, for the reason given for COMPLEX_INFINITY_VALUE.
Provenance: docs/plans/2026-09-28-indeterminate-value.md.
isComplexInfinityValue()
function isComplexInfinityValue(v): v is Readonly<{ complexInfinity: true }>
True if v is the COMPLEX_INFINITY_VALUE sentinel, i.e. the
value of the ~oo value-literal type. Reads the tag, never the identity.
v
unknown
isIndeterminateValue()
function isIndeterminateValue(v): v is Readonly<{ indeterminate: true }>
True if v is the INDETERMINATE_VALUE sentinel, i.e. the
value of the Indeterminate value-literal type. Reads the tag, never the
identity.
v
unknown