Skip to content

Language Specification

This page covers XPR v0.5

XPR is a sandboxed expression language with JS/Python-familiar syntax, designed for data pipeline transforms.

Literals

javascript
42          // integer
3.14        // float
"hello"     // string (double or single quotes)
true        // boolean
null        // null
[1, 2, 3]   // array
{"key": "value"}  // object

Operators

javascript
// Arithmetic
+  -  *  /  %  **

// Comparison
==  !=  <  >  <=  >=

// Logical
&&  ||  !

// Nullish coalescing
??

// Ternary
age >= 18 ? "adult" : "minor"

Access

javascript
user.name           // dot access
user["name"]        // bracket access
users[0]            // array index
user?.address?.city // optional chaining (returns null if any step is null)

Arrow Functions

Arrow functions are first-class values in XPR — they can be stored in let bindings and passed to higher-order methods.

javascript
x => x * 2
(x, y) => x + y
() => 42

Let Bindings

let creates an immutable binding scoped to the rest of the expression. Multiple bindings are chained with semicolons; the final expression (after the last ;) is the result.

javascript
let x = 1; x + 1                          // → 2
let x = 1; let y = x + 1; y               // → 2
let name = "world"; `hello ${name}`        // → "hello world"
let f = (x) => x * 2; f(5)                // → 10
let a = 10; let f = (x) => x + a; f(5)    // → 15
let items = [1,2,3,4,5]; items.filter(x => x > 2).map(x => x * 10)  // → [30,40,50]

Scoping rules:

  • Bindings are immutable — no reassignment
  • Shadowing is allowed: let x = 1; let x = 2; x2
  • Forward references work: each binding sees all previous bindings
  • Arrow functions close over let bindings

Errors:

  • let x = 1; (no body after semicolon) → error: "Expected expression after ';'"
  • let = 1; 2 (missing name) → error

Spread Operator

Array Spread

javascript
[...[1,2], 3, 4]          // → [1,2,3,4]
[...[1,2], ...[3,4]]      // → [1,2,3,4]
[0, ...[1,2]]             // → [0,1,2]
[...[]]                   // → []

Errors:

  • [...42] → "Cannot spread non-array into array"
  • [...null] → "Cannot spread null"
  • [..."hello"] → "Cannot spread string into array"

Object Spread

javascript
{...{"a": 1, "b": 2}}              // → {"a": 1, "b": 2}
{...{"a": 1}, "a": 2}              // → {"a": 2}  (override)
{...{"a": 1}, ...{"b": 2}}         // → {"a": 1, "b": 2}
{...user, "role": "admin"}         // merge user object, add role

Errors:

  • {...42} → "Cannot spread non-object"
  • {...null} → "Cannot spread null"
  • {...[1,2]} → "Cannot spread array into object"

Collection Operations

javascript
items.map(x => x.price * x.qty)
items.filter(x => x.active && x.price > 10)
items.reduce((sum, x) => sum + x, 0)
items.find(x => x.id == targetId)
items.some(x => x.price > 100)
items.every(x => x.active)
items.flatMap(x => x.tags)
items.sort((a, b) => a.price - b.price)
items.reverse()
items.length

// v0.2 additions
items.includes(value)          // → boolean (strict equality)
items.indexOf(value)           // → number (-1 if not found)
items.slice(1, 3)              // → array (same semantics as string slice)
items.join(", ")               // → string
items.concat(other)            // → new array
items.flat()                   // → array (one level deep)
items.unique()                 // → array (preserves first occurrence)
a.zip(b)                       // → array of [a[i], b[i]] pairs (truncates to shortest)
items.chunk(2)                 // → array of arrays (last chunk may be smaller)
items.groupBy(x => x.type)    // → object (keys alphabetical, values are arrays)

String Operations

javascript
name.upper()
name.lower()
name.trim()
name.len()
name.startsWith("Dr.")
name.endsWith("!")
name.contains("admin")
name.split(",")
name.replace("old", "new")
name.slice(0, 5)

// v0.2 additions
name.indexOf("el")             // → number (-1 if not found)
name.repeat(3)                 // → string (n must be non-negative integer)
name.trimStart()               // → string (removes leading whitespace)
name.trimEnd()                 // → string (removes trailing whitespace)
name.charAt(0)                 // → string (single char; "" if out of bounds)
name.padStart(5, "0")          // → string (pads start to length n; default pad char " ")
name.padEnd(5, ".")            // → string (pads end to length n; default pad char " ")

Template Literals

javascript
`Hello ${name}, you have ${count} items`

Math Functions

javascript
round(3.7)   // → 4
floor(3.7)   // → 3
ceil(3.2)    // → 4
abs(-5)      // → 5
min(a, b)
max(a, b)

