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; code is data plus a ?:
input.cx
[user id=1 email='ada@example.com']
prog.cx
[?for [in $u //user] [yield $u@email]]
$ cx --data=input.cx prog.cx
'ada@example.com'
Fixture program-for-g6-01-code-is-data-plus-a-question-mark in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The evaluator is cx_code_eval(input, program, output_target,
err_out) at the C ABI level; every binding's Layer-1
Doc.eval(code) wraps it (see bindings). The full
normative spec is
spec/03-approved/core/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/03-approved/core/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 eighty normative directives.
Adding or removing any directive requires a governance change per
spec/03-approved/process/governance.md. The
authoritative registry lives in
spec/03-approved/core/code.md §4.1; this
subsection is an at-a-glance grouping of the directives this
guide teaches — see the registry for the complete set.
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; the bareword [par] clause enables parallel evaluation and always preserves source order; [ordered] is a tombstoned no-op) |
| [?reduce] | Fold a sequence to a single value via a binary closure starting from [init …] (sequential left-fold by default; [par] 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 |
|---|---|
| [?http-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 (11). Workers + channels + subscriptions + 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 — [?channel name=… buffer=N]; buffer=0 is rendezvous |
| [?subscribe] | Attach a subscription to a retained channel — resume from a position within the retained window |
| [?monitor] | Subscribe to a worker lifecycle-event stream (spawned / cancelled / panicked / done) |
| [?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) |
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). [?stop] — initiate graceful shutdown on a
service or worker handle — 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. The reference evaluator carries the spec-conformant
implementation; the bindings follow per the parity matrix in
spec/03-approved/misc/parity-matrix.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/03-approved/core/code.md §5.5)
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 is every user anywhere in the document;
//user[= $_@active true] every active one; //user/@email the
attribute values; //section[1] the first section (1-indexed);
//section[h2] the sections that have an h2 child:
input.cx
[site
[user active=true email='ada@example.org']
[user active=false email='bob@example.org']
[section [h2 'A']]
[section [p 'B']]]
prog.cx
([$count //user],
//user[= $_@active true]/@email,
//user/@email,
//section[1],
[$count //section[h2]])
$ cx --data=input.cx prog.cx
(2, [email ada@example.org], ([email ada@example.org], [email bob@example.org]), [section [h2 A]], 1)
Fixture program-cxpath-g6-02-descendant-search-basics in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 |
The common idioms, one per axis — all descendants of a
section, the section enclosing a code block, the users after
the first, the table around a row, the sibling just before a
code block, an attribute filter, and the attribute axis spelled
in full (//user/attribute::email is //user/@email):
input.cx
[doc
[section [h2 'T'] [code 'x'] [p 'q']]
[users
[user role='admin' email='a@example.org']
[user role='user' email='b@example.org']]
[table [tr [td 1]]]]
prog.cx
([$count //section/descendant::*],
[$name [$first //code/ancestor::section]],
//user[1]/following-sibling::user/@email,
[$name [$first //tr/parent::table]],
[$name [$first //code/preceding-sibling::*[1]]],
//user[= $_@role 'admin']/@email,
//user/attribute::email)
$ cx --data=input.cx prog.cx
(3, section, [email b@example.org], table, h2, [email a@example.org], ([email a@example.org], [email b@example.org]))
Fixture program-cxpath-g6-03-axis-idioms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The same step vocabulary applies from a binding
(grammar [135a]): $u/name (child), $u//item
(descendant), $u@email (attribute), $u/.. (parent),
and $m.key (map-key access on a map-valued binding).
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 —
INTselects 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 anh2child. - Prefix expression — any prefix CX form over
$_: comparisons ([= $_@id 42]), boolean combinators ([and …]/[or …]/[not …]), and function calls ([$strings:contains $_@email '@example.com']).
input.cx
[site
[user id=42 active=true email='ada@example.com' age=30 country='US']
[user id=7 email='bob@else.org' age=17 country='US']
[section [h2 'A'] [p 'x']]
[section [p 'y']]]
prog.cx
[?lib 'cx-stdlib/strings']
(//user[1]/@id,
//user[= $_@id 42]/@email,
[$count //user[@active]],
//user[$strings:contains $_@email '@example.com']/@id,
//user[and [>= $_@age 18] [= $_@country 'US']]/@id,
[$count //section[h2]],
[$count //section[and [$count $_/h2] [$count $_/p]]])
$ cx --data=input.cx prog.cx
([id 42], [email ada@example.com], 1, [id 42], [id 42], 1, 1)
Fixture program-cxpath-g6-04-predicate-shapes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 //user/tag[= [$text $_] 'admin']].
input.cx
[site
[user age=42 email='ada@example.com' [tag 'admin'] [tag 'dev']]
[section [p 1] [p 2] [p 3] [p 4]]]
prog.cx
(//user[= $_@age 42]/@email,
[$count //section[> [$count $_/p] 3]],
//user[= $_@email 'ada@example.com']/@age,
[$exists //user/tag[= [$text $_] 'admin']])
$ cx --data=input.cx prog.cx
([email ada@example.com], 1, [age 42], true)
Fixture program-cxpath-g6-05-value-comparison in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Paths on bindings
A CXPath rooted at a binding navigates from that value instead of from the document root:
$u/name is the direct child, $u/* all children, $u//item
the descendants, $u@active the attribute shorthand and
$u/@active the full axis form:
prog.cx
[?let [= $u [user active=true region='EU'
[name 'Ada'] [item 1] [item 2]]]
($u/name, $u/*, $u//item, $u@active, $u/@active)]
$ cx prog.cx
(Ada, ([name Ada], [item 1], [item 2]), ([item 1], [item 2]), true, true)
Fixture program-cxpath-g6-06-paths-on-bindings in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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. Parent and map-key steps ride the
same surface (grammar [135a]): $u/.. steps to the
parent, and $m.key reads a key from a map-valued
binding.
Where CXPath values are accepted
CXPath is a value kind. Every construct that takes "a selector" accepts a CXPath value:
input.cx
[site [admin 'root'] [meta config='prod'] [user active=true status='new'] [prose 'p']]
prog.cx
[?let [= $n [$first //prose]]
([?for [in $u //user[= $_@active true]] [yield [seen $u@status]]],
[?if //admin [then [admin-panel]] [else ()]],
[?match $n [case //prose [p [$text $n]]] [else ()]],
[?let [= $cfg //meta/@config] [$string $cfg]],
[?modify $doc //user/@status [set 'verified']],
[$count //user[= $_@active true]])]
$ cx --data=input.cx prog.cx
([seen new], [admin-panel], [p p], prod, [site [admin root] [meta config=prod] [user active=true status=verified] [prose p]], 1)
Fixture program-cxpath-g6-07-path-in-every-position in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A path 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 — the six
positions above, in that order. This is the unification we
ratify: 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/03-approved/misc/parity-matrix.md.
Deliberate divergences from XPath 3.1
CXPath is a working subset of XPath 3.1 + deliberate
changes. The normative treatment is
spec/03-approved/core/code.md §5.5;
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](raisesCXER0100); XPath silently yields the empty sequence. - No general-purpose
for/letinside 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 normative CXPath treatment — semantics, the desugar
table, and predicate evaluation — is
spec/03-approved/core/code.md §5.5;
the XPath 3.1 parity corpus is
conformance/xpath_31_parity.cxd.
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/03-approved/core/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] binds the email attribute; [user @active=true
$u] filters on an attribute and binds the whole user;
[user @id=42 [name $n]] binds a child's body; [post
author=$a body=$b] binds two attributes:
input.cx
[site
[user id=42 active=true email='ada@example.com' [name 'Ada']]
[post author='ada' body='hello']]
prog.cx
([?for [user email=$e] [yield $e]],
[?for [user @active=true $u] [yield $u@id]],
[?for [user @id=42 [name $n]] [yield $n]],
[?for [post author=$a body=$b] [yield [pair $a $b]]])
$ cx --data=input.cx prog.cx
('ada@example.com', 42, Ada, [pair 'ada' 'hello'])
Fixture program-for-g6-08-element-shape-patterns in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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/03-approved/core/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:
prog.cx
[?for [in $status (200, 404, 500, 'admin', true, null, 7)]
[yield [?match $status
[case 200 :ok]
[case 404 :not-found]
[case 500 :server-error]
[case 'admin' :elevated]
[case true :enabled]
[case null :missing]
[else :unknown]]]]
$ cx prog.cx
:ok
:not-found
:server-error
:elevated
:enabled
:missing
:unknown
Fixture program-match-multi-g6-09-scalar-literal-arms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Integers, floats, strings, bools, dates, datetimes, and
null are all matchable as scalar literals. Float
equality follows IEEE-754 — nan is never equal
to nan. Test for NaN explicitly with
[?lib 'cx-stdlib/math'] and [$math:is-nan $x].
CXPath patterns
A CXPath value used as a pattern matches if the candidate value is in the path's result sequence:
input.cx
[doc [heading 'Title'] [prose 'Body.']]
prog.cx
[?let [= $node [$first //heading]]
[?match $node
[case //heading [h2 [$text $node]]]
[case //prose [p [$text $node]]]
[else $node]]]
$ cx --data=input.cx prog.cx
[h2 Title]
Fixture program-match-multi-g6-10-cxpath-arms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
In a [?for] body, the same CXPath as the generator
selects directly:
input.cx
[users [user email='ada@example.org' active=true]
[user email='bob@example.org' active=false]]
prog.cx
[?for [in $u //user[= $_@active true]] [yield $u@email]]
$ cx --data=input.cx prog.cx
'ada@example.org'
Fixture program-for-g6-11-cxpath-generator-selects in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 auseranywhere in the body and binds it._— anything, no bind._matches any value (including non-elements) without binding it. The idiomatic catch-all in[?match].
prog.cx
[?let [= $node [section [h2 'T'] [code 'x = 1']]]
[?match $node
[case [section * $body] [matched-section]]
[case [** [code $c]] [highlighted $c]]
[case _ $node]]]
$ cx prog.cx
[matched-section]
Fixture program-match-multi-g6-12-wildcards in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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:
prog.cx
[?let [= $cfg [server-config host='a.example.org']]
[?match $cfg
[case [:ServerConfig $c] [server $c]]
[case [:ClientConfig $c] [client $c]]
[else [result status=err reason='unknown config']]]]
$ cx prog.cx
error: cx-err:CXER0100: [:ServerConfig] names schema type `ServerConfig` but NO schema is in scope — declare one with `[?cx schema=PATH.cxs]`, carry it inline with `[?cx schema-inline …]`, or pin it in cx.lock (schema.md §13; code.md §5.4)
Fixture program-match-multi-g6-13-type-guard-without-schema in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Type-guard resolution is by the document's schema
binding (see schema in the data section); with no
schema in scope a type guard is refused at program load,
as the fixture above shows (CXER0100, naming the type and
the three ways to bring a schema into scope). 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).
input.cx
[users [user role='admin' email='root@example.com']
[user role='user' email='ada@example.com']]
prog.cx
[?let [= $admins //user[= $_@role 'admin']]
[?for [in $a $admins] [yield $a@email]]]
$ cx --data=input.cx prog.cx
'root@example.com'
Fixture program-let-g6-14-binding-then-iterate in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
input.cx
[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']]]
prog.cx
([; the path generator ]
([?for [in $u //user] [yield $u@email]]),
[; the pattern generator ]
[?for [user @active=true $u] [yield $u@email]],
[; a where filter ]
[?for [in $u //user] [where [>= $u@age 18]] [yield $u@id]],
[; two generators, in declared order ]
[?for [in $u //user]
[in $p //post[= $_@author $u@id]]
[yield [pair user=$u@id post=[$text $p/title]]]],
[; parallel evaluation, results in source order ]
([?for [in $r //request] [par] [yield [got [$text $r]]]]),
[; lazy evaluation over a large input ]
([?for [in $row //logs/row] [lazy] [yield [$text $row]]]))
$ cx --data=input.cx prog.cx
(('ada@example.com', 'grace@example.com'), 'ada@example.com', 1, [pair user=1 post=Notes], ([got r1], [got r2]), (a, b))
Fixture program-for-g6-15-six-generator-forms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[par] and [lazy] are evaluation-modifier clauses —
same surface, different scheduler. [par] distributes
iterations across a bounded worker pool (min(4, ncpu)
workers by default; [par N] caps it at N) and always
reassembles results in source order — the retired
[ordered] clause survives as a tombstoned no-op. [lazy]
couples the generator to a streaming input source so that
the iteration runs in bounded memory regardless of input
size. Full semantics in
spec/03-approved/core/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:
prog.cx
[?let [= $result [ok 42]]
[?match $result [ok $v] [yield [rendered $v]]]]
$ cx prog.cx
[rendered 42]
Fixture program-match-g6-16-single-arm-destructure in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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:
prog.cx
([?let [= $node [code 'x = 1']]
[?match $node
[case [prose $p] [p $p]]
[case [code $c] [pre $c]]
[case 'separator' [hr]]
[else ()]]],
[; predicate-only arms, no scrutinee ]
[?let [= $x 120] [= $y 10]
[?match
[when [> $x 100] :big]
[when [> $y 50] :medium]
[else :small]]],
[; a guard on a case arm ]
[?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]]]])
$ cx prog.cx
([pre x = 1], :big, [route /admin])
Fixture program-match-multi-g6-17-three-dispatch-shapes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 raiseCXER0100.[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/03-approved/misc/parity-matrix.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.
input.cx
[site [admin 'root'] [config debug=false]]
prog.cx
([?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 ()]]])
$ cx --data=input.cx prog.cx
([admin-panel], :adult)
Fixture program-if-g6-18-ebv-conditions in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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/03-approved/core/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 notnanfor 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 JSON-template engines and YAML/TOML processors share. 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.
input.cx
[meta host='api.example.com' port='8443']
prog.cx
[?let [= $meta [$first //meta]]
[= $base [$string $meta@host]]
[= $port [$string $meta@port]]
[= $url [$concat 'https://' $base ':' $port]]
[endpoint target=$url]]
$ cx --data=input.cx prog.cx
[endpoint target='https://api.example.com:8443']
Fixture program-let-g6-19-sequential-bindings in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?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 |
Every action on one document, in the table's order — then a three-step pipeline (see pipe); each result is a new document, and the twelve results below share one unchanged original:
input.cx
[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']]
prog.cx
([?modify $doc //user[= $_@id 1]/@name [set 'Alice']],
[?modify $doc //user[= $_@active false] [delete]],
[?modify $doc //price [using [?fn ($p) [* $p 1.1]]]],
[?modify $doc //para [rename p]],
[?modify $doc //user [set-attr verified true]],
[?modify $doc //user [delete-attr banned]],
[?modify $doc //section[= $_@id 'intro'] [append [para 'Added.']]],
[?modify $doc //list [prepend [item 'header item']]],
[?modify $doc //h2 [insert-before [hr]]],
[?modify $doc //code [insert-after [aside 'note']]],
[?modify $doc //deprecated [replace ()]],
[?pipe $doc
[?modify //user/@status [set 'verified']]
[?modify //user[= $_@banned true] [delete]]
[?modify //user [set-attr last-audit '2026-05-22']]])
$ cx --data=input.cx prog.cx
([doc [user id=1 name=Alice 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]], [doc [user id=1 name=Ada active=true] [section id=intro [para Hi.]] [list [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [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] 11.00 [para p] [deprecated old]], [doc [user id=1 name=Ada active=true] [user id=2 name=Bob active=false banned=true] [section id=intro [p Hi.]] [list [item a]] [h2 T] [code x] [price 10.0] [p p] [deprecated old]], [doc [user id=1 name=Ada active=true verified=true] [user id=2 name=Bob active=false banned=true verified=true] [section id=intro [para Hi.]] [list [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [doc [user id=1 name=Ada active=true] [user id=2 name=Bob active=false] [section id=intro [para Hi.]] [list [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [doc [user id=1 name=Ada active=true] [user id=2 name=Bob active=false banned=true] [section id=intro [para Hi.] [para Added.]] [list [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [doc [user id=1 name=Ada active=true] [user id=2 name=Bob active=false banned=true] [section id=intro [para Hi.]] [list [item header item] [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [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]] [hr] [h2 T] [code x] [price 10.0] [para p] [deprecated old]], [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] [aside note] [price 10.0] [para p] [deprecated old]], [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] ()], [doc [user id=1 name=Ada active=true status=verified last-audit='2026-05-22'] [section id=intro [para Hi.]] [list [item a]] [h2 T] [code x] [price 10.0] [para p] [deprecated old]])
Fixture program-modify-g6-20-eleven-actions in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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). The new allocation is O(depth × matches) —
the spine, not the document size — 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/03-approved/misc/parity-matrix.md.
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.
input.cx
[shop [user active=true age=30]
[user active=true age=20]
[user active=false age=40]]
prog.cx
[?pipe //user[= $_@active true]
[?fn ($us) [?for [in $u $us] [yield $u@age]]]
[$reverse]
[$nth _ 1]]
$ cx --data=input.cx prog.cx
20
Fixture program-pipe-g6-21-stages-are-transforms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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:
input.cx
[users [user name=ada] [user name=bob]]
prog.cx
[?pipe //user
[?fn ($us) [?for [in $u $us] [yield [decorated name=$u@name]]]]
[$count]]
$ cx --data=input.cx prog.cx
2
Fixture program-pipe-g6-22-fan-out-through-a-lambda in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[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/03-approved/core/code.md §6.5; the
subsections below organize the ~47 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 |
| present(value) | 1 | Boolean — true iff value is present; false only for absence (the empty sequence / empty node-set). The canonical "did this step match anything" test — unlike exists, it never looks inside: a childless element, an empty string, and null are all present |
| 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).
input.cx
[site [user active=true] [user active=false]
[tag 'a'] [tag 'b'] [tag 'a']]
prog.cx
([$count //user[= $_@active true]],
[$distinct [?for [in $t //tag] [yield [$text $t]]]],
[?take 3 [$range 1 100]],
[union (1, 2) (2, 3)])
$ cx --data=input.cx prog.cx
(1, (a, b), (1, 2, 3), (1, 2, 3))
Fixture program-builtin-g6-23-sequence-builtins in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
prog.cx
[?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'])
$ cx prog.cx
('HELLO WORLD', true, 'a, b, c', (a, b, c), barbar)
Fixture program-builtin-g6-24-string-builtins in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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. A numeric operand is an int, a float, or one
of the exact kinds decimal / bigint — and the exact
kinds are their own family, not floats in disguise: a
decimal or bigint operand folds exactly (int embeds
losslessly), while mixing one with a float raises
CXER0100 — [cast] is the only bridge. That is the
arithmetic heads' rule; the aggregates are a different
operand shape, never a different numeric semantics.
| builtin | arity | semantics |
|---|---|---|
| sum(seq) | 1 | Sum of numeric scalars; empty yields int 0; int/float mix → float; decimal/bigint fold exactly |
| min(seq) | 1 | Minimum numeric, in its own kind; empty yields int 0 |
| max(seq) | 1 | Maximum numeric, in its own kind; empty yields int 0 |
| avg(seq) | 1 | Arithmetic mean — float, or exact over decimal/bigint; 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; exact over decimal/bigint, as / |
| idiv(a, b) | 2 | Integer (truncating) division regardless of operand kinds; exact quotient over decimal/bigint |
Exact division either terminates or says so: a
decimal/bigint quotient that does not terminate raises
CXER3002 and the refusal names $math:div-decimal, the
verb carrying an explicit precision + mode. There is no
default rounding context. /, div and an exact avg
are one implementation, so they never disagree. idiv
is exempt — an integral quotient always terminates.
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; a
failed cast is CXER0290 (E_CAST_FAILED). CX floats are
IEEE-754: NaN and ±Inf exist and follow IEEE-754
semantics — test for NaN with [$math:is-nan] from
cx-stdlib/math. CXER0101 (E_ARITH_DIVIDE_ZERO) is
division or modulo by zero, specifically. Exponentiation
is [$math:pow] in cx-stdlib/math.
input.cx
[orders [order amount=10] [order amount=32]]
prog.cx
([$sum [?for [in $o //order] [yield $o@amount]]],
[$round 3.7],
[$mod 17 5],
[cast '42' :int])
$ cx --data=input.cx prog.cx
(42, 4, 2, 42)
Fixture program-builtin-g6-25-numeric-builtins in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 |
| = (a, b) | 2 | Structural equality as an operator head — types, values, attributes, children all match |
and / or / not are bare operator heads
([and P Q], no $ sigil), and so is =: over elements it
is structural equality, the same comparison the value
operators use. There is no xor / if() / instance-of
builtin — branch with [?if], and test types by schema
validation or [?match] shape dispatch.
input.cx
[site [user verified=true active=true banned=false] [error 'e1']]
prog.cx
[?let [= $u [$first //user]]
([and $u@verified $u@active [not $u@banned]],
[$exists //error],
[= [a 1] [a 1]])]
$ cx --data=input.cx prog.cx
(true, true, true)
Fixture program-builtin-g6-26-logical-builtins in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
input.cx
[doc [section 'one'] [section 'two']]
prog.cx
([$name [$first //section]],
[$string [$first //section]],
//section[$_last],
//section[= $_position 1])
$ cx --data=input.cx prog.cx
(section, one, ([section one], [section two]), [section one])
Fixture program-builtin-g6-27-node-accessors in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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/03-approved/core/code.md §9.5.
Propagation operators. Two postfix operators thread errors through expressions:
f($x)?— callf; if it errored, propagate the error unchanged through the surrounding directive (which catches it). This is the "early return" form.f($x)!— callf; 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.
A lookup that fails on one item recovers by matching the
[err] it produced, and the loop goes on:
input.cx
[items
[item id=1]
[item id=2 bad=true]
[item id=3]]
prog.cx
[?for [item $i]
[yield [?let [= $r [$validate-item $i]]
[?match $r
[case [err$err] [skip id=$i@id reason=$err@code]]
[else $r]]]]]
$ cx --data=input.cx prog.cx
[ok id=1]
[skip id=2 reason=cx-err:CXER0100]
[ok id=3]
Fixture program-err-003-on-error-recovery in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
Why not exceptions. Exceptions contaminate every signature
with an invisible \"may throw\" that no tool can see, and force
every caller to either catch or be transparent. In CX the
error is in the value, the propagation discipline is in the
operators (? and !), and the handling is in [?match] —
all three visible in the source. The cost of an error path is
the cost of one value: no stack scan, no unwind tables. And
because an error is just a value, it crosses every boundary
values cross — pipelines, [?await], worker channels — without
a parallel control-flow mechanism: a worker that errors sends
the [err] through its result channel and terminates
cleanly.
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/03-approved/core/code.md §9.5 — this
subsection is the developer-facing summary.
Parse / arithmetic / application — 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 | E_ARITH_DIVIDE_ZERO | Division or modulo by zero — [$div] / [$mod] / [$idiv] and the / and % operator heads | Guard the divisor, or [?match] [case [err code="cx-err:CXER0101"] …] |
| CXER0102 | E_PARTIAL_APP | Invalid partial application — a _ hole in a rest position, or a partial applied with more arguments than held positions | Author error — fix the call shape |
| CXER0103 | MULTI_VALUED_PREDICATE | Value-comparison or predicate with a multi-valued operand | Filter the sequence and compare single values ([$exists //x[= $_ V]]) |
Nearby classes that are not CXER010x: anchor / merge
errors are data-language E codes
(spec/03-approved/core/cxdm.md §11);
a sized-integer literal out of range is the lex-time
CXERLEX-RANGE refusal; a failed cast is CXER0290
(E_CAST_FAILED), recovered by matching on the code or
spelling a representable value — coercion is always the
explicit [cast V :int] form.
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 name=N] lookup miss | Recover with [?match] on the err code, or [?else] a default |
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=DURATION] 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 |
|---|---|---|---|
| CXER0281 | UNRENDERABLE_DIRECTIVE | Directive shape outside §10.1.2 locked render rules — the sole live renderer code (CXER0280 is retired to reserved) | 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/03-approved/core/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:
prog.cx
[?test-service-client
[service [?http-service on=http port=0 name="svc-001"
[resource [get "/hello"] [response status=200 body="world"]]]]
[client-call [?let [= $c [?http-client target=$test-target]] [?pipe $c [$get _ "/hello"]]]]]
$ cx prog.cx
[response status=200 body=world]
Fixture program-svc-001-get-happy-path in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The service above is declared on port=0 (the harness picks a
free port) and the client call rides through the corpus's own
[?test-service-client] helper, which binds $test-target to
the running service; the tunables — read-timeout=,
write-timeout=, max-body-bytes=, max-connections=,
grace-period= — are attributes on the same element.
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:
input.cx
[exchange
[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]]]]
prog.cx
(//request/@method, //request/path-params/id, //response/@status, //response/body/user/@id)
$ cx --data=input.cx prog.cx
([method GET], [id 42], [status 200], [id 42])
Fixture program-cxpath-g6-28-request-and-response-shapes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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:
prog.cx
[?test-service-client
[service [?http-service on=http port=0 name="svc-002"
[resource [post "/echo"] [response status=200 [body $request/body]]]]]
[client-call [?let [= $c [?http-client target=$test-target]] [?pipe $c [$post _ "/echo" [payload value=42]]]]]]
$ cx prog.cx
[response status=200 [body [payload value=42]]]
Fixture program-svc-002-post-happy-path in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A client takes timeout= and a [resilience …] child (any of
the resilience directives) on the same element as
target=.
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.
prog.cx
[?let [= $ch [?channel name='jobs' buffer=8]]
[= $sent1 [?send 1 to=$ch]]
[= $sent2 [?send 2 to=$ch]]
([?receive from=$ch], [?receive from=$ch])]
$ cx prog.cx
(1, 2)
Fixture program-conc-g6-29-channel-send-receive in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A producer worker and a consumer over one channel keep the producer's order:
prog.cx
[?let [= $ch [?channel name="c8" buffer=4]] [= $_ [?for [in $i (1, 2, 3, 4)] [yield [?send $i to=$ch]]]] [?for [in $j (1, 2, 3, 4)] [yield [?receive from=$ch]]]]
$ cx prog.cx
1
2
3
4
Fixture program-conc-008-fifo-single-producer in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Channel semantics. [?channel name=… 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. ReturnsCXER0200(CHANNEL_CLOSED) if CH is closed.[?try-send V to=CH timeout=DURATION]— same but returnsCXER0201(SEND_TIMEOUT) on buffer-full timeout.[?receive from=CH]— block when empty; returns next value. ReturnsCXER0200when CH is closed and drained.[?try-receive from=CH timeout=DURATION]— same but returnsCXER0202(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 raisesCXER0203.
Worker observability. Workers are handle-addressable:
prog.cx
[?let [= $w [?worker name="w3" [body 42]]] [= $_ [?wait-for worker=$w]] [= $_ [?cancel worker=$w]] [?wait-for worker=$w]]
$ cx prog.cx
42
Fixture program-conc-012-worker-cancel-after-done-noop in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?wait-for worker=$w] blocks until the worker is done,
[?cancel worker=$w] requests cooperative cancellation (a
no-op once the worker has finished, as above), and
[?check-cancel] is the polling point inside a hot loop (see
async).
Select — multi-way receive. Evaluates all
[case …] clauses concurrently; proceeds with the
first ready. Uniform-random selection on
simultaneous readiness:
prog.cx
[?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]]]]
$ cx prog.cx
[handled 42]
Fixture program-conc-g6-30-select-first-ready in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Shape. [?select] clauses are clause-children:
[case [from CH $msg] HANDLER] for a receive and
[case [timeout DURATION] HANDLER] for a deadline. The
spec/03-approved/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:
prog.cx
[?let [= $f1 [?async [+ 1 1]]]
[= $f2 [?async [* 2 2]]]
[= $f3 [?async [$upper 'go']]]
[?await-all ($f1, $f2, $f3)]]
$ cx prog.cx
(2, 4, GO)
Fixture program-async-g6-31-await-all in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 returnCXER0241(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, yieldCXER0240with 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, returningCXER0260(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.
prog.cx
[?def cancellable-compute impure ($n)
[?for [in $i [$range 1 $n]]
[where [= [?check-cancel] [ok]]]
[yield [* $i $i]]]]
[$cancellable-compute 4]
$ cx prog.cx
1
4
9
16
Fixture program-async-g6-32-check-cancel-in-a-loop in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 a stable surface of the
reference evaluator; binding parity ships per
spec/03-approved/misc/parity-matrix.md.
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/03-approved/core/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).
The full policy reads [?retry max=5 backoff=exponential
delay=200ms jitter=equal [on [?fn ($e) [= $e@code
'CXER0163']]] BODY] — retry only on a timeout. The fixture
below retries a body that fails twice and succeeds on the
third attempt:
prog.cx
[?retry max=5 [?test-err-then-ok err-count=2 ok-value=done]]
$ cx prog.cx
done
Fixture program-retry-002-success-on-third-attempt in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?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 |
prog.cx
[?timeout 100ms
[?let [= $_ [?sleep 500ms mock]] never]
[on-timeout fallback]]
$ cx prog.cx
fallback
Fixture program-timeout-003-on-timeout-recovery in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?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=exceedsthreshold=, with at leastmin-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 |
prog.cx
[?for [in $i (1, 2, 3, 4, 5)]
[yield [?circuit-breaker threshold=0.5 window=60s reset=30s
min-samples=2 name="cb-002" [?test-always-err]]]]
$ cx prog.cx
[err code=test-failure message='test failure' at='3:60']
[err code=test-failure message='test failure' at='3:60']
[err code=cx-err:CXER0150 message='circuit breaker open' at='2:14' [until [instant 30s]]]
[err code=cx-err:CXER0150 message='circuit breaker open' at='2:14' [until [instant 30s]]]
[err code=cx-err:CXER0150 message='circuit breaker open' at='2:14' [until [instant 30s]]]
Fixture program-cb-002-trips-open in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?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 |
prog.cx
[?fallback [body [err code='down' message='service down']] [recover-with secondary]]
$ cx prog.cx
secondary
Fixture program-fallback-002-primary-err-secondary-ok in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
$err in the recovery scope. 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 |
prog.cx
[?for [in $i (1, 2, 3, 4)]
[yield [?rate-limit max=2 per=1s name="rl-002" $i]]]
$ cx prog.cx
1
2
[err code=cx-err:CXER0151 message='rate limit exceeded' at='2:14' retry-after::duration=1s]
[err code=cx-err:CXER0151 message='rate limit exceeded' at='2:14' retry-after::duration=1s]
Fixture program-ratelimit-002-over-limit in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
[?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 |
prog.cx
[?test-concurrent tasks=(
[?bulkhead max-concurrent=1 queue=0 name="bh-002" [?let [= $_ [?sleep 500ms mock]] first]],
[?bulkhead max-concurrent=1 queue=0 name="bh-002" second])]
$ cx prog.cx
([err code=cx-err:CXER0152 message='bulkhead saturated' at='3:4' max=1], first)
Fixture program-bulkhead-002-saturated-no-queue in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Composition
Resilience directives compose by nesting. The outermost runs first, so its policy sees the inner policies' err codes. Standard pattern reads top-down:
prog.cx
[?retry max=3 backoff=constant delay=10ms jitter=none [?timeout 100ms
[?let [= $_ [?sleep 500ms mock]] never]]]
$ cx prog.cx
[err code=cx-err:CXER0140 message='retry budget exhausted after 3 attempts' at='1:1' attempts=3 [cause [err code=cx-err:CXER0141 message='operation timed out after 100ms' at='1:55' elapsed::duration=100ms]]]
Fixture program-compose-001-retry-over-timeout in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Reading: retry up to 3 times a deadline-bounded body; the
timeout's CXER0141 is what [?retry] sees on each attempt,
and the exhausted retry names it as the cause. A breaker in
the same position surfaces CXER0150, and [?retry] decides
whether to keep retrying (the 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]]) 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).
input.cx
[shop
[user [name 'Ada']]
[item active=true 'i1']
[pizza [name 'Margherita'] [price 12]]]
prog.cx
([?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]]])
$ cx --data=input.cx prog.cx
(Ada, [item active=true i1], [hit name=Margherita price=12])
Fixture program-for-g6-33-pattern-generators-walk-the-tree in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
prog.cx
(([?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]])
$ cx prog.cx
((4, 9), 42, 4)
Fixture program-for-g6-34-comprehension-let-pipe in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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.
Four worked examples (§W1–§W4) each have a paired
conformance fixture
program-slice-wN-* in
conformance/code.cxd;
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 fused
predicate-body closure
(spec/03-approved/formal/grammar.ebnf
[159b]) — bare-fn-call, path predicate, and slice
postfix coexist.
The W1 program — a strided range, a slice, a comprehension — is pinned today at its parse refusal, and flips to its output the day the surface lands:
prog.cx
[?def $odds [$range 1 99 2]]
[?def $first-ten $odds[:10]]
[?def $squares [?for [in $x $first-ten] [yield [* $x $x]]]]
$squares
$ cx prog.cx
error: cx-err:CXER0100: [?def] parse: CXDEF_PARSE: missing function name at line 1:1
Fixture program-slice-w1-strided-range-then-slice in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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];
walking an Iterator back to a concrete collection is the
registered force-materializer family [?to-sequence] /
[?to-array] / [?to-map] (a [?to-iterator]
directive is planned but not registered). 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/03-approved/formal/grammar.ebnf
productions [130]/[131]/[135] (path steps / binding paths) and
[159]/[160a] (general PredicateExpr + the (bind $name)
step annotation). XPath 3.1 alignment and divergences are
in spec/03-approved/core/code.md §5.5.
CXPath is a first-class value kind;
general predicates with $_ / $_position / $_last.
Quick reference
In order: every user; every active user; the active users'
emails; the third user in document order; everything two
levels under the root; the siblings after the first user; the
ancestors of a [para]; a general predicate — prefix code
over $_; a range; and the three set heads union,
intersect, except:
input.cx
[org
[team
[user active=true age=30 email='a@example.org']
[user active=false age=40 email='b@example.org']
[user active=true age=17 email='c@example.org']
[para 'p']]
[a 1] [b 2]]
prog.cx
([$count //user],
[$count //user[= $_@active true]],
//user[= $_@active true]/@email,
//user[3]/@email,
[$count /*/*],
[$count //user[1]/following-sibling::user],
([?for [in $e //para/ancestor::*] [yield [$name $e]]]),
//user[and [>= $_@age 18] $_@active]/@email,
[$range 1 10],
[union //a //b],
[intersect //user //team/user[= $_@active true]],
[except //user //user[= $_@active false]])
$ cx --data=input.cx prog.cx
(3, 2, ([email a@example.org], [email c@example.org]), [email c@example.org], 3, 2, (org, team), [email a@example.org], (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), ([a 1], [b 2]), ([user active=true age=30 email='a@example.org'], [user active=true age=17 email='c@example.org']), ([user active=true age=30 email='a@example.org'], [user active=true age=17 email='c@example.org']))
Fixture program-cxpath-g6-35-quick-reference in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 $name)).
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:
input.cx
[org
[team [member role='lead' [name 'a']] [member [name 'b']] [member [name 'c']]]
[team [member role='lead' [name 'e']]]]
prog.cx
[?for [in $t //team]
[in $m $t/member]
[where [and [= $m@role 'lead'] [>= [$count $t/member] 3]]]
[yield $m/name]]
$ cx --data=input.cx prog.cx
a
Fixture program-for-g6-36-nested-generators-see-outer-bindings in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The grammar reserves a (bind $name) path-step
annotation (grammar.ebnf [160a]) for naming a step's
focus inline; the nested-generator form above is the
conformance-exercised idiom.
In the host bindings
Every binding exposes CXPath identically through its
Layer-1 Doc.select_all — the same selector string, the
same bytes returned across the C ABI; the bindings page shows
each spelling. The selector, run here by cx itself:
input.cx
[users
[user active=true email='ada@example.org']
[user active=false email='bob@example.org']
[user active=true email='cy@example.org']]
prog.cx
//user[= $_@active true]/@email
$ cx --data=input.cx prog.cx
[email ada@example.org]
[email cy@example.org]
Fixture program-cxpath-g6-37-active-emails-one-selector in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
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 normative registry is
spec/03-approved/core/code.md
§9.4/§9.5, with the governance rules for code allocation in
spec/03-approved/process/governance.md
§9.6.
Capability and runtime caps
- CXER0271 — capability denied at an effect point.
Capabilities are deny-by-default; the param-less eval
entry points run with the empty capability set —
"pure-only" is the empty set, not a pragma.
- CXER0272 — E_STACK_EXHAUSTED: the evaluator's
native-stack exhaustion guard.
Purity and eval gates
- CXER0230 — purity gate: an impure callee where
purity is required (e.g. inside a CXPath predicate)
is rejected at parse / module-load.
- CXER0233 — a pure def reaching [?eval] /
cx:eval-tree (tree-eval is impure, like cx:eval).
- CXER4113 — lib-widen gate: [?lib] inside an eval
tree may not widen the capability set.
- CXER4114 — eval depth gate: cx:eval / [?eval]
recursion exceeded the shared depth budget.
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]
An ungranted read answers CXER0271 — the capability
refusal, an err value like any other:
prog.cx
[?lib 'cx-stdlib/io']
[$io:read-file "/var/log/app.log"]
$ cx prog.cx
[err code=cx-err:CXER0271 message='E_CAP_DENIED: read capability required for io-read-file; none granted (grant via --allow-read)' at='2:1']
Fixture io-001-read-file-cap-denied in deps/cx-core-code/conformance/stdlib/io.cxd — replayed against this cx when the page was built.
Under --allow-read, a read of a missing file answers
CXER3401; either way a [case [err @code=$c] …] arm
dispatches on the code by name, and the catch-all arms take
whatever is left:
prog.cx
[?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]]
$ cx --allow-read prog.cx
[other-error code=cx-err:CXER3401]
Fixture program-err-g6-38-recover-by-code in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.