CX · Reference guideTooling
PlaygroundDownloadsAboutv0.18.0-pre.1

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:

  • CXER prefix — runtime / parse errors from the V core. The namespace registry is spec/03-approved/process/governance.md §9.6; the language-core sub-blocks live in spec/03-approved/core/code.md §9.4.
  • CX-L prefix — lint findings from cx_lint (see lint); same codes the CLI emits.
  • CXLS prefix — 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 to sh, sql, html, css, json, yaml, toml, xml and markdown — injections.scm in cx-tooling is 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.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/*.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) or libcx-pthreads.js + libcx-pthreads.wasm (~30 MB wasm; needs cross-origin isolation for real threads).
  • cxlib.js — the host-side bridge (~26 KB), from scripts/wasm/cxlib.js.
  • Optionally the cx-diagram.js web component from tooling/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.