// v0.2 addition
range(5)           // → [0,1,2,3,4]
range(1, 5)        // → [1,2,3,4]
range(0, 10, 2)    // → [0,2,4,6,8]
range(5, 0, -1)    // → [5,4,3,2,1]
// Float step → error. Empty range if start ≥ end with positive step.

Type Functions

javascript
type(value)    // "null" | "boolean" | "number" | "string" | "array" | "object"
int("42")      // → 42
float("3.14")  // → 3.14
string(42)     // → "42"
bool(1)        // → true

Object Operations

javascript
{"a": 1, "b": 2}.keys()    // → ["a", "b"]
{"a": 1, "b": 2}.values()  // → [1, 2]

// v0.2 additions
{"a": 1, "b": 2}.entries()  // → [["a",1],["b",2]] (alphabetical key order)
{"a": 1, "b": 2}.has("a")   // → true
{"a": 1, "b": 2}.has("c")   // → false

Pipe Operator

Sugar for chaining transforms — the left side becomes the first argument:

javascript
data |> filter(x => x.active) |> map(x => x.name) |> sort()

// Works with let bindings too
let data = [1,2,3]; data |> map(x => x * 2)   // → [2,4,6]

Truthiness

The following values are falsy: false, null, 0, ""

Everything else is truthy.

Nullish Coalescing

?? returns the right side only when the left side is null (not false or 0):

javascript
user.name ?? "anonymous"   // "anonymous" only if name is null
count ?? 0                 // 0 only if count is null

Security

  • No access to host filesystem, network, or environment variables
  • Context is read-only — expressions cannot mutate input
  • No eval, no dynamic code construction
  • Blocked properties: __proto__, constructor, prototype
  • Expression timeout: 100ms (configurable)
  • Max AST depth: 50

Date/Time Functions (v0.3)

Dates are epoch milliseconds (number type). All operations are UTC. No separate date type.

FunctionSignatureReturns
now()() → numberCurrent UTC timestamp (epoch ms)
parseDate(str, format?)(string, string?) → numberEpoch ms. Default: ISO 8601. Custom: ICU tokens.
formatDate(date, format)(number, string) → stringFormatted date string
year(date)(number) → numberYear (e.g., 2024)
month(date)(number) → numberMonth 1–12
day(date)(number) → numberDay 1–31
hour(date)(number) → numberHour 0–23
minute(date)(number) → numberMinute 0–59
second(date)(number) → numberSecond 0–59
millisecond(date)(number) → numberMillisecond 0–999
dateAdd(date, amount, unit)(number, number, string) → numberAdd/subtract time
dateDiff(date1, date2, unit)(number, number, string) → numberSigned difference

Format tokens: yyyy, MM, dd, HH, mm, ss, SSS

Units: "years", "months", "days", "hours", "minutes", "seconds", "milliseconds"

javascript
parseDate("2024-01-15T12:00:00Z")                    // 1705320000000
formatDate(0, "yyyy-MM-dd")                           // "1970-01-01"
year(parseDate("2024-06-15T10:30:00Z"))               // 2024
dateAdd(parseDate("2024-01-15T00:00:00Z"), 7, "days") // epoch ms for 2024-01-22
dateDiff(parseDate("2024-01-01T00:00:00Z"), parseDate("2024-01-08T00:00:00Z"), "days")  // 7

Regex Functions (v0.3)

Function-based regex using RE2 flavor. No literal syntax. Inline flags via (?i) etc.

FunctionSignatureReturns
matches(str, pattern)(string, string) → booleanTrue if pattern found in string
match(str, pattern)(string, string) → string | nullFirst match or null
matchAll(str, pattern)(string, string) → arrayAll matches as strings
replacePattern(str, pattern, replacement)(string, string, string) → stringReplace all matches
javascript
matches("hello 42", "\\d+")                                              // true
matches("Hello", "(?i)hello")                                            // true
match("order-123", "\\d+")                                               // "123"
matchAll("a1b2c3", "\\d+")                                               // ["1", "2", "3"]
replacePattern("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1")  // "15/01/2024"

Negative Array Indexing (v0.3)

Arrays support negative indexing: items[-1] returns the last element.

javascript
[1, 2, 3][-1]   // 3 (last element)
[1, 2, 3][-2]   // 2 (second-to-last)
[][- 1]          // null (out of bounds)

Spread in Function Calls (v0.3)

The spread operator works in function call arguments:

javascript
max(...[1, 5, 3, 2, 4])     // 5
fn(a, ...rest)               // mix regular and spread args

Destructuring (v0.4)

Destructuring extracts values from objects and arrays into named bindings. Works in let bindings and arrow function parameters.

Object destructuring:

