Safe math evaluator with variables, dependencies, and precision.

evalla

Safe math evaluator with variables, dependencies, and precision.

import { evalla } from 'evalla';

const result = await evalla([
  { name: 'a', expr: 'c + 5' },          // depends on c
  { name: 'b', expr: 'a * 2' },          // depends on a
  { name: 'c', expr: '1 + 1' }           // base constant
]);

console.log(result.values.a.toString()); // "7"
console.log(result.values.b.toString()); // "14"
console.log(result.values.c.toString()); // "2"
console.log(result.order);               // ['c', 'a', 'b']

Features

Installation

npm install evalla

Usage

Input Format

interface ExpressionInput {
  name: string;         // Variable name (cannot start with $)
  expr?: string;        // Math expression (optional if value is provided)
  value?: any;          // Direct value (optional if expr is provided)
}

You must provide either expr or value per item. The value property allows you to pass objects and arrays directly without stringifying them into expressions.

Important Type Restrictions:

Using expressions:

const result = await evalla([
  { name: 'width', expr: '100' },
  { name: 'height', expr: '50' }
]);

Using direct values:

const result = await evalla([
  { name: 'point', value: { x: 10, y: 20 } },
  { name: 'offset', value: { x: 5, y: 10 } }
]);

Mixing both:

const result = await evalla([
  { name: 'data', value: { width: 100, height: 50 } },
  { name: 'area', expr: 'data.width * data.height' }
]);

Output Format

interface EvaluationResult {
  values: Record<string, Decimal | boolean | null>;  // Computed values
  order: string[];                                   // Evaluation order (topologically sorted)
}

Results can be:

Examples

Basic Arithmetic with Precision

const result = await evalla([
  { name: 'x', expr: '0.1 + 0.2' }
]);

console.log(result.values.x.toString()); // "0.3" (exact!)

Variable Dependencies

const result = await evalla([
  { name: 'd', expr: 'c * 2' },
  { name: 'b', expr: 'a + 10' },
  { name: 'c', expr: 'b * 3' },
  { name: 'a', expr: '5' }
]);

// Automatically orders: a, b, c, d
console.log(result.order); // ['a', 'b', 'c', 'd']
console.log(result.values.d.toString()); // "90"

Arrays and Objects (via value property)

Important: Arrays and objects can ONLY be supplied via the value property. They cannot be created in expressions.

Arrays passed via value can be accessed with computed property syntax:

const result = await evalla([
  { name: 'data', value: [10, 20, 30, 40, 50] },
  { name: 'first', expr: 'data[0]' },
  { name: 'sum', expr: 'data[0] + data[1] + data[2]' }
]);

console.log(result.values.first.toString()); // "10"
console.log(result.values.sum.toString());   // "60"
// Note: result.values.data is undefined (arrays from value property are not in output)

Object Property Access (via value property)

Objects must be passed via the value property, then accessed with dot notation:

const result = await evalla([
  { name: 'point', value: {x: 10, y: 20} },
  { name: 'offset', value: {x: 5, y: 10} },
  { name: 'resultX', expr: 'point.x + offset.x' },
  { name: 'resultY', expr: 'point.y + offset.y' }
]);

console.log(result.values.resultX.toString()); // "15"
console.log(result.values.resultY.toString()); // "30"
// Note: result.values.point and result.values.offset are undefined (objects from value property are not in output)

Special Property Names: Use string literals in computed property access for property names with special characters:

const result = await evalla([
  { name: 'obj', value: { 'y-y': 20, 'prop name': 42 } },
  { name: 'hyphen', expr: 'obj["y-y"]' },      // String literal for hyphenated property
  { name: 'space', expr: 'obj["prop name"]' }  // String literal for property with space
]);

console.log(result.values.hyphen.toString()); // "20"
console.log(result.values.space.toString());  // "42"

Note: Objects and arrays cannot be created within expressions. Use the value property to pass them, then access properties/elements in expressions.

Nested Property Access

const result = await evalla([
  { name: 'data', value: {pos: {x: 5, y: 10}, scale: 2} },
  { name: 'scaledX', expr: 'data.pos.x * data.scale' }
]);

console.log(result.values.scaledX.toString()); // "10"

Reserved Values

Algebra, not code: evalla is designed for natural mathematical expressions, not programming. A few special values are reserved as first-class mathematical primitives and cannot be used as variable names:

These reserved values allow evalla to return boolean results from comparisons and handle edge cases naturally, just like in mathematical notation.

