simplex-lang
v1.1.1
Published
SimplEx - simple expression language
Maintainers
Readme
SimplEx
SimplEx — a zero-dependency TypeScript compiler that turns expression strings into safe, sandboxed JavaScript functions.
Table of contents
- Why SimplEx?
- Quick Start
- Playground
- Like JS, but…
- Language Reference
- Data and Scope
- API Reference
- Customization
- Standard Library
- Using External Functions
- AI / LLM Integration
- License
Why SimplEx?
SimplEx is designed for scenarios where you need to evaluate user-provided expressions safely:
- ETL/ELT pipelines — calculated fields, data transformations, filtering rules
- Business rules engines — config-driven formulas and conditions
- Template engines — dynamic value computation
- Spreadsheet-like UIs — user-defined formulas
Why not just eval()? SimplEx expressions run in a fully sandboxed environment with no access to the global scope, prototype chains, or Node.js/browser APIs. Users can only work with data and functions you explicitly provide.
Why not a full language? Every expression computes a value — no statements, no assignments, no loops. This makes expressions easy to reason about and safe to store in configs and databases.
What you get: Familiar JS syntax, runtime type safety, full customizability, zero dependencies.
Quick Start
npm install simplex-langimport { compile } from 'simplex-lang'
compile('a + b')({ a: 2, b: 3 }) // 5// Pass custom functions via globals, data at runtime
const fn = compile('clamp(score, 0, 100) * weight', {
globals: { clamp: (v, lo, hi) => Math.max(lo, Math.min(hi, v)) }
})
fn({ score: 150, weight: 0.5 }) // 50// Pure data expression — no globals needed
const expr = compile('price * quantity * (1 - discount)')
expr({ price: 100, quantity: 5, discount: 0.1 }) // 450Playground
Try SimplEx in the browser — edit expressions, inspect the AST, and see results instantly:
Like JS, but…
SimplEx syntax is intentionally close to JavaScript. If you know JS, you can start writing SimplEx immediately. Here are the key differences:
| Concept | JavaScript | SimplEx | Why |
|---|---|---|---|
| String concatenation | "a" + "b" | "a" & "b" | + is reserved for numeric addition only |
| Conditional | x ? a : b | if x then a else b | Readable keyword syntax |
| Modulo | a % b | a mod b | % is the topic reference in pipes |
| Exponentiation | a ** b | a ^ b | Shorter syntax |
| Logical NOT | !x | not x | Word operator |
| Logical AND/OR | a && b returns a or b | a and b returns boolean | &&/\|\| also available, but return booleans too |
| Equality | === / !== | == / != | Always strict — no loose equality exists |
| Optional chaining | obj?.prop | obj.prop | Null-safe by default — null.x returns undefined |
| Optional call | fn?.() | fn() | Calling null/undefined returns undefined |
| Pipe | Stage 2 proposal | x \| % + 1 | Built-in with % as topic reference |
| Partial application | — | fn(#, 3) | # creates a curried function |
| let | Statement | let x = 5, x + 1 | Expression that returns a value |
| in operator | Checks prototype chain | Checks own keys only | Works with objects, arrays, and Maps |
Everything else works as you'd expect from JavaScript: arrow functions (x => x + 1), template literals (`hello ${name}`), tagged templates, arrays, objects, spread operators, dot/bracket property access, nullish coalescing (??), typeof, and comments (//, /* */).
Language Reference
Literals
| Expression | Description |
| --- | --- |
| 42, .5, 1.2e3, 0xFF | Numbers (integer, decimal, scientific, hex) |
| "hello", 'world' | Strings (supports \n, \t, \uXXXX escapes) |
| true, false | Booleans |
| null | Null |
| undefined | Undefined (identifier, not a keyword) |
Operators
Operators listed by precedence (highest first):
| Precedence | Operators | Description |
|---|---|---|
| 1 | +x -x not x typeof x | Unary |
| 2 | ^ | Exponentiation (right-associative) |
| 3 | * / mod | Multiplicative |
| 4 | + - | Additive (numbers only) |
| 5 | & | String concatenation (coerces to string) |
| 6 | < <= > >= in | Relational |
| 7 | == != | Equality (strict) |
| 8 | and && | Logical AND (short-circuit, returns boolean) |
| 9 | or \|\| | Logical OR (short-circuit, returns boolean) |
| 10 | ?? | Nullish coalescing |
| 11 | \| \|? \|> | Pipe operators |
Runtime type enforcement:
- Arithmetic (
+,-,*,/,mod,^) — operands must be finite numbers or bigints - Relational (
<,>,<=,>=) — operands must be numbers or strings &— coerces any value to string==/!=— strict comparison, no coercion
String Concatenation
The + operator only works with numbers. Use & to concatenate strings:
| Expression | Result |
| --- | --- |
| "Hello" & " " & "world" | "Hello world" |
| "Count: " & 42 | "Count: 42" (coerces to string) |
| "Values: " & [1, 2, 3] | "Values: 1,2,3" |
Collections
Arrays:
| Expression | Description |
| --- | --- |
| [1, 2, 3] | Array literal |
| [1, , 3] | Sparse array |
| [1, ...other, 4] | Spread (arrays only) |
Objects:
| Expression | Description |
| --- | --- |
| { a: 1, b: 2 } | Object literal |
| { "special-key": 1 } | Quoted key |
| { [dynamic]: value } | Computed key |
| { x, y } | Shorthand property ({ x: x, y: y }) |
| { ...base, extra: true } | Spread (objects only) |
Property Access
| Expression | Description |
| --- | --- |
| obj.name | Dot access (own properties only) |
| obj["key"] | Bracket access (own properties only) |
| arr[0] | Index access |
| obj.nested.deep | Chaining |
| null.anything | undefined (null-safe, no error) |
| expr! | Non-null assert — throws if null/undefined |
| a.b!.c.d! | Chainable non-null assertions |
| foo!(args) | Assert non-null, then call |
Note: Unlike JavaScript (which has optional chaining
?.and no runtime!), SimplEx has null-safe member access by default but explicit non-null assertion via!. This is inverted from JS — more practical for an expression language working with optional data structures.
Extension methods (::) — call methods registered via the extensions compile option. obj::method(args) calls extensionMap.method(obj, args). Requires extensions in CompileOptions. Null-safe: null::method() returns undefined. Throws if no extension methods are defined for the type or the method is not found.
const extensions = new Map([
['string', {
capitalize: (s: string) => s[0].toUpperCase() + s.slice(1),
truncate: (s: string, len: number) => s.length > len ? s.slice(0, len) + '...' : s
}]
])
compile('"hello"::capitalize()', { extensions })() // "Hello"
compile('"long text here"::truncate(8)', { extensions })() // "long tex..."Function Calls
| Expression | Description |
| --- | --- |
| min(1, 2) | Global function |
| obj.method(x) | Method call |
| fn()() | Chaining |
| null() | undefined (null-safe) |
Currying with #
The # placeholder in function arguments creates a partially applied function:
| Expression | Equivalent |
| --- | --- |
| add(#, 3) | x => add(x, 3) |
| add(1, #) | x => add(1, x) |
| mul(#, 2, #) | (a, b) => mul(a, 2, b) |
| [1, 2, 3] \| map(%, add(#, 10)) | [11, 12, 13] |
Conditionals
| Expression | Description |
| --- | --- |
| if score >= 90 then "A" else "B" | Conditional with else |
| if active then value | Else is optional (defaults to undefined) |
Falsy values: 0, "", false, null, undefined, NaN. Everything else is truthy.
Pipe Operators
Pipes chain a value through a series of expressions. The % topic reference holds the current value:
| Expression | Result |
| --- | --- |
| 5 \| % + 1 | 6 |
| 5 \| % * 2 \| % + 1 | 11 |
| 1 \| add(%, 2) \| % * 4 | 12 |
| value \|? toUpper(%) | If value is null, returns null (\|? short-circuits) |
|> (forward pipe) — reserved (not available by default). Override pipe in compile options to implement custom semantics.
Lambda Expressions
| Expression | Description |
| --- | --- |
| x => x + 1 | Single parameter |
| (a, b) => a + b | Multiple parameters |
| () => 42 | No parameters |
| a => b => a + b | Curried (nested) |
Lambdas are closures — they capture the enclosing scope. Parameters shadow outer variables.
Let Expressions
let creates local bindings and evaluates a body expression:
| Expression | Result |
| --- | --- |
| let x = 5, x + 1 | 6 |
| let a = 1, b = a + 1, a + b | 3 |
| let tax = price * 0.2, price + tax | Sequential binding |
Bindings are sequential — each initializer can reference previous bindings. The last comma-separated expression is the body. Duplicate names cause a CompileError.
Template Literals
| Expression | Description |
| --- | --- |
| `Hello ${name}, you have ${count} items` | String interpolation |
| `Price: ${price * (1 + tax)}` | Any expression inside ${} |
| `Nested: ${`inner ${x}`}` | Nested template literals |
| Multiline content | Allowed (unlike regular strings) |
Tagged template literals — any expression before a template literal calls it as a tag function:
| Expression | Description |
| --- | --- |
| sql`SELECT * FROM ${table}` | Tag receives (strings, ...values) |
| obj.escape`user input: ${value}` | Member expression as tag |
The tag function receives an array of static string parts and the interpolated values (not coerced to strings). It can return any type.
Comments
| Syntax | Description |
| --- | --- |
| // comment | Single-line comment |
| /* comment */ | Multi-line / inline comment |
Reserved Words
if, then, else, and, or, not, in, mod, typeof, let, true, false, null — cannot be used as identifiers.
Data and Scope
Identifiers are resolved in this order: local scope (lambda params, let bindings) -> closure -> globals -> data -> error.
// Globals — compile-time constants, always take priority
const fn = compile('x + y', { globals: { x: 10 } })
fn({ x: 999, y: 20 }) // 30 (x=10 from globals, y=20 from data)
// Data — runtime values passed when calling the compiled function
const expr = compile('firstName & " " & lastName')
expr({ firstName: 'John', lastName: 'Doe' }) // "John Doe"Globals take priority over data. This lets you provide trusted constants and functions that user expressions cannot override.
API Reference
compile()
import { compile } from 'simplex-lang'
function compile<
Data = Record<string, unknown>,
Globals = Record<string, unknown>
>(
expression: string,
options?: CompileOptions<Data, Globals>
): (data?: Data) => unknownCompiles a SimplEx expression string into a reusable function. The returned function accepts an optional data argument and returns the result of evaluating the expression.
CompileOptions
type CompileOptions<Data, Globals> = Partial<
ContextHelpers<Data, Globals> &
ExpressionOperators & {
globals: Globals
extensions: Map<string | object | Function, Record<string, Function>>
errorMapper: ErrorMapper | null
}
>All fields are optional. You can override any combination of:
| Option | Type | Description |
|---|---|---|
| globals | Globals | Compile-time constants and functions available to the expression |
| extensions | Map<string \| object \| Function, Record<string, Function>> | Extension methods for :: operator. Keys: typeof string or class/constructor. Values: method bags |
| errorMapper | ErrorMapper \| null | Error mapping strategy. Default: auto-detected (V8). null disables mapping |
| getIdentifierValue | (name, globals, data) => unknown | Custom identifier resolution |
| getProperty | (obj, key, extension) => unknown | Custom property access. extension is true for :: access |
| callFunction | (fn, args) => unknown | Custom function call behavior |
| pipe | (head, tail) => unknown | Custom pipe operator behavior |
| nonNullAssert | (val) => unknown | Custom non-null assertion for ! operator |
| castToBoolean | (val) => boolean | Custom truthiness rules (affects if, and, or, not) |
| castToString | (val) => string | Custom string coercion (affects & and template literals) |
| ensureFunction | (val) => Function | Custom function validation |
| ensureObject | (val) => object | Custom object validation (for spread) |
| ensureArray | (val) => unknown[] | Custom array validation (for spread) |
| unaryOperators | Record<op, (val) => unknown> | Override unary operators |
| binaryOperators | Record<op, (left, right) => unknown> | Override binary operators |
| logicalOperators | Record<op, (left, right) => unknown> | Override logical operators (args are thunks) |
Errors
All SimplEx errors include the original expression and source location for precise error reporting.
ExpressionError — runtime evaluation error (unknown identifier, type mismatch, invalid operation):
import { ExpressionError } from 'simplex-lang'
try {
compile('x + 1')({}) // x is not defined
} catch (err) {
if (err instanceof ExpressionError) {
err.message // "Unknown identifier - x"
err.expression // "x + 1"
err.location // { start: { offset, line, column }, end: { ... } }
}
}CompileError — compilation error (e.g., duplicate let bindings):
import { CompileError } from 'simplex-lang'
compile('let a = 1, a = 2, a') // throws CompileErrorUnexpectedTypeError — runtime type validation error:
import { UnexpectedTypeError } from 'simplex-lang'
compile('"hello" + 1')() // throws UnexpectedTypeError: expected numberCustomization
Every aspect of SimplEx evaluation can be customized through compile options.
Custom operators — override or extend any operator:
import {
compile,
defaultBinaryOperators,
defaultUnaryOperators
} from 'simplex-lang'
const fn = compile('not -a + b', {
unaryOperators: {
...defaultUnaryOperators,
not: val => Number(val) + 1 // redefine "not"
},
binaryOperators: {
...defaultBinaryOperators,
'+': (a, b) => Number(a) * Number(b) // make "+" multiply
}
})Custom identifier resolution — control how variables are looked up:
// Use a Map instead of a plain object for globals
const fn = compile('foo', {
globals: new Map([['foo', 'bar']]),
getIdentifierValue(name, globals, data) {
if (globals.has(name)) return globals.get(name)
return data[name]
}
})Custom property access — intercept or transform property lookups:
const fn = compile('a.b', {
getProperty: (obj, key, extension) => `custom:${String(key)}`
})
fn({ a: { b: 'real' } }) // "custom:b"Custom function calls — wrap or intercept function invocations:
const fn = compile('f(1, 2)', {
globals: { f: (a, b) => a + b },
callFunction: (fn, args) => {
if (args === null) return fn()
return `intercepted:${fn(...args)}`
}
})
fn() // "intercepted:3"Custom pipe — implement your own pipe semantics:
const fn = compile('1 | % + 1', {
pipe: (head, tail) => {
let result = head
for (const t of tail) {
result = `piped:${t.next(result)}`
}
return result
}
})
fn() // "piped:2"Custom boolean coercion — change what counts as truthy/falsy (affects if, and, or, not):
const fn = compile('if a then "yes" else "no"', {
castToBoolean: val => val === 'truthy'
})
fn({ a: 'truthy' }) // "yes"
fn({ a: true }) // "no" — only the string "truthy" is truthy nowStandard Library
SimplEx includes a built-in standard library with namespaced functions and extension methods:
import { compile } from 'simplex-lang'
import { createStdlib } from 'simplex-lang/stdlib'
const { globals, extensions } = createStdlib()
compile('Math.abs(x) + Str.upper(name)', { globals, extensions })({
x: -5,
name: 'hello'
}) // 5 + "HELLO" → uses Math and Str namespacesNamespaces: Str, Num, Math, Arr, Obj, Json, Date + top-level utilities (empty, exists, typeOf).
Extension methods let you use method-call syntax: x::abs(), items::map(fn), name::upper().
Key conventions:
- NaN → null — functions that would return
NaNin JS returnnullinstead. Use??to provide defaults:Math.sqrt(x) ?? 0 - Immutable — array operations return new copies (no mutation)
See Standard Library Reference for the full API.
Using External Functions
Beyond the Standard Library, you can provide any custom functions via globals. This is useful for domain-specific logic. To combine stdlib with your own functions, spread them together:
import { createStdlib } from 'simplex-lang/stdlib'
const { globals, extensions } = createStdlib()
const fn = compile(
`price * quantity * (1 - discount)
| Math.round(%)
| formatPrice(%)`,
{
globals: {
...globals,
formatPrice: (val) => `$${val.toFixed(2)}`
},
extensions
}
)
fn({ price: 19.99, quantity: 3, discount: 0.15 }) // "$51.00"Domain-specific helpers:
const fn = compile(
`
if classify(score) == "A" then
bonus(salary)
else
salary
`,
{
globals: {
classify: (score) => (score >= 90 ? 'A' : score >= 70 ? 'B' : 'C'),
bonus: (salary) => salary * 1.2
}
}
)
fn({ score: 95, salary: 50000 }) // 60000Combining with currying:
const fn = compile('items | map(%, mul(#, factor))', {
globals: {
map: (arr, fn) => arr.map(fn),
mul: (a, b) => a * b
}
})
fn({ items: [1, 2, 3], factor: 10 }) // [10, 20, 30]AI / LLM Integration
SimplEx is well-suited as a target language for AI-generated expressions:
- Safe by design — no access to globals, filesystem, or network
- Deterministic — same input always produces the same output
- Simple grammar — LLMs can generate valid SimplEx with minimal prompting
- Validation — compilation catches errors before runtime
// AI generates expression strings, SimplEx runs them safely
const userFormula = aiResponse.expression // e.g., "price * quantity * (1 - discount)"
const fn = compile(userFormula)
fn(data) // safe executionExpressions are compiled once to native JS functions via
new Function()— subsequent calls have near-native performance.