javascript
let {name, age} = user; name                    // shorthand
let {name: n, age: a} = user; n                 // rename
let {name = "Anonymous"} = {}; name             // default value
let {a, ...rest} = {a: 1, b: 2, c: 3}; rest    // rest: {b: 2, c: 3}
let {address: {city}} = user; city              // nested

Array destructuring:

javascript
let [first, second] = [1, 2, 3]; first          // 1
let [head, ...tail] = [1, 2, 3]; tail           // [2, 3]
let [a = 0, b = 0] = [1]; b                     // 0 (default for missing)
let [[a, b], [c, d]] = [[1, 2], [3, 4]]; c      // 3 (nested)

In arrow function parameters:

javascript
({name}) => name.toUpperCase()
([a, b]) => a + b
users.map(({name, age}) => `${name}: ${age}`)

Edge cases:

  • Missing property → null
  • Destructuring null → error
  • Out-of-bounds array element → null
  • Array rest of empty → []

Regex Literals (v0.4)

Regex literals are a first-class type (regex) in XPR. The tokenizer uses context-based disambiguation to distinguish / division from regex delimiters.

Syntax:

javascript
/pattern/flags
/\d+/
/hello/i
/^start/m

Supported flags: i (case-insensitive), m (multiline), s (dot-all)

Type system:

javascript
type(/\d+/)       // "regex"
string(/\d+/i)    // "/\\d+/i"
bool(/\d+/)       // true
/abc/ == /abc/    // true (pattern + flags comparison)
/abc/i == /abc/   // false (flags differ)

Regex methods:

javascript
/\d+/.test("abc123")    // true
/\d+/.test("abc")       // false

String methods with regex:

javascript
"hello world".match(/\w+/)              // "hello" (first match)
"a1b2c3".replace(/\d/, "X")            // "aXbXcX" (all matches)
"a1b2c3".replace(/(\d)/, "$1$1")       // "a11b22c33" (group refs)

Coexistence with v0.3 regex functions:

javascript
matches("hello", "\\w+")               // true (v0.3 function-based)
match("abc123", "\\d+")                // "123" (v0.3 function-based)

Math Functions (v0.5)

Six new math functions plus two constants:

FunctionReturnsNotes
sqrt(n)Square rootError on negative
log(n)Natural logarithmError on zero or negative
pow(x, y)x raised to ySame as x ** y
random()Float in [0, 1)Non-deterministic
sign(n)-1, 0, or 1
trunc(n)Truncate toward zeroNumber→number (use int() for string coercion)

Constants: PI (3.141592653589793), E (2.718281828459045) — bare global identifiers.

javascript
sqrt(16)                          // 4
log(E)                            // 1
pow(2, 10)                        // 1024
PI * pow(5, 2)                    // ~78.54

Type Predicates (v0.5)

javascript
isNumber(42)      // true
isString("x")     // true
isArray([1,2])    // true
isNull(null)      // true
isObject({a:1})   // true
isRegex(/re/)     // true

Note: isObject([])false (arrays are "array" type, not "object").

New Array Methods (v0.5)

MethodReturnsNotes
sortBy(fn)Sorted arrayAscending by fn result
take(n)First n elementsClamped, no error
drop(n)Without first n
count(fn)Count of matchesEquivalent to .filter(fn).length
sum()Sum[]0
avg()Mean[] → error
compact()Remove nullKeeps false, 0, ""
partition(fn)[matches, others]Two arrays
keyBy(fn)Object keyed by fnLast wins on dup
min() / max()Min/max (numeric)Error on empty
first() / last()First/last element[]null
javascript
[3,null,1,null,5].compact().sortBy(x => x)   // [1,3,5]
[1,2,3,4,5].partition(x => x > 3)             // [[4,5],[1,2,3]]
items.keyBy(x => x.id)                         // {"a": {...}, "b": {...}}

fromEntries (v0.5)

Inverse of .entries():

javascript
fromEntries([["a", 1], ["b", 2]])   // {"a":1,"b":2}
Object.entries(obj) |> fromEntries  // identity (round-trip)

str.split(/regex/) (v0.5)

javascript
"a1b2c3".split(/\d+/)   // ["a","b","c"]

Rest Parameters in Arrow Functions (v0.5)

javascript
(...args) => args.length          // rest-only
(first, ...rest) => rest          // named + rest
[1,2,3].map((...args) => args)    // [[1],[2],[3]]

... must be the last parameter.

What's Not Supported

  • while loops, class, I/O, imports
  • Variable reassignment (const, var) — use immutable let bindings instead
  • Standalone destructuring assignment ({a} = obj without let)
  • Array holes/elision ([a,,b])
  • Computed destructuring keys ({[expr]: val})
  • Pattern matching
  • Async expressions

Conformance Tests

The test suite is the authoritative spec. All runtimes must pass all conformance tests.

View conformance tests on GitHub