Compiling Expressions
The Compute Engine can compile LaTeX expressions to JavaScript functions!
Introduction
Some expressions can take a long time to evaluate numerically, for example if they contain a large number of terms or involve a loop $(\sum$ or $\prod$).
In this case, it is useful to compile the expression into a JavaScript function that can be evaluated much faster.
For example this approximation: \pi \approx \textstyle\sqrt{6\sum^{10^6}_{n=1}\frac{1}{n^2}}
Compiling
To get a compiled version of an expression use the compile() function:
import { compile } from '@cortex-js/compute-engine';
const f = compile("2\\prod_{n=1}^{\\infty} \\frac{4n^2}{4n^2-1}");
To evaluate the compiled expression call the run method on the
CompilationResult returned by compile():
console.log(f.run());
// ➔ 3.141592653589793
If the expression cannot be compiled, the result.success property will be
false.
Validation and Fallback Behavior
Compilation APIs enforce runtime contracts to catch malformed extension payloads early:
ce.registerCompilationTarget(name, target)validates target names and requiredLanguageTargetmethods (getOperators(),getFunctions(),createTarget(),compile()).compile(expr, options)validates option payload shape forto,target,operators,functions,vars,imports,preamble, andfallback.
By default, compile() falls back to interpretation (success: false with a
run function). To disable fallback and fail fast, set fallback: false.
What Can Be Compiled
Three kinds of expressions can be compiled, and each produces a run function
with a different calling convention.
Plain Expressions
A plain expression like x^2 + 1 has free variables (unknowns). The compiled
run function takes a vars object mapping variable names to values:
An expression with no unknowns can be called with no arguments:
Lambda Expressions
A lambda expression uses \mapsto (\mapsto) to explicitly declare
parameters. The compiled run function takes positional arguments:
Lambdas are useful when the variable name is user-specified rather than assumed
by convention. A lambda like \theta \mapsto 1 + \cos(\theta) makes the
parameter explicit, avoiding ambiguity.
Tuple Expressions
A tuple expression like (\cos(t), \sin(t)) compiles to a function that returns
a JavaScript array:
This is particularly useful for parametric curves. Both Tuple and List
expressions compile to arrays.
Control Structures
Control structures such as conditionals, loops, and blocks can be compiled to JavaScript.
If and Which (Conditionals)
If expressions compile to ternary operators and Which expressions (used by
\begin{cases}) compile to chained ternaries:
When no condition matches, Which returns NaN.
Sum and Product
Sum and Product with numeric bounds compile to for loops with accumulator
variables:
Loop, Break, Continue, Return
Loop expressions with a range compile to for loops. Break, Continue,
and Return compile to their JavaScript equivalents:
Block and where
Block expressions create scoped variable bindings. The where syntax provides
a convenient way to define local variables:
Variable Names
The compiled function expects variable names as they appear in the parsed MathJSON expression. Greek letters become their English names:
| LaTeX | Variable Name |
|---|---|
x, y | "x", "y" |
t | "t" |
\theta | "theta" |
\alpha | "alpha" |
u, v | "u", "v" |
Use the wrong name and you'll get NaN silently:
const f = compile("1 + \\cos(\\theta)");
f.run({ theta: 0.5 }); // ✓ correct → 1.8776
f.run({ x: 0.5 }); // ✗ wrong key — returns NaN
To discover the variable names in an expression, use expr.unknowns (or
its alias expr.freeVariables). These properties return only the free
variables — symbols that are not constants, operators, or bound by scoping
constructs like Sum or Product:
Complex Numbers
The JavaScript target has full complex arithmetic support. When the compiler detects that a subexpression involves complex values, it automatically emits complex-aware code for all operations touching that subexpression.
When Complex Arithmetic Is Used
The compiler decides at compile time whether each subexpression is complex-valued. This happens when:
- The expression contains
i(ImaginaryUnit) - A symbol has been declared with a complex type
- An operation is applied to a complex-valued operand
The detection is static — based on the expression's structure, not on
runtime values. So \sqrt{x} compiles to real Math.sqrt because x has no
declared complex type, even though Math.sqrt(-1) returns NaN at runtime.
To get complex results from \sqrt{x}, you'd need to make the expression
explicitly complex, e.g., \sqrt{x + 0i}.
Result Format
When an expression is complex-valued, the compiled run function returns a
{ re: number, im: number } object instead of a plain number:
Complex-aware operations include all the standard arithmetic (+, -, *,
/), trigonometric functions (sin, cos, tan, and their inverses and
hyperbolic variants), exp, ln, sqrt, pow, abs, arg, conjugate,
Re, and Im.
Extracting Real and Imaginary Parts
Use Re and Im to extract components from a complex expression:
Mixing Real and Complex Operands
When a complex operand is combined with a real operand, the result is complex:
GLSL Target
The GLSL target also supports complex arithmetic, but represents complex numbers
as vec2 values — the .x component is the real part, .y is the imaginary
part:
// Complex literal: 3 + 4i
vec2(3.0, 4.0)
// Imaginary unit
vec2(0.0, 1.0)
Simple operations like addition, subtraction, negation, and scalar multiplication
use native vec2 operations. More involved operations — complex multiplication,
division, exponentiation, and transcendentals — use helper functions emitted in
a preamble block:
| Operation | GLSL Helper |
|---|---|
z * w | _gpu_cmul(z, w) |
z / w | _gpu_cdiv(z, w) |
z ^ w | _gpu_cpow(z, w) |
sqrt(z) | _gpu_csqrt(z) |
exp(z) | _gpu_cexp(z) |
ln(z) | _gpu_cln(z) |
The preamble is dependency-aware: each helper declares its prerequisites
(e.g., _gpu_cpow depends on _gpu_cexp, _gpu_cmul, and _gpu_cln), and
only the needed functions are emitted in topological order. If the expression is
purely real, no preamble is generated.
Component extraction maps to native swizzle operations:
Re(z) → (z).x
Im(z) → (z).y
Abs(z) → length(z) // built-in
Arg(z) → atan(z.y, z.x)
Conjugate(z) → vec2(z.x, -z.y)
The same complex value analysis used by the JavaScript target determines whether each subexpression needs complex or real code paths in GLSL.
Real-Only Mode
If you don't need complex results (e.g., for plotting), pass { realOnly: true }
to automatically convert complex returns: the real part is returned when the
imaginary part is zero, and NaN is returned otherwise.
This avoids per-evaluation type checks in calling code.
Custom Operators
By default, operators like +, -, *, / compile to their JavaScript equivalents. However, you can override operators to use custom function calls instead. This is particularly useful for:
- Vector and matrix operations - where
[1,2,3] + [4,5,6]should call a customadd()function - Custom domain-specific languages - where operations have specialized semantics
- Type-specific operations - where the same operator behaves differently for different types
Basic Usage
Override operators by passing an operators option to compile():
import { compile } from '@cortex-js/compute-engine';
const result = compile("v + w", {
operators: {
Add: ['add', 11], // Convert + to add() function
Multiply: ['mul', 12] // Convert * to mul() function
},
functions: {
add: (a, b) => a.map((v, i) => v + b[i]),
mul: (a, b) => a.map((v, i) => v * b[i])
}
});
const value = result.run({ v: [1, 2, 3], w: [4, 5, 6] });
console.log(value);
// ➔ [5, 7, 9]
The operator override format is [functionName, precedence]:
- functionName: The name of the function to call (alphanumeric identifier)
- precedence: Numeric precedence level (higher = tighter binding)
Function-Based Overrides
You can also use a function to conditionally override operators:
import { compile } from '@cortex-js/compute-engine';
const result = compile("v + w", {
operators: (op) => {
// Only override Add, let other operators use defaults
if (op === 'Add') return ['vectorAdd', 11];
return undefined;
},
functions: {
vectorAdd: (a, b) => a.map((v, i) => v + b[i])
}
});
Complex Expressions
Operator overrides work with complex nested expressions:
import { compile } from '@cortex-js/compute-engine';
const result = compile("(a + b) * c", {
operators: {
Add: ['add', 11],
Multiply: ['mul', 12]
},
functions: {
add: (a, b) => a.map((v, i) => v + b[i]),
mul: (a, b) => a.map((v, i) => v * b[i])
}
});
const value = result.run({
a: [1, 2, 3],
b: [4, 5, 6],
c: [2, 2, 2]
});
console.log(value);
// ➔ [10, 14, 18] // (a + b) * c = ([1,2,3] + [4,5,6]) * [2,2,2]
Important Notes
Canonical Form: Expressions are canonicalized before compilation, which may affect operator usage:
- Subtraction
a - bis canonicalized toAdd(a, Negate(b)) - To handle subtraction with custom operations, override both
AddandNegate
Symbol vs Function Names:
- Function names (alphanumeric like
add,mul) compile to function calls:add(a, b) - Symbol operators (like
+,-,⊕) compile to infix operators:a + b
Partial Overrides: You can override only some operators; others will use their default JavaScript implementations.
Example: Complete Vector Math
Advanced: Custom Compilation Targets
For advanced use cases, you can create completely custom compilation targets by using the exported CompileTarget interface and BaseCompiler class.
Exported Interfaces
import {
ComputeEngine,
BaseCompiler,
JavaScriptTarget,
type CompileTarget,
} from '@cortex-js/compute-engine';
Creating a Custom Target
Define a custom target object that implements the CompileTarget interface:
import { BaseCompiler } from '@cortex-js/compute-engine';
const myTarget = {
language: 'my-dsl',
operators: (op) => {
const ops = {
Add: ['ADD', 11],
Multiply: ['MUL', 12],
Divide: ['DIV', 13],
};
return ops[op];
},
functions: (id) => id.toUpperCase(),
var: (id) => `VAR("${id}")`,
string: (s) => `"${s}"`,
number: (n) => n.toString(),
ws: () => ' ',
preamble: '',
indent: 0,
};
const code = BaseCompiler.compile("x + y * 2", myTarget);
console.log(code);
// → ADD(VAR("x"), MUL(VAR("y"), 2))
Example: SQL-like Target
import { BaseCompiler } from '@cortex-js/compute-engine';
const sqlTarget = {
language: 'sql',
operators: (op) => {
return {
Equal: ['=', 8],
NotEqual: ['<>', 8],
And: ['AND', 4],
Or: ['OR', 3],
}[op];
},
functions: (id) => ({ Abs: 'ABS', Sqrt: 'SQRT' }[id]),
var: (id) => `"${id}"`, // Quote column names
string: (s) => `'${s.replace(/'/g, "''")}'`,
number: (n) => n.toString(),
ws: () => ' ',
preamble: '',
indent: 0,
};
const sql = BaseCompiler.compile("x > 10 \\land y \\leq 20", sqlTarget);
console.log(`SELECT * FROM table WHERE ${sql}`);
// → SELECT * FROM table WHERE AND("x" > 10, "y" <= 20)
For more examples of custom targets, see examples/compile-custom-target.js.
Plugin Architecture: Registering Custom Targets
The Compute Engine includes a plugin architecture that allows you to register custom compilation targets and switch between them easily.
Built-in Targets
The Compute Engine comes with these compilation targets:
javascript(default) - Compiles to executable JavaScript functionsglsl- Compiles to GLSL (OpenGL Shading Language) for WebGL shaderspython- Compiles to Python/NumPy code for scientific computing (requires registration)interval-js- Compiles to JavaScript using interval arithmetic for reliable function plottinginterval-glsl- Compiles to GLSL using interval arithmetic for GPU-based plotting
Compiling to Different Targets
Use the to option to specify the target language:
import { parse, compile } from '@cortex-js/compute-engine';
const expr = parse("x^2 + y^2");
// Compile to JavaScript (default)
const jsResult = compile(expr);
console.log(jsResult.run({ x: 3, y: 4 })); // → 25
// Compile to GLSL
const glslResult = compile(expr, { to: 'glsl' });
console.log(glslResult.code); // → pow(x, 2.0) + pow(y, 2.0)
Python/NumPy Target
The Compute Engine includes a complete Python/NumPy compilation target for scientific computing:
import { ComputeEngine, PythonTarget, compile } from '@cortex-js/compute-engine';
const ce = new ComputeEngine();
// Register the Python target with NumPy support
ce.registerCompilationTarget(
'python', new PythonTarget({ includeImports: true })
);
// Compile expressions to Python/NumPy code
const f = compile("\\sin(x) + \\cos(y)", { to: 'python' });
console.log(f.code);
// → import numpy as np
//
// np.sin(x) + np.cos(y)
Generating Python Functions
The PythonTarget can generate complete Python functions:
import { PythonTarget } from '@cortex-js/compute-engine';
const python = new PythonTarget({ includeImports: true });
const func = python.compileFunction(
"\\sqrt{x^2 + y^2}",
'euclidean_distance',
['x', 'y'],
'Calculate Euclidean distance between two points'
);
console.log(func);
Generates:
import numpy as np
def euclidean_distance(x, y):
"""Calculate Euclidean distance between two points"""
return np.sqrt(x ** 2 + y ** 2)
Use Cases
The Python target is ideal for:
- Scientific Computing: Generate NumPy code for numerical analysis
- Machine Learning: Create feature engineering functions
- Data Analysis: Convert formulas to Pandas/NumPy operations
- Education: Show Python equivalents of mathematical notation
- Code Generation: Automated function creation from LaTeX
Lambda Functions
Generate Python lambda expressions:
const lambda = python.compileLambda("x^2 + 2x + 1", ['x']);
console.log(lambda);
// → lambda x: x ** 2 + 2 * x + 1
Supported Functions
The Python target maps to NumPy functions:
- Trigonometric:
sin→np.sin,cos→np.cos, etc. - Exponential:
exp→np.exp,ln→np.log - Power:
x^n→x ** n,sqrt→np.sqrt - Statistics:
sum→np.sum,mean→np.mean - Linear Algebra:
dot→np.dot,cross→np.cross
For complete documentation, see the Python Target Guide.
Interval Arithmetic Targets
The Compute Engine includes interval arithmetic compilation targets designed for
reliable function plotting. These targets operate on intervals [lo, hi] rather
than point values, providing guaranteed enclosures of the true result and
detecting singularities.
Why Interval Arithmetic?
Standard plotting approaches sample functions at regular intervals, which can:
- Miss features (spikes between sample points)
- Create aliasing (high-frequency oscillations appear as lower frequencies)
- Produce wild line segments at singularities (like
tan(π/2)) - Render discontinuities as vertical lines
Interval arithmetic addresses these by:
- Returning wide intervals when uncertainty is high (triggers refinement)
- Explicitly detecting division by zero and other singularities
- Indicating when function domains are restricted
JavaScript Interval Target (interval-js)
import { compile } from '@cortex-js/compute-engine';
const f = compile("\\sin(x) / x", { to: 'interval-js' });
// Call with interval inputs
const value = f.run({ x: { lo: -0.1, hi: 0.1 } });
console.log(value);
// → { kind: 'singular' } // Division by interval containing zero
The function accepts an object where keys are variable names and values are
Interval objects with lo and hi properties.
Result Types
The compiled function returns an IntervalResult discriminated union:
| Kind | Meaning | Example |
|---|---|---|
interval | Normal result with bounds | sin([0, π]) → { kind: 'interval', value: { lo: 0, hi: 1 } } |
empty | No valid output values | sqrt([-2, -1]) → { kind: 'empty' } |
entire | Result spans all reals | Division with mixed signs near zero |
singular | Contains a pole/asymptote | 1 / [-1, 1] → { kind: 'singular' } |
partial | Partially valid domain | sqrt([-1, 4]) → { kind: 'partial', value: { lo: 0, hi: 2 }, domainClipped: 'lo' } |
Examples
import { compile } from '@cortex-js/compute-engine';
// Simple function - normal result
const sinResult = compile('\\sin(x)', { to: 'interval-js' });
sinResult.run({ x: { lo: 0, hi: Math.PI } });
// → { kind: 'interval', value: { lo: 0, hi: 1 } }
// Singularity detection
const recipResult = compile('1/x', { to: 'interval-js' });
recipResult.run({ x: { lo: -1, hi: 1 } });
// → { kind: 'singular' }
// Partial domain
const sqrtResult = compile('\\sqrt{x}', { to: 'interval-js' });
sqrtResult.run({ x: { lo: -1, hi: 4 } });
// → { kind: 'partial', value: { lo: 0, hi: 2 }, domainClipped: 'lo' }
// Multi-variable expressions
const fnResult = compile('x^2 + y', { to: 'interval-js' });
fnResult.run({ x: { lo: 1, hi: 2 }, y: { lo: 0, hi: 0.5 } });
// → { kind: 'interval', value: { lo: 1, hi: 4.5 } }
GLSL Interval Target (interval-glsl)
For GPU-based plotting, compile to GLSL interval arithmetic:
import { IntervalGLSLTarget } from '@cortex-js/compute-engine';
const target = new IntervalGLSLTarget();
// Generate complete shader code
const shader = target.compileShaderFunction("\\sin(x) + y^2", {
functionName: 'evaluateInterval',
parameters: ['x', 'y'],
version: '300 es'
});
console.log(shader);
// Outputs complete GLSL shader with interval arithmetic library
In GLSL, intervals are represented as vec2 where .x is the lower bound and
.y is the upper bound. The generated shader includes status flags for
singularity detection.
Plotting Integration
The interval results enable adaptive plotting algorithms:
function shouldSubdivide(result, tolerance) {
switch (result.kind) {
case 'singular':
case 'entire':
return true; // Always refine near singularities
case 'interval':
case 'partial':
return (result.value.hi - result.value.lo) > tolerance;
case 'empty':
return false; // Nothing to plot
}
}
Registering Custom Targets
You can also create your own compilation targets using ce.registerCompilationTarget():
import { ComputeEngine, BaseCompiler, compile } from '@cortex-js/compute-engine';
const ce = new ComputeEngine();
// Define a custom target (e.g., for R, MATLAB, etc.)
class CustomTarget {
getOperators() {
return {
Add: ['+', 11],
Multiply: ['*', 12],
// ... other operators
};
}
getFunctions() {
return {
Sin: 'sin',
Cos: 'cos',
// ... other functions
};
}
createTarget() {
return {
language: 'custom',
operators: (op) => this.getOperators()[op],
functions: (id) => this.getFunctions()[id],
var: (id) => id,
string: (str) => JSON.stringify(str),
number: (n) => n.toString(),
indent: 0,
ws: () => ' ',
preamble: '',
};
}
compile(expr, options = {}) {
const target = this.createTarget();
const code = BaseCompiler.compile(expr, target);
return {
target: 'custom',
success: true,
code,
};
}
}
// Register and use
ce.registerCompilationTarget('custom', new CustomTarget());
const result = compile("x + y * 2", { to: 'custom' });
console.log(result.code);
Direct Target Override
For one-time use, you can provide a CompileTarget directly without registration:
import { compile } from '@cortex-js/compute-engine';
const customTarget = {
language: 'custom',
operators: () => ['+', 10],
functions: () => undefined,
var: (id) => id.toUpperCase(),
string: (str) => `"${str}"`,
number: (n) => n.toString(),
indent: 0,
ws: () => ' ',
preamble: '',
};
const result = compile("a + b", { target: customTarget });
console.log(result.code); // → A + B
The target option takes precedence over the to option if both are provided.
Creating Custom Language Targets
To create a custom language target, implement the LanguageTarget interface:
interface LanguageTarget {
/**
* Returns operator mappings for this language.
* Maps operator names to [operator_string, precedence] tuples.
*/
getOperators(): CompiledOperators;
/**
* Returns function mappings for this language.
* Maps function names to language-specific function names or implementations.
*/
getFunctions(): CompiledFunctions;
/**
* Creates a CompileTarget with the specified options.
*/
createTarget(options?: Partial<CompileTarget>): CompileTarget;
/**
* Compiles an expression to a CompilationResult.
* For non-JavaScript targets, this typically returns a result
* with the source code in the `code` property.
*/
compile(
expr: Expression,
options?: CompilationOptions
): CompilationResult;
}
For complete examples of custom targets including RPN (Reverse Polish Notation), Python, and more, see examples/compile-plugin-architecture.js.
Performance Benchmarks
JavaScript vs Python/NumPy
To compare performance across compilation targets, the project includes benchmark scripts:
JavaScript Benchmarks (Node.js):
npm run test compute-engine/compile-performance
Python Benchmarks (generated):
# Generate the Python benchmark script
npm run test compute-engine/compile-python-generate
# Run Python benchmarks (requires NumPy)
python benchmarks/python-performance.py
Expected Results
The benchmarks test the same mathematical expressions across different targets:
| Target | Performance | Use Case |
|---|---|---|
| JavaScript Compiled | 40-2900x faster than eval | CPU computation, real-time calculations |
| Python/NumPy | Fast for arrays, some overhead for scalars | Scientific computing, data analysis |
| GLSL | Massively parallel on GPU | Graphics, WebGL shaders |
Key Insights:
- JavaScript compilation is excellent for single-threaded CPU performance
- Python/NumPy excels at vectorized array operations
- GLSL enables GPU parallelism for millions of simultaneous operations
Limitations
Precision: Compiled functions use machine-precision (64-bit) floating-point arithmetic only. Arbitrary-precision and symbolic calculations are not available.
Complex numbers: The JavaScript target supports complex arithmetic when the
expression involves complex-valued operands (e.g., i, complex-typed symbols).
Complex results are returned as { re, im } objects. See
Complex Numbers for details and
Real-Only Mode to convert complex results to NaN.
Unsupported functions: Most standard mathematical functions are supported,
but some cannot be compiled. When compilation fails, compile() returns a
CompilationResult with success set to false.
By default, a fallback run function based on the CE's numerical evaluator is
provided so the expression still produces a result (at lower performance). Set
{ fallback: false } to throw instead.
Target coverage varies. Not every function that compiles to JavaScript also compiles to GLSL or interval-js. The JavaScript target has the broadest coverage (~120 functions including special functions, statistics, Bessel, and color operations). The GLSL target supports ~80 functions (basic math, trig, hyperbolic, gamma, erf, and color — but not statistics, Bessel, Zeta, LambertW, or iterative constructs like Sum/Product). The interval-js target covers ~60 functions (basic math, trig, hyperbolic, gamma).
When a target fails, callers should fall back to a more capable target (e.g., GLSL → JavaScript, interval-js → JavaScript).