This guide covers the breaking changes introduced in the latest architecture
revision of @cortex-js/compute-engine. Each section shows the old API, the new
API, and a brief rationale.
The old API used a confusing mix of canonical (boolean or array) and
structural (boolean) options. These have been unified into a single form
option.
// Full canonicalization (default)
ce.box(['Add', 1, 'x']);
ce.box(['Add', 1, 'x'], { canonical: true });
// No canonicalization, no binding
ce.box(['Add', 1, 'x'], { canonical: false });
// Structural: bound but not fully canonical
ce.function('Add', [1, 'x'], { structural: true });
// Selective canonicalization
ce.box(['Add', 1, 'x'], { canonical: ['Number', 'Order'] });import { ComputeEngine } from '@cortex-js/compute-engine';
const ce = new ComputeEngine();
// Full canonicalization (default)
ce.box(['Add', 1, 'x']);
ce.box(['Add', 1, 'x'], { form: 'canonical' });
// No canonicalization, no binding
ce.box(['Add', 1, 'x'], { form: 'raw' });
// Structural: bound but not fully canonical
ce.function('Add', [1, 'x'], { form: 'structural' });
// Selective canonicalization
ce.box(['Add', 1, 'x'], { form: ['Number', 'Order'] });The form option is accepted by ce.box(), ce.function(), and ce.parse().
The FormOption type is:
type FormOption =
| 'canonical' // Full canonicalization with binding (default)
| 'structural' // Binding + structural normalization, no full canonicalization
| 'raw' // No canonicalization, no binding
| CanonicalForm // A single canonicalization pass (e.g. 'Number')
| CanonicalForm[]; // Selected passes in orderProperties that were previously available on all Expression instances
(returning null or undefined when not applicable) have been removed from the
base interface. They are now only accessible after narrowing with a type guard.
| Property | Access via |
|---|---|
.symbol |
isSymbol(expr) or isSymbol(expr, 'Pi') then expr.symbol |
.string |
isString(expr) then expr.string |
.ops |
isFunction(expr) or isFunction(expr, 'Add') then expr.ops |
.nops |
isFunction(expr) or isFunction(expr, 'Add') then expr.nops |
.op1/.op2/.op3 |
isFunction(expr) or isFunction(expr, 'Add') then expr.op1 etc. |
.isFunctionExpression |
isFunction(expr) then expr.isFunctionExpression |
.numericValue |
isNumber(expr) then expr.numericValue |
.isNumberLiteral |
isNumber(expr) then expr.isNumberLiteral |
.tensor |
isTensor(expr) then expr.tensor |
if (expr.symbol !== null) {
console.log(expr.symbol);
}
if (expr.numericValue !== null) {
console.log(expr.numericValue);
}import { isSymbol, isNumber, sym, numericValue } from '@cortex-js/compute-engine';
if (isSymbol(expr)) {
// expr.symbol is `string` — guaranteed non-undefined
console.log(expr.symbol);
}
if (isNumber(expr)) {
// expr.numericValue is `number | NumericValue` — guaranteed non-undefined
console.log(expr.numericValue);
}
// Convenience helpers
if (sym(expr) === 'Pi') {
console.log('This is Pi');
}
const val = numericValue(expr); // number | NumericValue | undefinedSee Section 6 for the full list of type guards and role interfaces.
Note: The sym() helper combines isSymbol() check with symbol name
access, making simple symbol comparisons more concise.
.re/.im— typednumber, returnNaNwhen not applicable.shape— typednumber[], returns[]for scalars.operator— returns the operator name for all expression types- All arithmetic methods (
.add(),.mul(), etc.) — work symbolically on all expressions - All numeric predicates (
.isPositive,.isInteger, etc.) — meaningful with assumptions
The expr.compile() method has been replaced by a standalone compile()
function. The return type is now a CompilationResult object instead of a
callable-with-toString hybrid.
const expr = ce.parse('x^2 + 1');
const fn = expr.compile();
console.log(fn({ x: 3 })); // 10
// Get generated code
console.log(fn.toString());
// Target a different language
const code = expr.compile({ to: 'python' });import { compile } from '@cortex-js/compute-engine';
const expr = ce.parse('x^2 + 1');
const result = compile(expr);
// Execute (JavaScript target)
console.log(result.run({ x: 3 })); // 10
// Access generated source code
console.log(result.code);
// Check compilation status
console.log(result.success); // true
console.log(result.target); // 'javascript'
// Target a different language
const pyResult = compile(expr, { to: 'python' });
console.log(pyResult.code); // "x ** 2 + 1"interface CompilationResult {
target: string; // Target language name
success: boolean; // Whether compilation succeeded
code: string; // Generated source code
run?: (...args: any[]) => any; // Executable (JS targets only)
}The expr.expand() method has been replaced by a standalone expand()
function.
const expr = ce.parse('(x+1)(x+2)');
const expanded = expr.expand();
console.log(expanded.latex); // "x^2+3x+2"import { expand } from '@cortex-js/compute-engine';
const expr = ce.parse('(x+1)(x+2)');
const expanded = expand(expr);
console.log(expanded.latex); // "x^2+3x+2"Note:
expand()returnsnullif the expression cannot be expanded. Handle this withexpand(expr) ?? exprif you want the original expression as a fallback.
The constructor now accepts a libraries option for controlling which libraries
are loaded. Libraries declare their dependencies explicitly and are loaded in
topological order.
// No control over which libraries are loaded
const ce = new ComputeEngine();// Load only specific standard libraries
const ce = new ComputeEngine({
libraries: ['core', 'arithmetic', 'trigonometry'],
});
// Add a custom library alongside standard ones
const ce = new ComputeEngine({
libraries: [
...ComputeEngine.getStandardLibrary(),
{
name: 'physics',
requires: ['arithmetic'],
definitions: {
G: { value: 6.674e-11, type: 'real', isConstant: true },
c: { value: 299792458, type: 'real', isConstant: true },
},
},
],
});interface LibraryDefinition {
name: string;
requires?: string[];
definitions?: SymbolDefinitions | SymbolDefinitions[];
latexDictionary?: Readonly<Partial<LatexDictionaryEntry>[]>;
}Nine type guard functions are available for runtime type checking. They narrow
to role interfaces that provide typed access to properties specific to that
expression kind. These guards are now required to access role-specific
properties (.symbol, .ops, .numericValue, etc.) that have been removed
from the base Expression interface.
// Properties were on Expression, returned null when not applicable
if (expr.symbol !== null) {
console.log(expr.symbol);
}
if (expr.numericValue !== null) {
console.log(expr.numericValue);
}import {
isNumber,
isSymbol,
isFunction,
isString,
isTensor,
isDictionary,
isCollection,
isIndexedCollection,
isExpression,
} from '@cortex-js/compute-engine';
// Type guards narrow the type — no undefined checks needed
if (isNumber(expr)) {
// expr.numericValue is `number | NumericValue` (not undefined)
// expr.isNumberLiteral is `true` (not boolean)
console.log(expr.numericValue);
}
if (isSymbol(expr)) {
// expr.symbol is `string` (not undefined)
console.log(expr.symbol);
}
// Pass a symbol name to narrow and check the name in one step:
if (isSymbol(expr, 'Pi')) {
// expr is a symbol with name "Pi"
}
if (isFunction(expr)) {
// expr.ops is `ReadonlyArray<Expression>` (not undefined)
// expr.isFunctionExpression is `true`
console.log(expr.ops, expr.nops, expr.op1);
}
// Pass an operator name to narrow and check the operator in one step:
if (isFunction(expr, 'Add')) {
// expr is a function expression with operator "Add"
console.log(expr.op1, expr.op2);
}
if (isString(expr)) {
// expr.string is `string` (not undefined)
console.log(expr.string);
}
if (isTensor(expr)) {
// expr.tensor is `Tensor<any>` (not undefined)
// expr.shape is `number[]`, expr.rank is `number`
console.log(expr.shape, expr.rank);
}
if (isCollection(expr)) {
// expr.isCollection is `true`
for (const item of expr.each()) console.log(item);
}
if (isIndexedCollection(expr)) {
// expr.isIndexedCollection is `true`
console.log(expr.at(0));
}For quick symbol name checks, use the sym() helper:
import { sym } from '@cortex-js/compute-engine';
// Instead of:
if (isSymbol(expr) && expr.symbol === 'Pi') { /* ... */ }
// You can write:
if (sym(expr) === 'Pi') { /* ... */ }
// Returns symbol name or undefined
const name = sym(expr); // string | undefinedFor safe numeric value extraction, use the numericValue() helper:
import { numericValue } from '@cortex-js/compute-engine';
// Instead of:
const val = isNumber(expr) ? expr.numericValue : undefined;
// You can write:
const val = numericValue(expr); // number | NumericValue | undefined| Guard | Narrows to |
|---|---|
isNumber |
Expression & NumberLiteralInterface |
isSymbol |
Expression & SymbolInterface (optional second arg: symbol name) |
isFunction |
Expression & FunctionInterface (optional second arg: operator name) |
isString |
Expression & StringInterface |
isTensor |
Expression & TensorInterface |
isDictionary |
Expression & DictionaryInterface |
isCollection |
Expression & CollectionInterface |
isIndexedCollection |
Expression & IndexedCollectionInterface |
isExpression |
Expression (from unknown) |
Custom compilation targets can now be registered and unregistered dynamically.
Built-in targets ('javascript', 'glsl', 'wgsl', 'python',
'interval-javascript', 'interval-glsl') are pre-registered.
// Only built-in targets, no extension mechanism
const fn = expr.compile({ to: 'javascript' });import {
ComputeEngine,
compile,
PythonTarget,
LanguageTarget,
} from '@cortex-js/compute-engine';
const ce = new ComputeEngine();
// Register a custom target
ce.registerCompilationTarget('python', new PythonTarget());
// Use it
const result = compile(ce.parse('x^2 + 1'), { to: 'python' });
console.log(result.code); // "x ** 2 + 1"
// List available targets
console.log(ce.listCompilationTargets());
// Remove a target
ce.unregisterCompilationTarget('python');class MyTarget implements LanguageTarget {
getOperators(): CompiledOperators { /* ... */ }
getFunctions(): CompiledFunctions { /* ... */ }
createTarget(options?: Partial<CompileTarget>): CompileTarget { /* ... */ }
compile(expr: Expression, options?: CompilationOptions): CompilationResult {
/* ... */
}
}
ce.registerCompilationTarget('my-lang', new MyTarget());The simplification rules used by .simplify() are now accessible and
modifiable.
// No way to add custom simplification rules to the standard pipeline
expr.simplify();
// Only per-call rules were supported
expr.simplify({ rules: myRules });// Add a custom rule to the standard pipeline
ce.simplificationRules.push({
match: ['Power', ['Sin', '_x'], 2],
replace: ['Subtract', 1, ['Power', ['Cos', '_x'], 2]],
});
// All subsequent .simplify() calls will use the custom rule
expr.simplify();
// Replace the entire rule set
ce.simplificationRules = myCustomRules;
// Per-call override still works
expr.simplify({ rules: otherRules });Expression now refers to the compute-engine runtime expression type. The
MathJSON type has been renamed to MathJsonExpression.
// MathJSON type (old name)
import { Expression } from '@cortex-js/compute-engine';// Full engine
import { ComputeEngine } from '@cortex-js/compute-engine';
import type { Expression } from '@cortex-js/compute-engine';
// MathJSON types only (lightweight, no engine code)
import { MathJsonExpression } from '@cortex-js/compute-engine/math-json';The following properties have been removed from the Expression base interface.
They are now only available on the corresponding role interfaces, accessed via
type guards.
| Removed Property | Type Guard → Interface |
|---|---|
expr.numericValue |
isNumber() → NumberLiteralInterface |
expr.isNumberLiteral |
isNumber() → NumberLiteralInterface |
expr.symbol |
isSymbol() → SymbolInterface |
expr.string |
isString() → StringInterface |
expr.isFunctionExpression |
isFunction() → FunctionInterface |
expr.ops |
isFunction() → FunctionInterface |
expr.nops |
isFunction() → FunctionInterface |
expr.op1 / op2 / op3 |
isFunction() → FunctionInterface |
expr.tensor |
isTensor() → TensorInterface |
Accessing these properties without first narrowing with a type guard is now a TypeScript compile error.
// Compile error — .symbol does not exist on Expression
console.log(expr.symbol);
// Correct — narrow first, then access
if (isSymbol(expr)) {
console.log(expr.symbol); // string, guaranteed
}Before:
if (expr.symbol !== null) {
return expr.symbol;
} else if (expr.numericValue !== null) {
return expr.numericValue.toString();
} else if (expr.ops !== null) {
return expr.operator;
}After:
import { isSymbol, isNumber, isFunction } from '@cortex-js/compute-engine';
if (isSymbol(expr)) {
return expr.symbol;
} else if (isNumber(expr)) {
return expr.numericValue.toString();
} else if (isFunction(expr)) {
return expr.operator;
}Before:
if (expr.ops) {
for (const arg of expr.ops) {
process(arg);
}
}After:
import { isFunction } from '@cortex-js/compute-engine';
if (isFunction(expr)) {
for (const arg of expr.ops) {
process(arg);
}
}Before:
const value = expr.numericValue ?? 0; // Default to 0 if not a numberAfter:
import { isNumber } from '@cortex-js/compute-engine';
const value = isNumber(expr) ? expr.numericValue : 0;Before:
const name = expr.symbol || 'unknown';After:
import { sym } from '@cortex-js/compute-engine';
const name = sym(expr) ?? 'unknown';Before:
const [P, L, U] = luDecomposition.ops; // Unsafe - ops might be nullAfter:
import { isFunction } from '@cortex-js/compute-engine';
const lu = luDecomposition.evaluate();
if (isFunction(lu)) {
const [P, L, U] = lu.ops;
// Safe to use P, L, U here
}// Old
import { ComputeEngine } from '@cortex-js/compute-engine';
const ce = new ComputeEngine();
const expr = ce.parse('x^2 + 1');
// Old method calls
expr.expand();
expr.compile();
ce.box(json, { canonical: false });
// New
import {
getDefaultEngine,
compile,
expand,
isFunction,
} from '@cortex-js/compute-engine';
const expr = parse('x^2 + 1');
expand(expr); // or expand("x^2 + 1")
compile(expr);
getDefaultEngine().box(json, { form: 'raw' });