The code language
The CX code language
CX code is CX data with a leading `?` on the element name. A directive like `[?for]` is a normal element to the parser; the evaluator recognizes the `?` prefix and dispatches on the directive name. Everything else — patterns, paths, function calls, builtins, errors — is built from ordinary CX shapes.
[# Data is data: #]
[user id=1 email='ada@example.com']
[# Code is data, plus a `?`: #]
[?for [in $u //user] [yield $u@email]]
The evaluator is `cx_code_eval(doc, code, target_format)` at the C ABI level; every binding's Layer-1 `Doc.eval(code)` wraps it (see bindings). The full normative spec is [`spec/code.md`](../../spec/code.md). This section is the working developer guide.
Code can do five things: **select** (CXPath, see cxpath), **iterate** (`[?for]`, see for), **dispatch** (`[?match]`, see match), **bind** (`[?let]` / `[?def]` / `[?fn]`), and **update** (`[?modify]`, see modify). Every other directive is one of these five lifted into a specific domain — concurrency, async, resilience, services.
**Code is data.** A directive is an element whose name happens to start with `?`. You can pattern-match on `[?for ...]` exactly the way you pattern-match on `[user ...]`. You can transform code with `[?modify]`. You can hash it, project it to JSON, ship it across a wire, and evaluate it on the other end. The render rules in [`spec/code.md`](../../spec/code.md) §10.1 turn any well-formed program into a sequence diagram deterministically — because the program is just a tree.
**Pure-functional core.** Every directive returns a value; there is no assignment, no mutation, no rebinding. The concurrency primitives (channels, workers, async) exchange only immutable values, so there is no shared-mutable-state hazard by construction. See pure-updates for the lens/zipper foundation that `[?modify]` builds on.
Directive registry
There are currently **forty normative directives** — including `[?modify]`. Adding or removing any directive requires a governance change per [`spec/governance.md`](../../spec/governance.md) §10. The authoritative registry lives in [`spec/code.md`](../../spec/code.md) §4.1; this subsection is the at-a-glance grouping.
**Core constructs (11).** Built into the evaluator; everything else composes from these:
| directive | purpose |
|---|---|
| [?match] | Pattern dispatch — single-arm assertion or multi-arm first-match (see match) |
| [?modify] | Pure-functional update — apply an action at every CXPath focus (see modify) |
| [?for] | Iteration / comprehension over a CXPath, pattern, or sequence (see for) |
| [?let] | Local binding scoped to a body (see let) |
| [?fn] | Anonymous function literal — body of $bindings → expression |
| [?def] | Named top-level definition (function or value) |
| [?if] | Boolean branch with EBV condition (see if + ebv) |
| [?else] | Value-or-default coalesce — recover on [err] or absence (getOrElse; see errors) |
| [?pipe] | Pipeline — canonical form of the `|` operator (see pipe) |
| [?map] | Map a closure over a sequence (sequential by default; par=true enables parallel evaluation, ordered=true preserves source order) |
| [?reduce] | Fold a sequence to a single value via a binary closure starting from [init …] (sequential left-fold by default; par=true requires associativity) |
**Resilience (6).** Wrap an inner expression with a failure-handling policy. See resilience for the parameter tables:
| directive | purpose |
|---|---|
| [?retry] | Re-run on failure with backoff + jitter; bounded by max= |
| [?timeout] | Bound execution time; cancel + return [on-timeout …] on expiry |
| [?circuit-breaker] | Open after threshold-exceeded failures in a rolling window |
| [?fallback] | Run [recover-with …] secondary if primary returns [err …] ($err bound in the recovery) |
| [?rate-limit] | Token-bucket cap — max= calls per per= duration |
| [?bulkhead] | Bound max-concurrent= in-flight; optional [queue …] |
**Services + clients (3).** HTTP service definition + client construction. See services:
| directive | purpose |
|---|---|
| [?service] | Declare an HTTP service with resource handlers; returns a handle |
| [?service-handle] | Look up an existing service by :name |
| [?http-client] | Construct an HTTP client (optionally wrapped in resilience) |
**Concurrency (10).** Workers + channels + selection. See workers:
| directive | purpose |
|---|---|
| [?worker] | Spawn a worker; concurrent with siblings in the enclosing scope |
| [?worker-handle] | Look up an existing worker by :name |
| [?channel] | Create a FIFO channel with :buffer of given size |
| [?send] | Blocking send to a channel; returns [ok] or CHANNEL_CLOSED |
| [?try-send] | Send with :timeout — SEND_TIMEOUT if buffer stays full |
| [?receive] | Blocking receive — returns next value or CHANNEL_CLOSED |
| [?try-receive] | Receive with :timeout — RECV_TIMEOUT if empty |
| [?close] | Close a channel; second close raises CHANNEL_ALREADY_CLOSED |
| [?select] | Multi-way receive — first ready case wins (uniform-random on ties) |
| [?stop] | Initiate graceful shutdown on a service or worker handle |
**Async (8).** Futures + barriers + cancellation. See async:
| directive | purpose |
|---|---|
| [?async] | Evaluate EXPR asynchronously; return a future immediately |
| [?await] | Block until a future is terminal; return value or [err] |
| [?await-all] | Join N futures — all-or-error barrier |
| [?await-any] | Yield first to terminate; others continue running |
| [?await-race] | Yield first to terminate; cancel the rest |
| [?cancel] | Request cooperative cancellation on a future/worker handle |
| [?check-cancel] | Polling cancellation point inside a CPU-bound loop |
| [?sleep] | Cancellable sleep for DURATION |
**Lifecycle (2 shared).** `[?stop]` (above) and `[?wait-for]` — block until a service or worker terminates, returning its terminal value or `[err]`.
**Retired in v0.8.0.** `[?find]` is no longer a directive. Use [; version-literal-ok ] `[?for]` with a pattern generator, or CXPath `//pattern`. See migration for mechanical migration rules. The rationale: CXPath is the universal selector, so the `[?find]` shape was redundant with `[?for]`+CXPath.
**Implementation status.** Multi-arm `[?match]`, `[?modify]`, and the CXPath value kind in constructs other than `[?for]` are **fully specified** but not all are wired through every binding yet. V (the native reference) carries the spec-conformant implementation; Tier-2 bindings (Python, Go, Rust) follow per the parity matrix in [`spec/v0_8_0_status.md`](../../spec/v0_8_0_status.md). Where a feature is spec'd-but-not-shipped this guide says so explicitly.
CXPath — the universal selector
CXPath is the single selector vocabulary across reading, iteration, dispatch, and update. It aligns with XPath 3.1 (see [`spec/cxpath_alignment.md`](../../spec/cxpath_alignment.md)) with deliberate divergences kept minimal. **Path is a value kind**, not parser sugar — paths compile, cache, round-trip through every projection, and visualize as themselves.
The simplest CXPath is the descendant search:
//user [# every user, anywhere in doc #]
//user[= $_@active true] [# every active user #]
//user/@email [# scalar attribute values #]
//section[1] [# first section (1-indexed) #]
//section[h2] [# section that has an h2 child #]
The grammar is the XPath 3.1 **selection core**, lifted into CX with one comparison operator family:
PathExpr ::= '//' StepList | '/' StepList | StepList
Step ::= ( Axis '::' )? NodeTest Predicate*
Predicate ::= '[' PredExpr ']'
PredExpr ::= INT | AttrTest | StepTest | ScalarLiteral
| BindingOrPath | FusedPrefixForm
The twelve axes
CXPath supports twelve axes. The default axis is `child`, so `name` ≡ `child::name`. The full set:
| axis | direction | what-it-selects |
|---|---|---|
| child | down | Direct children of context node (default axis) |
| descendant | down | Children, grandchildren, deeper — excludes self |
| descendant-or-self | down | `descendant` plus the context node itself |
| parent | up | The unique parent of context (or empty at root) |
| ancestor | up | Parent, grandparent, up to root — excludes self |
| ancestor-or-self | up | `ancestor` plus the context node itself |
| following-sibling | lateral | Later siblings with same parent |
| preceding-sibling | lateral | Earlier siblings with same parent |
| following | document | Every node after context in document order (excluding descendants) |
| preceding | document | Every node before context in document order (excluding ancestors) |
| self | self | Context node itself — useful for predicate filters |
| attribute | attr | Attributes of context — shorthand is `@name` |
[# Common axis idioms #]
//section//* [# all descendants #]
//code/ancestor::section [# enclosing sections #]
//user/following-sibling::user [# later peer users #]
//tr/parent::table [# tables containing tr #]
//h2/preceding-sibling::*[1] [# immediate prior sib #]
//user[= $_@role 'admin'] [# only-if-admin filter #]
//user/attribute::email [# == //user/@email #]
Axis-qualified steps apply to document-anchored paths. From a **binding**, the child / descendant / attribute steps work directly (`$u/name`, `$u//item`, `$u@email`); axis-qualified steps on a binding (`$u/ancestor::…`) are not yet supported — anchor the path at the document and filter instead. See bindings.
Predicates — positional, attribute, function, composite
A predicate is `[...]` after a step. The predicate body is **prefix CX code over the context node `$_`** — plus a small set of atomic shorthands. The admitted shapes:
- **Positional** — `INT` selects the Nth match (1-indexed). `//section[1]` is the first section. Negative positions are NOT supported; `[$_last]` selects the last, `[= $_position N]` the Nth.
- **Attribute test** — `[@name]` (attribute present) and `[@!name]` (attribute absent). Value comparisons are prefix forms: `[= $_@name V]`, `[!= …]`, `[< …]` etc.
- **Step-existence** — a bare child name: `[h2]` keeps elements that have an `h2` child.
- **Prefix expression** — any prefix CX form over `$_`: comparisons (`[= $_@id 42]`), boolean combinators (`[and …]` / `[or …]` / `[not …]`), and function calls (`[$strings:contains $_@email '@example.com']`).
//user[1] [# first user #]
//user[= $_@id 42] [# attr equality #]
//user[@active] [# attr present #]
//user[$strings:contains $_@email '@example.com']
//user[and [>= $_@age 18] [= $_@country 'US']]
//section[h2] [# has an h2 child #]
//section[and [$count $_/h2] [$count $_/p]]
Predicates chain: `//user[= $_@active true][1]` is the first active user — the second predicate filters the result of the first. The order matters.
Comparison — one operator family
**One operator syntax: `= != < <= > >=`.** No keyword synonyms (`eq` / `ne` / `lt` / ...) — they are not in the grammar. Semantics are **value-comparison**: both sides must be single values; a multi-valued operand raises `CXER0103`; no type coercion.
This is the deliberate divergence from XPath 3.1's general-comparison default. In XPath, `//x = 1` is true if *any* `x` equals 1 — a common gotcha when the path silently returns multiple nodes. In CX, the path must return a single value, or the comparison raises. To express "any equals", filter the sequence itself: `[$exists //x[= $_ 1]]`.
//user[= $_@age 42] [# single attr → ok #]
//section[> [$count $_/p] 3] [# both sides single → ok #]
//user[= $_@email 'ada@example.com']
[# a multi-valued operand raises — filter the sequence
instead of comparing it wholesale #]
[$exists //user/tag[= $_ 'admin']]
Paths on bindings
A CXPath rooted at a binding navigates from that value instead of from the document root:
[# $u/name — direct child · $u/* — all children ·
$u//item — descendant · $u@active — attribute (shorthand) ·
$u/@active — attribute (full axis form) #]
[?let [= $u [user active=true region='EU'
[name 'Ada'] [item 1] [item 2]]]
($u/name, $u/*, $u//item, $u@active, $u/@active)]
The path operators that work on bindings are the same operators that work on the implicit document — one vocabulary. The leading `//` form is anchored at the **document root**, regardless of context; `$u//x` is anchored at `$u`. Use `//x` to escape upward when you already have a binding. Axis-qualified steps (`ancestor::` etc.) are document-path syntax and are not yet supported on a binding; map-key access on a map-valued binding uses `[$map:get $m 'key']`.
Where CXPath values are accepted
CXPath is a value kind. Every construct that takes "a selector" accepts a CXPath value:
[?for [in $u //user[= $_@active true]] [yield [$render $u]]]
[?if //admin [then [admin-panel]] [else ()]]
[?match $n [case //prose [p $n]] [else ()]]
[?let [= $cfg //meta/@config] [$render $cfg]]
[?modify $doc //user/@status [set 'verified']]
[$count //user[= $_@active true]]
The same path expression — `//user[= $_@active true]` — appears as a `[?for]` generator, as an `[?if]` condition, as a `[?match]` arm, as a `[?let]` initializer, as a `[?modify]` focus, and as a builtin argument. This is the unification CX ratifies: no separate "path syntax" per construct.
**Status note.** CXPath value-kind at non-`[?for]` positions is production-ready in V; full Tier-2 binding parity is tracked in [`spec/v0_8_0_status.md`](../../spec/v0_8_0_status.md) gate 26.4.
Deliberate divergences from XPath 3.1
CXPath is a working subset of XPath 3.1 + deliberate changes. The full alignment table is in [`spec/cxpath_alignment.md`](../../spec/cxpath_alignment.md); the headline differences:
- **One comparison family** (`= != < <= > >=`) — XPath's split into general (`=`) and value (`eq`) is gone; CX uses value-comparison semantics exclusively. Rationale: the general-comparison gotcha is XPath's most-reported source of subtle bugs.
- **Path is a value kind** — XPath paths are syntactic fragments inside XPath expressions only. In CX they are first-class values.
- **1-indexed positions** — adopted from XPath verbatim (`//x[1]` is the first), but CX explicitly rejects `[0]` (raises `CXER0100`); XPath silently yields the empty sequence.
- **No general-purpose `for` / `let` inside paths** — CX moves binding into the `[?for]` / `[?let]` directive surface; predicates take expressions but not binding clauses. Use the surrounding directive.
- **No paren-calls anywhere** — XPath's `count(//user)` spelling does not exist; every call is the head-dispatch form `[$count //user]`, in predicates too (`[> [$count $_/*] 0]`). `position()` / `last()` are the reserved predicate bindings `$_position` / `$_last`.
The full XPath 3.1 alignment table — what is adopted, what is deliberately not, what is on the XPath 4.0 watch list — is in [`spec/cxpath_alignment.md`](../../spec/cxpath_alignment.md).
Patterns and matching
A **pattern** is a literal CX subtree extended with binding holes (`$name`), wildcards (`*`, `**`, `_`), type guards (`:TypeName`), scalar literals, and CXPath values. Patterns appear in `[?for]`'s first generator, in `[?match]` arms, and in destructuring `[?let]`. The full pattern grammar is in [`spec/code.md`](../../spec/code.md) §5.
Element-shape patterns
An element-shape pattern matches an element by name, attributes, and (recursively) body content. The pattern uses ordinary CX syntax with bind-holes where you want to capture values:
[user email=$e] [# bind email attribute #]
[user @active=true $u] [# attr filter; bind whole user #]
[user @id=42 [name $n]] [# bind name's body #]
[post author=$a body=$b] [# bind two attrs #]
**Binding semantics depend on context.** A pattern `[NAME $x]` **without** attribute predicates auto-unwraps the body — text body binds the string, scalar body binds the scalar, empty body binds the element itself, structured body binds the element. A pattern `[NAME @attr… $x]` **with** attribute predicates always binds `$x` to the matched element. The rationale is in [`spec/code.md`](../../spec/code.md) §5.2 rule 5: when you name attributes you are filtering "what kind of element", so the bind value is the element; when you don't, the bind value is the inner content.
Scalar literal patterns
Scalar literals are valid patterns. They match by value-equality:
[?match $status
[case 200 :ok]
[case 404 :not-found]
[case 500 :server-error]
[case 'admin' :elevated]
[case true :enabled]
[case null :missing]
[else :unknown]]
Integers, floats, strings, bools, dates, datetimes, and `null` are all matchable as scalar literals. Float equality follows the EBV rules — `nan` is **never** equal to `nan` (per IEEE-754). Use `is-nan($x)` to test for NaN explicitly.
CXPath patterns
A CXPath value used as a pattern matches if the candidate value is in the path's result sequence:
[doc [heading 'Title'] [prose 'Body.']]
[?let [= $node [$first //heading]]
[?match $node
[case //heading [h2 [$text $node]]]
[case //prose [p [$text $node]]]
[else $node]]]
In a `[?for]` body, the same CXPath as the generator selects directly:
[users [user email='ada@ex.com' active=true]
[user email='bob@ex.com' active=false]]
[?for [in $u //user[= $_@active true]] [yield $u@email]]
CXPath patterns and element-shape patterns compose in the same arm list — `[?match]` can dispatch on a mix of both, with first-match-wins ordering (see match).
Wildcards — *, **, _
Three wildcard forms:
- **`*` — any single element.** `[* $x]` matches any element and binds it. `[section * $body]` matches any section and binds the body wildcard slot.
- **`**` — recursive descent.** `[**]` matches any element at this position or deeper. Wrapping an inner pattern — `[** [user $u]]` — matches a `user` anywhere in the body and binds it.
- **`_` — anything, no bind.** `_` matches any value (including non-elements) without binding it. The idiomatic catch-all in `[?match]`.
[?let [= $node [section [h2 'T'] [code 'x = 1']]]
[?match $node
[case [section * $body] [matched-section]]
[case [** [code $c]] [highlighted $c]]
[case _ $node]]]
Named bindings — $name
`$name` (lowercase, leading `$`) is a binding hole. It appears in attribute-value position (`[user email=$e]`), in body position (`[user $u]`), and in a destructuring `[?for]` generator (`[?for [in [user $u] /users/user] …]`).
A bind hole always succeeds at its position; the bind value depends on what the surrounding pattern dictates (see pattern-element-shape). Naming collisions inside one arm raise `CXER0100` (PARSE_ERROR) — `[user @id=$x @name=$x]` would require the values be equal, which the current spec does not support; use a single bind and check with a `[when …]` clause.
Type-guard heads
A type guard `:TypeName` at **pattern-head position** narrows by schema-typed shape:
[# Match only elements whose schema type is ServerConfig #]
[?match $cfg
[case [:ServerConfig $c] [$validate-server $c]]
[case [:ClientConfig $c] [$validate-client $c]]
[else [result status=err code='CXER0103' reason='unknown config']]]
Type-guard resolution is by the document's schema binding (see schema in the data section); with no schema in scope type guards are rejected at program load. Pair with schema binding (`[?cx schema='…']`) for type-driven dispatch.
Bindings and paths
A binding is a name introduced by `[?let]`, `[?for]`, `[?fn]`, or `[?match]` pattern. Bindings are referenced via `$name`. They are immutable — there is no rebinding (per CX's pure-functional discipline).
[users [user role='admin' email='root@example.com']
[user role='user' email='ada@example.com']]
[?let [= $admins //user[= $_@role 'admin']]
[?for [in $a $admins] [yield $a@email]]]
A binding's value is whatever expression produced it: a sequence, a scalar, an element, an array, a map, an error, a future, a CXPath, a function literal. Every value is a sequence (per the sequence-flat principle); single values are sequences of one.
**Navigation from a binding** uses CXPath syntax (see cxpath-bindings). The path operators that work on bindings are the same operators that work on the implicit document — one vocabulary. `$u/email` is a child step; `$u@email` is the attribute shorthand; `$u/@email` is the full attribute axis form.
**Scope.** A binding is in scope inside the directive body that introduced it. `[?let [= $x E] BODY]` puts `$x` in scope for `BODY` only; outside the let it does not exist. `[?for [in $u SEQ] [yield E]]` puts `$u` in scope for `E` (one iteration at a time, per item).
**Shadowing.** A nested binding with the same name shadows the outer for the inner scope; this is the standard lexical rule and matches every host language. CX does not warn on shadowing — readability is the author's responsibility.
Core constructs
The six core constructs are how every CX program is built. Each is documented in the following sub-sections.
[?for] — iteration
`[?for]` is the iteration / comprehension primitive. The expression in the `[in …]` clause is either a CXPath (yields a sequence to iterate), a pattern (matches the generator — the `[?find]` replacement), or any expression that evaluates to a sequence.
[site
[user id=1 email='ada@example.com' age=36 active=true]
[user id=2 email='grace@example.com' age=17 active=false]
[post author=1 [title 'Notes']]
[request 'r1'] [request 'r2']
[logs [row 'a'] [row 'b']]]
[# CXPath generator #]
[?for [in $u //user] [yield $u@email]]
[# Pattern generator (the `[?find]` replacement) #]
[?for [user @active=true $u] [yield $u@email]]
[# Filter with [where …] #]
[?for [in $u //user] [where [>= $u@age 18]] [yield $u@id]]
[# Multiple generators (Cartesian, in declared order) #]
[?for [in $u //user]
[in $p //post[= $_@author $u@id]]
[yield [pair user=$u@id post=[$text $p/title]]]]
[# Parallel evaluation #]
[?for [in $r //request] [par] [yield [got [$text $r]]]]
[# Streaming over large input #]
[?for [in $row //logs/row] [stream] [yield [$text $row]]]
`[par]` and `[stream]` are evaluation-modifier clauses — same surface, different scheduler. `[par]` distributes iterations across worker threads (add `[ordered]` to preserve source order). `[stream]` couples the generator to a streaming input source so that the iteration runs in bounded memory regardless of input size. Full semantics in [`spec/code.md`](../../spec/code.md) §7.
**Multiple `[where …]` clauses** AND together; **multiple `[yield …]` clauses** concatenate (per the sequence-flat principle — yields never produce a list-of-lists).
[?match] — dispatch (single + multi-arm)
`[?match]` is pattern dispatch. CX supports two forms: **2-arg destructure** (single arm, asserts the value matches or raises) and **multi-arm** (first-match- wins across N arms). The multi-arm form is a recent addition.
**Single-arm form** — pattern-bind a value and assert the shape:
[# Destructures $result as [ok V] or raises CXER0100 #]
[?let [= $result [ok 42]]
[?match $result [ok $v] [yield [rendered $v]]]]
**Multi-arm form** — first-match-wins across `[case …]` / `[when …]` / `[else …]` clauses. Arms may mix element-shape patterns, scalar literals, CXPath paths, wildcards, and predicate-only `[when …]` clauses:
[?let [= $node [code 'x = 1']]
[?match $node
[case [prose $p] [p $p]]
[case [code $c] [pre $c]]
[case 'separator' [hr]]
[else ()]]]
[# SQL Searched-CASE — predicate-only mode (no scrutinee) #]
[?let [= $x 120] [= $y 10]
[?match
[when [> $x 100] :big]
[when [> $y 50] :medium]
[else :small]]]
[# Guards on case arms — [where …] filters even on a match #]
[?let [= $req [http-get '/admin']]
[?match $req
[case [http-get $u] [where [= $u '/admin']] [route $u]]
[case [http-get $u] [status 403]]
[else [status 400]]]]
**Arm semantics:**
- **`[case PAT R]` — match against pattern `PAT`.** Arms evaluate top-to-bottom; the first one whose pattern matches "wins". Later arms are not tried, even if they would also have matched.
- **`[when PRED R]` — match iff predicate truthy.** No pattern; the predicate is evaluated with the scrutinee in scope as `$_` (and any earlier `[case …]` bindings still in scope only if the same arm).
- **`[else R]` — catch-all, always last.** Equivalent to `[case _ R]`. Multiple `[else …]` clauses raise `CXER0100`.
- **`[case PAT [where PRED] R]` — guard on a case arm.** The pattern must match AND the predicate must be truthy.
- **No implicit EBV in `[case …]`.** `[case …]` tests pattern equality / structural match; it does not apply EBV. `[when …]` uses EBV.
**No-match behavior differs by form.** The 2-arg form raises `CXER0100`; the multi-arm form yields `()` (the empty sequence) if no arm matches and no `[else …]` is present. An unreachable arm (covered by an earlier arm with no guard) raises a warning at parse time but does not fail compilation.
**Status.** Multi-arm `[?match]` is fully spec'd. V's evaluator carries the production-ready implementation; Tier-2 binding parity is per [`spec/v0_8_0_status.md`](../../spec/v0_8_0_status.md).
[?if] — boolean branch (with EBV)
`[?if cond [then E1] [else E2]]` is the explicit boolean branch. The condition is evaluated with the EBV (Effective Boolean Value) rule — see ebv below.
[site [admin 'root'] [config debug=false]]
[?if //admin [then [admin-panel]] [else [user-panel]]]
[?let [= $age 36]
[?if [>= $age 18] [then :adult] [else :minor]]]
[?let [= $cfg [$first //config]]
[?if $cfg@debug [then [debug-trace]] [else ()]]]
For multi-way conditionals prefer `[?match]` multi-arm (see match). `[?if]` stays separate from `[?match]` because they have different contracts — `[?if]` uses EBV (truthy/falsy with container rules), `[?match]` `[case …]` does **not** (it tests strictly by structural equality / pattern match).
**The `[else …]` clause is optional.** When omitted, an untrue condition yields `()` (the empty sequence). Many CX idioms exploit this — `[?if $n@hidden [then ()] [else [$render $n]]]` reads as "render unless hidden".
EBV — the Effective Boolean Value rule
The EBV rule, applied in order (per [`spec/cxdm.md`](../../spec/cxdm.md) §4.6):
- **Empty sequence `()`** → false.
- **Single Scalar** of kind `bool` → that bool's value.
- **Single Scalar** of kind `string` → length > 0.
- **Single Scalar** of kind `int` / `float` → value `!= 0` (and not `nan` for float).
- **Single Scalar** of kind `null` → false.
- **Single Scalar** of kind `date` / `datetime` / `bytes` → true (existence is truthy).
- **Single Node (element)** → true.
- **Single Array** (v1.1 / v0.8.0) → true iff [; version-literal-ok ] non-empty (length > 0).
- **Single Map** (v1.1) → true iff non-empty.
- **Sequence of length > 1** → true.
This is the rule that makes `[?if //service …]` mean "if any service exists", `[?if @debug …]` mean "if the debug attribute exists and is truthy", and `[?if @items …]` mean "if the items array is non-empty" when `@items` is an Array-typed attribute.
The container rules (Array/Map empty-is-falsy) follow the convention shared by Python lists/dicts, JSON-template engines, and YAML/TOML processors. CX deliberately diverges from XPath 3.1 here, which raises a type error on EBV(array); CX's pragmatic rule reads more naturally in template position.
[?let] — binding
`[?let]` introduces a local binding. The bound name is in scope inside the body that follows.
[meta host='api.example.com' port='8443']
[# Multiple bindings — flat, sequential, each sees the prior #]
[?let [= $meta [$first //meta]]
[= $base [$string $meta@host]]
[= $port [$string $meta@port]]
[= $url [$concat 'https://' $base ':' $port]]
[endpoint target=$url]]
`[?let]` is the only way to give an expression a name currently — there is no `def` inside a function body, no inline `where` (that is a `[?for]` filter). To name a value that is used multiple times, `[?let]` is idiomatic.
[?modify] — pure-functional updates
`[?modify]` is the update directive. It takes a document (or sub-tree), a CXPath focus, and one or more action slots; it returns a **new** document with the action applied at every focus match. The original is unchanged (pure-functional).
The **eleven action slots** are:
| action | description |
|---|---|
| [set V] | Replace matched value, element body, or attribute value with V |
| [delete] | Remove matched node from its parent body (or attribute from element) |
| [using F] | Apply function F to each match; F receives the match, returns the replacement |
| [rename N] | Rename matched element (preserves attrs + body) |
| [set-attr N V] | Add new attribute N=V, or overwrite if N already present |
| [delete-attr N] | Remove attribute N from matched element |
| [append V] | Add child V at end of matched body |
| [prepend V] | Add child V at start of matched body |
| [insert-before V] | Insert V as a sibling immediately before each match |
| [insert-after V] | Insert V as a sibling immediately after each match |
| [replace V] | Replace entire matched node (including its surrounding placement) with V |
[doc
[user id=1 name='Ada' active=true]
[user id=2 name='Bob' active=false banned=true]
[section id='intro' [para 'Hi.']]
[list [item 'a']]
[h2 'T'] [code 'x'] [price 10.0] [para 'p'] [deprecated 'old']]
[# Scalar attribute update #]
[?modify $doc //user[= $_@id 1]/@name [set 'Alice']]
[# Delete matching nodes #]
[?modify $doc //user[= $_@active false] [delete]]
[# Transform with [using …] #]
[?modify $doc //price [using [?fn ($p) [* $p 1.1]]]]
[# Rename — keep attrs + body, change name #]
[?modify $doc //para [rename p]]
[# Set + delete attrs #]
[?modify $doc //user [set-attr verified true]]
[?modify $doc //user [delete-attr legacy-id]]
[# Append + prepend children #]
[?modify $doc //section[= $_@id 'intro'] [append [para 'Added.']]]
[?modify $doc //list [prepend [item 'header item']]]
[# Insert siblings #]
[?modify $doc //h2 [insert-before [hr]]]
[?modify $doc //code [insert-after [aside 'note']]]
[# Whole-node replace #]
[?modify $doc //deprecated [replace ()]]
[# Multi-step pipeline composition (see [[pipe]]) #]
[?pipe $doc
[?modify //user/@status [set 'verified']]
[?modify //user[= $_@banned true] [delete]]
[?modify //user [set-attr last-audit '2026-05-22']]]
**Pure-functional — structural sharing.** `[?modify]` does not copy the whole tree. Only the spine from root to each matched node is copied; unchanged branches are shared with the original (immutable persistent data structure). For a 10 MB document with one matched node, the new heap cost is < 1 KB per matched node — and the original `$doc` value is still valid for read or hash.
**Multi-match.** The focus path selects a sequence; the action applies to every node in that sequence. Zero matches returns the original unchanged (not an error — the no-match-no-op invariant lets pipelines compose safely). All-matches-at-once means there is no ordering hazard between matches.
**Identity invariant.** `[?modify $doc //x [set v]]` where `//x` matches no nodes returns `$doc` unchanged — same canonical bytes, same SHA-256. `[?modify $doc //x [delete]]` where every `//x` is removed returns a value where `[$count //x]` is 0. Round-trip through ast-bin / data-bin / JSON preserves the post-modify shape exactly.
**Status.** `[?modify]` is fully spec'd. The V evaluator carries the production-ready implementation. Tier-2 binding parity is tracked in [`spec/v0_8_0_status.md`](../../spec/v0_8_0_status.md) gate 30.
Pipelines and composition
The `[?pipe]` directive threads the output of one expression into the input of the next. Pipelines compose `[?modify]`, `[?for]`, builtins, and user-defined functions into readable top-to-bottom data flow.
[shop [user active=true age=30]
[user active=true age=20]
[user active=false age=40]]
[?pipe //user[= $_@active true]
[?fn ($us) [?for [in $u $us] [yield $u@age]]]
[$reverse]
[$nth _ 1]]
**Stages are bare transforms.** `[?pipe SRC STAGE …]` evaluates `SRC`, then supplies the value to each `STAGE` left-to-right. A stage is a transform — a bare name, a `$ref`, or a `[$call …]` — never wrapped in a keyword (there is no `[through]` clause). `[?pipe 5 add1 add1]` → `7`.
**Stage invocation (§8.9.1).** How the value is supplied depends on the stage shape: a **hole-form** stage carries a single partial-application hole `_` and the value fills it (`[$get _ '/users/42']` → `get(client, '/users/42')`); a **no-hole** stage receives the value **appended as its final positional argument** (`count` ≡ `[$count _]`). A pipe threads exactly one value, so a stage may carry **at most one** `_` (two or more → `cx-err:CXER0100`). A non-callable stage (a literal, map, or data element) is `cx-err:CXER0100` — it never silently passes the value through.
**Fan-out behavior.** When `SRC` evaluates to a sequence, a stage sees the whole sequence as a single value (the sequence-flat principle — no implicit per-item dispatch). For a per-item transform, thread a `[?fn]` lambda that runs the comprehension:
[users [user name=ada] [user name=bob]]
[?pipe //user
[?fn ($us) [?for [in $u $us] [yield [decorated name=$u@name]]]]
[$count]]
**`[tap f]` — observe without diverting (§8.9.2).** A `[tap f]` stage applies `f` to the current value for its **side effect**; the value passes through unchanged and `f`'s return is discarded. Inside a stage, `_` names the current value, so `[tap [$log:info 'seen' fields={v: _}]]` logs and flows on. A tap propagates an `[err]` only when its code is `CXER0001` (panic) or `CXER0260` (cancellation); every other error is discarded for the data flow (the error-hook still observes it).
**Railway short-circuit (§8.9.3).** The pipe is a single-channel failure railway: the first stage to yield `[err …]` skips every remaining stage — taps included — and that `[err]` is the result. An empty result is **not** a short-circuit; it flows to the next stage. There is no infix `|` and no `[through]` wrapper — both retired.
Built-in functions
Built-ins are the standard library at the expression layer. They are pure-functional, total over their declared input types, and stable across bindings. Full normative table in [`spec/code.md`](../../spec/code.md) §6.5; the subsections below organize the ~80 names by category.
**Invocation.** Every call uses the bracket head-dispatch form: `[$name args…]` for the built-in surface, or `[$module:name args…]` for stdlib-module calls. There is **no paren-call form** — `name(args)` is a parse error everywhere. The whitespace-separated element form `[name a b]` without a leading `$` is **always** element construction; write `[$count //user]` (call) versus `[count //user]` (data element). The `name(args)` spelling in the tables below is arity notation (mirroring the spec), not invocation syntax.
Sequence built-ins
Operate on a Sequence. Argument may be a CX program sequence, the implicit document, or a single non-sequence value (treated as a 1-element sequence).
| builtin | arity | semantics |
|---|---|---|
| count(seq) / length(seq) | 1 | Integer count; scalar input yields 1 |
| empty(seq) | 1 | Boolean — true iff count(seq) == 0 |
| exists(seq) | 1 | Boolean — true iff count(seq) > 0; opposite of empty |
| first(seq) | 1 | First item; scalar passes through; XQuery fn:head parity |
| last(seq) | 1 | Last item; scalar passes through |
| head(seq) | 1 | Synonym for first |
| tail(seq) | 1 | All items except first; scalar yields () |
| reverse(seq) | 1 | Items in reverse order; scalar passes through |
| distinct(seq) | 1 | Items with structural duplicates removed; first-occurrence order |
| nth(seq, n) | 2 | 1-indexed item access; out-of-range raises CXER0100 |
| position(seq, item) | 2 | 1-based index of first structural match; zero if absent |
| range(lo, hi, step?) | 2-3 | Arithmetic progression, inclusive bound; [$range lo *] is a lazy open iterator |
| iterate(f, seed) | 2 | Lazy infinite iterator: seed, f(seed), f(f(seed)), … |
| unfold(f, seed) | 2 | Anamorphism: f(state) yields () to stop or [value, next-state] to emit |
| identity(x) | 1 | Returns x unchanged — for pipe stages and shape testing |
Set combinators are **reserved operator heads**, not `[$…]` builtins: `[union A B]`, `[intersect A B]`, `[except A B]`. Slicing / windowing / ordering / grouping are directives and comprehension clauses: `[?take]`, `[?drop]`, `[?filter]`, `[?flatten]`, `[order-by …]`, `[group-by …]` (see for).
[site [user active=true] [user active=false]
[tag 'a'] [tag 'b'] [tag 'a']]
([$count //user[= $_@active true]],
[$distinct [?for [in $t //tag] [yield [$text $t]]]],
[?take 3 [$range 1 100]],
[union (1, 2) (2, 3)])
String built-ins
Operate on a String scalar. Non-string scalars are stringified by the canonical scalar printer; element arguments raise `CXER0100` unless documented otherwise.
| builtin | arity | semantics |
|---|---|---|
| upper(s) | 1 | Unicode upper-cased copy of s |
| lower(s) | 1 | Unicode lower-cased copy of s |
| contains(s, sub) | 2 | Boolean substring test; empty sub yields true |
| starts-with(s, prefix) | 2 | Boolean prefix test; empty prefix yields true |
| ends-with(s, suffix) | 2 | Boolean suffix test; empty suffix yields true |
| substring(s, start, len?) | 2-3 | 1-indexed substring; positions clamp; never raises |
| string-length(s) | 1 | Integer count of Unicode codepoints (NOT bytes) |
| normalize-space(s) | 1 | Strip leading/trailing + collapse internal whitespace |
| concat(s1, s2, …) | ≥1 | String concatenation; non-string scalars stringified |
| text(elem) | 1 | Body text of elem as string; concatenates text children |
The wider string toolkit — trim, split/join, replace, case-folding variants, padding — is the `cx-stdlib/strings` module (`[?lib 'cx-stdlib/strings']`, then `[$strings:trim …]`, `[$strings:split …]`, `[$strings:join …]`, `[$strings:replace …]`). Regular expressions are `cx-stdlib/re`. See the Standard-library pages.
[?lib 'cx-stdlib/strings']
([$upper 'hello world'],
[$contains 'ada@example.com' '@example.com'],
[$strings:join ('a', 'b', 'c') ', '],
[$strings:split 'a,b,c' ','],
[$strings:replace 'foofoo' 'foo' 'bar'])
Numeric built-ins
Operate on numeric scalars. `sum`/`min`/`max`/`avg` operate on a sequence and skip non-numeric items; `floor`/`ceiling`/`round`/`abs` operate on a single value.
| builtin | arity | semantics |
|---|---|---|
| sum(seq) | 1 | Sum of numeric scalars; empty yields int 0; any float → float result |
| min(seq) | 1 | Minimum numeric; empty yields int 0 |
| max(seq) | 1 | Maximum numeric; empty yields int 0 |
| avg(seq) | 1 | Arithmetic mean as float; empty yields float 0.0 |
| abs(x) | 1 | Absolute value; preserves int/float kind |
| floor(x) | 1 | Largest integer ≤ x; int passes through |
| ceiling(x) | 1 | Smallest integer ≥ x; int passes through |
| round(x) | 1 | Half-away-from-zero rounding (XPath fn:round) |
| mod(a, b) | 2 | Remainder; sign follows dividend; b=0 raises CXER0101 |
| div(a, b) | 2 | True division if either is float; integer division if both int |
| idiv(a, b) | 2 | Integer (truncating) division regardless of operand kinds |
Type coercion is the single explicit `cast` operator — `[cast '42' :int]`, `[cast 3 :float]`, `[cast 42 :string]` — there is no `to-int` / `to-float` family. CX floats are **finite-only**: `NaN` and `±Inf` never arise (an operation that would produce one raises `CXER0101`), so there is nothing to test with an `is-nan`. Exponentiation is `[$math:pow]` in `cx-stdlib/math`.
[orders [order amount=10] [order amount=32]]
([$sum [?for [in $o //order] [yield $o@amount]]],
[$round 3.7],
[$mod 17 5],
[cast '42' :int])
Logical built-ins
Boolean combinators and EBV-aware tests. `and` / `or` short-circuit; `not` is total over any EBV-typed argument.
| builtin | arity | semantics |
|---|---|---|
| not(v) | 1 | Boolean negation under EBV (cxdm §6); an Iterator operand raises — force it first |
| and(a, b, …) | ≥1 | Boolean — true iff all args truthy; short-circuit L→R; bare operator head |
| or(a, b, …) | ≥1 | Boolean — true iff any arg truthy; short-circuit L→R; bare operator head |
| eq(a, b) | 2 | Structural equality — types, values, attributes, children all match |
`and` / `or` / `not` are bare operator heads (`[and P Q]`, no `$` sigil); `eq` is the structural equality the `=` operator also uses. There is no `xor` / `if()` / `instance-of` builtin — branch with `[?if]`, and test types by schema validation or `[?match]` shape dispatch.
[site [user verified=true active=true banned=false] [error 'e1']]
[?let [= $u [$first //user]]
([and $u@verified $u@active [not $u@banned]],
[$exists //error],
[eq [a 1] [a 1]])]
Node-accessor built-ins
Operate on an Element node — read structural properties. Script-level equivalents of the Layer-1 binding methods (see layer-1).
| builtin | arity | semantics |
|---|---|---|
| name(elem) | 1 | Element name as a string (including any namespace prefix); non-element raises CXER0100 |
| local-name(elem) | 1 | Element name without namespace prefix |
| string(value) | 1 | A scalar's canonical text, or an element/attribute's text content |
| text(elem) | 1 | Body text of elem; concatenates text children (string builtin, listed for discoverability) |
Structural navigation is CXPath, not accessor calls: children are `$e/*`, the parent is `//x/parent::*` (document-anchored), attributes are `$e@name`. Inside a predicate the reserved bindings `$_position` and `$_last` replace XPath's `position()` / `last()` — there are no zero-arity accessor calls.
[# name → 'section' · string → 'one' · [$_last] → the last
section · [= $_position 1] → the first #]
[doc [section 'one'] [section 'two']]
([$name [$first //section]],
[$string [$first //section]],
//section[$_last],
//section[= $_position 1])
Errors as values
CX treats errors as ordinary values. An error is a CX element with kind `[err :code "cx-err:CXERnnnn" …]`. It can be passed around, pattern-matched, transformed, and recovered from. Full error registry in [`spec/code.md`](../../spec/code.md) §9.5.
**Propagation operators.** Two postfix operators thread errors through expressions:
- **`f($x)?`** — call `f`; if it errored, propagate the error unchanged through the surrounding directive (which catches it). This is the "early return" form.
- **`f($x)!`** — call `f`; if it errored, raise immediately as a host exception (cross the CX/binding boundary). Use sparingly — it converts the error from a value to a runtime trap.
[?match [?let [= $u [$lookup-user $id]?] [$render $u]]
[case [err code='CXER0204'] [err message='no such user']]
[case [err code='CXER0163'] [err message='lookup timed out']]
[else [err message='lookup failed']]]
Recovery is `[?match]` (see match) over the `[err]` channel: the errorable expression is the scrutinee, and an `[err]` it produces is captured (not propagated) so a `[case [err …] …]` arm can dispatch on its code. Match by plain attribute equality (`[err code='CXER0204']`) or capture the code (`[err @code=$c]`). Arms evaluate first-match-wins; an `[else …]` (or bare `$err` capture) is the catch-all. With no catch-all and no match, the `[err]` propagates to the next enclosing `[?match]` or out of the program. For a plain value-or-default, `[?else EXPR DEFAULT]` (§8.13) is the lighter sugar; `[?fallback PRIMARY [recover-with …]]` (§10.2.4) binds `$err` in the recovery.
Error code registry (CXER0100–0299)
The CXER0100–0299 range is reserved for code-language errors. Errors are organized into subsystems by tens digits. The complete normative map is in [`spec/code.md`](../../spec/code.md) §9.5 — this subsection is the developer-facing summary.
**Pattern / parse — CXER010x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0100 | PARSE_ERROR | Pattern syntax invalid; directive name not in registry; arity mismatch | Fix source — this is author error, not runtime |
| CXER0101 | ANCHOR_ERROR | Duplicate &anchor; cyclic *merge; unresolved *name | Author error — flagged at parse time |
| CXER0102 | DUPLICATE_KEY | Map literal with duplicate key | Author error |
| CXER0103 | TYPE_ERROR | Value-comparison on multi-valued operand; sized-overflow; type-coerce failure | [?match] [case [err code="CXER0103"] …]; explicit coercion via to-int/to-float |
**Resilience — CXER014x / CXER015x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0140 | RETRY_EXHAUSTED | [?retry] reached max= attempts without success | [?match] [case [err code="CXER0140"] …]; tune max= / backoff= |
| CXER0141 | TIMEOUT | [?timeout] DURATION elapsed | [on-timeout EXPR] or [?match] [case [err code="CXER0141"] …] |
| CXER0150 | BREAKER_OPEN | [?circuit-breaker] tripped; rejecting calls | Wait :reset DURATION; fallback path |
| CXER0151 | RATE_LIMITED | [?rate-limit] tokens exhausted | Honor :retry-after slot in the err; back off |
| CXER0152 | BULKHEAD_FULL | [?bulkhead] max-concurrent= + queue= exhausted | Backpressure caller; reduce concurrency |
**Services — CXER016x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0160 | BAD_REQUEST | HTTP 400 — malformed body or path-param parse failure | Return [response status=400 [body $err]] to client |
| CXER0161 | UNAUTHORIZED | HTTP 401 — :auth returned err | Return 401; client re-authenticates |
| CXER0162 | NOT_FOUND | HTTP 404 — no resource matched path + method | Return 404; client checks URL |
| CXER0163 | REQUEST_TIMEOUT | HTTP 408 — :read-timeout exceeded | Retry with backoff; reduce request size |
| CXER0164 | PAYLOAD_TOO_LARGE | HTTP 413 — body > max-body-bytes= | Chunk upload; increase max-body-bytes= if appropriate |
| CXER0165 | INTERNAL_ERROR | HTTP 500 — unhandled err in resource body | Recover the body with [?match]/[?fallback]; log and return generic 500 to client |
| CXER0166 | SHUTTING_DOWN | HTTP 503 — service in shutdown drain | Client retries elsewhere; do not retry same instance |
**Client — CXER018x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0180 | CONNECTION_REFUSED | TCP connect failure | Retry with backoff; check :target URL |
| CXER0181 | TLS_HANDSHAKE_FAILED | TLS negotiation failed | Check :tls config; cert validity; retry |
| CXER0182 | INVALID_RESPONSE | Server response could not be parsed | Log raw response; report upstream issue |
**Concurrency — CXER020x / CXER022x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0200 | CHANNEL_CLOSED | Receive from drained-closed channel; send to closed channel | Treat as normal stream-end; [?match] on err code |
| CXER0201 | SEND_TIMEOUT | [?try-send] buffer-full timeout | Drop, queue elsewhere, or back off |
| CXER0202 | RECV_TIMEOUT | [?try-receive] empty timeout | Poll again; abandon work if pattern persists |
| CXER0203 | CHANNEL_ALREADY_CLOSED | Second [?close] on same channel | Idempotent close idiom — [?else [?close $ch] ()] |
| CXER0220 | WORKER_PANIC | Worker body raised unhandled err | Inspect cause= attribute; restart worker or supervisor pattern |
| CXER0221 | WORKER_CANCELLED | Worker terminated via [?cancel] | Expected on graceful shutdown; clean up resources |
| CXER0222 | WORKER_NOT_FOUND | [?worker-handle] lookup miss | Use [?worker-handle :name N :optional true] or check exists |
**Async — CXER024x / CXER026x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0240 | AWAIT_ALL_FAILED | [?await-all] saw ≥ 1 non-done future | Inspect :causes slot — sequence of [err …] per failed future |
| CXER0241 | AWAIT_TIMEOUT | [?await $f :timeout] deadline expired (future itself still running) | Tune :timeout; abandon or [?cancel $f] |
| CXER0260 | CANCELLED | Operation observed cooperative cancellation | Expected; clean up; do not retry |
**Visualization — CXER028x:**
| code | symbolic | meaning | recovery |
|---|---|---|---|
| CXER0280 | RENDER_FAILED | Sequence-diagram renderer could not produce output | Check directive shape; report renderer version + program |
| CXER0281 | UNRENDERABLE_DIRECTIVE | Directive shape outside §10.1.2 locked render rules | Adjust source; renderer is conservative on purpose |
The CXER0000–0099 range is reserved for data-language errors (parse, validate, canonicalize); see limits in the data section. CXER0300+ is reserved for future subsystems.
Concurrency
CX has three concurrency surfaces, all spelled as directives. They build on the pure-functional core: every channel send, every worker call, every async await sees only immutable values, so there is no shared-mutable-state hazard. Spec: [`spec/code.md`](../../spec/code.md) §10.3–§10.5.
Services and clients
`[?http-service]` declares a named HTTP service with resource handlers. `[?http-client]` is its client side. Resources handle requests and return responses; the bodies are ordinary CX code.
**Service definition:**
[?http-service on=http port=8080 name='users'
read-timeout=30s
write-timeout=30s
max-body-bytes=10485760 [# 10 MiB #]
max-connections=1000
grace-period=30s
[resource method=GET path='/users/:id'
[?let [= $u //user[@id=$request/path-params/id]]
[response status=200 [body $u]]]]
[resource method=POST path='/users'
consumes=application/cx
[?let [= $body $request/body]
[response status=201 [body [$create-user $body]]]]]]
**Resource parameters.** Each `[resource]` declares `method=METHOD path=PATH`, optional `produces=` / `consumes=` MIME attributes, optional `[auth …]` predicate-expression clause, and a body expression in positional position. PATH supports `:name` parameter binding — `/users/:id` exposes `$request/path-params/id` inside the body.
**Request and response shapes:**
[request method='GET' path='/users/42'
[path-params [id 42]]
[query-params]
[headers [header name='Accept' value='application/cx']]
[body]]
[response status=200
[headers [header name='Content-Type' value='application/cx']]
[body [user id=42]]]
**Lifecycle.** `[?http-service]` returns a handle. `[?service-handle name=N]` looks up an existing service. `[?stop $handle]` initiates graceful shutdown — drains in-flight requests for up to `grace-period=`, then force-closes; new requests during drain receive HTTP 503 (`CXER0166`). Process SIGTERM / SIGINT also triggers graceful shutdown.
**HTTP client.** `[?http-client target=URL …]` returns a client value; operations pipe through it:
[?let [= $c [?http-client target='https://api.example.com'
timeout=30s
[resilience [?retry max=3]]]]
[?pipe $c [$get _ '/users/42']]]
**Status.** Services are a stable surface; Tier-1 V binding ships the full implementation. Tier-2 bindings expose `[?http-client]` and `[?service-handle]` lookup but service-hosting may defer to the host's native HTTP framework. See bindings.
Workers and channels
`[?worker]` spawns a concurrent worker. `[?channel]` creates a FIFO message queue. `[?send]` and `[?receive]` move values across. `[?select]` is the multi-way receive.
[?let [= $ch [?channel name='jobs' buffer=8]]
[= $sent1 [?send 1 to=$ch]]
[= $sent2 [?send 2 to=$ch]]
([?receive from=$ch], [?receive from=$ch])]
[?let [= $ch [?channel name='jobs' buffer=100]]
([?worker name='producer'
[body [?for [in $i [$range 1 100]]
[yield [?send $i to=$ch]]]]],
[?worker name='consumer'
[body [?for [in $i [$range 1 100]]
[yield [$process [?receive from=$ch]]]]]])]
**Channel semantics.** `[?channel :buffer N]` has a bounded FIFO buffer. `:buffer 0` is synchronous (rendezvous — send blocks until receive). FIFO ordering is preserved per producer-consumer pair; with multiple producers, per-producer FIFO is preserved but there is no global cross-producer ordering.
**Send / receive variants.**
- `[?send V to=CH]` — block when buffer full; returns `[ok]` once buffered. Returns `CXER0200` (CHANNEL_CLOSED) if CH is closed.
- `[?try-send V to=CH timeout=DURATION]` — same but returns `CXER0201` (SEND_TIMEOUT) on buffer-full timeout.
- `[?receive from=CH]` — block when empty; returns next value. Returns `CXER0200` when CH is closed and drained.
- `[?try-receive from=CH timeout=DURATION]` — same but returns `CXER0202` (RECV_TIMEOUT) on empty-channel timeout.
- `[?close CH]` — mark closed. Subsequent `[?send]` fails immediately; subsequent `[?receive]` drains buffered values then returns CHANNEL_CLOSED. Double-close raises `CXER0203`.
**Worker observability.** Workers are handle-addressable:
[?let [= $w [?worker name='sync-job' [body [$sync-database]]]]
([?wait-for worker=$w], [# block until done #]
[?cancel worker=$w], [# cooperative #]
[?check-cancel])] [# polling point #]
**Select — multi-way receive.** Evaluates all `[case …]` clauses concurrently; proceeds with the first ready. Uniform-random selection on simultaneous readiness:
[?let [= $jobs [?channel name='jobs' buffer=1]]
[= $signal [?channel name='sig' buffer=1]]
[= $sent [?send 42 to=$jobs]]
[?select
[case [from $jobs $job] [handled $job]]
[case [from $signal $sig] [signalled $sig]]
[case [timeout 5s] [heartbeat]]]]
**Shape.** `[?select]` clauses are clause-children: `[case [from CH $msg] HANDLER]` for a receive and `[case [timeout DURATION] HANDLER]` for a deadline. The spec/core/code.md §10.4.7 registry holds the authoritative shape. The v0.7.x `:case [:from CH $msg H]` [; version-literal-ok ] colon-slot form is retired — there is no dual-accept.
Async, await, and cancellation
`[?async EXPR]` evaluates EXPR in a new async context and returns a future immediately:
[?let [= $f1 [?async [+ 1 1]]]
[= $f2 [?async [* 2 2]]]
[= $f3 [?async [$upper 'go']]]
[?await-all $f1 $f2 $f3]]
**Future state machine.** Futures progress through `pending → running → done` / `failed` / `cancelled`. Once terminal, the state is immutable.
**Barrier variants:**
- `[?await $f]` — block until terminal; return value on done, propagate err on failed, CANCELLED on cancelled.
- `[?await $f timeout=DURATION]` — add a hard deadline; on expiry return `CXER0241` (AWAIT_TIMEOUT). The future continues running — `timeout=` bounds the caller's wait, not the future itself.
- `[?await-all FUTURES]` — block until all terminal. If all done, yield sequence of values. If any failed/cancelled, yield `CXER0240` with a `[causes …]` child carrying every non-done cause in input order.
- `[?await-any FUTURES]` — yield first to terminate. Other futures continue running; their results are discarded.
- `[?await-race FUTURES]` — yield first to terminate; **other futures are cancelled**. Use for "I only need one".
**Cooperative cancellation contract.** `[?cancel $f]` requests cancellation — it is a signal, not a kill. The runtime guarantees cancellation observation at:
- HTTP client calls at next network read/write boundary.
- `[?sleep DURATION]` — immediately.
- `[?send]` / `[?receive]` / `[?try-send]` / `[?try-receive]` — immediately, returning `CXER0260` (CANCELLED).
- `[?for]` — at every iteration boundary.
- `[?await]` — immediately.
**Pure CPU loops without yield points do NOT observe cancellation.** Authors who want a cancellable hot loop must insert `[?check-cancel]` at appropriate boundaries — it evaluates to `[ok]` normally, or `CXER0260` (CANCELLED) if cancellation is pending.
[?def cancellable-compute impure ($n)
[?for [in $i [$range 1 $n]]
[where [= [?check-cancel] [ok]]]
[yield [* $i $i]]]]
[$cancellable-compute 4]
**Composition with resilience.** `[?async]` composes with every resilience directive. `[?async [?timeout 5s [$slow-call]]]` returns a future that resolves to `CXER0141` (TIMEOUT) after 5s if `slow-call` hasn't completed.
**Status.** Async/await is stable in V. Tier-2 binding parity (Python `asyncio`, Go `chan`, Rust `tokio`) ships per [`spec/v0_8_0_status.md`](../../spec/v0_8_0_status.md) gate 28.
Resilience directives
The six resilience directives wrap an inner expression with a failure-handling policy. They are not new control-flow primitives — they are normal CX code with cleanly-named policies. Spec: [`spec/code.md`](../../spec/code.md) §10.2.
**Common error shape.** Every resilience directive on terminal failure emits a structured `[err …]` per the §10.2.7 error-code matrix (see error-registry).
[?retry] — bounded re-run with backoff
Re-run an inner body on `[err …]` return, up to `max=` attempts, sleeping between attempts per `backoff=` / `delay=` / `jitter=`. Terminal failure raises `CXER0140` (RETRY_EXHAUSTED).
| parameter | default | semantics |
|---|---|---|
| max=INT | (required) | Maximum attempts; must be > 0 |
| backoff=STRATEGY | exponential | constant / linear / exponential / fibonacci |
| delay=DURATION | 100ms | Base delay; combined with backoff formula |
| jitter=MODE | equal | none / full / equal / decorrelated |
| [on PREDICATE] | [?fn ($e) true] | Per-err filter; retry iff truthy |
| body (positional) | (required) | Inner expression to retry |
**Backoff formulas** (delay for attempt N, N ≥ 1): `constant` = `delay=`; `linear` = `delay × N`; `exponential` = `delay × 2^(N-1)`; `fibonacci` = `delay × fib(N)`.
**Jitter modes** (D = pre-jitter delay): `none` = `D`; `full` = `uniform(0, D)`; `equal` = `D/2 + uniform(0, D/2)`; `decorrelated` = `uniform(delay, last_delay × 3)`.
[?retry max=5 backoff=exponential delay=200ms jitter=equal
[on [?fn ($e) [= $e@code 'CXER0163']]] [# only on timeout #]
[?pipe $client [$get _ '/users/42']]]
[?timeout] — hard deadline
Bound execution time of an inner body. On expiry, cooperatively cancel the body and return the `[on-timeout …]` clause body (if present) or `[result status=err code='cx-err:CXER0141' elapsed=DURATION]`.
| parameter | default | semantics |
|---|---|---|
| DURATION (positional) | (required) | Hard deadline; e.g. 5s, 250ms |
| body (positional) | (required) | Inner expression with cooperative cancel points |
| [on-timeout EXPR] | (none) | Fallback expression on expiry; otherwise raises CXER0141 |
[?timeout 5s
[$slow-database-query $q]
[on-timeout [result status=err code='CXER0141' reason='db slow']]]
[?circuit-breaker] — fail-fast on consecutive failures
Three states: **closed** (passes through), **open** (rejects with `CXER0150` (BREAKER_OPEN) without calling `:body`), **half-open** (allows one probe). Transitions:
- Closed → open when failure ratio in rolling `window=` exceeds `threshold=`, with at least `min-samples=` recorded.
- Open → half-open after `reset=` elapses.
- Half-open → closed if probe succeeds.
- Half-open → open if probe fails.
| parameter | default | semantics |
|---|---|---|
| threshold=RATIO | (required) | Failure ratio 0.0–1.0 to trip breaker |
| window=DURATION | (required) | Rolling-window length for sampling |
| reset=DURATION | (required) | Half-open delay after trip |
| min-samples=INT | 10 | Minimum sample count before threshold check |
| name=STR | (lexical position) | State-identity key for cross-position sharing |
| body (positional) | (required) | Inner expression |
[?circuit-breaker
threshold=0.5 window=60s reset=30s min-samples=20
name='users-api'
[?pipe $client [$get _ '/users/42']]]
[?fallback] — alternative on error
If primary returns `[err …]`, evaluate secondary and return its value (which may itself be `[err]` — `[?fallback]` does not wrap that case).
| parameter | default | semantics |
|---|---|---|
| body (positional) | (required) | First expression tried (primary) |
| [recover-with SECONDARY] | (required) | Fallback expression evaluated only on primary err |
[?fallback
[?pipe $client [$get _ '/users/42']]
[recover-with [$load-from-cache 42]]]
**`$err` in `[recover-with …]`.** Inside the recovery scope, `$err` is bound to the primary's `[err …]` value, so recovery can introspect the failure (e.g. `[recover-with [response status=500 reason=$err/@message]]`). The clause-child `[recover-with …]` form is the sole accepted shape (no legacy colon-slot).
[?rate-limit] — token-bucket cap
Permits at most `max=` invocations per `per=` window. Excess invocations return `CXER0151` (RATE_LIMITED) with a `retry-after=DURATION` attribute — the time until the next token frees.
| parameter | default | semantics |
|---|---|---|
| max=INT | (required) | Tokens per window; must be > 0 |
| per=DURATION | (required) | Refill window |
| name=STR | (lexical position) | State-identity key |
| body (positional) | (required) | Inner expression |
[?rate-limit max=100 per=1s name='github-api'
[?pipe $client [$get _ '/repos/cx-home/cx']]]
[?bulkhead] — bound concurrent calls
Bounds concurrent invocations. If `max-concurrent=` slots are busy and `queue=` slots are full, returns `CXER0152` (BULKHEAD_FULL). Queued requests wait FIFO for a free slot.
| parameter | default | semantics |
|---|---|---|
| max-concurrent=INT | (required) | In-flight cap; must be > 0 |
| queue=INT | 0 | Wait-queue depth; 0 = no queue, fail-fast on full |
| name=STR | (lexical position) | State-identity key |
| body (positional) | (required) | Inner expression |
[?bulkhead max-concurrent=10 queue=50 name='db-pool'
[$query-database $q]]
Composition
Resilience directives compose by nesting. The outermost runs first, so its policy sees the inner policies' err codes. Standard pattern reads top-down:
[?retry max=3
[?timeout 10s
[?circuit-breaker threshold=0.5 window=60s reset=30s
[?http-client target='https://api.example.com' method='get']]]]
Reading: retry up to 3 times a 10-second-bounded call gated by a 50%-failure circuit-breaker. If the breaker opens, `[?retry]` sees `CXER0150` and decides whether to keep retrying (default `[on …]` predicate returns truthy → yes; override to scope which errs retry).
**State identity (stateful directives).** `[?circuit-breaker]`, `[?rate-limit]`, and `[?bulkhead]` carry state across invocations. By default state is keyed by lexical source-text position — every evaluation of the same source node (including iterated under `[?for]` or `[?map par=true]`) shares state. Pass `name=STR` to key by name instead, enabling cross-position sharing (one rate-limit budget enforced by callers in different modules).
Evaluation framing — directives, patterns, comprehensions
The directive registry above lists each form; this section shows how the forms fit together — directives as the language layer over data, pattern matching as the way programs walk a tree, comprehensions as the composition glue. CXPath is a value kind (with `[?find]` retired) and `[expr]` is the general predicate.
Directives turn data into a language
A directive is a CX element whose name starts with `?`. It carries semantics — iteration, branching, substitution, include, function call — that the evaluator interprets when running a CX program. Directives parse as regular CX, evaluate to text or values, and compose with each other through their slot bodies.
EXPR [; bare expression evaluates ]
[?for [in $x PATH] [yield BODY]] [; iterate ]
[?if COND [then THEN] [else ELSE]] [; branch ]
[?let [= $x EXPR] BODY] [; bind a name ]
[?def name ($a $b) BODY] [; define a function ]
Pattern matching — walking the tree
The current program surface replaces XPath-style queries with `[?for]` pattern generators and CXPath path values (`[?find]` retired). A pattern is a CX literal where element names match by shape, attribute predicates restrict by value, and `$bindings` capture matched subtrees. `[?for]` walks the tree (deep by default) and binds for each match; `[?match]` tests a single value (multi-arm dispatch).
[shop
[user [name 'Ada']]
[item active=true 'i1']
[pizza [name 'Margherita'] [price 12]]]
[?for [user [name $n]] [yield $n]]
[?for [item @active=true $i] [yield $i]]
[?for [pizza [name $n] [price $p]]
[yield [hit name=$n price=$p]]]
Comprehensions and composition
`[?for]` iterates a sequence (or matches a pattern across the tree) and yields per-iteration values; `[where …]` filters, `[order-by …]` sorts, `[group-by …]` aggregates. `[?let]` binds names; `[?if]` branches. The `[?pipe]` directive composes them.
[?for [in $x (1, 2, 3)] [where [> $x 1]] [yield [* $x $x]]]
[?let [= $a 10] [= $b 32] [+ $a $b]]
[?pipe (1, 2, 3, 4) [$count]]
Future surface — ranges, slices, Iterator
A future surface extends the comprehension surface with **strided ranges** (`[$range 1 99 2]`, `[$range 1 *]`), **slice expressions on a binding postfix** (`$xs[2:5]`, `$xs[::-1]`, `$xs[:, "name"]`), **first-class slice values** (`[?def $s [2:$_last:2]]`), and a new **`Iterator` value kind** in CXDM §2 alongside Sequence / Array / Map. Comprehensions become lazy by default; named iterators (`[?def`]) memoize on first walk so re-use is safe.
Eight worked examples (§W1–§W8) each have a paired conformance fixture `program-slice-wN-*` in [`conformance/code.txt`](../../conformance/code.txt); until the implementation lands the fixtures are `out_err: cx-err:CXER0100` (parse-deferred), then flip to `out_text` in a single commit per fixture. The reconciliation test `program-cxpath-fncall-slice-composition` verifies grammar disambiguation against the B15 closure (`spec/cxpath.md` §2.2) — bare-fn-call, path predicate, and slice postfix coexist.
[# W1 — strided range, then slice, then comprehension #]
[?def $odds [$range 1 99 2]]
[?def $first-ten $odds[:10]]
[?def $squares [?for [in $x $first-ten] [yield [* $x $x]]]]
[# W5 — infinite range with [take …] #]
[?for [in $x [$range 1 *]] [where [$prime? $x]] [take 100] [yield $x]]
[# W7 — map comprehension over CXPath source #]
[?for [in $u //user] [yield-map [$u@id $u@email]]]
The path-to-iterator taxonomy (D26): a CXPath value is **not** an `Iterator` — it is a *description of a selection*. Evaluation materialises it to a `sequence` (eager, the kind currently in use). The `sequence` flows into Iterator combinators on opt-in via `[?for]` or the explicit `[?to-iterator]` directive. There is no `iterator → path` direction — paths describe; iterators walk.
CXPath reference — quick form
At-a-glance CXPath reference. The normative grammar lives in [`spec/grammar.ebnf`](../../spec/grammar.ebnf) productions [130]-[135] (path / predicate base) and [159]-[160] (general PredicateExpr + `:bind` step modifier). XPath 3.1 alignment and divergences are tabulated in [`spec/cxpath_alignment.md`](../../spec/cxpath_alignment.md). CXPath is a first-class value kind; general predicates with `$_` / `$_position` / `$_last`.
Quick reference
[; Every user in the document. ]
//user
[; Every user whose @active attribute equals true. ]
//user[= $_@active true]
[; Email addresses of every active user. ]
//user[= $_@active true]/@email
[; The third user in document order. ]
//user[3]
[; Anything two levels deep under root. ]
/*/*
[; Every sibling that follows the first user. ]
//user[1]/following-sibling::user
[; Every ancestor element of a [para] node. ]
//para/ancestor::*
[; General predicate — prefix code over the $_ context binding. ]
//user[and [>= $_@age 18] $_@active]
[; Range and set combinators. ]
[$range 1 10]
[union //a //b]
[intersect //x //y]
[except //all //banned]
Four leading forms
Every CXPath has one of four leading-token forms. - **`//` descendant** — `//user` — `descendant-or-self::node()/child::user`; the most common shape. - **`/` absolute** — `/root/item` — child of the document root; rare in well-shaped data. - **relative** — `user/email` — applied against the current evaluation context. - **`$name/` binding** — `$u/email` — rooted at a previously-bound value (per `[?for]` / `[?let]` / `:bind`).
Twelve axes
Every step can specify an axis. The default axis is `child::`; the `attribute::` axis has the dedicated `@` sigil. CX implements all twelve XPath 3.1 axes: `child`, `descendant`, `descendant-or-self`, `parent`, `ancestor`, `ancestor-or-self`, `self`, `following-sibling`, `preceding-sibling`, `following`, `preceding`, `attribute`.
Composite shortcut forms: `/name` ≡ `child::name`; `//name` ≡ `descendant-or-self::node()/child::name`; `@name` ≡ `attribute::name`; `..` ≡ `parent::node()`; `.` ≡ `self::node()`. Attribute axis is name-disjoint from element axes — `//user/@name` selects the attribute only; `//user/name` selects the child element only.
General predicate + reserved bindings
Predicates filter the step sequence. A predicate body is prefix CX code coerced to a boolean via the Effective Boolean Value (EBV) rule. The atomic shorthands — `[@name]` / `[@!name]` (attribute present / absent), `[N]` (position), a bare child name (step-existence) — remain available and round-trip via canonical re-emit. Value comparisons are always prefix: `[= $_@name V]`.
**Three reserved bindings** in scope inside every predicate body: `$_` (current candidate item, type matches the candidate kind), `$_position` (1-based int index — equivalent to XPath `position()`), `$_last` (int cardinality of the candidate sequence — equivalent to XPath `last()`). Predicates are eagerly materialised, so `$_last` is known on every predicate evaluation.
**Comparison sigils** are value-comparison only: `=`, `!=`, `<`, `<=`, `>`, `>=`. No keyword synonyms (no `eq`/`ne`/`lt`/etc.). Comparing a sequence with more than one item raises `CXER0103`. **Purity** — every predicate body MUST be pure; impure callees fail load with `CXER0230`.
Cross-step reference — nested generators
To reference an **outer** step's focus from an inner predicate ("leads of teams with at least three members"), stack `[?for]` generators — each generator names its focus, and every later clause sees the earlier bindings:
[org
[team [member role='lead' [name 'a']] [member [name 'b']] [member [name 'c']]]
[team [member role='lead' [name 'e']]]]
[?for [in $t //team]
[in $m $t/member]
[where [and [= $m@role 'lead'] [>= [$count $t/member] 3]]]
[yield $m/name]]
The grammar reserves a `:bind` path-step modifier (grammar.ebnf [159]-[160]) for naming a step's focus inline; the nested-generator form above is the conformance-exercised idiom.
In the host bindings
Every Tier-1 binding exposes CXPath identically through its Layer-1 surface — same selector, same bytes returned across the C ABI.
emails = doc.select_all("//user[= $_@active true]/@email")
emails, _ := doc.SelectAll("//user[= $_@active true]/@email")
let emails = doc.select_all("//user[= $_@active true]/@email")?;
emails := doc.select_all('//user[= $_@active true]/@email')!
Error code catalog
Every CX error carries a structured payload: `cx-err:CODE` then a unit separator (US, `0x1F`) then a human-readable description then optionally another unit separator and a value. Bindings preserve this shape so `[?match]` can match on code rather than on text. The catalog below covers the current ranges; the full normative table lives in [`spec/errors.md`](../../spec/errors.md).
CXER001x — security caps
- `CXER0010` — function call depth exceeded. - `CXER0011` — collection size cap reached (`sequence_len`, `map_entries`, `capture_size`). - `CXER0014` — eval-time include attempted without `--include-root`.
CXER004x — evaluator gates
- `CXER0040` — purity gate: `SideEffect` / `ReadOnly` filter raised under `[?cx pure-only]`. - `CXER0041` — module gate: `on_declaration` module called without `use-module`. - `CXER0042` — eval gate: `cx:eval` invoked without `[?cx allow-eval]`. - `CXER0043` — eval depth gate: `cx:eval` recursion exceeded `max-eval-depth`.
E9xx — include resolver
- `E901` — absolute path rejected. - `E902` — path escapes include root (lexical or symlink). - `E903` — URL-scheme path rejected. - `E904` — cycle detected. - `E905` — max depth exceeded. - `E906` — file not found. - `E907` — file not readable. - `E908` — path is a directory. - `E909` — I/O error during read. - `E910` — non-UTF-8 content. - `E911` — included file parse failure.
Recovering with [?match]
[?lib 'cx-stdlib/io']
[?match [$io:read-file '/no/such/file.cx']
[case [err @code=$c] [where [= $c 'cx-err:CXER0271']] [needs-capability]]
[case [err $e] [other-error code=$e@code]]
[else :ok]]