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 expr.compile() method:
const expr = ce.parse("2\\prod_{n=1}^{\\infty} \\frac{4n^2}{4n^2-1}");
const fn = expr.compile();
To evaluate the compiled expression call the function returned by
expr.compile():
console.log(fn());
// ➔ 3.141592653589793
If the expression cannot be compiled, the compile() method will throw an error.
Arguments
The function returned by expr.compile() can be called with an object literal
containing the value of the arguments:
To get a list of the unknowns of an expression use the expr.unknowns
property:
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():
const expr = ce.parse('v + w');
const fn = expr.compile({
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 result = fn({ v: [1, 2, 3], w: [4, 5, 6] });
console.log(result);
// ➔ [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:
const fn = expr.compile({
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:
const expr = ce.parse('(a + b) * c');
const fn = expr.compile({
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 result = fn({
a: [1, 2, 3],
b: [4, 5, 6],
c: [2, 2, 2]
});
console.log(result);
// ➔ [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:
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 expr = ce.parse('x + y * 2');
const code = BaseCompiler.compile(expr, myTarget);
console.log(code);
// → ADD(VAR("x"), MUL(VAR("y"), 2))
Example: SQL-like Target
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 whereExpr = ce.parse('x > 10 \\land y \\leq 20');
const sql = BaseCompiler.compile(whereExpr, 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:
const expr = ce.parse('x^2 + y^2');
// Compile to JavaScript (default)
const jsFunc = expr.compile();
console.log(jsFunc({ x: 3, y: 4 })); // → 25
// Compile to GLSL
const glslCode = expr.compile({ to: 'glsl' });
console.log(glslCode.toString()); // → 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 } 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 expr = ce.parse('\\sin(x) + \\cos(y)');
const pythonCode = expr.compile({ to: 'python' });
console.log(pythonCode.toString());
// → 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 expr = ce.parse('\\sqrt{x^2 + y^2}');
const func = python.compileFunction(
expr,
'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 expr = ce.parse('x^2 + 2x + 1');
const lambda = python.compileLambda(expr, ['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)
const expr = ce.parse('\\sin(x) / x');
const fn = expr.compile({ to: 'interval-js' });
// Call with interval inputs
const result = fn({ x: { lo: -0.1, hi: 0.1 } });
console.log(result);
// → { 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
// Simple function - normal result
const sin = ce.parse('\\sin(x)').compile({ to: 'interval-js' });
sin({ x: { lo: 0, hi: Math.PI } });
// → { kind: 'interval', value: { lo: 0, hi: 1 } }
// Singularity detection
const recip = ce.parse('1/x').compile({ to: 'interval-js' });
recip({ x: { lo: -1, hi: 1 } });
// → { kind: 'singular' }
// Partial domain
const sqrt = ce.parse('\\sqrt{x}').compile({ to: 'interval-js' });
sqrt({ x: { lo: -1, hi: 4 } });
// → { kind: 'partial', value: { lo: 0, hi: 2 }, domainClipped: 'lo' }
// Multi-variable expressions
const fn = ce.parse('x^2 + y').compile({ to: 'interval-js' });
fn({ 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();
const expr = ce.parse('\\sin(x) + y^2');
// Generate complete shader code
const shader = target.compileShaderFunction(expr, {
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 } 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: '',
};
}
compileToExecutable(expr, options = {}) {
const target = this.createTarget();
const code = BaseCompiler.compile(expr, target);
const result = function () { return code; };
Object.defineProperty(result, 'toString', { value: () => code });
Object.defineProperty(result, 'isCompiled', { value: true });
return result;
}
}
// Register and use
ce.registerCompilationTarget('custom', new CustomTarget());
const code = expr.compile({ to: 'custom' });
Direct Target Override
For one-time use, you can provide a CompileTarget directly without registration:
const expr = ce.parse('a + b');
const customTarget = {
language: 'custom',
operators: () => ['+', 10],
functions: () => undefined,
var: (id) => id.toUpperCase(),
string: (str) => `"${str}"`,
number: (n) => n.toString(),
indent: 0,
ws: () => ' ',
preamble: '',
};
const code = expr.compile({ target: customTarget });
console.log(code.toString()); // → 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 an executable form.
* For non-JavaScript targets, this typically returns a function
* that returns the source code as a string.
*/
compileToExecutable(
expr: BoxedExpression,
options?: CompilationOptions
): CompiledExecutable;
}
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
See benchmarks/README.md for detailed benchmark documentation.
Limitations
The calculations are only performed using machine precision numbers.
Complex numbers, arbitrary precision numbers, and symbolic calculations are not supported.
Some functions are not supported.
If the expression cannot be compiled, the compile() method will throw an
error. The expression can be numerically evaluated as a fallback: