Skip to content

JavaScript Runtime

Package: @xpr-lang/xpr · GitHub · v0.5.0

Install

bash
npm install @xpr-lang/xpr
# or
bun add @xpr-lang/xpr

API

new Xpr()

Creates a new XPR engine instance.

typescript
import { Xpr } from '@xpr-lang/xpr';
const engine = new Xpr();

engine.evaluate(expression, context?)

Evaluates an XPR expression string against an optional context object. Returns the result value.

Throws XprError if the expression is invalid or a runtime error occurs.

typescript
engine.evaluate('1 + 2')
// → 3

engine.evaluate('user.name ?? "anonymous"', { user: { name: null } })
// → "anonymous"

engine.evaluate('[1,2,3].map(x => x * 2)')
// → [2, 4, 6]

engine.evaluate('items.filter(x => x.active).map(x => x.name)', {
  items: [
    { name: 'a', active: true },
    { name: 'b', active: false },
  ]
})
// → ['a']

engine.addFunction(name, fn)

Registers a custom function that can be called from expressions.

typescript
engine.addFunction('slugify', (s: unknown) =>
  String(s).toLowerCase().replace(/\s+/g, '-')
);

engine.evaluate('slugify(product.name)', { product: { name: 'Hello World' } })
// → 'hello-world'

XprError

Thrown on parse errors and runtime errors. Has a position property (character offset).

typescript
import { Xpr, XprError } from '@xpr-lang/xpr';

try {
  engine.evaluate('1 / 0');
} catch (e) {
  if (e instanceof XprError) {
    console.error(e.message); // "Division by zero"
  }
}

TypeScript

The package ships with full TypeScript declarations.

typescript
import { Xpr, XprError } from '@xpr-lang/xpr';

const engine = new Xpr();
const result: unknown = engine.evaluate('1 + 2');

v0.2 Features

Let Bindings

typescript
engine.evaluate('let x = 1; let y = x + 1; y')
// → 2

engine.evaluate('let f = (x) => x * 2; f(5)')
// → 10

engine.evaluate('let items = [1,2,3,4,5]; items.filter(x => x > 2).map(x => x * 10)')
// → [30, 40, 50]

Spread Operator

typescript
engine.evaluate('[...[1,2], ...[3,4]]')
// → [1, 2, 3, 4]

engine.evaluate('{...defaults, ...overrides}', {
  defaults: { color: 'blue', size: 10 },
  overrides: { color: 'red' },
})
// → { color: 'red', size: 10 }

New Array Methods

typescript
engine.evaluate('[1,2,3].includes(2)')          // → true
engine.evaluate('[1,2,3].indexOf(2)')           // → 1
engine.evaluate('[1,2,3,4,5].slice(1, 3)')      // → [2, 3]
engine.evaluate('[1,2,3].join(", ")')           // → "1, 2, 3"
engine.evaluate('[1,2].concat([3,4])')          // → [1, 2, 3, 4]
engine.evaluate('[[1,2],[3,4]].flat()')         // → [1, 2, 3, 4]
engine.evaluate('[1,2,1,3].unique()')           // → [1, 2, 3]
engine.evaluate('[1,2,3].zip([4,5,6])')         // → [[1,4],[2,5],[3,6]]
engine.evaluate('[1,2,3,4,5].chunk(2)')         // → [[1,2],[3,4],[5]]
engine.evaluate('[1,2,3].groupBy(x => x > 1 ? "big" : "small")')
// → { big: [2, 3], small: [1] }

New String Methods

typescript
engine.evaluate('"hello".indexOf("ll")')        // → 2
engine.evaluate('"ab".repeat(3)')               // → "ababab"
engine.evaluate('"  hi  ".trimStart()')         // → "hi  "
engine.evaluate('"  hi  ".trimEnd()')           // → "  hi"
engine.evaluate('"hello".charAt(1)')            // → "e"
engine.evaluate('"42".padStart(5, "0")')        // → "00042"
engine.evaluate('"hi".padEnd(5, ".")')          // → "hi..."

New Object Methods

typescript
engine.evaluate('{"b": 2, "a": 1}.entries()')  // → [["a",1],["b",2]]
engine.evaluate('{"a": 1}.has("a")')            // → true
engine.evaluate('{"a": 1}.has("b")')            // → false

range() Function

