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 --versionoutput (build + commit line).- A minimal reproducer — ideally a
conformance/*.cxdcase — 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 budgets —
loads 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.