Skip to main content

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}}

const expr = ce.parse("\\sqrt{6\\sum^{10^2}_{n=1}\\frac{1}{n^2}}"); // Numerical evaluation using the Compute Engine console.time('evaluate'); console.timeEnd('evaluate', expr.evaluate()); // Compilation to a JavaScript function and execution console.time('compile'); const fn = expr.compile(); console.timeEnd('compile', fn());

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:

const expr = ce.parse("n^2"); const fn = expr.compile(); for (let i = 1; i < 10; i++) console.log(fn({ n: i }));

To get a list of the unknowns of an expression use the expr.unknowns property:

console.log(ce.parse("n^2").unknowns); console.log(ce.parse("a^2+b^3").unknowns);

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 custom add() 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 - b is canonicalized to Add(a, Negate(b))
  • To handle subtraction with custom operations, override both Add and Negate

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

// Define vector operations function add(a, b) { return a.map((v, i) => v + b[i]); } function mul(a, b) { return a.map((v, i) => v * b[i]); } function neg(a) { return a.map(v => -v); } const expr = ce.parse('u * v + w - z'); const fn = expr.compile({ operators: { Add: ['add', 11], Multiply: ['mul', 12], Negate: ['neg', 14] }, functions: { add, mul, neg } }); console.log(fn({ u: [2, 3, 4], v: [1, 1, 1], w: [5, 6, 7], z: [1, 2, 3] })); // ➔ [6, 7, 8] // u*v + w - z = [2,3,4] + [5,6,7] - [1,2,3]

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 functions
  • glsl - Compiles to GLSL (OpenGL Shading Language) for WebGL shaders
  • python - Compiles to Python/NumPy code for scientific computing (requires registration)
  • interval-js - Compiles to JavaScript using interval arithmetic for reliable function plotting
  • interval-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: sinnp.sin, cosnp.cos, etc.
  • Exponential: expnp.exp, lnnp.log
  • Power: x^nx ** n, sqrtnp.sqrt
  • Statistics: sumnp.sum, meannp.mean
  • Linear Algebra: dotnp.dot, crossnp.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:

KindMeaningExample
intervalNormal result with boundssin([0, π]){ kind: 'interval', value: { lo: 0, hi: 1 } }
emptyNo valid output valuessqrt([-2, -1]){ kind: 'empty' }
entireResult spans all realsDivision with mixed signs near zero
singularContains a pole/asymptote1 / [-1, 1]{ kind: 'singular' }
partialPartially valid domainsqrt([-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:

TargetPerformanceUse Case
JavaScript Compiled40-2900x faster than evalCPU computation, real-time calculations
Python/NumPyFast for arrays, some overhead for scalarsScientific computing, data analysis
GLSLMassively parallel on GPUGraphics, 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:

function compileOrEvaluate(expr) { try { const fn = expr.compile(); return fn() + " (compiled)"; } catch (e) { return expr.N().numericValue + " (evaluated)"; } } // `expr.compile()` can handle this expression console.log(compileOrEvaluate(ce.parse("\\frac{\\sqrt{5}+1}{2}"))); // `expr.compile()` cannot handle complex numbers, so it throws // and we fall back to numerical evaluation with expr.N() console.log(compileOrEvaluate(ce.parse("-i\\sqrt{-1}")));