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 getting-started half of that surface: the
subcommand catalog, editor integration, the language server, the
playground, recipes, and build-system embedding. The verbs
themselves live with the ring each serves — the data verbs (fmt,
canonical/hash/eq, diff, validate, select, table, projections) in
data-tooling, and the code verbs (running programs under
grants, lint, diagnostics, diagrams, profiling) in
code-tooling.
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. This
page carries the catalog and the Start-here verbs
(scaffold/demo/lock, the language server); each working verb
is documented in its ring — data-tooling for the Ring-0
verbs, code-tooling for the Ring-1 ones.
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).
A program runs one of two ways: interpreted, cx PROG.cx,
where the evaluator reads and runs it; or compiled,
cx build PROG.cx -o OUT, which writes one native executable
(cli-build), with --native compiling its hot pure defs
to machine code.
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 and layout to the canonical profile: indent 2, max width 80) |
| 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 fmt --code FILE | Code mode for a program: keeps the line breaks as written, indents by form, glues closers, keeps blank lines, breaks a line past 80; refuses a result that would not read as the same program |
| cx corpus FILE | Grade a module conformance corpus file (cases carrying an in-code program) with this binary — the grader a repository with no toolchain runs; --times prints the in-process wall time of each case |
| cx table VERB | Table API surface (info / dump / load) |
| cx schema VERB | Schema verb family — infer a .cxs from a corpus; export to JSON Schema |
| cx tools VERB | Agent-tool verb family — project command defs to MCP tool descriptors |
| cx xap init NAME | Scaffold a XAP project |
| cx xap check-surface DIR | Surface derivation check — every surface against the composed grammar + enabled features |
| cx demo | Self-contained showcase, no I/O, < 1s |
| cx scaffold KIND | Typed, commented skeleton on stdout (config / data / doc / log / table) |
| cx build PROG.cx -o OUT | One executable that runs a program as cx [RUN-FLAGS] PROG.cx does — no cx install needed where it runs (same OS and architecture) |
| cx lock | Generate / verify cx.lock from [?lib] directives |
| cx imports FILE | List the imports of a file against its calls (used / unused / missing); --fix adds the missing [?lib] lines, removes the unused and sorts them, touching only those lines |
| cx lsp | Language server on stdio |
| cx flow run FLOW.cx --env ENV.cx | Run a flow document over a journal — the local profile of cx-platform/flow; no engine to start |
| cx flow validate / simulate / status | Validate a flow document, run the runner law over a supplied result table, or read a run record back |
| cx diagram FILE | Program/data containment diagram (mermaid / svg / png; svg and png need --allow-subprocess for graphviz) |
| 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 (platform profile only) |
| cx store-health | Store health probe (platform profile only) |
| cx store-rotate-kek | Rotate the store key-encryption key (platform profile only) |
| cx store-mint-principal | Mint an XSP-AUTH principal offline — seed file + [grant …] stanza (platform profile only) |
| cx fabric-serve | Serve a cx-fabric (platform profile only) |
| 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 help subcommand — parsing and
conversion ride the projection flags below, updates are
programs you run with cx FILE, and schema checking is
cx validate (cx schema is inference and export, not
validation). Ad-hoc selection has its own verb:
cx select 'PATH' [FILE] evaluates one CXPath expression
against a document (see below).
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 build
cx build runs a program compiled; cx PROG.cx runs it
interpreted. cx build [RUN-FLAGS] PROG.cx -o OUT writes OUT, one
executable that runs PROG the way
cx [RUN-FLAGS] PROG.cx ARGS… does. OUT is this cx binary
with the program source and the run flags appended to it,
so the machine it runs on needs no cx, no V and no C
compiler — only the same operating system and architecture
as the cx that built it. The run flags (capability grants,
--data, --from / --to, …) are recorded at build time.
OUT also takes capability grants when it runs, ahead of its
own arguments, as cx does (OUT --allow-read notes.txt;
-- ends them): a recorded grant is a default, not a
ceiling. The arguments after the grants reach the program's
$env:argv, whose first member is OUT's own path. OUT
carries the program, not the files it reads. A program that
does not parse is refused at build time (exit 1).
OUT --version alone prints the program's name and the cx
version and commit that built it; beside any other
argument, --version is the program's.
cx build --native … also compiles the program's hot pure
defs to machine code. It runs PROG once first (the training
run: the same run flags, its output discarded, so a
program's effects happen at build time), compiles the C the
native backend generated for each def and the argument shapes
it saw, and links it into OUT with cx's link kit
(native-kit/ beside cx, written by make cli-kit;
CX_NATIVE_KIT names another). The C compiler is needed
where OUT is built, not where it runs. A call whose argument
shapes the training run did not see runs as it does under
cx.
$ cx build --allow-read wc.cx -o wc
$ ./wc notes.txt
$ ./wc --version
$ cx build --native nbody.cx -o nbody
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
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. No prebuilt .vsix is committed —
build one with npm run package in tooling/vscode/,
then install it with code --install-extension.
# 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.
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;
the install script
tooling/install/install-nvim.sh
installs the signed tree-sitter grammar (with a headless
load-proof) and writes a lazy.nvim dir = spec pointing at
the in-repo plugin root — LSP included via native
vim.lsp.config.
# 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
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.
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 + CXLS static analysis |
| textDocument/hover | shipped | element, attr, directive, CXPath |
| textDocument/completion | shipped | triggers : / @ $ |
| textDocument/semanticTokens/full | shipped | 12-token legend |
| textDocument/formatting | shipped | lossless fmt |
| textDocument/definition | shipped | cross-include and anchor |
| textDocument/references | shipped | anchor + alias + merge |
| textDocument/documentSymbol | shipped | outline of elements and directives |
| textDocument/foldingRange | shipped | bracket-structured folding |
| textDocument/selectionRange | shipped | structural expand-selection |
| textDocument/codeLens | shipped | diagram preview per top-level directive |
| textDocument/signatureHelp | shipped | directive arg slots |
| textDocument/inlayHint | shipped | inferred types |
| textDocument/rename | shipped | anchor / alias rename |
| textDocument/codeAction | advertised, empty | capability advertised; the handler returns no actions yet |
Implementation lives in vcx/cmd/lsp_features.v. The
CXLS001-004 diagnostic
emitters per
tooling/lsp/diagnostics.md.
Diagnostic codes
Diagnostics flow from three sources, each with its own code prefix:
CXERprefix — runtime / parse errors from the V core. The namespace registry isspec/03-approved/process/governance.md§9.6; the language-core sub-blocks live inspec/03-approved/core/code.md§9.4.CX-Lprefix — lint findings fromcx_lint(see lint); same codes the CLI emits.CXLSprefix — LSP-only static-analysis diagnostics that don't have a CLI surface. Currently shipped:
| 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) |
| CXLS005 | — | retired ([par] now owns its width as a bounded worker pool) |
| CXLS006 | hint | unbounded comprehension — a [?for] over a generator with no bounding clause |
| CXLS007 | hint | ambiguous whitespace-separated quoted-string body |
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
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
of a host-idiom call (a comprehension, a filter chain, an
iterator adapter). The binding's
idioms.explain()answers it.
[# 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:
:— atom literals, type annotations (::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.
Code actions
Code actions are quick-fixes the editor offers for
diagnostics. The server advertises the codeAction
capability but the handler currently returns an empty,
well-formed list — no quick-fixes ship yet. Quick-fix
synthesis is wired later, once canonical fix recipes land
for the common parse and lint diagnostics (removing an
unreachable [?match] arm for CXLS001, inserting an
[else …] arm for CXLS002, anchor cleanups for the
CX-L003 / CX-L004 lint findings).
Advertising the capability with an empty result keeps editors from breaking while the recipes land; nothing an editor shows under "quick fix" today comes from the CX server.
Semantic tokens
The LSP semanticTokens response is the canonical highlighting source for editors that consume it. The 12-token legend maps directly to TextMate scopes for cross-editor consistency:
| token | example |
|---|---|
| namespace | module prefixes (cx:, log:, fn:, …) |
| keyword | directive names ([?for], [?if], [?match]...) |
| variable | bound names ([= $x …], [in $x …]) |
| parameter | labeled slot names (:caps, :policy, …) |
| property | attribute names |
| string | quoted string values |
| number | numeric scalars |
| comment | # line comments and [; …] blocks |
| operator | |>, =>, ||, ->, !, to |
| decorator | #id, &anchor, *merge sigils |
| enumMember | atom literals (:paid, :active, …) |
| function | element / call head names |
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/03-approved/formal/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: twenty-one, from the bindings' host languages and the common systems, web and data languages tosh,sql,html,css,json,yaml,toml,xmlandmarkdown—injections.scmincx-toolingis the list.
These two are the only query files that ship. Indent and
locals queries do not exist — auto-indent comes from the
editor's bracket handling, and [?let] scope intelligence
comes from the LSP, not from tree-sitter.
Regenerating the parser
The committed src/parser.c is generated from
grammar.js by tree-sitter generate. The grammar
directory carries its own Makefile;
make -C tooling/tree-sitter-cx install-nvim builds the
parser and links it into the Neovim parser-search
path.
$ 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
There is no repo-root make target and no CI gate pinning
the committed parser.c to grammar.js — after editing
the grammar, regenerate and commit parser.c
yourself.
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
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,
a cx lint step in the pipeline, 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. (Schema-to-types codegen is planned design work with
no CLI surface yet — when it lands, the same step would
generate 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\");
}
// Schema-to-types codegen: planned, no CLI surface yet.
}
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://cxhome.org/install | sh
echo \"$HOME/.cx/bin\" >> $GITHUB_PATH
- name: cx fmt (drift check)
run: |
for f in $(find . -name '*.cx'); do
cx fmt \"$f\" | diff - \"$f\"
done
- 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
Copy the snippet as a starting point — the scaffold kinds
are data shapes (config / data / doc / log /
table); there is no workflow scaffold.
GitLab CI
Equivalent for GitLab CI:
# .gitlab-ci.yml — pin the image tag to the release you target
cx:
image: ghcr.io/cx-home/cx:latest
script:
- for f in $(find . -name '*.cx'); do cx fmt "$f" | diff - "$f"; done
- 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 a cx fmt drift check and cx lint into the
pre-commit framework. There is
no --check flag on cx fmt — diff the formatter output
against the file instead.
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: cx-fmt
name: cx fmt drift check
entry: bash -c 'for f in \"$@\"; do cx fmt \"$f\" | diff - \"$f\" || exit 1; done' --
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$'
Copy the snippet directly — there is no pre-commit
scaffold kind (cx scaffold emits data shapes only).
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.cxdfor the table of contents. - For each section file, runs
cxagainst 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/*.cxd 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
The wasm artifacts are the WASM build of the V core,
exporting a subset of the C ABI: cx_to_cx,
cx_to_cx_compact, cx_to_json / cx_to_xml /
cx_to_yaml / cx_to_toml, cx_to_ast_bin,
cx_code_eval (plus _with_len and _streaming),
cx_code_diagram (plus _with_level),
cx_code_ast_json, cx_code_tree, cx_canonical,
cx_hash, cx_free, cx_features, and cx_version.
The bridge layer lives at scripts/wasm/cxlib.js.
ABI capability bits the wasm advertises (per
spec/03-approved/core/abi.md §3):
- Bit 31 —
_cx_code_diagram(Mermaid diagram render). - Bit 32 —
_cx_code_tree(interactive tree model for the output pane). - The earlier cap bits the runtime would otherwise advertise on native, for the symbols the wasm build exports.
The <cx-diagram> web component
tooling/web/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
the wasm build:
<script type=\"module\" src=\"/cxlib.js\"></script>
<script type=\"module\" src=\"/cx-diagram.js\"></script>
<cx-diagram src=\"./users.cx\" max-depth=\"3\"></cx-diagram>
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 — an HTTP service that shells out to
graphviz for richer (DOT-based) renders. Designed
(the editor / CI integration story) but not
shipped — no service directory exists; the CLI's
graphviz hop (
cx diagram --format=svg|png --allow-subprocess) covers the richer-render need locally. - 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. The
conformance home for the diagram output is
conformance/code_diagram.cxd.
Embedding in a docs site
The minimum a host page needs:
- A wasm build —
libcx-async.js(single-file Asyncify build, ~36 MB; works under file:// and any static host) orlibcx-pthreads.js+libcx-pthreads.wasm(~30 MB wasm; needs cross-origin isolation for real threads). cxlib.js— the host-side bridge (~26 KB), fromscripts/wasm/cxlib.js.- Optionally the
cx-diagram.jsweb component fromtooling/web/.
A demo page for the diagram component ships as
tooling/web/demo.html; the full playground page (eval +
diagram + tree) is generated into the guide from
scripts/gen_guide/playground/. Any concept page that
wants an inline interactive surface reuses the same
components.
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 --allow-read --allow-write pipeline.cx
# Run a program against a separate input document ($doc).
cx --data=input.cx program.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 and injection queries ship alongside the
grammar (queries/highlights.scm and
queries/injections.scm — the only two). Indent, fold,
and locals intelligence comes from the editor and the
LSP, not from tree-sitter queries. Queries are versioned
with the grammar; updates flow through editor extensions
on each release.
Conformance, fuzzing, benchmarks
The conformance suite at conformance/*.cxd is the
cross-binding contract. Each fixture is a [case id=…]
block declaring inputs ([in-cx], [in-code], [call])
and an expected output ([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
# each binding grades itself in its own repository (make check there)
# Fuzzer — structured-random input over the parser.
# scripts/fuzz_cx.cx, run continuously by
# .github/workflows/fuzz.yml; run it locally with:
cx --allow-read --allow-write --allow-random --allow-clock \
scripts/fuzz_cx.cx
# Benchmarks — vcx/tests/runners/streaming_bench.v.
make bench # full bench harness
make bench-json # the same run as machine-readable JSON
Streaming throughput gate: the release gate requires a
≥200 MB/s floor on the reference corpus. The check does not
pass yet — [?for] streaming measures around the
200 MB/s line and [?map] at 129.2 MB/s on the current
tree; closing that gap is active work.
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
A document stream is written one record at a time through the streaming writer of the C ABI — see streaming-data for the event shape and cxcol-streaming-api for the table form — which each binding wraps in its host's idiom: open a sink, write records, close. One record is in flight at a time.
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
There is no [?cx output-target=html] escaping mode —
the key is reserved and refused (the pragma registry is
closed; it once passed through inert while reading like an
XSS defense). Until context-aware escaping ships, escape at
the boundary: [$strings:escape-html] for text,
[$html:sanitize] for untrusted markup, and
[$html:serialize] for trees built as data — escaping by
construction, never string concatenation.
[?lib 'cx-stdlib/strings']
[$strings:escape-html '<script>alert(1)</script>']
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. Its
toolbar carries the three readings of the one syntax:
Document (the text is the value, shown as CX, JSON and
XML), Query (a program over a document, which is bound
as $doc) and Program. Beside them, a picker of the
primer's examples for that reading: every fixture
cx primer cites, filed under the primer section that
cites it, its fixture id and suite shown beside it —
the page and the primer cannot show two programs under
one id. The page opens on the first Program example and
runs it; picking an example runs it too.
Under the output sits the fixture's recorded answer and
whether the engine in the tab reproduced it. An error
is a value: a program the engine refuses prints an
[err …] element, projected to JSON and XML like any
other. Share copies a link that reopens exactly the
text in the editors — an unedited example by its
fixture id. No server, no signup.
Visualize
The View pane, on the right, draws the Source (the document or program you wrote), the Output, or both, as a Tree or a Graph; Tree is the default.
The Graph renders the chosen subject as a
Mermaid diagram. The renderer runs
entirely in the browser via the libcx wasm build — 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 Tree view renders the parsed text 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 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 examples (projected from the
primer's fixtures), the script and the CSS all live under
scripts/gen_guide/playground/.
That covers the diagrams too. Both halves of the page
are local: the wasm engine is built SINGLE_FILE, so
it carries its own .wasm inline, and the Mermaid
renderer is vendored at a pinned version under
scripts/gen_guide/playground/vendor/ rather than
fetched from a CDN. Nothing on the page reaches the
network at load, so the Graph pane draws on a plane
or behind a filtering proxy exactly as it does
online — and the bytes that render a diagram move
only when the pin does, never under a released tag.
The one remaining remote reference is the Google Fonts stylesheet, shared with every other guide page. It degrades cleanly: offline you get the local monospace fallback and a fully working 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, the
bindings' host languages, 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.