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
- ✅ Decimal Precision: Uses decimal.js internally for accurate arithmetic
- ✅ Decimal, Boolean & Null Output: Results can be
Decimal,boolean, ornullfor natural mathematical expressions - ✅ Variable References: Support dependencies between expressions
- ✅ Dot-Traversal: Reference nested properties (e.g.,
point.x,offset.y) - ✅ Topological Ordering: Evaluates in correct dependency order (DAG)
- ✅ Circular Detection: Throws error on circular dependencies
- ✅ Safe Evaluation: Parses with Peggy parser, evaluates AST (no
eval()orFunction()) - ✅ Keywords as Variables: Unlike JavaScript, keywords like
return,if, etc. can be used as variable names - ✅ Namespaces: Built-in
$math,$unit, and$anglefunctions
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:
- Objects and arrays can ONLY be provided via
valueproperty - Expressions cannot create or return objects/arrays (only
Decimal,boolean, ornull) - Objects/arrays from
valueare stored in context for property access but not included in output values - This ensures clean output types:
Record<string, Decimal | boolean | null>
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:
- Decimal: Numeric values with arbitrary precision
- boolean: Results from comparisons (
a > b) or boolean literals (true,false) - null: The null literal or null branches in ternary expressions
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:
true- Boolean true valuefalse- Boolean false valuenull- Null value (for missing/undefined results)Infinity- Mathematical infinity
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:
<,>,<=,>=- Numeric comparisons=,==- Equality (both work the same)!=- Inequality&&,||,!- Logical operators
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:
- Constants:
PI,E,SQRT2,SQRT1_2,LN2,LN10,LOG2E,LOG10E - Functions:
abs,sqrt,cbrt,floor,ceil,round,trunc,sin,cos,tan,asin,acos,atan,atan2,exp,ln,log,log10,log2,pow,min,max
$unit Namespace
Unit conversion functions:
const result = await evalla([
{ name: 'inches', expr: '$unit.mmToInch(25.4)' },
{ name: 'mm', expr: '$unit.inchToMm(1)' }
]);
Available:
mmToInch,inchToMmcmToInch,inchToCmmToFt,ftToM
$angle Namespace
Angle conversion functions:
const result = await evalla([
{ name: 'radians', expr: '$angle.toRad(180)' },
{ name: 'degrees', expr: '$angle.toDeg($math.PI)' }
]);
Available:
toRad(degrees to radians)toDeg(radians to degrees)
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:
- ❌ No access to
eval(),Function(), or other dangerous globals - ❌ No access to
process,require, or Node.js internals - ❌ No access to dangerous properties:
prototype,__proto__,constructor, or any property starting with__ - ❌ No function aliasing - namespace functions must be called, not assigned to variables
- ✅ Only whitelisted functions in namespaces
- ✅ Uses AST parsing (Peggy) + safe evaluation
- ✅ Variable names cannot start with
$(reserved for system) - ✅ Sandboxed scope with
Object.create(null) - ✅ No prototype pollution
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:
inputs: Array of{ name, expr }objects
Returns:
- Promise resolving to
{ values, order }
Throws:
ValidationError- Invalid input (missing name, duplicate names, invalid variable names)- Properties:
variableName
- Properties:
CircularDependencyError- Circular dependencies detected- Properties:
cycle(array of variable names in the cycle)
- Properties:
ParseError- Syntax/parsing errors in expressions (extendsEvaluationError)- Properties:
variableName,expression,line,column
- Properties:
EvaluationError- Runtime evaluation errors (undefined variables, type errors)- Properties:
variableName
- Properties:
SecurityError- Attempt to access blocked properties (prototype, proto, constructor, __*)- Properties:
property
- Properties:
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:
expr: The expression string to check
Returns:
- Object with the following properties:
valid: boolean - Whether the syntax is validerror?: string - Error message if syntax is invalidline?: number - Line number where error occurred (1-indexed)column?: number - Column number where error occurred (1-indexed)
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:
checkSyntax()only validates expression syntax. It does not check variable names, detect circular dependencies, or validate that referenced variables exist.evalla()throwsParseErrorfor syntax errors with the same details (line, column, message) ascheckSyntax(), plus identifies which variable has the error.ParseErrorextendsEvaluationError, so catchParseErrorfirst if you want to handle syntax errors differently from runtime errors.- Use
checkSyntax()for pre-flight validation (e.g., real-time feedback as user types). Useevalla()for complete validation and evaluation.
Philosophy
- Minimal: Bare minimum dependencies and code
- Modular: Separated concerns (parser, evaluator, namespaces, toposort)
- DRY: No code duplication
- Testable: Small, focused functions with clear interfaces
- Safe & Secure: No arbitrary code execution, whitelist-only approach
- Efficient: Parse once, use for both dependency extraction and evaluation
Development
# Install dependencies
npm install
# Build
npm run build
# Test
npm test
For detailed API documentation and examples, see the sections above.
License
MIT