typescript
engine.evaluate('range(5)')           // → [0, 1, 2, 3, 4]
engine.evaluate('range(1, 5)')        // → [1, 2, 3, 4]
engine.evaluate('range(0, 10, 2)')    // → [0, 2, 4, 6, 8]
engine.evaluate('range(5, 0, -1)')    // → [5, 4, 3, 2, 1]

v0.3 Features

Date/Time

Dates are epoch milliseconds (UTC only). ICU format tokens: yyyy, MM, dd, HH, mm, ss.

typescript
engine.evaluate('formatDate(now(), "yyyy-MM-dd")')
// → "2026-03-15"

engine.evaluate('dateDiff(parseDate("2024-01-01T00:00:00Z"), now(), "days")')
// → 439

engine.evaluate('dateAdd(parseDate("2024-01-31T00:00:00Z"), 1, "months")')
// → 1709337600000

Regex Functions

RE2 flavor. Double-escape backslashes in JS string literals.

typescript
engine.evaluate('matches("hello 42", "\\\\d+")')               // → true
engine.evaluate('match("order-123", "\\\\d+")')                 // → "123"
engine.evaluate('matchAll("a1b2c3", "\\\\d")')                  // → ["1", "2", "3"]
engine.evaluate('replacePattern("2024-01-15", "(\\\\d{4})-(\\\\d{2})-(\\\\d{2})", "$3/$2/$1")')
// → "15/01/2024"

Negative Indexing and Spread in Calls

typescript
engine.evaluate('[1,2,3][-1]')           // → 3
engine.evaluate('max(...[1, 5, 3, 2])')  // → 5

v0.4 Features

Destructuring

typescript
engine.evaluate('let {name, age} = user; name', {user: {name: 'Alice', age: 30}})
// → 'Alice'

engine.evaluate('users.map(({name, age}) => `${name}: ${age}`)',
  {users: [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]})
// → ['Alice: 30', 'Bob: 25']

engine.evaluate('let [head, ...tail] = items; tail', {items: [1, 2, 3]})
// → [2, 3]

engine.evaluate('let {address: {city}} = user; city', {user: {address: {city: 'NYC'}}})
// → 'NYC'

Regex Literals

typescript
engine.evaluate('/\\\\d+/.test("order-123")')       // → true
engine.evaluate('"2024-01-15".match(/\\\\d{4}/)')   // → "2024"
engine.evaluate('"hello world".replace(/o/, "0")')  // → "hell0 w0rld"

v0.5 Features

Math Functions and Constants

typescript
engine.evaluate('sqrt(16)')        // → 4
engine.evaluate('log(E)')          // → 1
engine.evaluate('pow(2, 10)')      // → 1024
engine.evaluate('PI * pow(5, 2)')  // → 78.53981633974483
engine.evaluate('sign(-7)')        // → -1
engine.evaluate('trunc(3.9)')      // → 3

Type Predicates

typescript
engine.evaluate('isNumber(42)')       // → true
engine.evaluate('isString("x")')      // → true
engine.evaluate('isArray([1,2])')     // → true
engine.evaluate('isObject([1,2])')    // → false (arrays are "array" type)
engine.evaluate('isNull(null)')       // → true
engine.evaluate('isRegex(/\\\\d+/)')  // → true

New Array Methods

typescript
engine.evaluate('[3,null,1,null,5].compact().sortBy(x => x)')   // → [1, 3, 5]
engine.evaluate('[1,2,3,4,5].take(3)')                           // → [1, 2, 3]
engine.evaluate('[1,2,3,4,5].drop(2)')                           // → [3, 4, 5]
engine.evaluate('[1,2,3,4].sum()')                               // → 10
engine.evaluate('[1,2,3,4].avg()')                               // → 2.5
engine.evaluate('[1,2,3,4].partition(x => x > 2)')               // → [[3, 4], [1, 2]]
engine.evaluate('items.keyBy(x => x.id)', {items: [{id:'a',v:1},{id:'b',v:2}]})
// → { a: {id:'a',v:1}, b: {id:'b',v:2} }
engine.evaluate('[3,1,2].first()')  // → 3
engine.evaluate('[3,1,2].last()')   // → 2
engine.evaluate('[1,2,3].count(x => x > 1)')  // → 2

fromEntries, str.split(/regex/), Rest Parameters

typescript
engine.evaluate('fromEntries([["a", 1], ["b", 2]])')  // → { a: 1, b: 2 }
engine.evaluate('"a1b2c3".split(/\\\\d+/)')             // → ["a", "b", "c"]
engine.evaluate('[1,2,3].map((...args) => args.length)')  // → [1, 1, 1]