The data language
CX's data language is what you write in a .cx or .cxd file when
you mean "this is data." It is also what every CX code expression
evaluates to — programs and values share one shape. This section is
the working reference for the data surface; the normative grammar
is in spec/03-approved/formal/grammar.ebnf
and the value model is in
spec/03-approved/core/cxdm.md.
Lexical structure
Every CX source file is UTF-8 text. The lexer is small: identifiers,
scalar literals, brackets ([, ]), parentheses (used for sequences
and argument lists), curly braces (used for map literals), sigils,
strings, and comments. The full lexical rules are in
spec/03-approved/formal/lexicon.ebnf.
Identifiers
An identifier starts with an ASCII letter or underscore and
continues with letters, digits, underscores, and hyphens. Hyphens
are valid identifier characters — kebab-case is our idiom, and
the fixture below reads two hyphenated names back. The reserved tokens
true, false, null, and the directive names (starting with
?) are excluded. See reserved-tokens for the complete
reserved-name list.
input.cx
[user-profile first-name='Ada' last-name='Lovelace']
prog.cx
(//user-profile/@first-name, //user-profile/@last-name)
$ cx --data=input.cx prog.cx
([first-name Ada], [last-name Lovelace])
Fixture program-cxpath-g2-01-hyphenated-names in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Names of CX elements are stored as Unicode code-point sequences; a single canonical byte form is used at the wire level (see canonical). Unicode normalization is NFC — composed form is the canonical shape.
Whitespace
Whitespace separates tokens; otherwise it is not significant. Indentation does not change meaning. CX is bracket-structured, so you can compact a document to one line, or pretty-print it across many — the canonical form (see cx-canonical) is unique and hash-stable either way.
Inside element bodies, whitespace separates body items — it is structure, not content. The content itself is verbatim: quoted and triple-quoted strings preserve their internal whitespace exactly as authored. There is no whitespace-collapse pass and no mode attribute to switch one on (see xml-space).
Elements
An element is the basic CX data structure: a name, zero or more
attributes, and a body. Every bracketed form is an element. The
normative production is
spec/03-approved/formal/grammar.ebnf
[51] Element.
Element names
The first token inside [ is the element name. It is an
identifier (or a string literal, if you need spaces, but that is
unusual in idiomatic CX). Names starting with ? denote
directives (CX code, not data) — see code.
input.cx
[servers
[server]
[server host='a.com']
[server [host 'a.com']]]
prog.cx
([$count //server[1]/*], //server[2]/@host, //server[3]/host)
$ cx --data=input.cx prog.cx
(0, [host a.com], [host a.com])
Fixture program-cxpath-g2-03-three-element-shapes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Attributes
Attributes are key-value pairs in the form name=value. The key
is an identifier; the value is a bare scalar, a quoted string,
a numeric literal, a boolean literal, a date, or null. Multiple
attributes are space-separated within the element header.
input.cx
[user id=42 email='ada@example.com' active=true]
prog.cx
(//user/@id, //user/@email, //user/@active)
$ cx --data=input.cx prog.cx
([id 42], [email ada@example.com], [active true])
Fixture program-cxpath-g2-04-attribute-values in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A bare :name in body position is NOT attribute syntax — it is
an atom literal (:ok, :not-found; see type). The
labeled-slot directive syntax that once used :name is retired:
directive clauses are explicit child elements ([then …],
[yield …]). On a plain data element you always write
name=value.
Attribute order is preserved by the parser and is part of the
canonical identity — order carries meaning in CX, so
re-ordering attributes produces a different document with a
different SHA-256. Nothing in canonicalization sorts attributes
(see canonical). One sigil-prefixed attribute
shortcut exists: #name (the id sigil, see id). The
anchor sigil &name and the merge sigil *name are also
attribute-level — see anchor and merge.
Body content
The body of an element is everything between the attribute list
and the closing ]. A body may be empty, a single scalar, a
sequence of scalars and elements (mixed content / prose — see
prose), or a sequence of just elements.
input.cx
[bodies
[empty]
[scalar 42]
[prose 'Some ' [em 'mixed'] ' content.']
[list [item 1] [item 2] [item 3]]]
prog.cx
([$count //empty/*], //scalar, [$text [$first //prose]], [$count //list/item])
$ cx --data=input.cx prog.cx
(0, [scalar 42], 'Some content.', 3)
Fixture program-cxpath-g2-05-body-content-shapes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A bare scalar body is auto-typed per the rules in
spec/03-approved/core/ast.md
§Scalar — 42 becomes an int, 3.14 a float, 2026-05-22 a
date, etc. To force a type, glue an ascription to the name:
[port::u16 8080].
Scalar kinds
CX has nine scalar kinds per
spec/03-approved/core/cxdm.md
§2: string, int, float, bool, date, datetime, bytes, null, and
atom. Additional type names refine storage and precision on top
of those kinds — decimal, bigint, the sized integer / float
widths, duration, period, instant, secret. The
wire encoding for each kind is in
spec/03-approved/core/data-bin.md
§3. Each kind can be introduced literally (e.g. 42 for int) or
forced with a glued type ascription (count::int, port::u32).
Strings — single, double, triple-quoted
Strings come in three forms. Single-quoted ('…') and
double-quoted ("…") work the same way and support escapes.
Triple-quoted ('''…''' or \"\"\"…\"\"\") strings span lines,
preserve internal whitespace, and need no escaping for the other
quote characters.
input.cx
[strings
[greeting 'Hello, world']
[path "C:\\Users\\Ada"]
[readme """
This is a
multi-line string
with 'embedded' quotes.
"""]]
prog.cx
([$text [$first //greeting]], [$text [$first //path]], [$text [$first //readme]])
$ cx --data=input.cx prog.cx
('Hello, world', 'C:\\Users\Ada', "This is a\nmulti-line string\nwith 'embedded' quotes.")
Fixture program-cxpath-g2-06-three-string-forms in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Triple-quoted strings are the default for any body text longer
than one line. They are the building block for the prose form
(see prose) and for the [?modify] action :set values
when those values are documents in their own right.
The canonical form (cx canonical) emits single-quoted strings for
single-line content and triple-double-quoted strings for
multi-line content. Escapes are minimised to what the parser
strictly requires.
Numbers — int, float, sized, hex
Integer literals are plain digits, optionally signed:
42, -17, 0. Float literals contain a decimal point or
exponent: 3.14, 1e9, -2.5e-3. Hex literals are written
0x…. The default int is a 64-bit signed integer; the default
float is IEEE-754 double. Sized variants are available via
::T ascriptions when binding to host languages where the size
matters; see sized-types.
input.cx
[numbers
[answer 42]
[pi 3.14159]
[big 1e18]
[color 0xff8800]
[port::u16 8080]
[pixel-density::f32 1.5]]
prog.cx
[?for [in $n //numbers/*] [yield $n]]
$ cx --data=input.cx prog.cx
[answer 42]
[pi 3.14159]
[big 1.0e18]
[color 16746496]
[port::u16 8080]
[pixel-density::f32 1.5e0]
Fixture program-cxpath-g2-07-number-literals in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
nan, inf, and -inf are not float literals — those
tokens parse as ordinary strings, and CX floats are
finite-only in canonical form (a NaN or infinity has no
canonical bytes). Non-finite values can only arrive across
the FFI boundary; [$math:is-nan] exists for exactly that
boundary work. -0.0 and +0.0 compare equal under = but
produce different canonical bytes.
An integer literal that exceeds the 64-bit range
auto-promotes to bigint — [big 9223372036854775808]
parses fine. Only an explicit ascription pins the width and
makes overflow an error: [big::int 9223372036854775808]
refuses. Runtime arithmetic is checked, not promoted:
adding 1 to 9223372036854775807 (max i64) raises
CXER3000 (E_MATH_OVERFLOW).
Booleans
true and false. They are reserved tokens; the identifier
true always parses as the boolean true value. Use sigil
forms (see bool) for compact attribute syntax.
Dates and datetimes
Dates are ISO-8601 calendar dates: 2026-05-22. Datetimes are
ISO-8601 with time and optional zone offset:
2026-05-22T14:30:00Z or 2026-05-22T14:30:00-04:00. Both are
first-class scalars — they round-trip across the lossless
projections and have stable canonical byte encodings. See
datetime-detail for timezone, precision, and leap-second
semantics.
Null
null is the absent-value scalar. Distinct from the empty
sequence (). null is a value; () is the absence of any
value — null is a single-item sequence containing the null
scalar, () is an empty sequence, and presence tests tell
them apart. Under the effective-boolean-value rule
(spec/03-approved/core/cxdm.md
§6) both are falsy: a singleton null evaluates to
false, and () evaluates to false.
Bytes — raw binary scalars
:bytes is the eighth scalar kind: a length-prefixed byte
sequence with no character semantics. Use it for image data,
hashes, encrypted payloads, or any opaque binary value.
input.cx
[binary
[hash::bytes 0x3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b]
[magic::bytes 0x89504e470d0a1a0a]]
prog.cx
[?for [in $b //binary/*] [yield $b]]
$ cx --data=input.cx prog.cx
[hash::bytes 0x3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b]
[magic::bytes 0x89504e470d0a1a0a]
Fixture program-cxpath-g2-08-bytes-literals in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A bytes value is a 0x-prefixed hex literal (even digit count)
under a ::bytes ascription. The canonical form emits the hex
string with the 0x prefix; the data-bin wire format uses raw
length-prefixed bytes (no encoding overhead). The conversion
contract maps bytes to base64 / tagged forms per format — see
bytes-wire in surfaces for the per-surface table.
Sized type tags
When binding CX to host languages where integer size matters
(Rust i32 vs i64, Go uint8 vs int, C int16_t),
a sized ascription is glued to the name with ::. The full
set:
- Signed integers:
::i8,::i16,::i32,::i64. - Unsigned integers:
::u8,::u16,::u32,::u64. - Floats:
::f16,::f32,::f64. - Default-int:
::int— alias for::i64. - Default-float:
::float— alias for::f64.
A literal without an ascription is auto-typed as int (for whole
numbers) or float (for numbers with . or e). To force a
sized variant: [port::u16 8080], [pixel-density::f32 1.5].
Sized variants flow through ast-bin and data-bin with their
type identifier preserved; bindings translate to the host's
native numeric type.
Overflow on a sized scalar (writing 300 into ::u8, or
2147483648 into ::i32) is refused at parse time with
CXERLEX-RANGE — the error names the value and the width
it does not fit.
Collection literals
CX has three collection literal forms: sequence, array,
and map. The distinction is load-bearing for function
signatures — see
spec/03-approved/core/cxdm.md
§2 and container-atom below.
Sequence literals (a, b, c)
A comma-separated parenthesized list is a sequence: (1, 2, 3).
Sequences are flat — concatenating sequences never produces a
sequence-of-sequences (the XQuery sequence-flat rule, see
seq-flat).
input.cx
[ports (80, 443, 8080)]
prog.cx
[?let [= $p [$first //ports]] ([$count $p/*], $p/*)]
$ cx --data=input.cx prog.cx
(1, (80, 443, 8080))
Fixture program-cxpath-g2-09-sequence-literal in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A sequence with one element is equivalent to that single
element: (42) ≡ 42. The empty sequence is () — distinct
from null (see null).
Array literals [a, b, c]
A bracketed comma-separated list (distinguished from an element by the absence of a name and the presence of commas) is an array. Arrays preserve nesting — an array of arrays stays shaped that way.
input.cx
[matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]]]
prog.cx
[?let [= $m [$first //matrix]] ([$count $m/*], $m/*)]
$ cx --data=input.cx prog.cx
(1, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
Fixture program-cxpath-g2-10-array-literal in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Array indexing is 1-based, like CXPath positional
predicates ($doc/item[1] selects the first item) — per
spec/03-approved/core/cxdm.md
§2.5. Out-of-range access yields the empty sequence rather
than an error. There is no negative-index shorthand.
Map literals {k: v}
Curly braces wrap a map literal: keys are atomic scalars, values
are one expression-shaped item each — a scalar, a quoted
string, a nested collection, or an entity reference. Multi-word
prose must be quoted ({note: 'two words'}); an unquoted run
is a loud parse error, never silently absorbed. Keys must be
unique; a duplicate key raises
W014 (CXERMAP-DUPKEY). Insertion order is preserved for round-trip
but not for canonical identity — two maps with the same
key-value pairs hash to the same canonical bytes regardless of
insertion order.
input.cx
[env {host: 'a.com', port: 8080, tls: true}]
prog.cx
[$first //env]
$ cx --data=input.cx prog.cx
[env {host: 'a.com', port: 8080, tls: true}]
Fixture program-cxpath-g2-11-map-literal in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Entries separate by comma or plain whitespace/newlines — the
same rule arrays follow ([1 2 3] ≡ [1, 2, 3]); the two
spellings compare equal:
input.cx
[env {host: 'a.com' port: 8080
tls: true}]
prog.cx
[= [$first //env] [env {host: 'a.com', port: 8080, tls: true}]]
$ cx --data=input.cx prog.cx
true
Fixture program-cxpath-g2-12-map-entries-by-whitespace in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The map key type is ScalarKey per cxdm §2: bool, int, float,
string, date, datetime, or bytes. null is not a valid key
(null = null would not satisfy the uniqueness invariant
under value-comparison semantics). Mixed-type keys are
permitted; comparison is by canonical bytes of each key.
The :: annotation takes one meaning per position. Glued to the
key it types the key ({1.10::decimal: x} — a decimal key,
scale-distinct from {1.1::decimal: x}). In the value slot
it types the value, coercion-checked, spelled either way:
type-first ({age: ::int 30}) or postfix
({price: 19.99::decimal}) — one canonical image either way
({price: soon::decimal} is a loud CXER0290, and an unknown
tag like 5::bogus is a loud CXER0107 — never silent text).
Standing alone after the colon it declares the field:
typed, value ABSENT — not null:
input.cx
[props {order: ::string, compact: ::bool, tags: ::string[]}]
prog.cx
[$first //props]
$ cx --data=input.cx prog.cx
[props {order: ::string, compact: ::bool, tags: ::string[]}]
Fixture program-cxpath-g2-13-declared-map-entries in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A declared entry's kind draws from the same kind vocabulary bind
patterns test (int, string, element, array, path, …,
plus [] for array kinds). Reading a declared entry's value
refuses — absence is not null — and only the cx and
round-trip-XML projections can carry a declaration; every lossy
target (JSON, YAML, CSV, …) refuses the document loudly.
The sequence-flat principle
Every value in CX is a sequence; sequences do not nest at the sequence boundary. This is XQuery's sequence-flat rule (cxdm §1) — the property that makes the expression family compose.
//userreturns a sequence whether the document has 0, 1, or N users — consumers iterate uniformly.- Concatenation is total and associative:
concat(a, b, c)never raises "can't concatenate scalar to list". - A directive's return value is "what it evaluates to"; multiple emits naturally concatenate.
What sequence-flat is NOT: a list-of-lists model. Concatenating two sequences yields one flat sequence, never a sequence-containing-two-sub-sequences. To preserve nesting use arrays (arr) — arrays are container Items, not sequence boundaries.
Container vs atom distinction
The value model draws a load-bearing distinction between atom
Items (Node, Scalar) and container Items (Array, Map,
Sequence-as-Item) per
spec/03-approved/core/cxdm.md
§2.1:
- Atoms are leaf values from the value-model perspective.
A Scalar
42is a single Item; a Node is a single Item even though its tree contents are richly structured. - Containers hold further Items by composition at the
value level.
[1, 2, 3]is one Array Item that holds three Scalar Items.{name: 'a'}is one Map Item that holds one (key, value) pair.
This governs which operations apply where, and the rule is a
read/construct split. Readers — [$count], [$length],
[$empty], [$exists], [$first], [$last], [$nth],
[$head], [$tail], [$distinct] — destructure any
collection kind and answer over its items:
[$count [1, 2, 3]] is 3 on an Array exactly as it is on a
Sequence. Orderedness is a property of the value, not a wrapper
the reader must peel first. Constructors and combiners keep
the strict container typing: [$concat [1, 2] [3]] is a type
error, because building a Sequence out of Arrays would erase
the very distinction the container kinds carry. Map values are
read by key — $m.name on {name: 'a'} yields 'a'.
The practical impact: readers never make you convert just to ask a question, so there is no silent wrong answer of a collection counting as one scalar — while anything that builds a value type-checks its containers strictly, keeping construction intent visible to readers and tools.
Sigils
Sigils are one-character prefixes that change how a token is
interpreted. CX has three element-level sigils — & (anchor),
* (merge), # (id) — plus @ (reference to a declared id,
in attribute-value position or in a body [ref @name]), and
:, which serves the glued ::T type ascription and the
:name atom literal.
The & anchor sigil
&name declares a reusable shape anchor on an element. Other
elements can refer to it later in the same document via the
merge sigil *name. Anchors are document-scoped — they
resolve within the same document and do not cross include
boundaries (per include-capabilities).
input.cx
[services
[defaults &server-base host='localhost' tls=true]
[service-a *server-base port=8001]
[service-b *server-base port=8002]]
prog.cx
[?lib 'cx-stdlib/cx']
[$cx:canonical $doc]
$ cx --data=input.cx prog.cx
'[services\n [defaults host=localhost tls=true]\n [service-a host=localhost tls=true port=8001]\n [service-b host=localhost tls=true port=8002]\n]\n'
Fixture program-lib-g2-14-anchors-expand-in-canonical in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
A duplicate &name declaration is not an error — the first
definition in document order wins, deterministically. The
lossless canonicalizer preserves anchors and aliases; the strict
canonicalizer expands them and the canonical bytes are the
expanded form — the fixture above asks [$cx:canonical] for
it. The lossy projections (--json / --yaml / --csv / …)
and a program's $doc under cx --data= read that same
expanded form — the semantic document — while default CX and
XML output keep the authored sharing.
The * merge sigil
*name expands the anchor's attributes and body into the
current element. Local attributes override merged ones, so you
can specialize. A cyclic * reference has no finite expanded
form, so canonicalization refuses the document outright (a
hard error naming the cycle, without a CXER code).
The # id sigil
#name declares an identity on the element; @name
references it. Identities are document-unique.
Identities differ from anchors: anchors are for shape reuse
(data-level), identities are for cross-references (semantic
link). Full semantics in
spec/03-approved/core/cxdm.md §4.
input.cx
[blog
[user #ada email='ada@example.com']
[post author-id=@ada 'First post']]
prog.cx
(//post/@author-id, [$text [$first //post]])
$ cx --data=input.cx prog.cx
([author-id ada], 'First post')
Fixture program-cxpath-g2-15-id-and-reference in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
An @name reference (as in author-id=@ada above) resolves to
the element bearing #ada. The reference does not embed; it
remains a pointer. In body position the reference form is
[ref @ada]. Cross-format identity: only CX and XML carry
the ID/IDREF mechanism losslessly; the JSON/YAML/TOML/MD
projections flatten it to an ordinary id field plus a $ref
envelope, one-way. See identity.
The : type sigil
: is reserved for two surface roles. Glued to a name as ::T
([port::u16 8080]) it is the type-ascription sigil — the
doubled form is the only ascription spelling; a spaced :name
in body position is never a type. In atom position (:ok,
:not-found) it introduces a symbolic atom literal. The
parser disambiguates by context. The : sigil is no
longer used to introduce labeled directive slots — every
clause is now an explicit child element ([then …],
[else …], [yield …], [where …]).
Type ascriptions are explicit; the nine scalar kinds (see
scalars) plus sized variants (see sized-types) all use
the glued ::T form when written explicitly. The code
language's value-kind test uses the same vocabulary.
Boolean attributes
Booleans are ordinary attributes: active=true,
banned=false. The bare tokens true and false auto-type
as bool (see scalars); there is no shorthand sigil form —
an attribute is always spelled name=value.
input.cx
[user email='ada@example.com' active=true banned=false]
prog.cx
(//user/@active, //user/@banned)
$ cx --data=input.cx prog.cx
([active true], [banned false])
Fixture program-cxpath-g2-16-boolean-attributes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Mixed content and prose
Mixed content — text interleaved with elements — is first-class
CX, not a separate "string mode". The body of any element may
alternate freely between scalars and child elements. This makes
Markdown projection lossless: a paragraph with inline code is
stored as a [p] element containing a text run, a [code]
element, and more text.
input.cx
[para 'The function ' [code 'parse'] ' returns a '
[link to='spec/03-approved/core/cxdm.md' 'Document'] ' value.']
prog.cx
([$count //para/*], [$text [$first //para]])
$ cx --data=input.cx prog.cx
(2, 'The function returns a value.')
Fixture program-cxpath-g2-17-mixed-content in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Entity references
CX text accepts an entity reference &name; under any
name — the five XML/HTML ones (&, <, >,
", ') and undeclared names alike, since CX
stores refs without requiring an entity declaration. They are
not decoded — an entity reference parses to an
entity-reference node and is re-emitted verbatim,
byte-for-byte, in both canonical forms. [t 'a & b']
canonicalizes to exactly that.
Numeric character references (A, A) are not
carried that way: a char reference is resolved to its
Unicode character and stored as Text, so the spelling does
not survive the read — [p © 2026] reads as the single
text © 2026. The asymmetry is deliberate and mirrors XML: a
char reference is character sugar XML does not preserve,
whereas a named entity is markup that round-trips. A
surrogate or a value above U+10FFFF is rejected. Either way
there is no textual expansion pass — which is why
recursive-expansion attacks (the XML "billion laughs") have
nothing to attack (see limits). External entity
resolution (the XML <!ENTITY foo SYSTEM "…"> &foo;
pattern) does not exist in CX; DTD content rides through as
verbatim prolog data (see surfaces).
Whitespace preservation (xml:space)
Quoted body text is verbatim — CX has no whitespace-collapse pass, so there is nothing for a mode attribute to switch. Internal runs of spaces and newlines inside a quoted or triple-quoted string survive parse, canonicalization, and round-trip exactly as authored.
input.cx
[poem
' Roses are red,
Violets are blue,
Whitespace preserved,
just for you.']
prog.cx
[$text [$first //poem]]
$ cx --data=input.cx prog.cx
' Roses are red,\n Violets are blue,\n Whitespace preserved,\n just for you.'
Fixture program-cxpath-g2-18-whitespace-verbatim in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The XML-interop attribute xml:space is accepted and carried
as an inert pass-through attribute — it round-trips
through XML identically and rides through JSON/YAML/TOML as a
regular attribute, but CX itself attaches no behavior to
it.
CDATA sections
XML CDATA blocks (<![CDATA[...]]>) project to CX raw-text
nodes ([#…#]) on import — the contents are opaque bytes,
untouched by canonicalization. CX itself has no separate
CDATA syntax: raw text carries opaque payloads, and
triple-quoted strings carry verbatim string content.
Typed columnar tables
For analytical workloads CX has a typed columnar table form: a
glued [table[…]] clause on the element head declares the
columns once, and the body is the cells. The physical wire
format is CXCol (see cxcol) and the normative table API is
in spec/03-approved/misc/table-api.md.
input.cx
[t [table[name age::int tag]]
alice 30 admin
bob 25 user
]
$ cx --from=cx --to=cx input.cx
[t [table[name age::int tag]]
alice 30 admin
bob 25 user
]
Fixture tab-003-mixed-typed-untyped in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
Column spec: each column is a name with an optional glued
::T type (age::int); an untyped column defaults to string
(name and tag above). Types use the full scalar vocabulary
(::int, ::float, ::bool, ::date, ::datetime,
::bytes, plus sized variants ::i32/::u64/::f32 etc.).
There is no nullable marker in the header — a null cell is
legal in any typed column, and the binary wire carries such
columns under a nullable-column wrapper
(spec/03-approved/core/data-bin.md
§3.10.5).
Rows are count-driven: whitespace separates both cells and rows, and every N consecutive cells form one row, where N is the declared column count. A cell that does not satisfy its column type, or a total cell count that is not a multiple of N, is a parse error.
Dictionary encoding: on the binary wire, a column is dictionary-encoded exactly when the dictionary actually saves bytes — the writer evaluates the trade per column. There is no cardinality threshold to configure and no per-column attribute; the choice is automatic and canonical.
Streaming: oversized tables are emitted as chunked tables — column specs once, then length-prefixed row groups (canonical chunk size 2²⁰ rows per group). The length prefixes are the skip-ahead mechanism; there are no per-chunk statistics. See cxcol for the native streaming format and parquet-bridge for the Parquet round-trip.
Arrow / Parquet bridges: tables convert to Apache Arrow record batches (zero-copy via the C Data Interface) and to Parquet files. The bridges live on the analytics-bridge surface — see arrow-bridge and parquet-bridge.
Schema language (.cxs)
A .cxs file is a CX schema: an element-shape declaration that
the validator can enforce. Schemas are CX documents themselves —
no separate grammar to learn. Full spec in
spec/03-approved/core/schema.md.
The same declarations, written as a program's [schema …]
value, drive the cx-stdlib/validate module — the fixture below
validates one record against a two-field schema and reads the
verdict:
prog.cx
[?lib 'cx-stdlib/validate']
[$validate:is-ok
[$validate:validate-shape
[user [email "alice@example.com"] [score 87]]
[schema
[field name="email" type="string" pattern=".+@.+\\..+"]
[field name="score" type="int" min=0 max=100]]]]
$ cx prog.cx
true
Fixture validate-001-shape-ok in deps/cx-core-code/conformance/stdlib/validate.cxd — replayed against this cx when the page was built.
Open / strict / closed modes
Schemas declare one of three validation modes:
- open — the schema's declarations are required; extra attributes / extra child elements not declared are permitted silently, with no diagnostic. Use this for forward-compatible evolution.
- strict — declarations are required; extras are
reported as warnings (
S001for unknown elements,S012for unknown attributes). Use for schema authoring — find missing declarations without failing the build. - closed — declarations are required; extras are
errors (the same
S001/S012, at error severity). Use for security-critical surfaces where the schema is the authoritative contract.
The default mode is open. Override per-schema with
[schema of=… mode=strict] — mode is per-schema, not
per-element. See
spec/03-approved/core/schema.md
§9 for the full normative behavior.
Constraint vocabulary
Beyond type-tag matching, schemas support a constraint vocabulary on attributes and element bodies:
[pattern S]— string value matches the RE2 regex[range M N]/[min M] [max N]— numeric bounds[len M N]/[min-length M] [max-length N]— string/bytes length[enum V ...]— value is one of the listed values[req]/[opt]— presence policy[card "M..N"]— child element cardinality range[default V]— inserted when absent
Constraint violations produce S-prefixed diagnostics
(S002 missing required attribute, S006 range violation,
…) with the failing constraint identified in the message.
Full vocabulary + error-code registry:
spec/03-approved/core/schema.md
§7 + §12.
Defaults and inheritance
An attribute with a [default V] clause is materialized only
when default application is requested — --apply-defaults on
the CLI, or the cx_validate_apply_defaults ABI entry point,
which returns a modified copy; plain validation never
changes the document. A default whose literal type does not
match the declared type fails at schema-load time, so schema
typos surface immediately. Schemas can compose via
[?cx include] (see includes); imported schemas'
declarations layer over the importing schema's.
Binding a schema to a document
There is one way to associate a .cxs schema with a
document today: the explicit CLI flag —
cx validate doc.cx --schema=users.cxs. There is no
sibling-file convention and no implicit lookup. The
in-document [?cx schema=…] directive is defined by
spec/03-approved/core/schema.md
§13 and parses cleanly, but the shipped validator does not
consume it — it requires the flag.
Schema-driven binary encoding (see data-bin) uses the schema's declarations to omit per-value type tags from the wire form — producing wire sizes comparable to Protobuf or Avro. Encoding is decoupled from validation and is its own explicit opt-in.
Include resolver
[?cx include] resolves a path to another CX document and
splices its contents in place. Resolution is opt-in: with no
include root supplied, the directive is left in place
unresolved — and because strict canonicalization strips
directives, an unresolved include silently vanishes from
cx canonical / cx hash output. Resolve before you hash.
The refusal catalog (cx-err:E901–E905) is in
spec/03-approved/core/cxdm.md.
input.cx
[config
[?cx include=base.cx]
[override port=9090]]
prog.cx
$doc
$ cx --data=input.cx prog.cx
[config [?cx include=base.cx] [override port=9090]]
Fixture program-cxpath-g2-20-include-left-in-place in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Path resolution
Include paths are resolved relative to the including
document's directory, not the working directory — and the
whole resolution is root-bounded: the embedding supplies
an absolute include root, and every resolved include must lie
under it. Everything that could reach outside the root is
refused, each with its own error: an absolute path
(cx-err:E901), a relative path that traverses out of the
root (cx-err:E902), and any URL scheme — https: included
(cx-err:E903). An include is a local, root-bounded file
read by construction; there is no search path and no
environment variable — one root, one resolution rule.
Cycle detection
An include cycle (A includes B which includes A) is refused
with cx-err:E904 at the second visit, and the error names
the whole chain (A → B → A). The resolver tracks include
ancestors per document; sibling includes that happen to
reference the same file are fine, only cyclic ancestry
errors.
Depth limit
Default maximum include nesting depth is 8. Exceeding it is
refused with cx-err:E905, naming the limit and the path
that crossed it. The bound is configurable by the embedding
(the resolver options), not by environment variable.
Capability gating
An include is a filesystem read, and reads are capabilities:
[?cx include=…] refuses with CXER0271 unless the run was
granted --allow-read — deny-by-default, like every effect
(see capabilities). Resolution is rooted: relative
include paths resolve against the including document's
directory, or against an explicit --include-root=DIR.
Untrusted-input processing grants nothing and parses pure;
see security for the threat model.
Namespaces (xmlns:)
CX supports XML-namespace-style scoping via xmlns: attribute
declarations. The normative spec is
cxdm.md §3 (model)
with the canonical-form rules in
canonical.md §2.7a.
Prefix declarations
xmlns:prefix=URI declares a namespace binding scoped to the
declaring element and all of its descendants until a
redeclaration ends the scope. A prefix is then a prefix:local
qualified name on any element or attribute in scope.
input.cx
[doc xmlns:dc=http://purl.org/dc/elements/1.1/
[dc:title 'CX Spec']
[dc:author 'Ada Lovelace']]
prog.cx
[?for [in $c /doc/*] [yield [$name $c]]]
$ cx --data=input.cx prog.cx
'dc:title'
'dc:author'
Fixture program-cxpath-g2-21-namespace-prefix in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Default namespace
xmlns=URI declares a default namespace. Applies to the
declaring element and all unprefixed descendant elements
until redeclared. Per XML Namespaces 1.0 §6.2 it does not
apply to attribute names.
input.cx
[doc xmlns=urn:doc
[section 'Unprefixed elements live in urn:doc']]
prog.cx
[$text [$first //section]]
$ cx --data=input.cx prog.cx
'Unprefixed elements live in urn:doc'
Fixture program-cxpath-g2-22-default-namespace in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Reserved prefixes
Three prefixes are reserved by CX and cannot be redeclared:
| prefix | URI | notes |
|---|---|---|
| xml | http://www.w3.org/XML/1998/namespace | XML built-in; always resolves; xml:lang accepted as alias for cx:lang |
| cx | tag:cxhome.org,2026:ns/cx | CX metadata; cannot be redeclared; carries cx:lang etc. |
| xmlns | declaration syntax only | Never resolves as a name prefix |
Equality and canonical form
Two namespace bindings are equal iff their URIs are equal as
byte sequences (no normalization beyond percent-decoding).
Canonical form rewrites prefixes to a deterministic sequence
(ns0, ns1, …) so two documents with the same data but
different prefix choices hash to the same canonical bytes.
Round-trip preserves the original prefix names via the
lossless canonicalizer.
Locale and language (cx:lang)
cx:lang declares the language of an element's text content
using a BCP 47 language tag. Inherited-scope: the declaration
applies to the element and all descendants until a redeclaration.
The reserved cx: prefix that carries it is pinned in
ns-reserved.
input.cx
[doc cx:lang=en-US
[title "'Welcome'"]
[section cx:lang=fr-FR
[title 'Bienvenue']]
[section cx:lang=ja-JP
[title 'ようこそ']]]
prog.cx
[?for [in $t //title] [yield [$text $t]]]
$ cx --data=input.cx prog.cx
"'Welcome'"
Bienvenue
'ようこそ'
Fixture program-cxpath-g2-23-cx-lang-scopes in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The xml:lang attribute is accepted on input as an alias for
cx:lang (per XML interop). On canonical emit, cx:lang is the
normalized form. Every binding exposes a .lang() accessor on
Nodes that resolves to the inherited language tag at that
position.
Bidirectional text direction (LTR/RTL) is inferred from the
language tag unless explicitly overridden with
cx:dir=rtl/cx:dir=ltr (Unicode bidi algorithm).
Doctype declarations
CX reserves a doctype declaration slot — the [!…] declaration
lexical space inherited from XML; an XML DOCTYPE imports as a
[!DOCTYPE …] prolog node (see surfaces) — but does not yet
define an active doctype surface.
The roadmap direction: a doctype names the document type
(convention <name>-v<major>) and a future revision promotes
it to an active declaration — auto-load the schema by name
and validate on parse. Until that lands, associate a schema
explicitly (see schema-binding).
Document encoding (UTF-8, BOM, alternates)
CX source is UTF-8. The parser accepts a UTF-8 BOM at the start of a file (and strips it) but does not emit a BOM in canonical form.
Canonical-form output is always UTF-8 without BOM. Decoding errors raise a parse failure with a byte offset; the parser does not silently replace invalid bytes. Alternate source encodings (utf-16, legacy 8-bit code pages, CJK encodings) are on the roadmap and are not accepted today — transcode to UTF-8 before parsing.
Streaming parse and emission
CX defines a SAX/StAX-style streaming event API — read events
via cx_events_open / next / close, write events via the
cx_events_writer_* family. Normative spec:
spec/03-approved/core/streaming.md.
The API contract is streaming — the consumer sees one event at
a time and never needs the whole tree. The current engine
behind it is eager: it parses the full document and then
delivers the event list, so today the API bounds the
consumer's memory, not the parser's. Line-oriented inputs
can be processed with genuinely bounded residency by iterating
lines and parsing each record separately — over a file, the lines
come from [$io:line-iter [$io:open 'logs.cxl' 'r']] under
--allow-read; the fixture below runs the same loop over three
in-memory records:
prog.cx
[?lib 'cx-stdlib/cx']
[?for [in $line ('[error code=7 where=disk]', '[info code=1]', '[error code=9 where=net]')]
[where [= [$name [$cx:parse $line]] 'error']]
[yield [?let [= $rec [$cx:parse $line]] $rec/@code]]]
$ cx prog.cx
7
9
Fixture program-cxpath-g2-24-record-per-line in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Performance is tracked against the per-operation budgets in
spec/03-approved/core/abi.md
§4 (parse and event-delivery targets per input size), enforced
by the perf-regression suite rather than quoted as a single
throughput figure.
For columnar tables specifically, the streaming format is CXCol (chunked column-oriented binary, see cxcol); the per-chunk manifest carries statistics enabling predicate pushdown.
Limits and quotas
The parse surface carries a small set of structural guards against adversarial input. They are deliberately few, they are built in rather than tuned by environment variables, and each refusal names its limit.
| guard | bound | refusal |
|---|---|---|
| Element nesting (text parser) | 64 | parse refusal naming the line, column, and limit |
| Include depth | 8 (embedding-configurable) |
cx-err:E905
|
| Include cycle | refused at the repeat visit | cx-err:E904 naming the chain |
| Include escape — absolute path / root traversal / URL scheme | refused outright | cx-err:E901 / cx-err:E902 / cx-err:E903 |
| CXCol decode recursion | 64 | decode refusal naming the limit |
Two classes of bomb are structurally absent rather than capped. Entity references parse to nodes — there is no textual expansion pass — so recursive expansion attacks (the XML "billion laughs") have nothing to attack. And includes cannot fan out beyond the root-bounded tree above: no search paths, no URLs, no absolute paths.
What the engine deliberately does not carry: attribute-count caps, body-size caps, parse-time budgets, or canonical-form size caps. Parse cost is linear in input bytes, so the untrusted-input posture is boundary-side: bound the input size where it enters, run with the empty grant set (the parser needs no capability grants at all), and pair with OS-level process limits. See security for the threat model. Evaluation-side bounds (recursion, time) are the code language's story, not the data format's — Ring 0 cannot execute anything.
Date / time semantics — timezones, precision, leap seconds
Dates and datetimes are first-class scalars (see dates). This section is the developer-facing detail.
Calendar: proleptic Gregorian only. Dates before 1582-10-15 are accepted but represent calendar-extension (the calendar in use today extended backward), not historical Julian dates.
Datetime form: YYYY-MM-DDTHH:MM:SS[.fff[fff[fff]]][offset].
Optional fractional seconds at millisecond / microsecond /
nanosecond precision. Offset is Z (UTC) or ±HH:MM. CX accepts
both ISO 8601 and RFC 3339 forms on input; canonical output is
RFC 3339 with offset Z for UTC or ±HH:MM otherwise.
Timezone handling: the lossless form (cx fmt) preserves
the offset as authored; the strict canonical form normalizes
to UTC — 2026-05-22T10:00:00-04:00 canonicalizes (and
hashes) as 2026-05-22T14:00:00Z. Identity comparison =
compares the absolute instant, and sort order is by absolute
instant.
Leap seconds: CX datetimes do not represent leap seconds
(no 23:59:60). Input containing 23:59:60 is normalized to
the following 00:00:00. Bindings that need leap-second
awareness use an external library.
In the bindings: each binding's Layer-1 surface maps a
datetime to its host's zone-aware type and preserves the offset;
conversion to UTC is opt-in via .to_utc() (see bindings).
Reserved tokens and keyword list
A complete enumeration of identifiers that have special meaning and cannot be used as bare element names or attribute keys without quoting.
Reserved literal tokens (always have their literal meaning):
true,false— boolean literalsnull— null scalar
Type names (after the glued :: ascription, per the
lexicon's TypeName production):
int,i8,i16,i32,i64u8,u16,u32,u64float,f16,f32,f64bool,string,null,date,datetime,bytes,atomdecimal,bigint— exact numeric refinementsduration,period,instant— temporal refinementssecret— declassification-gated values
Directive name keywords (the first token after [?):
All 80 directives in the current registry — see
directives in the code section. Notable: for, match,
if, let, fn, def, try, modify, cx, pipe.
CXPath axis keywords (before :: inside path expressions):
child,descendant,descendant-or-selfparent,ancestor,ancestor-or-selfself,attributefollowing,preceding,following-sibling,preceding-sibling
XPath/CXPath builtin function names — see builtins for
the full catalog (~80 names). Notable: count, name,
local-name, position, last, string, number,
boolean, not, exists, empty, concat,
string-length, contains.
Reserved namespace prefixes: xml, cx, xmlns (see
ns-reserved).
If you need an element or attribute named after a reserved
token, quote it: ['true' value=1], [user 'true'=yes]. The
single-quoted form is the canonical escape; double-quoted also
works on input.
Concept framing — data model, sigils, types
The reference above describes each feature in isolation; the concept framing below shows how they fit together — the one tree shape behind six surfaces, the five sigils as structural reach, the auto-typing rules that pick a type from a lexical shape.
One tree, six surfaces
CXDM is the shape every CX parse produces and every emit walks. Whether the source is CX, XML, JSON, YAML, TOML, or Markdown, the parser yields a tree in this single shape; whether the target is any of those six dialects, the emitter walks the same tree back out. Downstream code never branches on input format because the data model has already absorbed the difference.
A CXDM node is an Element with a name, an ordered list of attributes, an ordered list of body items, and a few optional structural slots — anchor, merge target, declared id, data type. Body items are Text nodes, Scalar nodes, child Elements, or the collection literals — Sequences, Arrays, Maps.
input.cx
[pizza #margherita size=14 crust=thin
[topping cheese]
[topping basil]
Stone-baked since 1987.]
prog.cx
(//pizza/@size, //pizza/@crust, [$count //topping], [$text [$first //pizza]])
$ cx --data=input.cx prog.cx
([size 14], [crust thin], 2, ' Stone-baked since 1987.')
Fixture program-cxpath-g2-25-one-node-outside-in in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Reading this element from the outside in: name pizza,
declared id margherita, attributes size=14 and
crust=thin, then a body of three items — two child
elements and a text node. A type ascription, when one is
wanted, is glued to the name as ::T (see type); a bare
:name in the head is an atom in body position, not a type.
Whitespace inside a body is significant when it sits between text characters and collapses to a single space when it separates body items. The canonical form normalises this so byte-identical content carries the same hash regardless of source author choice.
The five sigils + bool sugar
CX uses five sigils. Each is a single character in a fixed lexical position that gives a value structural meaning beyond what bare text can carry.
@— reference in attribute-value position to a declared id (#); in body position the form is[ref @name]. Never references anchors.&— anchor declaration; names an element so others can refer to or merge from it.*— merge target; folds the named anchor's attrs and children into this element.#— declared id; stable handle within the document's scope; target of@references.:— data type annotation (glued::T) and atom literal (:name); the ascription pins the value type, overriding auto-typing.
input.cx
[menu
[pizza #margherita price=12]
[pizza #pepperoni price=14]
[order #o1234 customer=alice
[line item=@margherita count=2]
[line item=@pepperoni count=1]]]
prog.cx
[?for [in $l //line] [yield [pick item=$l@item count=$l@count]]]
$ cx --data=input.cx prog.cx
[pick item=margherita count=2]
[pick item=pepperoni count=1]
Fixture program-cxpath-g2-26-references-by-id in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
In CXPath the same @ sigil is attribute access:
input.cx
[order [line sku='margherita' count=2]]
prog.cx
[?for [in $line //order/line] [yield [row sku=$line/@sku]]]
$ cx --data=input.cx prog.cx
[row sku=margherita]
Fixture program-cxpath-g2-27-at-is-attribute-access in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
An anchor's attributes fold into every element that merges it; the strict canonical form is where the fold is visible:
input.cx
[fleet
[defaults &shared timeout=30 retries=3]
[server *shared name=api]
[server *shared name=worker]]
prog.cx
[?lib 'cx-stdlib/cx']
[$cx:canonical $doc]
$ cx --data=input.cx prog.cx
'[fleet\n [defaults timeout=30 retries=3]\n [server timeout=30 retries=3 name=api]\n [server timeout=30 retries=3 name=worker]\n]\n'
Fixture program-lib-g2-28-shared-defaults-expand in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The strict canonical form expands anchors and removes the anchor structure entirely, so two documents that bind the same data with different anchor names hash to the same identity — no anchor name survives into the canonical bytes.
Boolean attributes have no shorthand sigil — they are
ordinary name=value pairs (see bool).
input.cx
[pizza vegan=true nuts=false gluten=false organic=true]
prog.cx
(//pizza/@vegan, //pizza/@nuts, //pizza/@gluten, //pizza/@organic)
$ cx --data=input.cx prog.cx
([vegan true], [nuts false], [gluten false], [organic true])
Fixture program-cxpath-g2-29-bool-attributes-plain in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Types — auto-typing, sized, decimal, bigint
CX values fall into a small set of atomic types — int,
float, bool, string, null, date, datetime,
bytes, decimal, bigint, atom — and
arrays of any of these. Most values are auto-typed from
their lexical shape; explicit annotations cover the
rest.
input.cx
[demo
count=42
pi=3.14
enabled=true
name=Alice
missing=null
date=2026-05-09
start=2026-05-09T10:00:00Z]
prog.cx
([+ //demo/@count 1], [* //demo/@pi 2], //demo/@enabled, //demo/@name, //demo/@date, //demo/@start)
$ cx --data=input.cx prog.cx
(43, 6.28, [enabled true], [name Alice], [date 2026-05-09], [start 2026-05-09T10:00:00Z])
Fixture program-cxpath-g2-30-auto-typing in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Leading-zero strings. Tokens with a leading zero
stay strings unless a radix prefix follows it — 0x (hex),
0o (octal) and 0b (binary) read as the int they spell
(0o755 is 493, 0b1101 is 13). Postal codes, area
codes, account numbers, BIC codes — anywhere the leading
zero is data, not a numeric value — round-trip as the
strings they are.
input.cx
[literals
[zip 02134]
[mask 0xFF00FF]
[perms 0o755]
[bits 0b1101]]
prog.cx
[?for [in $x //literals/*] [yield $x]]
$ cx --data=input.cx prog.cx
[zip '02134']
[mask 16711935]
[perms 493]
[bits 13]
Fixture program-cxpath-g2-31-leading-zero-stays-string in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Sized integer and float. A colon-type pins the runtime representation and (for sized variants) the legal range. Out-of-range values fail at parse time, not at runtime in some downstream consumer.
input.cx
[sized
[port::u16 8080]
[balance::i64 -1234567890]
[ratio::f32 3.14]
[count::u8 200]]
prog.cx
[?for [in $x //sized/*] [yield $x]]
$ cx --data=input.cx prog.cx
[port::u16 8080]
[balance::i64 -1234567890]
[ratio::f32 3.14e0]
[count::u8 200]
Fixture program-cxpath-g2-32-sized-ascriptions in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Decimal and bigint. IEEE 754 floats lose precision
for money and high-precision quantities; signed 64-bit
overflows for some IDs. CX has :decimal and :bigint
for both cases, with binding mappings to each language's
native big-number type.
input.cx
[exact
[total::decimal 19.99]
[user-id::bigint 18446744073709551616]]
prog.cx
[?for [in $x //exact/*] [yield $x]]
$ cx --data=input.cx prog.cx
[total::decimal 19.99]
[user-id::bigint 18446744073709551616]
Fixture program-cxpath-g2-33-decimal-and-bigint in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Typed bodies and arrays. A glued ::T ascription
pins not just attribute values but body content as
well.
input.cx
[typed
[count::int 42]
[ratio::decimal 3.14159]
[oven-temps::int[] 220 240 260 480]
[tags::[] cheap fast tasty]]
prog.cx
[?for [in $x //typed/*] [yield $x]]
$ cx --data=input.cx prog.cx
[count::int 42]
[ratio::decimal 3.14159]
[oven-temps::int[] 220 240 260 480]
[tags::[] cheap fast tasty]
Fixture program-cxpath-g2-34-typed-bodies-and-arrays in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Reference quick-refs — schemas, includes, streaming, tables
At-a-glance references for the four big data-language subsystems that get their own normative spec docs. Schemas validate shape; includes compose documents; streaming bounds RSS; tables carry columnar data. The primary children (§2.7, §2.8, §2.9, §2.14) carry the per-feature depth; this section is the cookbook-style summary.
Schemas — validate before processing
A schema in CX is itself a CX document. It declares
which elements may appear where, what attributes they
carry, and what types those attributes hold. The cx
validate subcommand walks the schema against an input
document and reports diagnostics. Three modes
determine what happens to undeclared content: open
(the default) admits it silently; strict reports it
as warnings; closed rejects it as errors. The schema
sets its own mode in the header; the --mode flag
overrides it. An enum-typed field refuses a value outside
its set, and the violation names its code:
prog.cx
[?lib 'cx-stdlib/validate']
[?let [= $r [$validate:validate-shape
[rec [color "purple"]]
[schema [field name="color" type="string" [enum "red" "green" "blue"]]]]]
$r/violation/@code]
$ cx prog.cx
ENUM_MISMATCH
Fixture validate-012-shape-enum-mismatch in deps/cx-core-code/conformance/stdlib/validate.cxd — replayed against this cx when the page was built.
cx validate order.cx --schema=order.cxs
cx validate order.cx --schema=order.cxs --mode=strict
cx validate order.cx --schema=order.cxs --fail-on=warn
Type catalog. string; int with sized variants
(i8, u16, i64, …); float; decimal; bigint;
bool; date; datetime; bytes; atom; plus the
composite [enum V…] / [or T…] / [list T] forms. A
regex requirement is not a type — it is the [pattern S]
constraint clause on a string declaration. The validator
coerces and bounds-checks each value against the
declared type.
Includes — parse-time vs eval-time
[?cx include=path] is the include directive — a
parse-time expansion that splices files before the
document reaches downstream tools. Use it for static
composition: config files, schema fragments, page
templates with known paths. Resolution is opt-in — the
directive is preserved untouched unless a resolution root
is supplied (--include-root=DIR on the CLI). A program
that needs a computed path reads the file itself with
the standard library's [$io:read-file] + [$cx:parse].
cx --cx --include-root=./conf main.cx
input.cx
[users
[?cx include=lib/admins.cx]
[user name=carol]]
prog.cx
$doc
$ cx --data=input.cx prog.cx
[users [?cx include=lib/admins.cx] [user name=carol]]
Fixture program-cxpath-g2-36-include-quickref in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Unresolved includes and hashing. Without a resolution
root the directive rides along unresolved — and strict
canonicalization strips directives, so the include
silently disappears from cx canonical / cx hash
output. Always resolve (supply --include-root) before
canonicalizing or hashing a document that uses includes.
Security and cycle detection. Resolution rejects
absolute paths (E901), paths that escape the root
after lexical normalisation (E902), and URL-shaped
paths (E903). Cycle detection (E904) and depth
limits (E905, default 8) protect against runaway
recursion. The error catalog lives in
spec/03-approved/core/cxdm.md.
Streaming — event shape and throughput
The streaming API is a SAX/StAX-style event sequence
(spec/03-approved/core/streaming.md).
The read side is lossless: 32 event types in six
groups — lifecycle (StartDoc/EndDoc), prolog and
markup, element + body (StartElement carries the name
AND all attributes; there is no separate attribute
event, and text arrives as whole Text / Scalar
events, not chunks), collection literals and
first-class kinds, the programmable surface, and the
chunked-table triplet. The write side accepts a
14-event subset and rejects the rest at emit time.
The same event shape works on every input format.
Today the engine behind the read side is eager — the document is parsed whole and the events are delivered from the completed parse. The event contract is what consumers program against; bounded-memory parsing is the contract's headroom, not yet its implementation.
The read side is the C ABI's cx_events_open / next /
close triple over the document text, and every binding's
stream call wraps it; the write side is the
cx_events_writer_* family (14 emit events × 6 output
formats). See bindings for each binding's spelling.
Backpressure. The write side has no buffering knobs —
emitted bytes flow to the destination and the only
backpressure is the operating system's own write(2)
blocking. Performance is tracked against the budgets in
spec/03-approved/core/abi.md
§4.
Tables and binary forms
A CX table is a structural form for columnar data. Columns declare a name and a type; rows are records. The Table API exposes count, schema introspection, dump, and load — symmetric inverses with declared types preserved across formats. The on-disk forms are ast-bin (binary AST, cap bit 36 advertises v8) and data-bin (CXCol v1 — columnar, used by Arrow/Parquet bridges).
input.cx
[users [table[name::string age::int active::bool]]
alice 30 true
bob 25 false
]
$ cx --from=cx --to=cx input.cx
[users [table[name::string age::int active::bool]]
alice 30 true
bob 25 false
]
Fixture tab-001-typed-columns in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
cx table info orders.cx # column / row counts, types, byte size
cx table dump orders.cx --to=cx
cx table dump orders.cx --to=parquet --output=orders.parquet
cx table load orders.parquet --to=cx
# CSV enters through the convert surface, not `cx table load`:
cx --from=csv --to=cx orders.csv
Comments (line and block)
CX has two comment forms: a
#line comment that runs to end-of-line, and a[; … ]block comment that spans any number of lines and is allowed anywhere whitespace is allowed. The block comment is a real lexical element, not a hack on top of a string — the parser drops it before AST construction. Comments are preserved by the lossless canonicalizer (cx fmt) and stripped by the strict canonicalizer (cx canonical); see canonical.input.cx[; A single-line note. ] [config [; A block comment can nest inside an element and span lines. ] [host 'localhost']]prog.cx([$count //config/*], //config/host)$ cx --data=input.cx prog.cx (1, [host localhost])Fixture
program-cxpath-g2-02-block-comments-are-droppedindeps/cx-core-code/conformance/code.cxd— replayed against this cx when the page was built.Older CX dialects used
[--]for block comments. CX currently uses the[; … ]form only;[# … #]is raw text / CDATA, not a comment. See migration for the rename.