// ❌ Cannot use reserved values as variable names
await evalla([{ name: 'true', expr: '10' }]);     // ValidationError
await evalla([{ name: 'false', expr: '10' }]);    // ValidationError
await evalla([{ name: 'null', expr: '10' }]);     // ValidationError
await evalla([{ name: 'Infinity', expr: '10' }]); // ValidationError

// ✅ Can use them as values
const result = await evalla([
  { name: 'isValid', expr: 'true' },
  { name: 'isEmpty', expr: 'false' },
  { name: 'missing', expr: 'null' }
]);

Why reserve these? Unlike programming keywords (which evalla allows as variable names), these mathematical primitives need special handling to support boolean logic and comparisons as first-class results.

Boolean Output

Results can now be boolean values, not just numbers. This enables natural mathematical questions:

Standalone Comparisons

Comparisons return true or false:

const result = await evalla([
  { name: 'slope', expr: '0.75' },
  { name: 'isSteep', expr: 'slope > 0.5' }  // Returns boolean true
]);

console.log(result.values.isSteep); // true
console.log(typeof result.values.isSteep); // "boolean"

All comparison operators return boolean:

Boolean Branches in Ternary

Ternary expressions can have boolean or null branches:

const result = await evalla([
  { name: 'score', expr: '85' },
  { name: 'passed', expr: 'score >= 60 ? true : false' },
  { name: 'bonus', expr: 'score >= 90 ? 10 : null' }
]);

console.log(result.values.passed);  // true (boolean)
console.log(result.values.bonus);   // null

Boolean Literals

Use true and false directly:

const result = await evalla([
  { name: 'enabled', expr: 'true' },
  { name: 'disabled', expr: 'false' },
  { name: 'result', expr: 'enabled && !disabled' }
]);

console.log(result.values.result); // true

Equality Operators

evalla supports two equality operators that work identically:

Single Equals = (Algebraic)

In mathematics, = tests equality. evalla follows this convention:

const result = await evalla([
  { name: 'x', expr: '5' },
  { name: 'y', expr: '5' },
  { name: 'equal', expr: 'x = y' }  // Algebraic equality
]);

console.log(result.values.equal); // true

Double Equals == (Programmer-Friendly)

For programmers familiar with ==, it works exactly the same:

const result = await evalla([
  { name: 'a', expr: '10' },
  { name: 'b', expr: '20' },
  { name: 'test', expr: '(a + 10) == b' }
]);

console.log(result.values.test); // true

No loose vs. strict: Unlike JavaScript, there’s no distinction between = and == in evalla. Both = and == perform the same strict equality check. Use whichever feels more natural.

Namespaces

Variables may not begin with $, this is reserved for namespaces for built-in functions and constants.

$math Namespace

Mathematical constants and functions:

const result = await evalla([
  { name: 'circumference', expr: '2 * $math.PI * 10' },
  { name: 'absVal', expr: '$math.abs(-42)' },
  { name: 'sqrtVal', expr: '$math.sqrt(16)' },
  { name: 'maxVal', expr: '$math.max(10, 5, 20, 3)' }
]);

Available:

$unit Namespace

Unit conversion functions:

const result = await evalla([
  { name: 'inches', expr: '$unit.mmToInch(25.4)' },
  { name: 'mm', expr: '$unit.inchToMm(1)' }
]);

Available:

$angle Namespace

Angle conversion functions:

const result = await evalla([
  { name: 'radians', expr: '$angle.toRad(180)' },
  { name: 'degrees', expr: '$angle.toDeg($math.PI)' }
]);

Available:

Circular Dependency Detection

try {
  await evalla([
    { name: 'a', expr: 'b + 1' },
    { name: 'b', expr: 'a + 1' }
  ]);
} catch (error) {
  console.log(error.message); // "Circular dependency detected: b -> a -> b"
}

Variable naming

Variables may not begin with a number, double underscore(__), or $ (see namespaces above).

Variables cannot use reserved values: true, false, null, Infinity (see Reserved Values).

Keywords as Variable Names

Unlike JavaScript, algebra-like variable names can include JavaScript keywords:

const result = await evalla([
  { name: 'return', expr: '10' },
  { name: 'if', expr: '20' },
  { name: 'for', expr: 'return + if' }
]);

console.log(result.values.for.toString()); // "30"

Security

Safe by design:

Blocked property access examples:

