exp
A small expression language toolkit: parse expressions, get a typed AST with spans, and evaluate safely.
import { evaluateExpression } from "jsr:@claudiu-ceia/exp";
const res = evaluateExpression('status == "open" && priority >= 3', {
env: { status: "open", priority: 4 },
throwOnError: false,
});
if (res.success) {
console.log(res.value); // true
}Overview
exp is a tiny, deterministic expression parser + evaluator intended for βmini-languageβ use cases:
- API-rich filters (
status == "open" && priority >= 3) - data-wrangling pipelines (
input |> map(...) |> filter(...)) - backtesting strategies (signals, conditions, thresholds) in a constrained DSL
Design goals:
- No
eval/new Functionβ all evaluation is interpreter-based. - Typed AST + spans β nodes carry
{ start, end }indices for diagnostics. - Safe-by-default access β expressions only touch data/functions you place
in
env. - Budgeted evaluation β max steps, recursion depth, and array literal size.
Documentation
- Installation
- Getting started
- Supported syntax
- Safe evaluation model
- API reference
- Errors and diagnostics
- CLI
- Development
- License
Why
The expression language is intentionally small, but ergonomic enough for real application DSLs.
Non-goals
- Full JavaScript parsing.
- Executing untrusted code via
eval/new Function.
Supported syntax (today)
Expressions:
- literals: numbers, strings,
true,false,null - identifiers:
[A-Za-z_]followed by[A-Za-z0-9_]*(withtrue/false/nullreserved) - arrays:
[expr, expr, ...] - grouping:
(expr) - postfix chaining:
expr.identandexpr(arg1, arg2, ...)(chainable) - unary:
!,+,- - binary (with precedence):
* / %,+ -,< <= > >=,== !=,&& || - conditional:
test ? consequent : alternate - pipeline:
lhs |> fnandlhs |> fn(arg1, arg2, ...)(desugars tofn(lhs)/fn(lhs, ...))
Equality semantics (== / !=)
Equality is intentionally JS-like for primitives, but never coerces
objects/arrays/functions via implicit ToPrimitive (so no surprise
toString() / valueOf() calls).
- Primitives: loosely coerced similar to JavaScript
null == undefinedistrue- booleans coerce to numbers (
trueβ1,falseβ0) - strings and numbers may coerce via
Number(...)
- Non-primitives (plain objects, arrays, functions): reference equality only
user == usercan betrueuser == "[object Object]"isfalse(no coercion)
String literals:
- single or double quotes
- ECMAScript-oriented escape semantics (see
src/string_literal.tsfor tc39 links) - strict-mode-style failures for digit/octal escapes
Examples
Filters:
status == "open" && priority >= 3user.plan != "free" && (user.age >= 18 || user.admin == true)
Chaining:
user.profile.namefn(1, 2).next(3).done
Safe evaluation model
Use evaluateExpression to parse + evaluate in one step, with an explicit
environment and resource budgets.
At a high level:
- Identifiers read from
envonly. - Member access is restricted.
- Calls are only possible through functions present in
env. - Evaluation has configurable budgets.
env and runtime values
env is the only way expressions can access data and functions. Identifiers
resolve to properties on env.
- Missing identifiers throw by default (
unknownIdentifier: "error"). - Set
unknownIdentifier: "undefined"to treat missing identifiers asundefined. - Values must be made of supported runtime values:
- primitives:
undefined | null | boolean | number | string - arrays of supported values
- plain objects (
{...}) whose values are supported values - functions that accept/return supported values
- primitives:
Member access (obj.prop) is intentionally conservative:
- Works on plain objects (and arrays only expose
.length). - Blocks
__proto__,prototype, andconstructor.
env is validated at runtime: it must be a plain object (or proto-null object),
and all nested values must be supported runtime values.
Member access restrictions
- Only plain objects expose own-properties.
- Arrays expose
.lengthonly. - Everything else returns
undefined.
This is designed to avoid prototype leakage and surprise access to inherited properties.
Resource budgets
Evaluation supports a few defensive limits (all optional):
maxSteps(default10_000): max AST nodes visitedmaxDepth(default256): max recursion depth while evaluatingmaxArrayElements(default1_000): max elements in an array literal
Example: filter over an input object
import { evaluateExpression } from "jsr:@claudiu-ceia/exp";
const env = {
status: "open",
priority: 4,
};
const res = evaluateExpression('status == "open" && priority >= 3', {
env,
throwOnError: false,
});Example: allow-listed helper functions
import { evaluateExpression } from "jsr:@claudiu-ceia/exp";
const env = {
lower: (s: unknown) => (typeof s === "string" ? s.toLowerCase() : ""),
contains: (s: unknown, sub: unknown) => {
if (typeof s !== "string") throw new Error("contains: expected string");
if (typeof sub !== "string") throw new Error("contains: expected string");
return s.includes(sub);
},
user: { plan: "Free" },
};
const res = evaluateExpression('contains(lower(user.plan), "free")', {
env,
maxSteps: 5_000,
throwOnError: false,
});Installation
Deno / JSR
import { evaluateExpression } from "jsr:@claudiu-ceia/exp";Or add it to your project:
deno add jsr:@claudiu-ceia/expnpm
This package is published for npm via a generated build.
npx jsr add @claudiu-ceia/expThen:
import { evaluateExpression } from "@claudiu-ceia/exp";Getting started
Parse only
import { parseExpression } from "jsr:@claudiu-ceia/exp";
const parsed = parseExpression("1 + 2 * 3", { throwOnError: false });
if (parsed.success) {
console.log(parsed.value.kind); // "binary"
}Evaluate a pre-parsed AST
import { evaluateAst, parseExpression } from "jsr:@claudiu-ceia/exp";
const ast = parseExpression("x + 1").value;
const out = evaluateAst(ast, { env: { x: 41 }, throwOnError: false });API reference
parseExpression(input, opts?)
Parse a single expression into a typed AST.
- Import:
import { parseExpression } from "jsr:@claudiu-ceia/exp" - Returns:
ParseResult - Throws:
ExpParseError(default behavior)
ParseOptions
throwOnError?: booleanβ defaulttrue
ParseResult
- Success:
{ success: true, value: Expr } - Failure:
{ success: false, error: ParseError }
ParseError
message: stringβ compact parser error messageindex: numberβ byte index into the input string
evaluateExpression(input, opts?)
Parse + evaluate in one step.
- Import:
import { evaluateExpression } from "jsr:@claudiu-ceia/exp" - Returns:
EvalResult - Throws:
ExpEvalError(default behavior)
EvaluateExpressionOptions
Includes all EvalOptions plus:
throwOnParseError?: booleanβ defaulttrue
Parse errors:
- If
throwOnParseErroristrue(default), parse errors throwExpParseError. - If
throwOnParseErrorisfalse, parse errors return{ success: false, error: { message, index, steps: 0 } }.
evaluateAst(expr, opts?)
Evaluate a pre-parsed AST.
- Import:
import { evaluateAst } from "jsr:@claudiu-ceia/exp" - Returns:
EvalResult - Throws:
ExpEvalError(default behavior)
env is validated at runtime before evaluation begins.
Options and result types
EvalOptions
env?: Record<string, RuntimeValue>β default{}unknownIdentifier?: "error" | "undefined"β default"error"maxSteps?: numberβ default10_000maxDepth?: numberβ default256maxArrayElements?: numberβ default1_000throwOnError?: booleanβ defaulttrue
EvalResult
- Success:
{ success: true, value: RuntimeValue } - Failure:
{ success: false, error: EvalError }
EvalError
message: stringβ user-facing error messagespan?: Spanβ present for evaluation errors tied to an AST nodesteps?: numberβ step counter at time of failureindex?: numberβ present when failure is due to parse error (only returned whenthrowOnParseError: false)
AST types
All AST nodes include span: { start: number; end: number }.
Expr is a tagged union with these kinds:
number,string,boolean,nullidentifierarrayunarybinarymembercallconditional
Runtime values
RuntimeValue is the allowed runtime data model:
- primitives:
undefined | null | boolean | number | string - arrays of
RuntimeValue - plain objects (
{...}orObject.create(null)) withRuntimeValuevalues - functions:
(...args: RuntimeValue[]) => RuntimeValue
Notes:
envmust be a plain/proto-null object at runtime; class instances (e.g.Date) are rejected.- Function return values are validated; returning an unsupported value fails evaluation.
Errors and diagnostics
When you enable throwing (the default), youβll get typed errors.
ExpParseError
- Extends
Error - Fields:
index: number
ExpEvalError
- Extends
Error - Fields:
span?: Spansteps?: numberindex?: number
This makes it easy to render caret diagnostics from either a byte index or an
AST span.
Example caret formatter:
import { formatCaret } from "jsr:@claudiu-ceia/exp";
console.log(formatCaret("1 + ", 4));This package also exports a richer report-style formatter (used by the CLI):
import { formatDiagnosticReport } from "jsr:@claudiu-ceia/exp";
console.log(
formatDiagnosticReport("1 + ", {
message: "expected expression at 1:4",
index: 4,
}),
);Diagnostics helpers exported from mod.ts:
formatCaret/formatSpanCaretformatDiagnosticCaret(prefersindex, falls back tospan.start)formatDiagnosticReport(Elm/OCaml-inspired report output)ExpParseError: includesindex(byte index into the input string)ExpEvalError: includesspan(AST span) andsteps(budget counter)
If you prefer non-throwing control flow, use throwOnError: false and inspect
the returned { success: false, error: { message, span?, steps?, index? } }.
Development
deno task checkdeno test
CLI
This repo includes a small Deno-only CLI (not part of the npm build), built with
@stricli/core.
deno task repldeno task exp -- run [file]
Providing env
For real usage, you typically want helper functions in env (so JSON alone is
often not enough). The CLI supports both:
--env path/to/env.ts(JS/TS module; supports functions)--env-json path/to/env.json(JSON object; values only)
Example env.ts:
export const env = {
lower: (s: unknown) => (typeof s === "string" ? s.toLowerCase() : ""),
user: { plan: "Free" },
};Run:
deno task exp -- run --env ./env.ts program.expr
echo '1 + 2*3' | deno task exp -- run
deno task repl -- --env ./env.ts
# value-only env (no functions)
echo 'x + 1' | deno task exp -- run --env-inline '{"x": 41}'
# see full flag docs
deno task exp -- --helpLicense
MIT