Comparison to alternatives
Reach for CX for nearly every data-format need and for most general-purpose programs. One syntax carries the document, the query, the transform and the program, so the layers other stacks glue together are one surface here. This section states CX's case: first beside Python — the standard CX is built to meet and exceed (cx-vs-python) — then against each format, covering feature parity, performance, ecosystem, learning curve, schema, identity / hash and round-trip lossiness, with a closing "when to reach for X" for the cases where one layer is the whole problem.
CX and Python — the standard to meet and exceed
Python is not what CX sets out to replace. It is the standard CX
measures itself against: readable on day one, batteries included,
as good for a ten-line script as for a system. CX is built to
meet that standard and to go past it where data work hurts. Each
claim below stands beside a conformance case this guide's cx
replayed when the page was built.
Exact numbers by default. In Python a fraction is a binary
float until you import decimal. In CX a fraction literal is an
exact base-10 decimal, and arithmetic keeps it one:
prog.cx
[?let [= $subtotal 19.99] [= $shipping 5.01] [total [+ $subtotal $shipping]]]
$ cx prog.cx
[total 25.00]
Fixture ap-decimal-float-mix-right in conformance/llm/antipatterns.cxd — replayed against this cx when the page was built.
Mixing a decimal with a binary float is refused out loud, naming the one bridge between them, rather than rounded in silence:
prog.cx
[+ 1.5 1.0e0]
$ cx prog.cx
[err code=cx-err:CXER0100 message='+: decimal/bigint arithmetic admits only int/bigint/decimal operands — [cast] is the only decimal↔float bridge (L44)' at='1:1']
Fixture ap-decimal-float-mix-wrong in conformance/llm/antipatterns.cxd — replayed against this cx when the page was built.
Effects are granted, never inherited. A Python script can read every file its process can. A CX program reads nothing until you grant it, and the refusal is an ordinary value that names the flag which would allow it:
prog.cx
[?lib 'cx-stdlib/io']
[$io:read-file "/var/log/app.log"]
$ cx prog.cx
[err code=cx-err:CXER0271 message='E_CAP_DENIED: read capability required for io-read-file; none granted (grant via --allow-read)' at='2:1']
Fixture io-001-read-file-cap-denied in deps/cx-core-code/conformance/stdlib/io.cxd — replayed against this cx when the page was built.
Regular expressions that do not backtrack. Python's re is a
backtracking engine. CX's re is RE2: a construct that would
need backtracking, such as a backreference, is refused when the
pattern compiles:
prog.cx
[?lib 'cx-stdlib/re']
[$re:compile "(.)\\1"]
$ cx prog.cx
[err code=cx-err:CXER3200 message='E_RE_FEATURE_UNSUPPORTED: CXER3200:E_RE_FEATURE_UNSUPPORTED: (.)\1' at='2:1']
Fixture re-003-compile-backreference-unsupported in deps/cx-core-code/conformance/stdlib/re.cxd — replayed against this cx when the page was built.
Errors are values. An error flows out of anything built from it, so a failure is a value you can inspect where it surfaces, not an exception unwinding past you:
prog.cx
[?let [= $e [err code='c' message='m']] [report $e]]
$ cx prog.cx
[err code=c message=m]
Fixture program-construction-err-004-captured-err-propagates in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Updates never mutate. [?modify] answers a new document; the
one it started from is unchanged:
input.cx
[users [user name=Alice]]
prog.cx
[?let [= $orig $doc] [= $new [?modify $doc //user/@name [set "Bob"]]] [pair orig=$orig/user@name new=$new/user@name]]
$ cx --data=input.cx prog.cx
[pair orig=Alice new=Bob]
Fixture program-modify-003-original-unchanged in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
One value model for every format. JSON parses into the same values the language computes with, and a map is a map whatever order its entries were written in:
prog.cx
[?lib 'cx-stdlib/json']
[$json:parse "{\"a\": 1}"]
$ cx prog.cx
{a: 1}
Fixture json-007-parse-object-map in deps/cx-core-code/conformance/stdlib/json.cxd — replayed against this cx when the page was built.
prog.cx
[?let [= $z 0] ([= {a: 1, b: 2} {b: 2, a: 1}], [= {a: 1, b: 2} {a: 1, b: 3}], [= {a: 1} {a: 1, b: 2}])]
$ cx prog.cx
(true, false, false)
Fixture program-map-115-equality-order-independent in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Identity is built in. A value's SHA-256 is taken over its canonical bytes, so the same value written two ways hashes alike:
prog.cx
[$eq [$cx:hash [$cx:parse "[a 1]"]] [$cx:hash [a 1]]]
$ cx prog.cx
true
Fixture cx-025-hash-value-and-source-agree in deps/cx-core-code/conformance/stdlib/cx.cxd — replayed against this cx when the page was built.
When to reach for CX
CX is the default for data, config and interchange. The specialists further down this list are the cases where one layer is the whole problem. Top to bottom, first match wins:
- You need one syntax for data and code (config that is also a program, templates that are also data, a schema that is also a value). CX is the only format in this comparison that is homoiconic; pick CX.
- You need content-addressable identity across languages and platforms (cache keys, dedup, signed documents, distributed ledgers). The CX SHA-256 of canonical bytes is the most boring possible implementation; pick CX.
- You round-trip between JSON / YAML / TOML / XML / Markdown and lose information each time. CX is the superset that preserves all of them losslessly; pick CX.
- Your workload is bulk analytical scan on tabular data at terabyte scale, exchanged with the broader ecosystem (DuckDB / Spark / pandas). Use Parquet for storage, Arrow for in-memory, optionally with CX as the metadata / config layer.
- Your workload is wire-format throughput between microservices with a fixed schema, and you control both ends. Use Protocol Buffers or MessagePack — they will be smaller and faster on the wire than any text format.
- You need a single-file human-edited config with no programmability and no cross-system identity requirements. TOML (Cargo, pyproject) or YAML (Kubernetes, CI) are everywhere; the ecosystem advantage is real.
- You need a typed, programmable config language with a strong type system. Dhall or Pkl are good fits if the type-system rigor matters more than the homoiconic data-and-code unification.
- Otherwise (general data / config / interchange with mixed workloads, mid-size documents, multiple consumers in different languages): CX is the all-purpose default.
CX vs JSON
JSON is the universal interchange baseline. CX is a superset projection — anything that round-trips through JSON also round-trips through CX. The interesting comparisons are where CX has features JSON does not, and where JSON has ecosystem JSON does not.
| dimension | json | cx |
|---|---|---|
| Schema | JSON Schema (separate spec) | .cxs — same syntax as data (see schema) |
| Identity / hash | No canonical form; hash drifts on key reorder | SHA-256 of canonical bytes; stable across platforms (see hash) |
| Comments | None (in the standard); JSON5 / JSONC are extensions | [; … ] blocks + # line comments, preserved by cx fmt, stripped by cx canonical |
| Trailing commas | Forbidden | Not applicable — bracket form, no commas in attribute list |
| Number precision | Underspecified; JS doubles by default | Sized type annotations (::i64, ::u32, ::f32) (see sized-types) |
| Multi-line strings | Escaped only | Triple-quoted, whitespace-preserving (see strings) |
| Date / datetime | String only | First-class :date / :datetime scalar (see dates) |
| Bytes | Base64 in a string | First-class :bytes scalar (see bytes) |
| Tooling ecosystem | Vast — every language, every editor, every CDN | Tier-1 bindings V/Python/Go/Rust; LSP + tree-sitter + VS Code; growing |
| Wire size | Baseline | Comparable text form; data-bin / CXCol smaller |
| Parse speed | Highly optimized parsers everywhere | Fast reference parser; the release gate holds streaming to a >=200 MB/s floor (not yet passing) |
| Learning curve | Trivial | Bracket-and-sigil form; ~one hour for the data subset |
When to pick JSON instead of CX. Your consumers are browser JavaScript, a CDN-served config endpoint, or any interface where you cannot install a CX parser. JSON remains the right wire-format choice for ubiquity alone.
When to pick CX instead of JSON. You want identity, schema, comments, mixed content, dates, or bytes without layering JSON Schema + JSON5 + base64 + custom date string conventions on top of JSON. CX gives you the whole stack in one syntax.
CX vs YAML
YAML optimises for human readability of multi-line strings and config-as-code. The cost is the famously large surface area (indentation sensitivity, tag system, anchor / alias / merge keys, the Norway problem).
| dimension | yaml | cx |
|---|---|---|
| Indentation | Semantic (whitespace-sensitive) | Insignificant — bracket-structured |
| Anchors and aliases | &anchor / *alias / << merge keys | &anchor / *merge sigils — same idea, no separate merge keyword (see anchor / merge) |
| Multiline strings | | (literal) and > (folded) | Triple-quoted (always literal) |
| Norway problem | no parses as false in YAML 1.1 | Only true/false are bools; no /yes is a string |
| Schema | JSON Schema (separate) | .cxs (same syntax) |
| Identity / hash | No canonical form | SHA-256 of canonical bytes (see hash) |
| Comments | # line comments only | [; … ] blocks (span lines) + # line comments, preserved by cx fmt |
| Tooling | Kubernetes, GitLab CI, Ansible | Growing — LSP + tree-sitter + VS Code; analytical pipelines |
| Round-trip | Lossy in many YAML 1.1 → 1.2 cases | Lossless by spec (see roundtrip) |
When to pick YAML. Your config consumers are Kubernetes manifests, Ansible playbooks, GitHub Actions workflows, or any tool whose schema is in YAML. The ecosystem advantage is decisive.
When to pick CX. Anywhere you would have wanted YAML minus the indentation footguns, the Norway problem, and the lossy round-trip. The triple-quoted multiline string is as readable as YAML literal blocks without the indentation rules.
CX vs TOML
TOML's pitch is "obviously correct config for configuration files." It excels at flat key-value configs (Cargo.toml, pyproject.toml) and struggles with deep nesting.
| dimension | toml | cx |
|---|---|---|
| Nesting | Dotted keys / [[arrays of tables]] — awkward past 2-3 levels | Native — every element nests |
| Schema | No standard schema language | .cxs (see schema) |
| Identity / hash | No canonical form | SHA-256 of canonical bytes |
| Tooling | Cargo, pip, Poetry — narrow but deep | LSP + tree-sitter; broader workloads than configs alone |
| Round-trip | Lossy — comment placement and key order | Lossless |
| Mixed content | Not supported | Native (prose form, see prose) |
| Date / datetime | Native datetime | Native ::date / ::datetime (plus duration and instant kinds) |
When to pick TOML. Your config is flat — language toolchain manifests, Cargo dependencies, app configuration with two levels of nesting. TOML reads beautifully at that scale.
When to pick CX. Your config is genuinely hierarchical, you need cross-config identity, or the same documents will also be processed as data (e.g. shipped as configuration and hashed for a release manifest). CX scales up where TOML hits its ceiling.
CX vs XML
XML is CX's closest structural cousin — both are tree-shaped, both support attributes and mixed content, both have namespaces. The differences are syntactic density and the surrounding ecosystem (XSLT, XPath, XSD).
| dimension | xml | cx |
|---|---|---|
| Syntax verbosity | Open + close tag pair | Single bracket pair |
| Attribute syntax | name="value" inside open tag | name=value inside bracket (see attributes) |
| Mixed content | Native | Native (see prose) |
| Namespaces | xmlns:prefix=URI | xmlns:prefix=URI (XML-aligned, see namespaces) |
| Selector | XPath 3.1 | CXPath — XPath 3.1 aligned (see cxpath) |
| Schema | XSD (large, complex) or RELAX NG | .cxs (CX syntax, small) |
| Identity / hash | C14N (XML Canonicalization) | cx canonical + SHA-256 |
| Templating | XSLT (separate language) | [?for] / [?modify] — same language as data |
| Tooling | Vast in enterprise / publishing; less in modern web | Growing; same ecosystem reach is the long-term goal |
| Learning curve | Familiar to a generation of developers | Lower if XML is new; comparable if XML is known |
When to pick XML. You are working in publishing / document workflows (DITA, DocBook, SVG), or your consumers are XML-native (SOAP, XHTML, RDF/XML). The tooling depth is unmatched in those niches.
When to pick CX. You want the XML data model (attributes, mixed content, namespaces, XPath) without the syntactic verbosity, without XSD, and with code-as- data unification. CX is what XML would look like if designed today.
CX vs Protocol Buffers
Protobuf is the schema-first binary wire format for service-to-service traffic. It is the right tool when you control both ends, the schema rarely changes, and every byte on the wire matters.
| dimension | protobuf | cx |
|---|---|---|
| Wire format | Binary — varint + length-prefix | Text (CX), or data-bin / CXCol (binary) |
| Schema requirement | Required — wire is unparseable without .proto | Optional — value is self-describing |
| Wire size | Typically smallest of any format here | Comparable text size; data-bin closer to protobuf |
| Throughput | Very high — compiled (de)serializers | High but text-shaped; data-bin closes the gap |
| Schema evolution | Tag-numbered fields — strong forward/backward compat | Identity is schema-independent (content-addressed values never re-key); migration is always additive — new representations join, history stays replayable |
| Tooling | protoc + every major language | Tier-1 V/Python/Go/Rust; growing |
| Human readability | None — binary opaque | Text-first; binary is an optimization |
| Identity / hash | Wire bytes — stable iff schema is fixed | Canonical bytes — stable regardless of schema |
When to pick Protobuf. You have a controlled RPC system, the schema is well-understood, and the bandwidth / CPU savings are real (mobile, embedded, hot-path services). Honest concession: Protobuf wins on wire throughput in the steady state.
When to pick CX. Your data is also config, also logs, also human-edited at the boundary, also stored on disk for later inspection. Protobuf loses to text formats outside the wire-only path.
CX vs MessagePack
MessagePack is "binary JSON" — schemaless, more compact than text JSON, slightly slower than protobuf because it carries kind tags.
| dimension | msgpack | cx |
|---|---|---|
| Wire format | Binary, schemaless | Text (CX) or binary (data-bin) |
| Schema | None | .cxs (optional) |
| Wire size | Smaller than JSON; comparable to data-bin | data-bin similar; CXCol smaller for tabular |
| Type richness | Scalars + map + array; ext-type extension | Eleven atomic kinds, elements with attributes, mixed content, namespaces |
| Identity / hash | Wire bytes — stable iff serializer is deterministic | Canonical bytes — stable by construction |
| Tooling | Wide language coverage | Tier-1 V/Python/Go/Rust |
When to pick MessagePack. You want JSON semantics on the wire without JSON wire size, and your data is flat / tabular / homogeneous. MessagePack is excellent at being "JSON minus the text.".
When to pick CX. Your data has shape — attributes, mixed content, namespaces, hierarchical structure that MessagePack would force into nested maps. data-bin covers the wire-size argument without giving up the shape.
CX vs Apache Pkl
Pkl is Apple's configuration language — declarative, strongly typed, programmable. The closest "config that is also code" peer to CX, with a different design centre.
| dimension | pkl | cx |
|---|---|---|
| Surface | Custom syntax — class-oriented | Bracket form — homoiconic |
| Typing | Strong, static, gradual | Schema-validated (.cxs); type tags on scalars |
| Computation | Lambdas, comprehensions, methods | Directives — [?for], [?match], [?modify], [?let] |
| Output formats | JSON / YAML / Plist / Properties + custom renderers | CX / JSON / YAML / TOML / XML / Markdown / CSV / JSONL |
| Identity / hash | Not built-in | SHA-256 of canonical bytes |
| Module system | Imports, packages, semantic versioning | [?lib] / [?def] / [?const] with cx.lock SRI integrity |
| Tooling | LSP, IntelliJ plugin, CLI — solid | LSP + tree-sitter + VS Code — comparable |
| Ecosystem | Apple-backed; growing community | Open-source; growing community |
When to pick Pkl. You want a strongly typed config language with class-oriented composition and your consumers are JSON / YAML output. Pkl's type system is richer than the CX schema language.
When to pick CX. The same documents are also data to be hashed, queried, transformed, or shipped as payloads. Pkl is config-first; CX is data-and-code by design.
CX vs Dhall
Dhall is the typed-config purist's pick — total, non-Turing-complete, with a sound type system and normalised import semantics.
| dimension | dhall | cx |
|---|---|---|
| Type system | Total, sound, dependent-ish (System Fω) | Schema validation; type tags on scalars |
| Computation | λ-calculus — total, no recursion without bound | Directives — [?for] / [?match] / [?modify]; bounded by resilience directives |
| Surface | Haskell-flavoured ML syntax | Bracket form |
| Output formats | JSON / YAML / Text | CX / JSON / YAML / TOML / XML / Markdown / CSV / JSONL |
| Imports | Content-addressed by hash — same idea as CX | [?lib] modules pinned by cx.lock SRI hashes |
| Identity / hash | Standard form hash for imports | Canonical bytes + SHA-256 for any value |
| Learning curve | Steep — ML / Haskell familiarity helps | Moderate — bracket form is unfamiliar but small |
| Tooling | LSP, type-aware; narrow | LSP + tree-sitter + VS Code |
When to pick Dhall. You want totality, soundness, and a small surface area as design goals; you are willing to pay for the steeper learning curve. Dhall is the right tool for safety-critical config.
When to pick CX. You want the content-addressed identity, the schema-validated config, and the programmable updates of Dhall — plus the ability to carry rich data (mixed content, columnar tables, dates, bytes) in the same syntax. Dhall is config; CX is config + data + code.
CX vs Parquet
Parquet is the columnar storage standard for analytical workloads — terabyte-scale scans, predicate pushdown, compression. CX is not Parquet's competitor; CXCol is, and Parquet is CX's export target.
| dimension | parquet | cx-cxcol |
|---|---|---|
| Layout | Column-oriented, on-disk | Column-oriented (CXCol), in-memory; on-disk possible |
| Compression | Snappy / Gzip / Zstd / LZ4 page-level | Dictionary encoding on; compression optional |
| Schema | Embedded — required for read | .cxs schema or embedded type tags |
| Ecosystem | Spark, DuckDB, Polars, pandas, Athena, BigQuery | Native CX pipelines; Arrow bridge for everything else |
| Bridge | — | CX ↔ Parquet bridge via Arrow (see parquet-bridge) |
| Identity / hash | File hash — sensitive to write order | Canonical bytes of the logical value |
| Streaming | Row-group oriented | Native streaming evaluator (see streaming) |
| Write speed | Pays for compression + conversion on write | No compression overhead by default (no maintained benchmark numbers to quote) |
When to pick Parquet. Your data is large, tabular, queried by external tools (Spark / Athena / BigQuery / DuckDB), and stored on object storage. Parquet is the lingua franca.
When to pick CXCol. Your data is tabular but the producers and consumers are CX-native (pipelines, in- memory analytical workloads, services that ship columnar payloads). When you need to interchange with the broader ecosystem, export to Parquet.
CX vs Arrow
Arrow is the in-memory columnar standard. Like MessagePack, it is not a CX competitor — it is a layer CX integrates with.
| dimension | arrow | cx-cxcol |
|---|---|---|
| Layout | Column-oriented, in-memory | Column-oriented, in-memory (compatible) |
| Wire format | Flight (gRPC streaming) / IPC | data-bin chunked tables |
| Interop | pandas, Polars, DuckDB, Datafusion — zero-copy | CX ↔ Arrow bridge (zero-copy, see arrow-bridge) |
| Schema | Embedded | .cxs or embedded |
| Identity / hash | None standard | Canonical bytes + SHA-256 |
| Use case | Cross-process / cross-language analytics | Same plus document semantics |
When to pick Arrow. You are doing pure analytics — no document semantics, no identity requirements, no schema as a value. Arrow is the right cross-engine in-memory layout.
When to pick CXCol over Arrow. You want Arrow's column-orientation plus CX's document semantics (mixed content rows, hierarchical headers, namespaced columns, hash-stable identity). Use the bridge — they share memory zero-copy.
Summary table
At a glance, the headline trade-offs for each alternative. Pick CX is the column that matters most for an evaluator question.
| format | strength | weakness | pick-cx-when |
|---|---|---|---|
| JSON | Universal ecosystem | No schema, no comments, no identity | You need any of: schema / comments / identity / mixed content / dates / bytes |
| YAML | Human-readable config | Indentation footguns, lossy round-trip, Norway problem | You want YAML readability without the footguns |
| TOML | Flat config readability | Awkward at depth, no schema | Your config is hierarchical or grows beyond config |
| XML | Tree-shaped, mixed content, XPath ecosystem | Verbose, XSD complexity, no homoiconic code-as-data | You want the XML data model in a denser syntax with code-as-data unified |
| Protobuf | Wire size and throughput | Schema-required, opaque without .proto, no document semantics | Your data is also config / docs / hashed payloads, not just RPC wire |
| MessagePack | Compact binary JSON | Flat / no document semantics | Your data has shape (attributes / mixed content / namespaces) |
| Pkl | Strongly typed config language | Config-only; no data identity, narrower output formats | The same documents are data and code, not just config |
| Dhall | Total, sound, content-addressed imports | Steep learning curve, config-only | You want Dhall semantics plus rich data shape and broad outputs |
| Parquet | Analytical scan, ecosystem | File-format only, no document semantics | Producer and consumer are CX-native; export to Parquet for interchange |
| Arrow | Zero-copy in-memory columnar | No document semantics, no identity | You want Arrow plus CX semantics (use the bridge — they coexist) |
CX's case, in one paragraph. CX wins on unification: the document, the transform, the value model and the identity are one syntax — cx-vs-python shows each, replayed: the update that leaves its original unchanged, JSON arriving as a CX map, the hash over canonical bytes. Where one layer IS the whole problem — a browser-only JSON consumer, a Kubernetes manifest, a terabyte Parquet scan — reach for the specialist, and read its format into CX where the rest of the system lives. Everywhere else, CX is the default.
If you have a workload that does not match any row of this table or §13.1's decision tree, file an issue at https://github.com/cx-home/cx — the comparison is maintained as a living document; new alternatives and new use-case scenarios are added as the ecosystem evolves.
At-a-glance feature matrix
One-screen feature matrix across CX, JSON, YAML, TOML, and XML. The per-format children (§13.2-§13.11) carry the depth; this matrix is the spreadsheet view.
| Feature | CX | JSON | YAML | TOML | XML |
|---|---|---|---|---|---|
| Syntax weight | brackets, no closing tags | curly braces + brackets | indent-significant | tables + key=val | open + close tags |
| Strong types | ✅ int / float / bool / null / sized / decimal / bigint / date / datetime / bytes / atom | ❌ number only | partial (auto-detect) | ✅ int / float / bool / datetime | partial (xs:type) |
| Comments | ✅ block and line | ❌ | ✅ # … | ✅ # … | ✅ |
| Mixed content | ✅ first-class | ❌ | ❌ | ❌ | ✅ first-class |
| Multiple top-level docs | ✅ no wrapper required | ❌ requires array | ✅ via --- separator | ❌ single document | partial |
| Attribute / element distinction | ✅ explicit | ❌ flat keys | ❌ flat keys | ❌ flat keys | ✅ explicit |
| Type fidelity through round-trip | ✅ via --lossless | ❌ int↔float coerced | partial | ✅ preserved | partial |
| Tabular data efficiency | ✅ :table block, columnar binary |
❌ verbose array-of-objects | ❌ verbose | partial (array of tables) | ❌ verbose |
| Streaming parser | ✅ pull-based handle API | partial | ❌ usually whole-file | ❌ | ✅ SAX |
| Built-in query language | ✅ CXPath value kind, 12 axes (XPath 3.1's 13 minus the deprecated namespace axis) | ❌ third-party (jq) | ❌ third-party (yq) | ❌ | ✅ XPath 1.0 / XQuery |
| Pure-functional transform | ✅ [?modify] focus + 11 actions, structural sharing |
❌ | ❌ | ❌ | partial (XSLT) |
| Pattern-match dispatch | ✅ [?match] heterogeneous arms |
❌ | ❌ | ❌ | ❌ |
| Module system | ✅ [?def] / [?lib] / cx.lock SRI |
❌ | ❌ | ❌ | partial (XInclude) |
Conversion-loss matrix
Lossless conversion means the data round-trips without semantic loss; presentation details (comments, whitespace, attribute order) may be normalized. The table below is the honest accounting of what survives, what gets normalized, and what is genuinely lossy.
| CX → format → CX | Lossless? | What is preserved | What is normalised or lost |
|---|---|---|---|
| CX → JSON → CX | data ✅ / presentation 📋 | Element shape, attributes, types via --lossless, values, nesting | Comments dropped; element vs attribute distinction collapses to keys |
| CX → YAML → CX | data ✅ / presentation 📋 | Element shape, types via !!tags, multiline strings |
Comments dropped by most parsers; flow-vs-block style normalised |
| CX → TOML → CX | data ✅ / presentation 📋 | Element shape via table headers, strong types | Comments dropped; section ordering normalised; deep nesting flattened |
| CX → XML → CX | data ✅ / presentation 📋 | Element shape, attributes, mixed content, namespaces, PIs | Whitespace normalised per xml:space; entity references resolved |
| CX → CXCol → CX | data ✅ | Values with full type fidelity — CXCol is the columnar binary export | Presentation (comments, whitespace); the strict-canonical text form is cx canonical, not CXCol |
| CX → CSV → CX | data partial | :table block columns and rows |
Non-table data not encodable; cell subtyping collapses |
Presentation-layer differences — comments (only XML and CX preserve them; JSON has none; YAML and TOML lose them through most parsers), whitespace (normalised to the target conventions), attribute order (emission may pick a canonical order), anchor names (canonical form renumbers them). These all show up as identical hashes via cx hash because the hash operates on strict-canonical bytes, where presentation is stripped.
Where CX is fast — measured
CX's performance culture is gates, not brags: every number
below names its runner, was measured on the reference dev
machine (Apple Silicon, -prod build, 2026-08-20), and is
re-measured each release. Claims without a measurement do not
ship in these docs.
HTTP serving — the headline
A CX HTTP service runs deep in compiled-server territory:
~122,000 requests/second measured against the static
guide server (wrk -t8 -c128, containerized, the B18
allocation-work findings), with ~135k req/s on the
uncontainerized reference run. That is two orders of
magnitude past a typical scripting-language app server and
in the league of Go/Rust HTTP frameworks — from [?http-service]
declarations, not hand-tuned handler code. (Multi-core
scaling is the honestly-stated frontier: see the README
status note.)
Server-rendered surfaces
The reference storefront (1,004 products, live derived state, zero view code) renders a full department page in ~7 ms server-side and full category pages over the whole catalog in ~0.13 s. Declarative surfaces are not the slow path: the tree-walk evaluator is compiled native, and per-node dispatch cost is independent of program size.
Streaming evaluation
The lazy-record streaming path evaluates [?for] over
streamed documents at ~200 MB/s (the release gate's
five-trial protocol) with bounded memory regardless of
input size — it never materializes the whole tree, so a
multi-gigabyte input streams at full speed in constant
residency. [?map] measures 129 MB/s against the same
200 MB/s floor the gate holds every shape to. Streaming is
the high-throughput path for text-form bulk data.
XSP — the binary wire, small-and-frequent to bulk
XSP is CX's binary frame and session layer, and it is built for both ends of the shape spectrum. Small and frequent: a frame costs 17 bytes of header plus the principal, a u64 stream-id multiplexes every exchange over one connection (requests, replies, events, and cancels interleave without head-of-line blocking between streams), and credit-based flow control keeps a slow consumer from stalling anyone else. Measured on the reference bench: durable publish commits at p50 2.5 ms flat with respect to journal size, embedded ingest folds ~1,100 events/s, and a checkpointed boot replays 500 entries in 28 ms. Large and bulk: payloads ride CX data-bin — the binary form, no text re-parse on either end — and columnar CXCol pages with zstd compression under the store profile, where result streams and live ∂-updates flow as credit-governed event frames. One frame format serves both shapes across TCP, TLS, Unix sockets, and WebSocket unchanged.
Parsing — honest numbers
The buffered parser runs 35–42 MB/s (median across the fixture sizes; the lexer alone runs 84–104 MB/s) — and every one of those bytes buys far more than a JSON byte does: type ascriptions, sized numerics, anchors and merges, mixed content, comments, and whole typed tables come out of the same single pass, with identity-bearing canonical bytes one step away. That is very fast for a grammar this rich — one parse replaces the parse-then-validate-then-decode tower other stacks bolt together. Parse cost is linear in input bytes and gated to stay that way (the amplification step runs in every post-merge pipeline), so throughput is a constant, not a cliff. For raw bulk, the binary formats below and the streaming path above are the fast path by design.
One engine, thin bindings
Identity operations (hash, canonical, eval) route through
the one compiled libcx from every binding, so they are
bit-identical AND uniformly fast: the V-native path parses
the 35 KB bench fixture in ~1.0 ms, Rust lands within
1.6× of that through its hand-written FFI layer.
There is no interpreter tax between your host language and
the engine.
When CX wins
Every alternative on this page does one layer. CX wins when the layers are the problem. A real system is data plus a schema plus a query language plus a transform layer plus config plus a wire format plus, increasingly, an agent interface — and the incumbent answer is seven tools with six impedance boundaries between them: JSON with JSON Schema bolted on, jq for queries, a template engine for transforms, YAML for config, protobuf for the wire, and a hand-written tool layer for the agents.
CX is one surface for the whole stack. The same bracket
form is the document, the schema that validates it, the query
that selects from it, the transform that rewrites it, the
config that deploys it, and the program that serves it —
[?match] and [?modify] operate on the same values cx
hash addresses. Identity is machine-checked, not
promised: strict canonical bytes give every value one
SHA-256 address on every platform and every binding, which is
what makes caching, dedup, signing, and content-addressed
storage boring instead of bespoke. Adoption is ring-scoped:
take the data format alone (it cannot execute anything), and
nothing above it can reach down and make it heavier. The
runtime is compiled native — the numbers above, from one
binary with no interpreter tax. And the same definitions that
serve humans serve agents at parity: the tools face and
the ux projection derive from the identical [?def]s, so an
agent interface is something you already have, not something
you build.
Reach for a specialist when one layer IS the whole problem — §13.1 names those cases. Reach for CX when you would otherwise be gluing the specialists together.