Parse and evaluate filters, conditions, and formulas without executing JavaScript source.
exp parses expression text into a typed AST and evaluates it against an
explicit environment. Expressions can read application data and call functions
that the application provides. The package also includes source spans,
diagnostics, and configurable limits for parser + interpreter work.
Use it for saved filters, rollout conditions, workflow guards, alerts, and application-defined formulas.
import { evaluateExpression } from "@claudiu-ceia/exp";
const result = evaluateExpression(
'account.plan == "pro" && usage.requests >= 1000',
{
env: {
account: { plan: "pro" },
usage: { requests: 1400 },
},
throwOnParseError: false,
throwOnError: false,
},
);
if (!result.success) {
console.error(result.error);
} else {
console.log(result.value); // true
}
const matches = result.success && result.value === true;Identifiers resolve from env or the built-in std namespace. No other
globals are added implicitly.
evaluateExpression() can return any supported runtime value. Applications
using expressions as predicates should require a boolean result rather than
relying on truthiness.
npm install @claudiu-ceia/expbun add @claudiu-ceia/expdeno add jsr:@claudiu-ceia/expImport the npm package from Node.js or Bun:
import { evaluateExpression } from "@claudiu-ceia/exp";Import the JSR package from Deno:
import { evaluateExpression } from "jsr:@claudiu-ceia/exp";| Job | Expression |
|---|---|
| Saved filter | status == "open" && priority >= 3 |
| Rollout condition | user.country == "RO" && user.bucket < rolloutPercent |
| Alert condition | errorRate >= 0.05 && requestCount >= 100 |
| Formula | subtotal >= 100 ? subtotal * 0.9 : subtotal |
| Text condition | user.email |> std.lower |> std.endsWith("@company.com") |
These are expression examples, not built-in domain concepts. The application
provides status, priority, user, errorRate, and the other values through
env.
The package provides:
exp
expression parser
typed AST with source spans
interpreter
diagnostics
standard helper functions
resource limits
It does not provide:
rule persistence
rule priority or conflict handling
actions or workflow execution
a visual rule builder
static type checking against an environment schema
cross-language expression compatibility
process isolation
wall-clock timeouts
arbitrary JavaScript execution
Use exp when the expression is readable text, a TypeScript application owns
evaluation, and the application can expose a narrow environment. It also fits
when source spans and diagnostics matter, JavaScript-shaped operators are
appropriate, and cross-language compatibility is not required.
Choose another approach when rules must run identically in several languages, JSON is the required interchange format, expressions need static type checking, or evaluation must be asynchronous. A different product is also a better fit for arbitrary scripting or for rule storage, actions, priority, audit history, and visual editing. See Choosing an approach.
A model can generate a filter expression instead of arbitrary JavaScript. Treat the generated expression as untrusted source, parse it with the normal limits, expose a narrow environment, and require the expected result type. See the checked model-generated filter example.
import { evaluateAst, parseExpression } from "@claudiu-ceia/exp";
const expressionSource = 'issue.status == "open" && issue.priority >= 3';
const parsed = parseExpression(expressionSource, {
throwOnError: false,
});
if (!parsed.success) {
throw new Error(parsed.error.message);
}
const issues = [
{ status: "open", priority: 4 },
{ status: "closed", priority: 5 },
];
const matching = issues.filter((issue) => {
const result = evaluateAst(parsed.value, {
env: {
issue: {
status: issue.status,
priority: issue.priority,
},
},
throwOnError: false,
});
return result.success && result.value === true;
});Parsing and evaluation are separate APIs. Parse once when the same expression
will be evaluated against several environments. parseExpression() produces an
AST. It does not generate executable code or bytecode.
Every AST node includes a zero-based, half-open UTF-16 source span:
type Span = {
start: number;
end: number;
};The returned AST is a TypeScript tagged union. exp does not statically check
an expression against the types or shape of env. Parsing priority >= "high"
succeeds. Whether evaluation produces a useful result depends on the coercion
rules below.
numbers
strings
true
false
null
undefined
arrays
identifiers
! + -
* / %
+ -
< <= > >=
== !=
&& || ??
? :
|>
user.plan
std.lower(user.email)
score(value)
assignments
statements
loops
object literals
computed property access
optional chaining
class instances
implicit access to JavaScript globals
Pipeline syntax is part of this expression language:
user.email |> std.lower |> std.endsWith("@company.com")
It desugars to:
std.endsWith(std.lower(user.email), "@company.com")
See the language reference for precedence, literal syntax, and the complete operator behavior.
Equality loosely coerces primitive values:
"1" == 1 // true
null == undefined // true
true == 1 // true
Objects, arrays, and functions are not implicitly converted to primitives:
user == user // reference equality may be true
user == "[object Object]" // false
Relational operators convert both values to numbers. They do not perform lexicographic string comparison:
"10" > 2 // true after numeric conversion
"b" > "a" // false because both values convert to NaN
+ concatenates when either operand is already a string. Otherwise it performs
numeric addition:
1 + 2 // 3
"1" + 2 // "12"
&&, ||, and ?? short-circuit. && and || return operand values rather
than coercing the result to boolean. A top-level expression can therefore return
a string, number, array, object, or function from the environment. Applications
using exp for filters must check the result type.
These semantics affect persisted expressions. Their pre-1.0 status and upgrade guidance are documented under Persisted expressions.
env contains the values and functions available to an expression. The
evaluator normalizes supported data into arrays and prototype-free plain
objects before evaluation.
Supported runtime values are:
undefined
null
boolean
number
string
arrays of supported values
plain objects containing supported values
functions accepting and returning supported values
The object model is intentionally limited:
- Only own enumerable data properties are readable.
- Inherited properties are not exposed.
- Accessor properties are rejected.
- Class instances are rejected.
- Arrays expose
.lengthonly. __proto__,prototype, andconstructormember access is blocked.- Computed property access is not part of the language.
user.name.toLowerCase() is not supported because string prototype methods are
not exposed. Use std.lower(user.name) instead.
The fixed std namespace contains:
std.len
std.abs
std.min
std.max
std.clamp
std.floor
std.ceil
std.round
std.trunc
std.sqrt
std.pow
std.lower
std.upper
std.trim
std.startsWith
std.endsWith
std.includes
std.slice
The built-in std functions are deterministic and side-effect-free. env.std
is reserved.
An expression can call functions supplied in env. Those functions run with
the same authority as the application and may access values captured by their
closure.
Expose narrow, synchronous, bounded functions. Prefer functions without side effects when expressions come from users or stored configuration.
const env = {
now: () => Date.now(),
};The same expression can return different values when the environment contains such a function. Environment functions must return a supported value synchronously. A Promise return is rejected after the function has already been invoked.
This parser failure is formatted by the real package API:
account.plan == && usage.requests >= 1000
1 | account.plan == && usage.requests >= 1000
| ╰─▶ expected expression at 1:17
Parser errors use a UTF-16 index. Evaluator errors tied to an AST node use its
span. Evaluation failures include the interpreter steps counter when it is
available. See Diagnostics for checked parser and
evaluator examples.
exp interprets its own expression grammar. It does not pass expression text to
eval() or new Function().
Identifiers resolve from the normalized env object and the fixed std
namespace. Member access is limited to supported own data properties.
This is not process isolation. Functions supplied through env run as normal
application code and are outside the interpreter's time and memory limits. Read
the security model before evaluating expressions
from untrusted sources.
| Limit | Default | Covers |
|---|---|---|
maxInputLength |
100_000 |
UTF-16 code units in expression source |
maxNestingDepth |
64 |
Parentheses, arrays, and conditional nesting |
maxNodes |
10_000 |
Parsed AST nodes |
maxSteps |
10_000 |
AST validation and nodes visited during evaluation |
maxDepth |
256 |
AST validation and interpreter recursion depth |
maxArrayElements |
1_000 |
Elements in one array literal |
maxRuntimeDepth |
64 |
Environment and function-return graph depth |
maxRuntimeEntries |
10_000 |
Environment and function-return graph entries |
The current maxNestingDepth pre-scan also counts parentheses, brackets, and
conditional markers inside comments.
These counters bound the parser and AST traversal described above. They do not
bound every individual operation, string size, numeric magnitude, or work
inside application-provided functions. They are not a timeout. maxSteps
counts AST work. It does not interrupt a slow environment function.
Store the expression source as the canonical value.
exp is currently pre-1.0. Syntax, AST shape, standard functions, and coercion
semantics may change between releases. Pin the package version and run saved
expressions through a regression corpus before upgrading.
The public AST is useful inside an application, but it is not currently a versioned storage format. See Persisted expressions for upgrade guidance and the compatibility areas to review.
parseExpression(input, options?)
evaluateExpression(input, options?)
evaluateAst(expression, options?)
formatDiagnosticReport(input, error)
Parsing throws by default. parseExpression() uses throwOnError, while
evaluateExpression() uses separate throwOnParseError and throwOnError
controls. throwOnError: false does not cover parser failures.
Use the generated JSR API reference for complete option types, result unions, AST variants, runtime value types, diagnostic helpers, and standard library exports.
Bun is the primary development toolchain.
The npm package is ESM-only and currently supports Node.js 22+. CI tests the packed package on Node.js 22, 24, and 26.
The same TypeScript source is published through JSR and smoke-tested with Deno 2.
bench/comparison.bench.ts runs the same parse and evaluation scenarios against
the base and candidate revisions. It is an internal regression check, not a
competitor comparison or a performance claim.
The scenarios cover short and larger expressions, parsing, pre-parsed AST evaluation, and parse + evaluation. Environment normalization is included in the evaluation scenarios.
The repository includes a Bun-based REPL and file runner for development. It is not installed with the published package.
bun run repl
bun run exp -- run expression.exp--env imports and executes a JavaScript or TypeScript module. Only load trusted
environment modules. --env-json contains data only, but expressions may still
call the built-in std functions.
bun install
bun run check
bun run package:check
bun run benchDeno remains part of JSR validation and runtime smoke coverage. The test suite includes generated parser and evaluator inputs alongside targeted runtime and member-access cases.
The README examples are executable through
examples/readme.test.ts.
MIT