CX · Reference guideCode tooling
PlaygroundDownloadsAboutv0.17.0

Code tooling

The working surface of the application author: run a program under an explicit grant set (capabilities), lint it, debug it from the errors' own positions, draw it, and measure it. The purely data-shaped verbs (fmt, hash, diff, validate, …) live with Ring 0 in data-tooling; the full subcommand catalog is in tooling.

Running programs and capability flags

cx program.cx evaluates a program. A separate data input rides --data=INPUT.cx (or --data=- for stdin): the input is loaded via the data reading and bound as $doc / $input before evaluation, and the caller-supplied input wins — the program's own data roots never rebind it. Without --data, the program's first data root binds implicitly; a document-driven program run without any input raises the loud unbound-$doc error rather than silently matching nothing. The four document-driven examples/ programs (code-tour, cxpath-tour, match-multi, modify-crud) run exactly this way (cx examples/code-tour.cx --data=examples/code-tour.input.cx).

Unknown flags on this surface are hard usage errors (exit 2, naming the flag) — including misspelled --allow-* grants. Nothing on the run surface is silently ignored.

Effects are deny-by-default: a program that reads files, writes files, touches the network, reads environment variables, spawns subprocesses, or asks for randomness must be granted that capability explicitly on the command line (denied effect points raise the catchable capability error CXER0271):

      $ cx --data=orders.cx report.cx       # orders.cx binds as $doc
         $ cx --allow-read --allow-write build_site.cx
         $ cx --allow-net serve.cx
         $ cx --allow-common pipeline.cx       # the common working set
    

The capability set: --allow-read, --allow-write, --allow-net, --allow-env, --allow-clock, --allow-random, --allow-subprocess, --allow-eval, --allow-secret-reveal. Two broad grants stand beside them: --allow-common is all of the above EXCEPT --allow-secret-reveal, and --allow-all is everything including it. Prefer --allow-common while exploring — it keeps [?secret] values sealed. See security for the security model.

A cx eval FILE alias exists for the same action; prefer the plain cx FILE spelling — the alias adds nothing.

cx lint

Style + correctness warnings. See lint for the rule catalog. Flags: --format=text|json|summary (default text), --fail-on=info|warn|error|none (default error), --disable=ID1,ID2 to suppress checks, --only=ID to run one.

      $ cx lint config.cx
         $ cx lint --fail-on=warn --format=json config.cx
         $ cx lint --only=CX-L004 pipeline.cx
    

Lint rules

cx lint warns about issues a formatter can't or shouldn't fix automatically. Lint and fmt are distinct: cx fmt applies safe transformations; cx lint warns about issues that need human judgement to fix. The two never overlap — if a check has a safe auto-fix, it belongs to cx fmt.

Rule catalog

Seven lint checks are currently shipped:

id severity name
CX-L001 info comment-style consistency
CX-L003 warn unused anchor
CX-L004 error dangling alias / merge
CX-L005 warn deprecated v3.3→v3.4 pattern
CX-L006 info flatten nested single-binding [?let] staircases
CX-L007 warn aggregation over a simple field accessor
CX-L008 warn declared shape flow across [?pipe] stages

Each check has a normative ID, a default severity, and a documented rationale. Suppression directives and CLI flags accept the ID verbatim. CX-L002 (type-annotation form consistency) was removed; its ID is retired, never reused.

Severity levels

Three severity levels, normative names:

  • info — stylistic drift. Doesn't affect correctness. Doesn't fail CI by default.
  • warn — likely-bug. The code parses, but the result probably isn't what was intended. Fails CI with --fail-on=warn.
  • error — almost-certainly-bug. Document semantics are wrong. Fails CI by default (--fail-on=error).

Configuration via .cxlint.cx

