Documentation / Language guide

Language Guide

Yexp is a small language for querying and transforming JSON-like data. It feels familiar if you know JavaScript, but it adds the concise selectors and pipelines of dedicated query languages.

$.orders[.status == "paid"]
  |> groupBy(.customer)
  |> mapEntries(entry => {
    key: entry.key,
    value: entry.value |> map(.amount) |> add
  })

Yexp is inspired by JavaScript; it is not JavaScript. A program is one expression. There are no statements, imports, classes, prototypes, global object, or arbitrary code execution.

The Yexp combination

Yexp brings four ideas together:

  • JavaScript-shaped expressions, literals, ternaries, templates, and arrow lambdas
  • Query-language selectors for filtering, projection, negative indexing, and recursive search
  • Pipes and dot lambdas for short, readable transformations
  • Bytecode compilation for reusable, inspectable execution without eval()

The same filter can be written at different levels of concision:

$.products.filter(product => product.inStock && product.price < 100)
$.products.filter(.inStock && .price < 100)
$.products[.inStock && .price < 100]

That gradual path from familiar to compact is central to the language: start with normal-looking expressions, then use query shorthand where it improves clarity.

1. Start with values

Yexp supports null, booleans, numbers, strings, arrays, and objects:

null
true
42
"hello"
[1, 2, 3]
{ name: "Ada", active: true }

Use the normal arithmetic and comparison operators:

1 + 2 * 3
$.price * $.quantity
$.age >= 18
$.score >= 0 && $.score <= 100

Equality never coerces types. Both == and === are non-coercing in Yexp:

1 == 1       // true
1 == "1"    // false
0 == false   // false

Comments in this guide explain results; comments are not valid inside an expression.

2. Read the input

The primary input is $:

$
$.user.name
$.orders[0].total

Two optional roots keep non-input data explicit:

$context.userId
$env.region

$.ownerId == $context.userId && $env.region == "eu"

Missing properties resolve to null. Optional access and null coalescing make fallback intent clear:

$.user.profile?.displayName ?? "Anonymous"
$.items?.[0]

Only null and false are falsy in Yexp. Values such as 0, "", and [] are truthy. Like JavaScript, && and || return the selected operand and short-circuit. The difference is the smaller Yexp falsy set. ?? falls back only for null.

3. Navigate like a query language

Negative indexing

Read from the end of an array or string:

$.events[-1]
$.events[-2]
$.name[-1]

Predicate selection

Filter an array directly inside its path:

$.products[.price < 100]
$.users[.active && .role == "admin"]

The expression after [ is evaluated once per item. The leading dot refers to that item.

Wildcard projection

Project a property from every value:

$.users[*].name
$.products[.inStock][*].price
$.settings[*]

[*] preserves array elements and returns object values. Its optional form returns an empty array for null:

$.users?.[*].name

Recursive descent

Search an unknown tree for every occurrence of a property:

$..email
$.payload..id
$.payload?..id

Recursive descent returns an array in depth-first discovery order.

4. Transform collections

Arrow lambdas

$.items.filter(item => item.price < 100)
$.items.map(item => item.price * item.quantity)
$.values.reduce((total, value) => total + value, 0)

Dot lambdas

For the common case of reading from one item, use dot shorthand:

.price < 100       // item => item.price < 100
.active            // item => item.active
.profile?.name     // item => item.profile?.name

Dot shorthand is a lambda in the core language. In the CLI only, a leading dot at the start of the whole expression is also accepted as shorthand for the input root: .name becomes $.name.

Pipes

Pipes pass the left value as the first argument of the next function:

$.items
  |> filter(.inStock)
  |> map(.price)
  |> add

Method and pipe syntax are equivalent:

$.items.filter(.active)
$.items |> filter(.active)
filter($.items, .active)

Methods are syntax sugar, not JavaScript prototype calls.

Pipes bind tightly. Parenthesize arithmetic before piping:

($.subtotal + $.tax) |> round(2)

5. Shape new results

Build arrays and objects from expressions:

[$.first, $.last]

{
  name: $.user.name,
  total: $.items |> map(.price) |> add,
  active: true
}

Spread existing values into new ones:

[...$.items, $.newItem]
{ ...$.defaults, ...$context.overrides, enabled: true }
max(...$.scores)

Template strings interpolate any expression:

`Hello, ${$.user.name}!`
`Total: $${$.items |> map(.price) |> add}`

Use ternaries for conditional output:

$.age >= 18 ? "adult" : "minor"

6. Build a complete query

This report filters paid orders, groups them by customer, and calculates a count and total for each group:

$.orders[.status == "paid"]
  |> groupBy(.customer)
  |> mapEntries(entry => {
    key: entry.key,
    value: {
      orders: entry.value |> length,
      total: entry.value |> map(.amount) |> add
    }
  })

It demonstrates the intended progression of the language:

  1. navigate from $;
  2. select values with query syntax;
  3. compose transformations with pipes;
  4. use lambdas only where a local name improves clarity;
  5. return a new JSON-shaped result.

Operator reference

CategoryOperatorsNotes
Arithmetic+ - * / %Numbers; + also joins two strings
Comparison< <= > >=Numbers only
Equality== != === !==Never coerces types
Logical&& || !Selects operands and short-circuits
Fallback??Falls back only for null
Conditional? :Evaluates one branch
Pipe|>Passes the left value first
Spread...Arrays, objects, and arguments

From highest to lowest, precedence is: access/call, unary, pipe, multiplication, addition, comparison, equality, &&, ||, ??, ternary, assignment, lambda. Use parentheses when the intended grouping is not obvious.

Built-in families

Collections

filter  map  find  reduce  every  some  sort  flatMap
groupBy  uniqueBy  minBy  maxBy  first  last  limit
add  concat  unique  reverse  flatten  slice  includes  join

Objects

keys  values  entries  fromEntries  mapEntries  del  pick  has  select

Strings

startsWith  endsWith  trimPrefix  trimSuffix
toLowerCase  toUpperCase  index  rindex  split
replace  replaceAll  slice  substring
trim  trimStart  trimEnd  padStart  padEnd  repeat

Numbers, dates, and types

round  floor  ceil  abs  min  max  sqrt  pow
sin  cos  tan  log  log10  log2  exp  random
now  parseDate  toISOString
type  toString  length

random() and now() are nondeterministic. Avoid them when repeatable results matter.

Applications can add host functions. The CLI, for example, adds filesystem functions; those are not portable core-language built-ins.

Language boundaries

Yexp intentionally does not provide:

  • statements, loops, declarations, or user-defined named functions;
  • imports, modules, classes, prototypes, or a global object;
  • JavaScript coercion for equality or arithmetic;
  • comments inside expressions;
  • implicit filesystem, network, or process access.

The default runtime has no eval() or generated JavaScript. It is still the host’s responsibility to apply CPU, memory, and input limits when expressions are untrusted.

For exact semantics, the complete built-in registry, compatibility behavior, grammar, and port conformance rules, read the Yexp language specification.

Yexp documentationEdit this page ↗