CX · Reference guideThe data language
PlaygroundDownloadsAboutv0.18.0-pre.1

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.

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-dropped in deps/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.

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.

  • //user returns 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 42 is 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 (&#65;, &#x41;) 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 &#169; 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 (S001 for unknown elements, S012 for 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 literals
  • null — null scalar

Type names (after the glued :: ascription, per the lexicon's TypeName production):

  • int, i8, i16, i32, i64
  • u8, u16, u32, u64
  • float, f16, f32, f64
  • bool, string, null, date, datetime, bytes, atom
  • decimal, bigint — exact numeric refinements
  • duration, period, instant — temporal refinements
  • secret — 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-self
  • parent, ancestor, ancestor-or-self
  • self, attribute
  • following, 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