Per-project lint config lives in .cxlint.cx at the repo root (or any ancestor directory of the linted file). The config file is CX-formatted — same parser, same schema, dogfooded all the way down.

        [# .cxlint.cx — repo-level lint config #]
           [lint
             [# don't warn on unused anchors in this repo #]
             [disable check=CX-L003]

             [# v3.3 transition warnings only as info, we'll fix gradually #]
             [severity check=CX-L005 level=info]

             [# treat dangling-alias as warn (not error) in this repo #]
             [severity check=CX-L004 level=warn]]
      

A [disable …] applies repo-wide — there is no per-path scoping. cx lint --config=PATH overrides auto-discovery, and --no-config skips config discovery entirely. Missing config file is not an error — defaults apply.

Per-element suppression

For one-off suppressions inside a document, use the [?cx lint-disable=...] / [?cx lint-enable=...] directives. Scope is the enclosing element and its descendants.

        [?cx lint-disable=CX-L003]
           [defaults &base host=localhost]   [# no L003 warning #]
           [?cx lint-enable=CX-L003]

           [# lint-disable=all suppresses every check #]
           [?cx lint-disable=all]
           [legacy-block ...]
      

Comma-separated lists accepted: lint-disable=CX-L003,CX-L005. lint-disable=all is the catch-all.

LSP-only diagnostic codes (CXLS\*)

The CXLS\\* codes (lsp-diagnostics) are surfaced only through the LSP and have no corresponding cx lint check. They live in the LSP because they need document-wide static analysis (cross-arm reachability, focus-path type-check) and aren't general-enough to be CLI default behaviour. Suppression inside a document uses [?cx lint-disable=CXLS001] (same syntax as the CX-L\\* codes); CLI suppression is moot since cx lint doesn't emit them.

Custom rule plugins (planned)

A plugin model for user-defined checks is planned (previously rejected; the question is reopened in ROADMAP.md "Later"). Currently, extending the check set means contributing a check to vcx/cx/lint.v upstream. Schema-violation checks are a separate path — they layer in automatically as the schema language gains rules.

cx diagram / code-diagram / code-tree

Visualization commands. cx diagram FILE renders a containment diagram (--format=mermaid|svg|png, -o out.file); cx code-diagram FILE renders a locked sequence-diagram view of a program; cx code-tree FILE renders the evaluation tree. The playground's Diagram toggle uses the same renderers via WASM (see playground). mermaid is pure text and needs nothing. The svg and png formats render through graphviz dot, which is a subprocess — so they require the ordinary --allow-subprocess grant, exactly like any other effect in CX. Without it the command refuses (CXER0271) rather than quietly handing back a diagram you didn't ask for. With the grant but no dot installed you get the dot-less envelope: a 1×1 placeholder that still carries the program source, so it still reverse-parses.

      $ cx diagram config.cx --format=mermaid
         $ cx diagram config.cx --format=svg --allow-subprocess -o config.svg
         $ cx code-diagram pipeline.cx
         $ cx code-tree pipeline.cx
    

Diagnostics and debugging

When a CX program isn't doing what you expect, work the ladder: read the error value (every failure is a position-carrying [err …]), lint the file, visualize the program (cx code-tree / cx code-diagram), reach for the LSP if you want to drive from the editor, and capture a fixture (conformance/code.cxd) when you need a reproducible bug report.

Errors carry positions

Every parse or evaluation failure is a structured error with a source position and a stable code — read it first. A raised error that reaches the top level prints as an [err code=… message=…] value; the message names the offending line:column and, for retired syntax, the current replacement form.

        $ cx broken.cx
           error: cx-err:CXER0100: parse: infix attribute comparison
           `[@active=…]` in a CXPath predicate is retired —
           predicates are homoiconic CX code; write the prefix
           operator form `[= $_@active …]` (code.md §5.5.2)
           at line 2:8
      

Visualize the program

cx code-tree FILE renders the evaluation tree of a program — which directives nest where, what each binds. cx code-diagram FILE renders the locked sequence-diagram view (concurrency, channels, service interactions). cx --ast FILE shows the parsed shape when you suspect the parser saw something different from what you meant.

        $ cx code-tree pipeline.cx
           $ cx code-diagram workers.cx
           $ cx --ast pipeline.cx | jq '.elements[0]'
      

LSP-driven debugging

The LSP surfaces diagnostics live in the editor — parse errors, lint findings, and hover documentation over directives and paths. See lsp for the shipped capability set.

Interactive REPL

An interactive REPL is planned. Until it ships, the equivalent is stdin evaluation with a quick shell wrapper — append each expression to the context document and pipe through cx:

        # Poor man's REPL
           while read -r line; do
             printf '%s\\n%s\\n' \"$(cat ctx.cx)\" \"$line\" | cx
           done
      

Reproducing a bug as a conformance fixture

When you find behaviour that differs from the spec or from another binding, the fastest way to land a fix is to drop a minimal repro into the conformance corpus. Fixtures under conformance/*.cxd are the normative contract — every binding runs them in CI.

Fixtures are CX documents — one [case …] element per repro, with the input document, the program, and the expected output as children:

        [case id=my-bug-id level=core
             [tags predicate my-area]
             [in-cx [#
           [doc [item id=1] [item id=2]]
           #]]
             [in-code [#
           [?for [in $i //item[= $_@id 1]] [yield $i]]
           #]]
             [out-text [#
           [item id=1]
           #]]]
      

Drop the case into the corpus file that matches your area (e.g. conformance/code.cxd for evaluator bugs, conformance/core.cxd for data-layer bugs, conformance/lint.cxd for linter bugs), run make test-vcx-suite, watch it fail. Then fix the V core to make it pass.

Filing a bug report

A good bug report has four pieces:

  • cx --version output (build + commit line).
  • A minimal reproducer — ideally a conformance/*.cxd case — that triggers the bad behaviour.
  • Expected output (what you think should happen).
  • Actual output (what does happen).

For LSP / editor bugs, also include the cx lsp --verbose 2>... log around the failing request and the editor's LSP trace setting (most editors expose "LSP: Toggle Trace" or similar).

Profiling and benchmarking

CX ships a standard bench harness for the V core (runners under vcx/tests/runners/) and per-binding bench scripts that exercise the FFI path. Binding-level performance budgets are governed by the performance-SLA policy in spec/03-approved/process/governance.md §6.

In-program profiling

No profiling CLI flag ships yet — but the in-program profiler does: the bundled cx-stdlib/prof pack covers timing reads, named counters, HDR histograms, structured trace events, memory snapshots, and flamegraph emission, callable from any program (timing reads consult the host clock, so they need --allow-clock). For source-position debugging see diag-trace; for structure, cx code-tree.

Bench harness

The bench harness is V-native; runners live under vcx/tests/runners/. Run individually:

        $ make bench-code-streaming   # streaming evaluator
           $ make bench-code-pattern-compile   # pattern compile cost
           $ make bench-code-http        # http client perf
           $ make bench-code-soak        # long-running soak
           $ make bench-code-cancel      # cancellation responsiveness
           $ make bench-code-gates       # the three gate-tracked perf benches

           $ make bench-streaming        # data-streaming bench
           $ make bench-eval             # general eval cost
           $ make bench-json             # bench-report JSON (report generator, not a workload)
      

Each bench prints a multi-line human summary (MB/s, ops/sec, or latency percentiles, depending on the dimension). The machine-readable JSON is produced downstream: make bench-json runs the streaming bench and parses its stdout into a stable JSON report (scripts/run_bench_json.cx), and make bench-compare BASELINE=a.json CURRENT=b.json diffs two such reports for regressions. make bench-code-gates chains the three gate-tracked benches (pattern-compile, streaming, http).

Per-binding bench

The per-binding bench shim is bench_report.cx. It drives every binding through a common harness and prints comparative numbers — per binding, the parse and stream medians on the common fixture, then a comparative table. Filters are positional binding names; the only flag is --verbose/-v, and any other flag is a hard error (exit 1) rather than a silent no-op:

        $ cx --allow-read --allow-write --allow-subprocess \
                --allow-clock --allow-env bench_report.cx python go rust
           benchmarking python       ... parse=…ms  stream=…ms
           benchmarking go           ... parse=…ms  stream=…ms
           benchmarking rust         ... parse=…ms  stream=…ms
      

The Go shim's former crash on 1 MB+ inputs is fixed — the FFI call path now locks its goroutine to an OS thread and registers it with the CX runtime. Python's decoder ceiling is architectural (a pure-Python floor); a C extension is a planned future candidate.

Regression tracking

Two tracking planes. The binding-level budgetsloads and select latencies per input size — are normative in spec/03-approved/process/governance.md §6.1: regressions beyond the threshold against a recorded baseline fail CI (make bench-compare, default threshold 30%, STRICT=1 for 10%). The V-core streaming gate lives in vcx/tests/runners/code_streaming_throughput_bench.v with a 200 MB/s line — honest status: the gate is not yet green ([?for] streaming sits at the ~200 MB/s line; [?map] last measured 129.2 MB/s).

cx diff (1 MiB, 1% change), cx lint (1 MiB, all checks), and cx hash (1 MiB) are tracked as unofficial interactive-latency targets; the governance SLA table does not cover them.