Migration
The v0.8.0 reshape (the historical big break)
v0.8.0 is the pre-release API/format-stability boundary and remains the one breaking migration in CX's history: releases since have been additive under that stability boundary, so migrating to the current release means crossing the v0.8.0 reshape and nothing else. There were no external users at the time, so the renames below were applied directly across the codebase rather than shipped as user-facing tooling. This section is the reference for the shape of the changes.
Rename: programs → code. Every internal programs
reference becomes code. The brand
layer ("CX is data and code") was renamed immediately on
d-2026-05-22-01; the internal rename cascades through:
| was | is |
|---|---|
| cx_program_eval | cx_code_eval |
| vcx/programs/ | vcx/code/ |
| spec/programs.md | spec/03-approved/core/code.md |
| conformance/programs.txt | conformance/code.cxd (today’s CX-native suite) |
| _cx_program_diagram (wasm) | _cx_code_diagram (wasm) |
| --- in_cxl (fixture marker) | --- in_code (fixture marker) |
[?find] is retired. [?find] was always sugar for
[?for] over a pattern. The migration is one of two
forms:
| was | is |
|---|---|
| [?find [user $u] :yield $u] | //user or [?for [user $u] [yield $u]] |
| [?find [user @active=true $u] :yield $u] | //user[@active=true] |
| [?find [user [email $e]] :yield $e] | //user/email |
| count([?find [user @active=true $u] :yield $u]) | [$count //user[@active=true]] |
CXPath is now a value kind. A path expression like //user
parses to a cx.PathNode value, evaluates to a sequence, and
round-trips canonically as the terse form. The retired v0.7.0
CXPath subset is replaced by XPath 3.1 alignment — see the
path-expression sections of
spec/03-approved/core/code.md.
Multi-arm [?match]. [?match] now supports N arms
with [case …] / [when …] /
[else …] clause children, scalar literal patterns, _
wildcard, and [where …] guards. The 2-arg form (single arm,
raises CXER0100 on miss) is preserved unchanged.
[# v0.7.x — nested if chain (legacy slot form) #]
[?if (name($n) = 'prose') :then [p $n]
:else [?if (name($n) = 'code') :then [pre $n]
:else ()]]
[# v0.8.0 — multi-arm match (clause-child form) #]
[?match $n
[case [prose $p] [p $p]]
[case [code $c] [pre $c]]
[else ()]]
[?modify] is new. There was no idiomatic update
directive in v0.7.x — code had
to rebuild via [?for]. v0.8.0 ships the eleven-action
vocabulary ([set V] / [delete] / [using F] /
[rename N] / [set-attr N V] / [delete-attr N] /
[append V] / [prepend V] / [insert-before V] /
[insert-after V] / [replace V]). See modify.
Two-layer bindings. See
spec/03-approved/misc/bindings.md. Layer 1 is the
canonical 16-method surface, identical across V/Python/Go/Rust.
Layer 2 is opt-in host idiom packs (cxlib.idioms in Python,
cxlib/idioms in Go, cxlib::idioms in Rust). v0.7.x had a
single flat surface per binding; v0.8.0 codifies the
separation. See bindings.
Binding scope reduced to four. v0.8.0 ships only V
(native), Python, Go, Rust. The
TypeScript, Java, C#, Ruby, Kotlin, and Swift bindings are
moved to lang/_archived/ (preserved, not deleted).
Restoration is opt-in once Layer-1 is stable; community
contributions accepted but not blocked on. The make tests for
archived bindings are no longer in the default make test
target.
The renames above are mechanical — they do not attempt to
convert hand-rolled nested [?if] chains to multi-arm
[?match] (that requires semantic understanding of what each
branch tests). The conversion is straightforward by hand once
you know the multi-arm form (see match).
From XML
XML is the closest projection to CX — same element / attribute /
body shape. Migration is one-shot:
cx --from=xml --to=cx input.xml > input.cx.
- DOCTYPE / DTD — drop. Use a CX schema (
.cxs) instead. See schema. - Namespaces —
xmlns:*declarations become CX attributes on the root element; namespace-qualified element names use the sameprefix:nameform. See namespaces. - CDATA — projects to triple-quoted strings.
- Processing instructions — preserved as
[?pi …]elements; their bodies are opaque text. - XPath queries — port to CXPath. The selection core
is identical (see the path-expression sections of
spec/03-approved/core/code.md); you drop XPath'sfor/let/if/functionconstructs in favor of the CX directives. - XSLT / XQuery transforms — port to CX code. There is no automated converter; the structural mapping is direct because the data model is the same.
From JSON / YAML / TOML
These are projection formats. Migration is again one-shot:
cx --from=json --to=cx input.json > input.cx.
The projection rules are deterministic and reversible (see json, yaml, toml). What you gain on the CX side:
- Comments. JSON has none; YAML and TOML have ad-hoc
comments. CX has structured
[; … ]comments that survivecx fmtand round-trip across projections. - Schemas. JSON Schema / YAML schemas are external artifacts. CX schemas are first-class CX, in the same parser, with the same toolchain.
- Queries. JSONPath /
jq/ YAML query systems are format-specific. CXPath works on any projected document regardless of source format. - Updates. No idiomatic pure-functional update for
JSON / YAML / TOML at the format level.
[?modify]gives you one. - Identity. Canonical JSON, JCS, and
jq -Sexist but are not universally adopted. CX has one canonical form built into every tool.
Retired surfaces and replacements
Surfaces removed or renamed across v0.7.x → v0.8.0, with their replacement.
| retired | replacement | source |
|---|---|---|
| [?find] | [?for] over pattern, or CXPath //name | CXPath value-kind alignment |
| [- comment -] | [; comment ] | lexer consolidation |
| :contains predicate hack | ancestor:: axis | CXPath value-kind alignment |
| :direct modifier hack | following-sibling:: axis | CXPath value-kind alignment |
| XPath general-comparison = | value-comparison only | CXPath value-kind alignment |
| cx_program_eval (C ABI) | cx_code_eval | programs → code rename |
| in_cxl (fixture marker) | in_code | programs → code rename |
| cxl-* playground starter ids | code-* starter ids | Phase 7.3 |
| TypeScript / Java / C# / Ruby / Kotlin / Swift bindings | archived under lang/_archived/ | d-2026-05-22-03 |
| Layer-1-only flat binding surface | Layer 1 + Layer 2 | d-2026-05-22-07 |
Archived bindings are not deleted. Files move to
lang/_archived/<lang>/ and remain in git history. The
restoration path: each archived binding can return on opt-in
once Layer 1 is stable. Community contributions are welcome
on the archived path; they are not blocked on by the core
team.
Doc-gen pipeline consolidated. v0.8.0 ships a single
pre-publish documentation generator at
scripts/gen_guide/ rendering docs-src/canonical/ into
the multi-page guide at docs/guide/ — eat-our-own-dog-food
by running cx on every section. See docgen for the
pipeline and [make guide] for the
entry point.
Extend and contribute
CX is open to contribution at every layer. This subsection is the developer-facing how-to: filing issues, adding fixtures, proposing changes, shipping a binding, debugging a failure.
Filing an issue
Issues live in the project tracker. Every report should
include: minimal CX reproducer (one file), cx --version
output, error code (e.g. CXER0100) + message, expected vs
observed behaviour, and the workaround you used if any.
For parse / canon / hash bugs, a 5-line CX snippet is
usually enough; for evaluator bugs include the directive
shape and the input doc.
Adding a conformance fixture
The conformance suite is conformance/*.cxd files in
per-feature buckets (code.cxd for directives, data_bin.cxd
for binary wire, etc.) — CX-native suites of
[case id=…] blocks with [in-cx] / [in-code] /
[call] inputs and an [out-text] (or [out-err])
expectation. (The older === test: / --- in_cx text
form is legacy.) To add one:
- Identify the right file (
code.cxdfor code-language features;binding_api.cxdfor Layer-1 parity). - Add the fixture block following existing format.
- Run the runner (
devbox run -- make test-vcxfor V conformance;make test-binding-api-parityfor Layer-1 cross-binding). - If the runner errors on your fixture, the implementation doesn't handle it — that's the bug; file it. If the run passes, your fixture is now a regression gate.
Proposing a design change
CX changes that affect the spec / ABI / surface are made by editing the spec — the spec is the single source of truth. The workflow:
- Draft the change as an edit to the relevant spec
file under
spec/, with was-vs-is tables, context, the specific decisions, alternatives considered, and conformance gates. - Review window — submit for owner + community review. Iterate the tables until reviewers can scan the design in 2-3 minutes.
- Ratify — the reviewed edit graduates into
spec/03-approved/. - Implement — landing PRs cite the ratified spec change. Its conformance gates block the next release.
Keep spec edits scannable: lead with was-vs-is tables and keep the prose tight so reviewers can grasp the design quickly.
Shipping a new binding
Contributing a binding for a language not currently in the Tier-1 set (V / Python / Go / Rust):
- Implement the 16-method Layer-1 surface (spec/03-approved/misc/bindings.md §2.1) — case-adjusted for host conventions but otherwise identical.
- FFI extern declarations against the C ABI symbols in
include/cx.h. Match the version's cap bits. - Call
cx_init()once per process; callcx_thread_register()at every FFI chokepoint (or use a per-thread guard pattern). - Add a quickstart in
docs-src/canonical/sections/07-bindings.cxd. - Wire conformance — your binding must pass
conformance/binding_api.cxdbyte-identically (gate 28.6). The runner pattern lives atlang/python/conformance_code.py/lang/go/cxlib/conformance_code_test.go— mirror in your language. - PR to
lang/<your-name>/cxlib/with the binding code plus the conformance harness.
For restoring an archived binding (TypeScript / Java / C# /
Ruby / Kotlin / Swift): see
lang/_archived/README.md for the recipe.
Debugging an implementation
Reproducing and triaging a CX bug:
- Reduce the repro. Get to the smallest input that triggers the failure (one CX file, ideally < 20 lines).
- Capture diagnostic output.
cx code-tree repro.cxshows the parsed program tree with source spans;cx --ast repro.cxdumps the full AST as JSON; andcx code-diagram repro.cx --level=min|compact|fullrenders the program's structure at increasing detail. - Bisect if the bug appeared between versions —
git bisect run sh -c 'cx repro.cx; test $? = 0'. - Check the conformance suite — does a similar fixture exist? If yes, what's different about yours?
- File a fixture alongside the bug report (see §add-fixture) so the fix lands a regression gate.
For V-side debugging: the impl lives at vcx/code/*.v
(post-rename, was vcx/programs/). Build with devbox run
-- make -C vcx build; run unit tests with make test-vcx.