// These will throw SecurityError
await evalla([{ name: 'bad', expr: 'obj.prototype' }]);
await evalla([{ name: 'bad', expr: 'obj.__proto__' }]);
await evalla([{ name: 'bad', expr: 'obj.constructor' }]);
await evalla([{ name: 'bad', expr: 'obj.__defineGetter__' }]);

Blocked function aliasing examples:

// These will throw SecurityError - functions must be called with ()
await evalla([{ name: 'myabs', expr: '$math.abs' }]);
await evalla([{ name: 'mysqrt', expr: '$math.sqrt' }]);
// Correct usage - call the function:
await evalla([{ name: 'result', expr: '$math.abs(-5)' }]); // ✅ Works

Blocked namespace head usage:

// Namespace heads cannot be used as standalone values
await evalla([{ name: 'a', expr: '$math' }]); // ❌ EvaluationError
await evalla([{ name: 'a', value: 5 }, { name: 'b', expr: 'a < $angle' }]); // ❌ EvaluationError
// Correct usage - access properties or call methods:
await evalla([{ name: 'pi', expr: '$math.PI' }]); // ✅ Works
await evalla([{ name: 'rad', expr: '$angle.toRad(180)' }]); // ✅ Works

Properties starting with __ are blocked because they typically provide access to JavaScript internals that could be exploited for prototype pollution or other security vulnerabilities.

API

evalla(inputs: ExpressionInput[]): Promise<EvaluationResult>

Evaluates an array of math expressions with dependencies.

Parameters:

Returns:

Throws:

All errors include structured details for programmatic access - no need to parse error messages!

Error Handling:

import { evalla, ParseError, SecurityError, CircularDependencyError, ValidationError, EvaluationError } from 'evalla';

try {
  const result = await evalla(inputs);
} catch (error) {
  // Catch ParseError first since it extends EvaluationError
  if (error instanceof ParseError) {
    console.error(`Syntax error in "${error.variableName}" at ${error.line}:${error.column}`);
    console.error(`Expression: ${error.expression}`);
  } else if (error instanceof EvaluationError) {
    console.error(`Runtime error in "${error.variableName}"`);
  } else if (error instanceof SecurityError) {
    console.error(`Security violation: attempted to access "${error.property}"`);
  } else if (error instanceof CircularDependencyError) {
    console.error(`Circular dependency: ${error.cycle.join(' -> ')}`);
  } else if (error instanceof ValidationError) {
    console.error(`Invalid variable: "${error.variableName}"`);
  }
}

checkSyntax(expr: string): SyntaxCheckResult

Checks the syntax of an expression without evaluating it. Useful for text editors to validate expressions before sending them for evaluation.

Parameters:

Returns:

Example:

import { checkSyntax } from 'evalla';

// Valid expression
const result1 = checkSyntax('a + b * 2');
console.log(result1.valid); // true

// Invalid expression - missing closing parenthesis
const result2 = checkSyntax('(a + b');
console.log(result2.valid); // false
console.log(result2.error); // "Parse error at line 1, column 7: Expected..."
console.log(result2.line); // 1
console.log(result2.column); // 7

Usage Patterns:

When to use checkSyntax() vs just calling evalla():

import { checkSyntax, evalla, EvaluationError } from 'evalla';

// Pattern 1: Pre-validate for immediate user feedback (recommended for text editors/UI)
const inputs = [
  { name: 'a', expr: 'c + 5' },
  { name: 'b', expr: 'a * 2' }
];

// Check syntax of each expression before calling evalla
for (const input of inputs) {
  if (input.expr) {
    const check = checkSyntax(input.expr);
    if (!check.valid) {
      console.error(`Invalid syntax in "${input.name}": ${check.error}`);
      return; // Don't call evalla with invalid syntax
    }
  }
}

// All syntax valid, now evaluate
const result = await evalla(inputs);

// Pattern 2: Let evalla handle all validation (simpler for batch processing)
try {
  const result = await evalla(inputs);
  // Success - use result
} catch (error) {
  // Catch ParseError specifically to handle syntax errors
  if (error instanceof ParseError) {
    console.error(`Syntax error in "${error.variableName}" at ${error.line}:${error.column}`);
  } else if (error instanceof EvaluationError) {
    console.error('Runtime evaluation error:', error.message);
  }
  // Handle other error types (ValidationError, CircularDependencyError, etc.)
}

Note:

Philosophy

Development

# Install dependencies
npm install

# Build
npm run build

# Test
npm test

For detailed API documentation and examples, see the sections above.

License

MIT