Tooling
The CX toolchain is one binary (`cx`) and a small fleet of editor integrations built on top of it. The binary is the same V codebase that backs `libcx` — every binding, every CI gate, and every editor talks to the same parser, the same canonicalizer, the same evaluator. There is no second implementation to drift against. This section is the working developer reference for that surface: what each subcommand does, how the editor tooling wires up, what the linter checks, what the diff reports, and how you embed CX into existing build systems.
The cx CLI
`cx` is the one-stop command-line interface. It evaluates, formats, canonicalizes, hashes, compares, diffs, lints, validates, converts, and renders CX documents. Every subcommand is a thin shim over the same V core (`vcx/cmd/main.v` dispatches on `argv[1]`), so behaviour is identical to what every host-language binding sees.
The CLI follows a small handful of conventions. **Running a file is the default action**: `cx program.cx` parses and evaluates it (a pure-data document evaluates to itself). Input comes from a file path or stdin; output goes to stdout. Exit code 0 means success, 1 means a user-visible diagnostic (lint finding, diff delta, validation failure, non-equivalent compare), 2 means an internal or I/O error. Subcommands that gate on severity (`cx lint`, `cx validate`) accept `--fail-on=THRESHOLD` to tighten the gate; default is `--fail-on=error`.
The CLI surface is exercised by the conformance corpora under `conformance/*.cxd` (lint, diff, schema-validate, delimited projections, …) run by `make test-vcx-suite` — the fixtures are the normative reference where this guide and the binary disagree.
Subcommand catalog
The full shipped set. `cx --help` lists every subcommand — the catalog and the dispatch are generated from one table in `vcx/cmd/main.v`, so they cannot drift — and every subcommand answers `cx <subcommand> --help` with its own usage:
| command | purpose |
|---|---|
| cx FILE | Parse + evaluate a document / program (the default action) |
| cx fmt FILE | Lossless canonical formatter (preserves comments, anchors; normalizes whitespace/quoting) |
| cx canonical FILE | Strict canonical text (strips presentation; data-equivalent output) |
| cx hash FILE | SHA-256 hex of the strict-canonical bytes |
| cx eq A B | Exit 0 iff strict-canonical(A) == strict-canonical(B) |
| cx diff A B | Semantic diff over strict-canonical forms |
| cx lint FILE | Style + correctness warnings |
| cx validate FILE --schema=S.cxs | Schema validation |
| cx table VERB | Table API surface (info / dump / load) |
| cx demo | Self-contained showcase, no I/O, < 1s |
| cx scaffold KIND | Typed, commented skeleton on stdout (config / data / doc / log / table) |
| cx lock | Generate / verify cx.lock from [?lib] directives |
| cx lsp | Language server on stdio |
| cx diagram FILE | Program/data containment diagram (mermaid / svg / png) |
| cx code-diagram FILE | Sequence diagram of a CX program |
| cx code-tree FILE | Evaluation-tree rendering of a CX program |
| cx store-serve | Serve a cx-store (reference store server) |
| cx store-health | Store health probe |
| cx store-token | Mint store access tokens |
| cx store-rotate-kek | Rotate the store key-encryption key |
| cx eval FILE | Alias of the default run action; prefer cx FILE |
| cx select PATH FILE | CXPath query over a document — matches in canonical CX, one per line |
| cx version | Version / build info (same output as -v / --version) |
There is **no** `cx parse` / `cx convert` / `cx modify` / `cx schema` / `cx help` subcommand — parsing and conversion ride the projection flags below, updates are programs you run with `cx FILE`, and schema checking is `cx validate`. Ad-hoc selection has its own verb: `cx select 'PATH' [FILE]` evaluates one CXPath expression against a document (see below).
Projection and conversion flags
Format projection is a top-level flag surface, not a subcommand. One flag picks the output projection of an input document; `--from` / `--to` drive explicit conversions:
$ cx --ast config.cx # JSON AST
$ cx --json config.cx # JSON projection
$ cx --yaml config.cx # YAML projection
$ cx --xml --compact config.cx # XML, minimised
$ cx --from=md --to=cx README.md # Markdown in, CX out
$ cx --from=cx --to=json --lossless order.cx
$ echo '[hello world]' | cx --json - # stdin
- Projection flags: `--ast` `--cx` `--xml` `--json` `--yaml` `--toml` `--md` `--csv` `--tsv` `--psv`, each optionally with `--compact`.
- `--from=cx|xml|json|yaml|toml|md|csv|tsv|psv --to=cx|xml|json|yaml|toml|md|csv|tsv|psv` — explicit conversion; `--lossless` makes XML carry per-value types (`<cx:T>`) for an exact round-trip.
- `--include-root=DIR` — resolve `[?cx include=…]` against DIR before projecting (include resolution is opt-in; without a root the directive is preserved).
Running programs and capability flags
`cx program.cx` evaluates a program. A separate data input rides `--data=INPUT.cx` (or `--data=-` for stdin): the input is loaded via the data reading and bound as `$doc` / `$input` before evaluation, and the caller-supplied input **wins** — the program's own data roots never rebind it. Without `--data`, the program's first data root binds implicitly; a document-driven program run without any input raises the loud unbound-`$doc` error rather than silently matching nothing. The four `examples/` tours run exactly this way (`cx examples/code-tour.cx --data=examples/code-tour.input.cx`).
Unknown flags on this surface are **hard usage errors** (exit 2, naming the flag) — including misspelled `--allow-*` grants. Nothing on the run surface is silently ignored.
Effects are **deny-by-default**: a program that reads files, writes files, touches the network, reads environment variables, spawns subprocesses, or asks for randomness must be granted that capability explicitly on the command line (denied effect points raise the catchable capability error `CXER0271`):
$ cx report.cx --data=orders.cx # orders.cx binds as $doc
$ cx build_site.cx --allow-read --allow-write
$ cx serve.cx --allow-net
$ cx pipeline.cx --allow-all # every capability
The capability set: `--allow-read`, `--allow-write`, `--allow-net`, `--allow-env`, `--allow-clock`, `--allow-random`, `--allow-subprocess`, `--allow-eval`, `--allow-secret-reveal`, and the blanket `--allow-all`. See security for the security model.
A `cx eval FILE` alias exists for the same action; prefer the plain `cx FILE` spelling — the alias adds nothing.
cx fmt
Pretty-prints in **lossless canonical** form. Comments, anchors, and authorial structure are preserved; whitespace and quoting are normalised. Reads the file argument (or stdin) and writes the formatted document to stdout.
$ cx fmt config.cx # formatted, to stdout
$ cx fmt config.cx > config.fmt.cx && mv config.fmt.cx config.cx
cx canonical / cx hash / cx eq
`cx canonical` emits the **strict canonical text** per [`spec/canonical.md`](../../spec/canonical.md): comments stripped, attributes sorted, anchors expanded, whitespace collapsed, quoting normalised. Two data-equivalent inputs produce identical output — that is the contract `cx hash` (SHA-256 of those bytes) and `cx eq` (exit 0 iff the strict-canonical forms match, 1 if they differ, 2 on error) build on.
$ cx canonical config.cx | shasum -a 256 # equivalent to cx hash
$ cx hash config.cx
$ cx eq prod.cx proposed.cx && echo unchanged || echo changed
cx diff
Semantic diff — walks the strict-canonical forms, so presentation-only edits produce no delta. See diff for the output contract. Flags: `--format=unified|json|summary` (default `unified`), `--no-color`. Exit codes match `diff(1)`: 0 equal, 1 differs, 2 error.
$ cx diff prod.cx proposed.cx
$ cx diff --format=json prod.cx proposed.cx
$ cx diff prod.cx proposed.cx && echo no-op # CI gate
cx lint
Style + correctness warnings. See lint for the rule catalog. Flags: `--format=text|json|summary` (default `text`), `--fail-on=info|warn|error|none` (default `error`), `--disable=ID1,ID2` to suppress checks, `--only=ID` to run one.
$ cx lint config.cx
$ cx lint --fail-on=warn --format=json config.cx
$ cx lint --only=CX-L004 pipeline.cx
cx validate
Validates a document against a `.cxs` schema ([`spec/schema.md`](../../spec/schema.md)). Flags: `--schema=SCHEMA.cxs` (required), `--fail-on=info|warn|error|none` (default `error`), `--mode=open|strict|closed` (overrides the schema-mode directive), `--apply-defaults` (materialise schema-default attribute values). Exit 0 if no diagnostics at/above the threshold, 1 if any, 2 on I/O or schema failure.
$ cx validate users.cx --schema=users.cxs
$ cx validate users.cx --schema=users.cxs --mode=strict --fail-on=warn
cx table
The public Table API surface for CXCol / `[table[…]]` payloads: `cx table info FILE` (column/row counts, types, byte size), `cx table dump FILE --to=cx` (round-trip via the Table API), `cx table load FILE --to=cx` (symmetric inverse). Parquet / Arrow output (`--to=parquet|arrow`) defers to the `libcx_arrow` bridge. See analytics.
$ cx table info data.cxcol
$ cx table dump data.cxcol --to=cx | head
cx diagram / code-diagram / code-tree
Visualization commands. `cx diagram FILE` renders a containment diagram (`--format=mermaid|svg|png`, `-o out.file`); `cx code-diagram FILE` renders a locked sequence-diagram view of a program; `cx code-tree FILE` renders the evaluation tree. The playground's Diagram toggle uses the same renderers via WASM (see playground).
$ cx diagram config.cx --format=mermaid
$ cx code-diagram pipeline.cx
$ cx code-tree pipeline.cx
cx scaffold / cx demo / cx lock
`cx scaffold KIND` drops a typed, commented skeleton on stdout — KIND ∈ `config`, `data`, `doc`, `log`, `table`. `cx demo` runs a self-contained showcase (typed round-trip, `[table[…]]` → CSV, a CX program) with no file I/O and no network. `cx lock` generates or verifies `cx.lock` from a project's `[?lib]` directives — `--check`, `--update NAME`, `--output PATH`.
$ cx scaffold config > app.cx
$ cx demo
$ cx lock --check
cx lsp
Starts the language server on stdio (JSON-RPC 2.0 per LSP 3.17). See lsp for the capability surface. Editors spawn it on demand — see editors.
$ cx lsp # editor connects on stdio
cx select
CXPath query over one document — `cx select 'PATH' [FILE]` (stdin when FILE is `-` or absent). The document binds as `$doc`; PATH is a single CXPath value expression (`$doc/…`, `/…`, or `//…`, predicates included). Matches print one per line in canonical CX, in document order; attribute-axis matches materialize as `[name value]` fields. Exit 0 with at least one match, 1 on an empty match set (grep-style), 2 on error. A pure read — no capability grants accepted or needed. Contract: [`spec/misc/cli.md`](../../spec/03-approved/misc/cli.md) §3.8.
$ cx select '//user[= $_@role admin]' users.cx
[user name=Alice role=admin]
$ cx select '$doc/user@name' users.cx
[name 'Alice']
[name 'Bob']
$ cat users.cx | cx select '//user' && echo matched
Editor integration
CX ships first-class editor support for VS Code, Neovim, Helix, and any editor that speaks LSP. Three layers stack on top of the `cx` binary: TextMate grammar for display contexts, LSP for live editing intelligence, tree-sitter for structural / injection duty. See [`tooling/install/README.md`](../../tooling/install/README.md) for one-command install scripts.
VS Code
The extension lives at `tooling/vscode/`. It bundles: TextMate grammar (`syntaxes/cx.tmLanguage.json`), language configuration (brackets, comments), snippets (`snippets/cx.json`), and an LSP client that spawns `cx lsp` on demand. Install via the published `.vsix` or `code --install-extension tooling/vscode/cx-language.vsix`.
# Build and install locally
cd tooling/vscode
npm install
npm run package # produces cx-language.vsix
code --install-extension cx-language.vsix
# Or from the bundled installer
bash tooling/install/install-vscode.sh
The extension keys off `.cx`, `.cxs`, and `.cxd` file extensions. LSP client config (server path, env vars, tracing) lives in the VS Code settings panel under "CX Language". A sample workspace settings file is at [`tooling/lsp/vscode.example.json`](../../tooling/lsp/vscode.example.json).
Neovim
Neovim integration uses `nvim-treesitter` for structural highlighting + LSP via `nvim-lspconfig` (or the built-in LSP client on Neovim 0.11+). A sample config is at [`tooling/lsp/neovim.example.lua`](../../tooling/lsp/neovim.example.lua); the install script [`tooling/install/install-nvim.sh`](../../tooling/install/install-nvim.sh) drops the config block into `~/.config/nvim/lua/cx.lua` and registers the LSP.
# One-shot install
bash tooling/install/install-nvim.sh
# Manual: copy the example into init.lua
cat tooling/lsp/neovim.example.lua >> ~/.config/nvim/init.lua
The tree-sitter grammar is installed via `:TSInstall cx` once the parser is available on the tree-sitter registry. Until then, the install script builds the parser locally from `tooling/tree-sitter-cx/` and links it into the Neovim parser-search path.
Helix
Helix consumes the tree-sitter grammar directly and connects to `cx lsp` for diagnostics, hover, and completion. Drop the [`tooling/lsp/helix.example.toml`](../../tooling/lsp/helix.example.toml) snippet into your `~/.config/helix/languages.toml`.
# ~/.config/helix/languages.toml
[[language]]
name = "cx"
scope = "source.cx"
file-types = ["cx", "cxs", "cxd", "cxl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-servers = ["cx-lsp"]
[language-server.cx-lsp]
command = "cx"
args = ["lsp"]
IntelliJ / JetBrains via LSP
JetBrains IDEs gain CX support through their generic LSP plugin: install "LSP4IJ" (or the Ultimate-edition built-in LSP support on 2024.3+), and register `cx lsp` as the server for `*.cx`, `*.cxs`, `*.cxd`. TextMate highlighting comes from the bundled `tooling/syntax/cx.tmLanguage.json` via the TextMate plugin. The setup is identical for Rider, GoLand, PyCharm, RustRover, and IDEA.
A dedicated JetBrains plugin (richer than the LSP-only path) is planned; tracked in [`ROADMAP.md`](../../ROADMAP.md).
Sublime / Zed / other LSP clients
Any editor that speaks LSP 3.17 over stdio can drive the CX language server. The minimal configuration is: file pattern `*.cx`, command `cx lsp`, transport stdio. The install scripts under `tooling/install/` cover the common cases; for anything else, the contract is just LSP.
Language server (LSP)
The LSP server provides the same intelligence to every editor that speaks LSP. Implementation: `vcx/cmd/lsp.v` plus `vcx/cmd/lsp_features.v`. Speaks JSON-RPC 2.0 over stdio; no separate binary, no port, no daemon. Run-time entry point is `cx lsp`.
Capabilities
The server advertises the following capabilities in its `initialize` response:
| method | status | notes |
|---|---|---|
| textDocument/didOpen / didChange / didClose | shipped | full document sync |
| textDocument/publishDiagnostics | shipped | parse + lint + schema + CXLS001-004 |
| textDocument/hover | shipped | element, attr, directive, CXPath |
| textDocument/completion | shipped | triggers : / @ $ |
| textDocument/semanticTokens/full | shipped | 10-token legend |
| textDocument/formatting | shipped | lossless fmt |
| textDocument/definition | shipped | cross-include and anchor |
| textDocument/references | shipped | anchor + alias + merge |
| textDocument/codeLens | shipped | diagram preview, eval result |
| textDocument/codeAction | shipped | quick-fix for CXLS001-004 |
| textDocument/signatureHelp | planned | for directive arg slots |
| textDocument/inlayHint | planned | inferred types |
| textDocument/rename | planned | cross-include rename |
Implementation lives in `vcx/cmd/lsp_features.v`. The CXLS001-004 diagnostic emitters per [`tooling/lsp/diagnostics.md`](../../tooling/lsp/diagnostics.md).
Diagnostic codes
Diagnostics flow from three sources, each with its own code prefix:
- **CXER\\*** — runtime / parse errors from the V core. See [`spec/cx_native_error_codes.md`](../../spec/cx_native_error_codes.md) for the full catalog.
- **CX-L\\*** — lint findings from `cx_lint` (see lint); same codes the CLI emits.
- **CXLS\\*** — LSP-only static-analysis diagnostics that don't have a CLI surface. Currently reserved:
| code | severity | trigger |
|---|---|---|
| CXLS001 | warning | unreachable [?match] arm (after [else …] or [case _ …]) |
| CXLS002 | hint | [?match] with no [else …] (silent () fallout) |
| CXLS003 | hint | sibling [case …] arms with similar [when …] predicates (consolidation suggestion) |
| CXLS004 | error | [?modify] [set-attr] / [delete-attr] on attribute-step path (compile-time mirror of CXER0100) |
Codes are stable strings; editor configs may key off them to surface custom UI (e.g. fold a CXLS003 cluster into a single inlay). See [`tooling/lsp/diagnostics.md`](../../tooling/lsp/diagnostics.md) for the emit contracts.
Hover
Hover content depends on where the cursor sits:
- **Element name** — element-doc from the active schema (if any), attribute table, link to the schema source line.
- **Attribute name** — type, default, schema source.
- **Directive name** — one-line synopsis + link to the directive page in this guide.
- **CXPath path expression** — resolved XPath production, live match-count against the current document, axis-name doc.
- **Layer-2 host idiom** — desugared Layer-1 form (Python comprehension, Go filter chain, Rust iterator). Implementation calls `cxlib.idioms.explain()`.
[# Hovering '//section[2]/p[@lang=\"en\"]' shows: #]
[# /section[2]/p[@lang=\"en\"] #]
[# → child::section[position()=2]/child::p[@lang=\"en\"] #]
[# → matches 3 nodes in this document #]
[# → axis `child::` selects direct children (default) #]
Completion
Completion triggers on `:`, `@`, `$`, and `/`. The trigger character determines the candidate set:
- **`:`** — type tags (`:int`, `:string`, `:date`...), directive names (`[?for`, `[?match`...), labeled-slot names inside an active directive.
- **`@`** — attribute names from the schema or the live document.
- **`$`** — variable names from `[?let]` bindings in scope.
- **`/`** — CXPath axes + element names + node-kind tests.
All completion items carry `data.kind` so editors can sort and group them. CXPath items are tagged `data.path_axis=true` per [`tooling/lsp/diagnostics.md`](../../tooling/lsp/diagnostics.md).
Code actions
Code actions are quick-fixes the editor offers for diagnostics. Currently shipped:
- **CXLS001** (unreachable arm) — "remove unreachable arm". Deletes the arm.
- **CXLS002** (no `[else …]`) — "add `[else]` arm". Inserts an empty `[else ()]` clause.
- **CX-L001** (comment style) — "convert `[# #]` to `# line comment`" (and vice versa).
- **CX-L002** (type-alias style) — "expand to long-form" / "shorten to alias".
- **CX-L003** (unused anchor) — "remove anchor".
- **CX-L004** (dangling alias) — "create missing anchor" (with a placeholder body).
CXLS003 consolidation auto-fix is planned (recognise-only currently per [`tooling/lsp/diagnostics.md`](../../tooling/lsp/diagnostics.md) §out-of-scope).
Semantic tokens
The LSP semanticTokens response is the **canonical highlighting source** for editors that consume it. The 10-token legend maps directly to TextMate scopes for cross-editor consistency:
| token | example |
|---|---|
| namespace | module prefixes, schema-qualified names |
| keyword | directive names ([?for], [?if], [?match]...) |
| variable | $ctx, $-bound names from [?let] |
| parameter | function-parameter names and pattern $bindings |
| property | attribute names |
| string | string literals (single, double, triple quoted) |
| number | int, float, hex, sized-type literals |
| comment | [# block #] and # line comments |
| operator | = + - * / sigils, CXPath operators |
| decorator | type annotations (:int, :date, ...) |
Debugging the server
Run with `--verbose` and pipe stderr to a file to trace incoming methods. The server logs each request, its dispatch path, and timing.
$ cx lsp --verbose 2>/tmp/cx-lsp.log
$ tail -f /tmp/cx-lsp.log
[lsp] initialize ← capabilities: hoverProvider=true, ...
[lsp] textDocument/didOpen ← config.cx (1342 bytes)
[lsp] textDocument/hover @ 12:8 → 'host' element-doc
Smoke-test the JSON-RPC handshake without an editor:
python3 - <<'EOF' | cx lsp
import sys, json
def msg(o):
b = json.dumps(o)
sys.stdout.write(f\"Content-Length: {len(b)}\\r\\n\\r\\n{b}\")
msg({\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}})
msg({\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"shutdown\"})
msg({\"jsonrpc\":\"2.0\",\"method\":\"exit\"})
EOF
Tree-sitter grammar
`tooling/tree-sitter-cx/` is the tree-sitter grammar for CX. Tree-sitter is scoped to **structural and embedded-language- injection duty** — element / attribute / scalar tokenisation + opaque-PI rendering of `[? ... ]` directives. Per-directive highlighting comes from the LSP semanticTokens, not from tree-sitter.
Grammar shape
The grammar (`grammar.js`) covers everything in [`spec/grammar.ebnf`](../../spec/grammar.ebnf) at the **structural** level: bracketed elements, attribute key=value pairs, scalar literals (string / int / float / bool / date / datetime / null), sigil-prefixed attributes (`#id`, `&anchor`, `*alias`), collection literals (sequence `(...)`, map `{...}`, array — see [[code#collection-literals]]), and comments (`[# #]` and `# line`). Directives — every `[?Name ...]` form — are tokenised as a single opaque `(directive)` region; the internal labeled slots and bodies are not pre-parsed at the tree-sitter layer.
Highlight + injection queries
Queries live in `tooling/tree-sitter-cx/queries/`:
- **`highlights.scm`** — structural scopes only (element-name, attribute-name, string, number, comment, directive-opaque). Per-directive interior highlighting is delegated to LSP semanticTokens.
- **`injections.scm`** — embedded-language injection for fenced code blocks (`[code lang='rust' [| ... |]]`). Recognised injection languages: `python`, `rust`, `go`, `js`, `ts`, `java`, `c`, `cpp`, `csharp`, `ruby`, `swift`, `kotlin`, `sh`, `sql`, `html`, `css`, `json`, `yaml`, `toml`, `xml`, `markdown`.
- **`indents.scm`** — auto-indent rules. CX is bracket-structured; indent inside `[`, dedent at `]`.
- **`locals.scm`** — variable scoping for `[?let]` bindings.
Regenerating the parser
The committed `src/parser.c` is generated from `grammar.js` by `tree-sitter generate`. The `make tree-sitter` target rebuilds it; CI gates that the committed `parser.c` matches `grammar.js`.
$ cd tooling/tree-sitter-cx
$ npm install
$ npx tree-sitter generate # regenerate src/parser.c
$ npx tree-sitter test # run grammar tests
$ npx tree-sitter parse sample.cx # smoke-test on a file
`make tree-sitter` from the repo root wraps all of the above and is what the CI lane runs.
Lint rules
`cx lint` warns about issues a formatter can't or shouldn't fix automatically. Lint and fmt are distinct: `cx fmt` *applies* safe transformations; `cx lint` *warns* about issues that need human judgement to fix. The two never overlap — if a check has a safe auto-fix, it belongs to `cx fmt`.
Rule catalog
Five lint checks are currently shipped:
| id | severity | name |
|---|---|---|
| CX-L001 | info | comment-style consistency |
| CX-L002 | info | type-annotation form consistency |
| CX-L003 | warn | unused anchor |
| CX-L004 | error | dangling alias / merge |
| CX-L005 | warn | deprecated v3.3→v3.4 pattern |
Each check has a normative ID, a default severity, and a documented rationale. Suppression directives and CLI flags accept the ID verbatim.
Severity levels
Three severity levels, normative names:
- **`info`** — stylistic drift. Doesn't affect correctness. Doesn't fail CI by default.
- **`warn`** — likely-bug. The code parses, but the result probably isn't what was intended. Fails CI with `--fail-on=warn`.
- **`error`** — almost-certainly-bug. Document semantics are wrong. Fails CI by default (`--fail-on=error`).
Configuration via .cxlint.cx
Per-project lint config lives in `.cxlint.cx` at the repo root (or any ancestor directory of the linted file). The config file is CX-formatted — same parser, same schema, dogfooded all the way down.
[# .cxlint.cx — repo-level lint config #]
[lint
[# ignore unused anchors in the legacy templates dir #]
[disable check=CX-L003 path='legacy/**']
[# v3.3 transition warnings only as info, we'll fix gradually #]
[severity check=CX-L005 level=info]
[# treat dangling-alias as warn (not error) in this repo #]
[severity check=CX-L004 level=warn]]
Path globs follow `.gitignore` syntax. `cx lint --config PATH` overrides auto-discovery. Missing config file is not an error — defaults apply.
Per-element suppression
For one-off suppressions inside a document, use the `[?cx lint-disable=...]` / `[?cx lint-enable=...]` directives. Scope is the enclosing element and its descendants.
[?cx lint-disable=CX-L003]
[defaults &base host=localhost] [# no L003 warning #]
[?cx lint-enable=CX-L003]
[# lint-disable=all suppresses every check #]
[?cx lint-disable=all]
[legacy-block ...]
Comma-separated lists accepted: `lint-disable=CX-L003,CX-L005`. `lint-disable=all` is the catch-all.
LSP-only diagnostic codes (CXLS\*)
The CXLS\\* codes (lsp-diagnostics) are surfaced only through the LSP and have no corresponding `cx lint` check. They live in the LSP because they need document-wide static analysis (cross-arm reachability, focus-path type-check) and aren't general-enough to be CLI default behaviour. Suppression inside a document uses `[?cx lint-disable=CXLS001]` (same syntax as the CX-L\\* codes); CLI suppression is moot since `cx lint` doesn't emit them.
Custom rule plugins (planned)
A plugin model for user-defined checks is planned (previously rejected; the question is reopened in [`ROADMAP.md`](../../ROADMAP.md) "Later"). Currently, extending the check set means contributing a check to `vcx/cx/lint.v` upstream. Schema-violation checks are a separate path — they layer in automatically as the schema language gains rules.
Diff semantics
`cx diff` produces a **semantic** diff — tree-structured, canonical-byte-aware, formatting-blind. Two data-equivalent inputs diff to nothing, even if they differ at the text level. This is what makes `cx diff` usable as a CI gate: it reports semantic deltas, not formatting noise.
What cx diff reports (and ignores)
`cx diff` reports differences in the strict canonical form ([`spec/canonical.md §1.2`](../../spec/canonical.md)). Specifically it **reports**:
- Element added / removed (by canonical path).
- Element renamed (different canonical name at the same path).
- Attribute added / removed / value-changed (by canonical name).
- Body content changed (atomic-value or text-content differences).
- Type annotation changed (e.g., `:int` vs `:u32` — the annotation IS part of the data).
- Element order changed *when order is data-significant* (per [`spec/canonical.md §2.1`](../../spec/canonical.md)).
And **ignores**:
- Comments (`# line` and `[# block #]`).
- Attribute order on the same element.
- Anchor / alias / merge details — expanded to the resolved form before diff.
- Whitespace, indentation, line breaks.
- Numeric formatting (`1_000_000` vs `1000000`).
- Quoting style (`'foo'` vs `"foo"`).
- Element-name aliases that resolve to the same canonical name.
Output formats
Three output modes via `--format`:
- **`unified`** (default) — element-rooted hunks, `-` / `+` / ` ` prefixes. Human-readable.
- **`json`** — structured array of change records. Suitable for CI gates and editor integrations.
- **`summary`** — one-line counts. Useful for shell prompts and PR bots.
$ cx diff prod.cx proposed.cx
--- prod.cx
+++ proposed.cx
@@ /config/database @@
host='primary.db'
-port=5432
+port=5433
replica='secondary.db'
[
{
\"kind\": \"attribute-changed\",
\"path\": \"/config/database/@port\",
\"before\": {\"name\": \"port\", \"value\": 5432, \"type\": \"int\"},
\"after\": {\"name\": \"port\", \"value\": 5433, \"type\": \"int\"}
}
]
JSON change-record shape
Each record has a stable shape:
- `kind` — one of `element-added`, `element-removed`, `element-renamed`, `attribute-added`, `attribute-removed`, `attribute-changed`, `body-changed`, `type-changed`, `order-changed`.
- `path` — CXPath expression locating the change. The same expression drops straight into a CX program (`[?for [in $x PATH] …]`).
- `before` / `after` — relevant value for the kind. Absent when the side has no value (e.g. `element-added` has no `before`). The field is **omitted**, not `null` — per [`spec/policies.md §2.6`](../../spec/policies.md)'s four-way null/empty/missing distinction this is *missing*.
Git integration
`cx diff` plugs into `git diff` via the standard custom- diff-driver mechanism. Add this to `.gitattributes`:
*.cx diff=cx
*.cxd diff=cx
*.cxs diff=cx
*.cxcol diff=cx-binary
# One-time per repo
git config diff.cx.command 'cx diff'
git config diff.cx-binary.command 'cx diff --binary'
# Or scaffold it
cx scaffold gitattributes >> .gitattributes
With this in place, `git diff path/to/config.cx` and `git show HEAD path/to/config.cx` use the semantic diff automatically. PR review UIs that respect `.gitattributes` (GitHub, GitLab, Gerrit) pick it up too.
Exit codes
Match `diff(1)`:
- **`0`** — inputs are data-equivalent (no differences).
- **`1`** — inputs differ (at least one semantic delta).
- **`2`** — error (parse failure, file not found, invalid arguments).
# Use directly as a CI gate
cx diff prod.cx proposed.cx && echo no-change || review-changes
Cookbook — common recipes
Worked examples of CLI compositions that show up repeatedly in real use. Pair-and-match — the CX CLI is designed to compose along the standard Unix pipe.
Filter JSON, project to YAML
Read JSON, query / filter / transform via a CX program, project the result to YAML.
$ cx --cx input.json > input.cx # JSON in, CX out
$ cat input.cx filter.cx | cx > result.cx
$ cx --yaml result.cx # project to YAML
The CX form is the pivot — every conversion is a round-trip through the same AST. Appending a program to its data (`cat data.cx program.cx | cx`) evaluates the program against the preceding document.
Validate config before deploy
Strict-profile validation, JSON output for the CI summary.
$ cx validate config.cx --schema=config.cxs --mode=strict --fail-on=warn \\
&& deploy.sh
Semantic diff between two YAML files
Cross-format diff: read YAML on both sides, diff the canonical CX form.
$ cx diff a.yaml b.yaml --format summary
3 attributes changed, 1 element added
Content hash as cache key
Use the canonical-bytes hash as a cache key in a build pipeline. The hash is stable across formatting changes — reformatting `config.cx` will not bust the cache, but changing a value will.
$ KEY=$(cx hash config.cx)
$ test -f cache/$KEY.bin || compile config.cx > cache/$KEY.bin
$ cp cache/$KEY.bin out.bin
Convert CSV to Parquet via CX
Inspect and round-trip a columnar payload through the Table API; Parquet / Arrow output rides the `libcx_arrow` bridge.
$ cx table info data.cxcol
$ cx table dump data.cxcol --to=cx | head
[# --to=parquet|arrow defers to the libcx_arrow bridge (Phase C) #]
Lint a whole tree before commit
Recursive lint pre-commit, with shared config.
$ find . -name '*.cx' -print0 \\
| xargs -0 cx lint --format=summary --fail-on=warn
Render the canonical guide
Build the canonical guide (this document) from sources. See docgen for the pipeline.
$ make docs
# outputs into _docs_staging/
$ make docs-diff
# diff staging vs live publish
$ make docs-publish
# copy staging → docs/
Inspect the AST
Read the JSON AST — useful when debugging how the parser shaped a document.
$ cx --ast doc.cx | jq '.name, (.items | length)'
Round-trip through every projection
Assert that a projection round-trip is data-lossless. `--lossless` XML carries per-value types, so the round-tripped document is strict-canonical-equal to the original — the identity contract from canon-cross-format as a CI one-liner.
$ cx --from=cx --to=xml --lossless doc.cx \\
| cx --from=xml --to=cx - > back.cx
$ cx eq doc.cx back.cx && echo lossless
Diagnostics and debugging
When a CX program isn't doing what you expect, work the ladder: read the error value (every failure is a position-carrying `[err …]`), lint the file, visualize the program (`cx code-tree` / `cx code-diagram`), reach for the LSP if you want to drive from the editor, and capture a fixture (`conformance/code.cxd`) when you need a reproducible bug report.
Errors carry positions
Every parse or evaluation failure is a structured error with a source position and a stable code — read it first. A raised error that reaches the top level prints as an `[err code=… message=…]` value; the message names the offending line:column and, for retired syntax, the current replacement form.
$ cx broken.cx
error: cx-err:CXER0100: parse: infix attribute comparison
`[@active=…]` in a CXPath predicate is retired — write the
prefix operator form `[= $_@active …]` (code.md §5.5.2)
at line 3:20
Visualize the program
`cx code-tree FILE` renders the evaluation tree of a program — which directives nest where, what each binds. `cx code-diagram FILE` renders the locked sequence-diagram view (concurrency, channels, service interactions). `cx --ast FILE` shows the parsed shape when you suspect the parser saw something different from what you meant.
$ cx code-tree pipeline.cx
$ cx code-diagram workers.cx
$ cx --ast pipeline.cx | jq '.items[0]'
LSP-driven debugging
The LSP surfaces diagnostics live in the editor — parse errors, lint findings, and hover documentation over directives and paths. See lsp for the shipped capability set.
Interactive REPL
An interactive REPL is **planned**. Until it ships, the equivalent is stdin evaluation with a quick shell wrapper — append each expression to the context document and pipe through `cx`:
# Poor man's REPL
while read -r line; do
printf '%s\\n%s\\n' \"$(cat ctx.cx)\" \"$line\" | cx
done
Reproducing a bug as a conformance fixture
When you find behaviour that differs from the spec or from another binding, the fastest way to land a fix is to drop a minimal repro into the conformance corpus. Fixtures under `conformance/*.cxd` are the normative contract — every binding runs them in CI.
Fixtures are CX documents — one `[case …]` element per repro, with the input document, the program, and the expected output as children:
[case id=my-bug-id level=core
[tags predicate my-area]
[in-cx [#
[doc [item id=1] [item id=2]]
#]]
[in-code [#
[?for [in $i //item[= $_@id 1]] [yield $i]]
#]]
[out-text [#
[item id=1]
#]]]
Drop the case into the corpus file that matches your area (e.g. `conformance/code.cxd` for evaluator bugs, `conformance/core.cxd` for data-layer bugs, `conformance/lint.cxd` for linter bugs), run `make test-vcx-suite`, watch it fail. Then fix the V core to make it pass.
Filing a bug report
A good bug report has four pieces:
- `cx --version` output (build + commit line).
- A minimal reproducer — ideally a `conformance/*.cxd` case — that triggers the bad behaviour.
- Expected output (what you think should happen).
- Actual output (what does happen).
For LSP / editor bugs, also include the `cx lsp --verbose 2>...` log around the failing request and the editor's LSP trace setting (most editors expose "LSP: Toggle Trace" or similar).
Build-system integration
`cx` is built to slot into the build system you already have. The CLI returns conventional exit codes, reads/writes on stdio, and produces deterministic output — all the properties that let it compose with `make`, `cargo`, `npm`, `bazel`, and CI systems without surprises.
Make
A typical pattern: per-`.cxs` schema validation rule, gated `cx lint` lane, generated outputs as first-class targets.
CX := cx
CX_FILES := $(shell find config -name '*.cx')
CX_STAMPS := $(CX_FILES:.cx=.cx.valid)
%.cx.valid: %.cx config/config.cxs
\t$(CX) validate $< --schema=config/config.cxs --mode=strict
\t@touch $@
.PHONY: lint
lint:
\t$(CX) lint --fail-on=warn $(CX_FILES)
.PHONY: validate
validate: $(CX_STAMPS)
.PHONY: check
check: lint validate
Cargo (Rust build.rs)
Use `build.rs` to validate `.cxs`/`.cx` pairs at compile time; once the planned schema-codegen tool ships the same step generates Rust types directly from the schema.
// build.rs
use std::process::Command;
fn main() {
println!(\"cargo:rerun-if-changed=config/config.cxs\");
println!(\"cargo:rerun-if-changed=config/default.cx\");
let status = Command::new(\"cx\")
.args([\"validate\", \"config/default.cx\",
\"--schema\", \"config/config.cxs\",
\"--mode=strict\"])
.status()
.expect(\"failed to run cx validate — is cx on PATH?\");
if !status.success() {
panic!(\"config validation failed\");
}
// Planned: codegen
// Command::new(\"cx\")
// .args([\"schema\", \"codegen\", \"--target\", \"rust\",
// \"config/config.cxs\",
// \"--output\", \"src/config_gen.rs\"])
// .status()
// .unwrap();
}
npm scripts
Wire `cx validate` and `cx lint` into `package.json` lifecycle scripts.
{
\"scripts\": {
\"lint:cx\": \"cx lint --fail-on=warn config/**/*.cx\",
\"validate:cx\": \"cx validate config/app.cx --schema=config/app.cxs --mode=strict\",
\"pretest\": \"npm run lint:cx && npm run validate:cx\",
\"prebuild\": \"npm run validate:cx\"
}
}
Bazel
A custom rule wrapping `cx validate` for hermetic builds. Sketch (treats validation as a generated stamp file):
# tools/cx.bzl
def _cx_validate_impl(ctx):
stamp = ctx.actions.declare_file(ctx.label.name + \".valid\")
ctx.actions.run_shell(
inputs = [ctx.file.src, ctx.file.schema],
outputs = [stamp],
command = '''
cx validate {src} --schema={schema} --mode=strict \\
&& touch {out}
'''.format(
src = ctx.file.src.path,
schema = ctx.file.schema.path,
out = stamp.path,
),
)
return [DefaultInfo(files = depset([stamp]))]
cx_validate = rule(
implementation = _cx_validate_impl,
attrs = {
\"src\": attr.label(allow_single_file = [\".cx\", \".cxd\"]),
\"schema\": attr.label(allow_single_file = [\".cxs\"]),
},
)
GitHub Actions
Lint + validate as a PR gate. Reusable workflow that installs `cx` from the release artefacts, runs the toolchain, fails the PR on diagnostics.
# .github/workflows/cx.yml
name: CX checks
on: [pull_request]
jobs:
cx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cx
run: |
curl -sSfL https://cx-home.dev/install.sh | sh
echo \"$HOME/.cx/bin\" >> $GITHUB_PATH
- name: cx fmt --check
run: find . -name '*.cx' -print0 | xargs -0 cx fmt --check
- name: cx lint
run: cx lint --fail-on=warn $(find . -name '*.cx')
- name: cx validate
run: |
for f in config/**/*.cx; do
cx validate \"$f\" --schema=config/schema.cxs --mode=strict
done
- name: cx diff (vs base)
run: |
git fetch origin ${{ github.base_ref }}
for f in $(git diff --name-only origin/${{ github.base_ref }} | grep '\\.cx$'); do
cx diff <(git show origin/${{ github.base_ref }}:\"$f\") \"$f\" --format summary
done
`cx scaffold github-workflow` emits this file as a starting point.
GitLab CI
Equivalent for GitLab CI:
# .gitlab-ci.yml
cx:
image: ghcr.io/cx-home/cx:0.8.0
script:
- find . -name '*.cx' -print0 | xargs -0 cx fmt --check
- cx lint --fail-on=warn $(find . -name '*.cx')
- for f in config/**/*.cx; do cx validate \"$f\" --schema=config/schema.cxs --mode=strict; done
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
Pre-commit hooks
Wire `cx fmt --check` and `cx lint` into the [pre-commit](https://pre-commit.com/) framework.
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: cx-fmt
name: cx fmt --check
entry: cx fmt --check
language: system
files: '\\.(cx|cxd|cxs|cxl)$'
- id: cx-lint
name: cx lint
entry: cx lint --fail-on=warn
language: system
files: '\\.(cx|cxd)$'
- id: cx-validate
name: cx validate (strict)
entry: cx validate --schema=config/schema.cxs --mode=strict
language: system
files: '^config/.*\\.cx$'
`cx scaffold pre-commit` emits the same snippet.
Doc-gen pipeline
The documentation site (and this canonical guide) is built from `.cxd`/`.cx` sources under `docs-src/` by a Make-driven pipeline at `scripts/gen_docs/`. The pipeline is itself dogfood — the inputs are CX, the templates are CX, the outputs are HTML rendered by running the generator with `cx`.
Make targets
The top-level Makefile splices `-include scripts/gen_docs/docs.mk` to bring the doc targets into scope:
| target | purpose |
|---|---|
| make docs | build the site into _docs_staging/ |
| make docs-diff | diff staging vs the live docs/ tree |
| make docs-publish | copy staging → docs/ with backup on divergence |
| make docs-clean | wipe _docs_staging/ |
| make docs-check | broken-link + missing-anchor verification |
| make docs-all | docs + docs-check |
Staging vs live
`make docs` writes to `_docs_staging/` (in `.gitignore`). The live tree at `docs/` is touched only by `make docs-publish`, which copies staging → live with a backup on any divergence. This separation means doc builds never dirty the working tree on incidental builds — only an explicit publish step lands changes.
The canonical guide
This document — the canonical CX guide — is built by `make docs-canonical-guide` from sources under `docs-src/canonical/sections/*.cxd`. The pipeline:
- Reads `docs-src/canonical/manifest.cxd` for the table of contents.
- For each section file, runs `cx` against the rendering template, emitting an HTML fragment.
- Concatenates fragments, threads the TOC, expands `[[anchor]]` cross-links.
- Runs the link-verifier — every `[[anchor]]` and every relative URL must resolve.
- Writes to `_docs_staging/canonical-guide.html`.
Broken links fail the build. `make docs-check` runs the verifier without the render — useful for fast iteration.
Example snippets
Code examples in the doc sources can pull from `conformance/*.txt` fixtures so they can never drift from working code. The `{{EXAMPLE:fixture-id field}}` placeholder expands at doc-gen time:
[example lang=cx
'''{{EXAMPLE:cxpath-basic program}}''']
[output
'''{{EXAMPLE:cxpath-basic expected}}''']
Outputs are re-recorded from the live `cx` binary, so the docs are always pinned to current behaviour. See `scripts/gen_docs/helpers/example_expand.cx` for the expander logic.
Playground
The CX playground is a browser-embeddable interactive evaluator. It runs the same `libcx` core as the CLI, compiled to WebAssembly. No server round-trip — everything runs in the browser.
WASM distribution
`libcx.wasm` is the WASM build of the V core, exporting a **subset** of the C ABI. The exported surface is sufficient for: `cx_parse`, `cx_canon`, `cx_hash`, `cx_eval`, `cx_code_eval`, `cx_code_diagram`, `cx_select`, `cx_lint`, `cx_diff`. The bridge layer lives at `tooling/playground/cxlib.js`.
ABI capability bits the wasm advertises (per [`spec/abi.md`](../../spec/abi.md) §3):
- **Bit 29** — `_cx_code_diagram` (Mermaid containment diagram render).
- **Bit 30** — `_cx_code_tree` (interactive tree model for the output pane).
- All earlier cap bits the runtime would otherwise advertise on native (0-28).
The web component
`tooling/playground/cx-diagram.js` is a self-contained web component that renders a CX document as a Mermaid containment diagram. Drop it into any page that loads `libcx.wasm`:
The component reads `src` (URL or inline `<textarea>` content), calls `cxlib.code_diagram()` to produce Mermaid source, and hands the result to a bundled Mermaid renderer. Bidirectional selection: clicking a tree node in the output pane highlights the source span, and vice versa.
Render tiers
Three tiers per the gate-17 design:
- **Tier 1** — browser-only Mermaid render. What the playground uses. No external service. Currently shipped.
- **Tier 2** — HTTP service that shells out to graphviz for richer (DOT-based) renders. Editor / CI integration story. Shipped as `tooling/diagram-svc/`.
- **Tier 3** — wasm-graphviz inline. Planned follow-up. Eliminates the Tier-2 service hop while keeping the richer-render output quality.
JSON and XML are **not** valid program sources for the diagram — `_cx_code_diagram` accepts CX text only. Containment-only relationships, no FK inference. See [`spec/audits/playground_gate17_design_v1.md`](../../spec/audits/playground_gate17_design_v1.md) for the design rationale.
Embedding in a docs site
The minimum a host page needs:
- `libcx.wasm` (~ 2.4 MB compressed) served with the right MIME type.
- `cxlib.js` — the host-side bridge (~ 60 KB).
- Optionally `cx-diagram.js`, `cx-tree.js`, and `cx-editor.js` web components.
A full playground page (eval + diagram + tree) ships as `tooling/playground/demo.html`. The docs site re-uses these same components, so any concept page that wants an inline interactive surface gets it for free with a one-tag drop-in.
Profiling and benchmarking
CX ships a standard bench harness for the V core (`vcx/tests/runners/code_*_bench.v`) and per-binding bench scripts that exercise the FFI path. The bench numbers feed into the regression-tracking dashboard and the [`spec/governance.md §6`](../../spec/governance.md) SLA table.
Per-directive profiling
A pprof-compatible per-directive profiler is **planned**; no profiling flag ships yet. Today the closest tools are the structured error positions, the `cx code-tree` view, and the V-native bench harness below — see diag-trace.
Bench harness
The bench harness is V-native; runners live at `vcx/tests/runners/code_*_bench.v`. Run individually:
$ make bench-code-streaming # streaming evaluator
$ make bench-code-pattern-compile # pattern compile cost
$ make bench-code-http # http client perf
$ make bench-code-soak # long-running soak
$ make bench-code-cancel # cancellation responsiveness
$ make bench-code-gates # all gate-tracked benches
$ make bench-streaming # data-streaming bench
$ make bench-eval # general eval cost
$ make bench-json # JSON-shape workload
Each bench prints a one-line summary (`MB/s`, ops/sec, p50/p95/p99 latency depending on dimension) plus a JSON sidecar that the regression dashboard consumes.
Per-binding bench
The per-binding bench shim is `bench_report.py`. It drives every binding through a common harness and prints comparative numbers.
$ python3 bench_report.py --bindings python,go,rust
python parse 1MB: 92ms eval count(//x): 12ms
go parse 1MB: 38ms eval count(//x): 4ms
rust parse 1MB: 31ms eval count(//x): 3ms
The Go shim has a known SIGSEGV at 1 MB+ inputs as of 2026-05. The fix is tracked. Python's decoder ceiling (~90 ms at 1 MB, pure-Python floor) is architectural — a C extension is a planned future candidate.
Regression tracking
The bench numbers are tracked against [`spec/governance.md §6`](../../spec/governance.md) SLA targets. Current headline numbers:
- **Streaming evaluator** — 340 MB/s on the JSON-shape workload (`bench_streaming_json.v`); 300 MB/s target met.
- **`cx diff` 1 MiB / 1% change** — < 50 ms.
- **`cx lint` 1 MiB / all checks** — < 50 ms.
- **`cx hash` 1 MiB** — < 30 ms (canonical-bytes + SHA-256).
CI gates on these numbers via a percentile-aware regression check: a single run > 1.5× the rolling median is flagged; > 2× fails the build.
Tooling quick references
Compact reference snippets for the CLI subcommand surface, the editor installer scripts, the tree-sitter grammar workflow, and the conformance / fuzz / bench harnesses. The primary children above (§8.1-§8.12) carry the normative depth; this section is the at-a-glance summary.
CLI quick reference
Every subcommand reads from a file or `-` (stdin) and writes to stdout unless `--output FILE` is passed. Format is auto-detected from the extension or set explicitly with `--from FMT` / `--to FMT`.
# Format conversion (one-shot).
cx --cx pizza.cx # canonical CX form
cx --xml pizza.cx # XML
cx --json pizza.cx # semantic JSON
cx --ast pizza.cx # AST as JSON
cx --toml pizza.cx # TOML
# From-To.
cx --from=md --to=cx README.md
cx --from=cx --to=json --compact order.cx
cx --from=cx --to=xml --lossless order.cx
# Subcommands.
cx fmt FILE # lossless canonical formatter
cx canonical FILE # strict canonical text
cx hash FILE # SHA-256 over canonical bytes
cx eq A.cx B.cx # exit 0 if data-equivalent
cx diff A.cx B.cx # semantic diff
cx lint FILE # style + correctness
cx validate FILE --schema=S.cxs
cx table info FILE
cx scaffold KIND # drop a skeleton on stdout
cx select '//user' FILE # CXPath query (exit 1 = no match)
cx version # build info (same as -v)
# Run a program (data + program in one input).
cat data.cx program.cx | cx
cx pipeline.cx --allow-read --allow-write
# Run a program against a separate input document ($doc).
cx program.cx --data=input.cx
# Include resolution.
cx --xml --include-root=./conf main.cx # resolve [?cx include=…]
**Exit codes.** `0` = success (no diagnostics at or above the configured `--fail-on` threshold); `1` = diagnostics found (lint / validate) or comparison returned not-equal; `2` = I/O failure, schema load failure, or unrecoverable parse error.
Editor installer scripts
The `tooling/install/` directory ships one shell script per editor. Each script writes the editor config, registers the `cx lsp` server, and drops the syntax-highlighting plugin. The CX language server is built into the `cx` binary itself — no separate install.
./tooling/install/install.sh # interactive — detects editors on PATH
./tooling/install/install-vscode.sh # VS Code only
./tooling/install/install-nvim.sh # Neovim only
./tooling/install/install-lsp.sh # LSP wiring only, no extension
./tooling/install/uninstall.sh # remove everything
**VS Code** — extension at `tooling/vscode`; tree-sitter syntax + vscode-languageclient LSP harness; supports diagnostics, hover, format-on-save via `cx fmt`. **Neovim** — config under `tooling/neovim`; install script writes LSP client config and tree-sitter grammar. **Other editors** — Helix, Emacs (lsp-mode), Sublime Text (LSP plugin), IntelliJ (LSP4IJ) drive `cx lsp` directly. Point the editor at the `cx` binary; register `.cx` / `.cxd` / `.cxs` as file types.
Tree-sitter grammar workflow
The tree-sitter grammar lives at `tooling/tree-sitter-cx`. It is the source of syntax highlighting, code folding, structural selection, and lightweight static analysis in every editor. Prebuilt artifacts ship inside each editor extension; build from source for grammar work.
cd tooling/tree-sitter-cx
tree-sitter generate
tree-sitter parse example.cx
Highlight, indent, fold, and locals queries ship alongside the grammar. They drive syntax colours, structural selection, and cursor-jump-to-definition behaviour. Queries are versioned with the grammar; updates flow through editor extensions on each release.
Conformance, fuzzing, benchmarks
The conformance suite at `conformance/*.txt` is the cross-binding contract. Each fixture declares an input and an expected output (`out_cx`, `out_xml`, `out_json`, `out_text`, or `out_err`). Every binding implements a runner that walks the fixture set; a release is gated on byte-identical output across every binding.
# Conformance.
make test # all bindings, all suites
make test-vcx # V core conformance
make test-rust # Rust binding
make test-python # Python binding
# Fuzzers — vcx/tests/fuzz/, structured-random input.
make fuzz # 60-second run of every fuzzer
make fuzz-parser # parser only, runs until interrupted
make fuzz-replay # replay the saved crash corpus
# Benchmarks — vcx/tests/runners/streaming_bench.v + eval_features_bench.v.
make bench # full bench harness, target/bench.json
scripts/run_bench_json.py --include-eval
Streaming throughput target: 340 MB/s on the comparable bench corpus (M2 Pro, libcx `-prod`) — about 17% under the comparable JSON benchmark on the same workload.
Cookbook — additional recipes
Recipes carried forward from the pre-Phase-B docs cookbook + showcase. The §8.7 children above (recipes §8.7.1-§8.7.9) cover the most common nine; this section adds 13 more focused recipes.
Extract a single value
echo '//server/@host' | cat config.cx - | cx
Appending a query to its data and running the result through `cx` evaluates the query against the preceding document. The CXPath finds the `host` attribute of any `server` element at any depth.
Filter records by attribute
A one-line program filters by predicate — the CXPath predicate `//user[= $_@role 'admin']` selects only admins:
echo \"[?for [in \$u //user[= \$_@role 'admin']] [yield \$u]]\" \\
| cat users.cx - | cx
Count records
echo '[$count //order]' | cat orders.cx - | cx
Generate a config from a template
[env host='db.example.com' port=5432 env='prod']
[?let [= $env [$first //env]]
[server host=$env@host port=$env@port
workers=[?if [= $env@env 'prod'] [then 32] [else 4]]
cache=[?if [= $env@env 'prod'] [then '/var/cache/app']
[else '/tmp/cache']]]]
cat env.prod.cx server.tpl.cx | cx | tail -1 > server.cx
cx --toml server.cx > server.toml
Write a stream
with cx.stream_writer(open("out.cx", "wb")) as w:
for record in fetch_records():
w.write(record)
Compose a document from files
[server
[?cx include=lib/defaults.cx]
port=8080
log-level=info]
cx --cx --include-root=./conf server.cx > resolved.cx
Render a template with includes
[page
[?cx include=partials/header.cx]
[?for [in $item //order/line] [yield [li $item@sku]]]
[?cx include=partials/footer.cx]]
cx --cx --include-root=./templates page.cx
Read a Parquet file into CX
cx table load orders.parquet --to=cx > orders.cx
Share defaults across records
[defaults &shared timeout=30 retries=3]
[server *shared name=api]
[server *shared name=worker retries=5]
`&` declares an anchor; `*` merges from it. Local fields override; missing ones inherit.
HTML-safe templating
[?cx output-target=html]
[h1 //page/@title]
[?for [in $u //user] [yield [li $u/@name]]]
`[?cx output-target=html]` switches the evaluator into context-aware escaping. Substituted values containing `<`, `>`, `&`, or quotes escape correctly with no manual sprintf.
Emit structured log records
[?lib 'cx-stdlib/log']
[$log:info 'sale' fields={pizza: 'Margherita', price: 12}]
[$log:warn 'low-stock' fields={pizza: 'Diavola', stock: 2}]
The `log` module emits structured records to stderr (default) or a configured sink. Fields are a map; level is one of `trace` / `debug` / `info` / `warn` / `error`.
Logfmt parser in CX code
Take a logfmt-style line of key=value records and split it into structured CX.
ts=2026-05-20T18:42:00Z level=info request_id=abc123 msg=ok
[?lib 'cx-stdlib/strings']
[?let [= $input 'ts=2026-05-20T18:42:00Z level=info msg=ok']
[event
[?for [in $kv [$strings:split $input ' ']] [yield
[?let [= $pair [$strings:split $kv '=']]
[pair name=[$nth $pair 1] value=[$nth $pair 2]]]]]]]
Build system in CX
Targets, depends, run — declared in CX, evaluated by a thirty-line CX code driver.
[targets
[target name=all depends='lib bin']
[target name=lib depends='src/foo.o src/bar.o' run='ar rcs lib.a src/foo.o src/bar.o']
[target name=bin depends='main.o lib' run='cc -o bin main.o lib.a']]
[?lib 'cx-stdlib/strings']
[?lib 'cx-stdlib/process']
[?def build impure ($name)
[?for [in $t //target[= $_@name $name]] [yield
([?for [in $d [$strings:split [$string $t@depends] ' ']]
[yield [$build $d]]],
[?if $t@run
[then [$process:run ('sh', '-c', [$string $t@run])]]
[else ()]])]]]
[$build 'all']
Playground — visualize, offline, highlight
Deep-dive on the in-browser playground UI. Section §8.11 above frames the WASM / web component / tier story; this section is the operator reference for the Tree / Graph / Source-diagram bridge.
How it works
The playground runs entirely in the browser. Pick a starter example from the dropdown, edit it in the Source pane, and the three output tabs underneath show the canonical CX form, the JSON projection, and the XML projection of the parsed tree. No server, no signup. Twelve starter examples ship in the corpus — three data shapes (atom, nested element, whole-shop) and nine program snippets covering the directive surface.
Visualize
The Source pane carries a Diagram toggle in its header; the Output pane carries a Tree | Graph view toggle adjacent to the CX | JSON | XML projection tabs. Tree is the default for every fresh source — Graph is opt-in.
The Source-pane diagram renders the program as a Mermaid graph in a sibling pane. The renderer runs entirely in the browser via libcx.wasm — Mermaid text emitted by `cx_code_diagram`, then rendered to inline SVG by Mermaid.js. Zoom, fit-to-pane, and pan controls layer on top.
The Output-pane Tree view renders the parsed value as an interactive containment-only tree — folder icons for elements with body, file-text icons for prose leaves, tag and at-sign rows for elements and attributes. Built from `cx_code_tree(source)` whose JSON contract carries a `{kind, name?, value?, loc:{start,end}, children?}` record per node. Search, count badges, and standard tree keyboard navigation come along for free.
The Output-pane Graph view auto-detects mode: sources whose top-level program contains at least one `[?directive]` render as a Control-Flow Graph (basic blocks; diamonds for `[?if]`; dispatcher round-rects with per-arm edges for `[?match]`; loop-boxes for `[?for]` with `:bind $x` labels; one update-block per `[?modify]`); pure-data sources render as an Entity-Relationship Diagram with per-instance cardinality inference (`||--o{` for repeating children, `||--||` for singletons). Every `[?def]` body is its own Mermaid sub-graph.
**Bidirectional selection bridge**. Click a tree node and the source-pane editor selects bytes `[loc.start, loc.end]` and scrolls them into view. Move the caret in the source pane and the tree pane binary-searches its loc index for the deepest containing node and scrolls/selects it. Visualizations re-render on Run only — no live debounce — to keep the playground predictable.
Offline use
The browser playground is a static page. Clone the repo, render the guide with `make guide`, and serve `docs/guide/` from any HTTP server (or open via `file://`) — the playground works without an internet connection. The example corpus, the TypeScript bundle, and the CSS all live under `scripts/gen_guide/playground/`.
Syntax highlighting
Every code block on the guide is syntax-highlighted by a small in-house JS module at `scripts/gen_guide/highlight/`. It speaks the languages the docs actually use — `cx`, `shell`, `python`, `go`, `rust`, `javascript`, `typescript`, `java`, `c#`, `ruby`, `kotlin`, `swift`, `v`, `sql`, `ebnf`, `xml`, `html`, `json`, `yaml`, `toml`, `md`. The highlighter is regex-based, around 150 lines, no external library. The Playground panes use the same module on every Run.