Frequently Asked Questions
If you ran into something confusing in your first week of CX, it is probably here. Each answer leads with the recovery command and follows up with the why. Grouped by topic; use your editor's search if you arrived from a search engine.
Setup and install
Why does cx --version fail with "command not found"?
cx is on disk but not on your shell PATH. The installer
(curl -sSL https://cxhome.org/install | sh) puts the
binary under its PREFIX, ~/.local unless you set one, so
it lives at ~/.local/bin/cx: confirm it with
ls -l ~/.local/bin/cx, add the directory for this shell
with export PATH="$HOME/.local/bin:$PATH", and put the same
line in your shell's startup file to keep it. A source build
leaves the binary where the build wrote it — see
install-status. Then check that cx answers: a
one-line program with -e, which prints its value:
$ cx -e '[+ 1 2]'
3
Fixture cli-e-one-liner in conformance/llm/cli.cxd — replayed against this cx when the page was built.
Build fails with "V not found"
The reference build uses devbox to pin the V compiler
version. If you ran make build outside a devbox shell
and got "V not found," either install V manually
(https://vlang.io) or — better — wrap the build in
devbox so the toolchain matches CI:
devbox run -- make build.
Devbox is at https://www.jetify.com/devbox — brew
install jetify-com/devbox/devbox or
curl -fsSL https://get.jetify.com/devbox | bash.
Can I use CX on Windows?
Today: yes, via WSL2 (Ubuntu or Debian inside Windows).
The Linux build runs unchanged. Native Windows — a
PE-format cx.exe and cxlib.dll — is on the
roadmap; the bottleneck is the V toolchain's Windows
support, not CX itself. Track issue
#1724
for the current state.
Parsing and syntax
Why does my [?find] fail with CXER0100?
[?find] was retired in v0.8.0 — it was [; version-literal-ok ]
always sugar for [?for] over a pattern. Rewrite each
[?find pattern [yield body]] to either a direct CXPath
(when the pattern is just an element with optional
predicates) or to [?for] (when the pattern is
structurally more complex). See from-v07x for the
full mapping table.
Why does [user :id 42] parse differently from [user id=42]?
They are two different syntactic forms that both parse.
name=value — the data-element attribute syntax, per
spec/03-approved/formal/grammar.ebnf [55] Attribute —
attaches a named value to the element.
:name on its own is an atom literal — so
[user :id 42] is a user element whose body is two
items: the atom :id and the int 42. No error is
raised; it just doesn't mean "the id attribute is 42".
Inside a [?directive …] it is the same: a directive is
slot-free (spec/03-approved/formal/grammar.ebnf [59], no
:name value slots), so :id there is the atom literal
and 42 a positional value; a directive takes its named
arguments as clause-children [name …] and name=value
attributes (grammar.ebnf [127a]–[127c]). And
if you wanted a type annotation, that is the double-colon
glue: [user::int 42]. The JSON projection tells the two
data forms apart — the attribute becomes a key, the atom
body a list of its two items:
input.cx
[users
[user id=42]
[user :id 42]
]
$ cx --from=cx --to=json input.cx
{
"users": {
"user": [
{
"id": 42
},
[
":id",
42
]
]
}
}
Fixture atom-028-attribute-vs-atom-body-json in conformance/../deps/cx-core-data/conformance/atoms.cxd — replayed against this cx when the page was built.
The two-form distinction is by design — see the body discussion at attributes.
What is the difference between […] and (…) collections?
A sequence ((…)) is the flat iteration shape — every CX
value is a sequence, directives iterate them, and a sequence
never nests inside another. An array ([…]) is a container
Item that preserves nesting: a matrix written as an array
of three rows is ONE item of its element, and the rows stay
rows — not nine numbers:
input.cx
[matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]]]
prog.cx
[?let [= $m [$first //matrix]] ([$count $m/*], $m/*)]
$ cx --data=input.cx prog.cx
(1, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
Fixture program-cxpath-g2-10-array-literal in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The distinction is the container vs atom principle in
spec/03-approved/core/cxdm.md §2.0. See
container-atom for the full rules; sequences are flat,
arrays are containers.
Why does my hash differ on Linux vs macOS?
It should not. cx hash is the SHA-256 of the strict
canonical bytes, and those are platform-independent by
construction: the canonical form drops comments, normalises
quotes and whitespace, and always writes LF line endings. The
same document written two ways is one digest:
input.cx
[; the service, as one team wrote it]
[server host="localhost"
port=8080]
$ cx hash input.cx
sha2-256:d8ff354eba4f49456ef0d364f18ef3d772a4faab862da34d49f3ea40172f2825
Fixture cli-hash-presentation-a in conformance/llm/cli.cxd — replayed against this cx when the page was built.
input.cx
[server host=localhost port=8080]
$ cx hash input.cx
sha2-256:d8ff354eba4f49456ef0d364f18ef3d772a4faab862da34d49f3ea40172f2825
Fixture cli-hash-presentation-b in conformance/llm/cli.cxd — replayed against this cx when the page was built.
What the canonical form never normalises is attribute order —
it is data in CX
(spec/03-approved/core/canonical.md
§2.1), and strings are stored as written, with no Unicode
normalisation — so two attributes swapped are a different
document and a different digest:
input.cx
[server port=8080 host=localhost]
$ cx hash input.cx
sha2-256:2ea0b5acdae2502d7d0c6b85796f7c0e397ccf0a9ad2126b0f72af57a6330aab
Fixture cli-hash-attribute-order in conformance/llm/cli.cxd — replayed against this cx when the page was built.
Run cx hash on both platforms over the same bytes. If the
digests differ, that is a defect and a release blocker.
File a bug at https://github.com/cx-home/cx/issues with
the source document and the divergent hashes from both
platforms. Include cx --version output from each.
Does the JSON projection reorder my attributes?
No — attribute order is data in CX, and nothing sorts it.
The canonical form keeps the authored order (canonical.md
§2.1: attribute order is never a normalisation axis), so
cx fmt tidies the spacing and the quotes and leaves the
order alone, and the JSON projection writes its keys in the
same order:
input.cx
[server port=8080 host="localhost"]
$ cx fmt input.cx
[server port=8080 host=localhost]
Fixture cli-fmt-keeps-attribute-order in conformance/llm/cli.cxd — replayed against this cx when the page was built.
input.cx
[server port=8080 host=localhost]
$ cx --from=cx --to=json input.cx
{
"server": {
"port": 8080,
"host": "localhost"
}
}
Fixture cli-json-keeps-attribute-order in conformance/llm/cli.cxd — replayed against this cx when the page was built.
Because the order is part of the document, two orders are two identities — the hash question above shows the two digests.
Can I have two &anchor declarations with the same name?
You can, and it is not an error: when a name is declared twice
the first definition in document order wins, deterministically
(spec/03-approved/core/canonical.md
§2.8, rule 6). Here the merge [c *x] and the alias [*x]
both resolve to the first &x, and the strict form strips
both declarations:
input.cx
[root [a &x value=1] [b &x value=2] [c *x] [*x]]
$ cx canonical input.cx
[root
[a value=1]
[b value=2]
[c value=1]
[a value=1]
]
Fixture id-037-duplicate-anchor-first-definition-wins in conformance/../deps/cx-core-data/conformance/identity.cxd — replayed against this cx when the page was built.
So keep a name to one declaration, and to reuse its content,
declare it once and merge it where you need it with *name —
a host attribute of the same name replaces the merged one in
its place:
input.cx
[root [defaults &def timeout=30 retries=3] [production *def host=prod retries=5]]
$ cx canonical input.cx
[root
[defaults timeout=30 retries=3]
[production timeout=30 retries=5 host=prod]
]
Fixture id-035-merge-inlines-with-host-override in conformance/../deps/cx-core-data/conformance/identity.cxd — replayed against this cx when the page was built.
Code language
What is the difference between [?for] and the old [?find]?
They are the same operation. [?find] was retired because it was always
pattern-driven [?for] with one less keyword. Anything
you wrote in [?find] form can be expressed as [?for]
over a pattern, or — more directly — as a CXPath
expression. See from-v07x for the migration table. The
pattern form, as it reads today:
input.cx
[users
[user active=true [name Alice]]
[user active=false [name Bob]]]
prog.cx
[?for [user @active=true $u]
[yield [active-card [name $u/name]]]]
$ cx --data=input.cx prog.cx
[active-card [name Alice]]
Fixture program-for-006-conditional-emit in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
How do I dispatch on element type?
Use multi-arm [?match] with [case …] clause-children
that name the element type. Each [case …] is a pattern +
result, the first one that matches wins, and [else …] is
the catch-all. Here a [prose] node takes the first arm:
prog.cx
[?let [= $n [prose "hello"]] [?match $n
[case [prose $p] [p $p]]
[case [code $c] [pre $c]]
[else ()]]]
$ cx prog.cx
[p hello]
Fixture program-match-multi-001-element-dispatch in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Put the dispatch inside a [?for] over //doc/* and every
child takes the arm its element name picks.
How do I update a value?
Use [?modify]. It takes a document, a CXPath
focus, and an action; it returns a new document. There is
no in-place mutation in CX — see the next question.
input.cx
[users [user id=1 [name Alice]] [user id=2 [name Bob]]]
prog.cx
[?modify $doc //user[= $_@id 1]/@name [set "Alicia"]]
$ cx --data=input.cx prog.cx
[users [user id=1 name=Alicia [name Alice]] [user id=2 [name Bob]]]
Fixture program-modify-001-set-attribute in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Eleven actions, one per clause: [set …], [delete],
[using …], [rename …], [set-attr …], [delete-attr …],
[append …], [prepend …], [insert-before …],
[insert-after …] and [replace …].
Why does [?modify] return a new Doc instead of mutating?
Pure-functional semantics — no in-place writes in CX.
Concurrency is safe by construction (no reader ever
observes a half-modified tree), equality is trivially
preservable (the old hash still names the old document),
and structural sharing makes it cheap: unchanged branches
of the tree are reused between the input and the output,
so [?modify] is O(log n) in the document size for a
single-focus update. See pure-updates.
What does _ mean in a pipeline stage?
_ is the partial-application hole: a [?pipe] stage
that carries a single _ receives the threaded value at
that position. A stage with no hole receives the value
appended as its final positional argument — a bare name like
add1 is called with it. The hole form first, then the bare name:
prog.cx
[?pipe 5 [+ _ 1]]
$ cx prog.cx
6
Fixture program-ophole-007-pipe-stage-one-hole in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
prog.cx
[?def add1 ($x) [+ $x 1]]
[?pipe 5 add1 add1]
$ cx prog.cx
7
Fixture program-sap-pipe-01-bare-stages in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Bindings
Which binding should I use?
Pick by deployment shape. The native reference binding for embedded work — no FFI overhead, the smallest binary footprint. The scripting binding for data-engineering pipelines and analytical work already living in a dataframe library. The services binding for high-throughput servers, especially when CX is the wire format. The systems binding for the lowest-overhead FFI into a systems language's crates. Layer 1 (the 16 canonical methods) is identical across all four — bindings names each one beside its repository and its ABI vector — so you can change your mind later.
Are CX values thread-safe?
Yes. CX values are immutable by construction — every
update returns a new value via structural sharing.
Multiple threads can read the same Doc concurrently
without locks. Bindings handle their own GC threading
via cx_thread_register (see next).
When do I call cx_thread_register()?
It depends on the binding. The systems binding: never
— every FFI chokepoint calls ensure_thread before the C
call and unregisters at thread exit. The services
binding: never — each foreign call runs on a locked OS
thread that the internal guard registers. The scripting
binding: it does NOT register threads at all; it assumes
the interpreter's global lock plus a single calling thread,
so a host thread that calls into libcx is unprotected —
keep libcx calls on one thread, or call
cx_thread_register yourself before the first call from
a worker thread. Manually always when you call the C
ABI directly from a host-spawned thread — that thread is
opaque to libcx's Boehm GC until it registers. See
ffi-thread-register and
spec/03-approved/core/abi.md §1.6.
Do I need to close a Doc to release memory?
No — there is nothing to close. In the scripting binding a
Doc is an ordinary refcounted host object: the binding
copies every string libcx returns and frees the C buffer at
the call boundary, so no Doc holds libcx memory between
calls. Dropping the last reference reclaims it like any
other host value. Doc has no close(), no finalizer, and
no scoped-resource protocol.
The handle-backed classes are the ones with an explicit
lifecycle — Stream, TableReader, TableWriter, and
EventWriter carry close() and support the host's
scoped-resource form, so a reader opened over a columnar
file is closed on the way out of the block whatever the
block did; the shape, per binding, is on bindings.
See ffi-doc-lifetime for the full per-binding lifetime contract. An archived binding's collector interaction notes live in that binding's own repository, frozen with it.
Tooling
How do I lint a tree of CX files?
cx lint works file by file; for a tree, hand it every file
find lists: find . -name '*.cx' -print0 | xargs -0 cx lint.
One file and its finding — an anchor nothing references:
input.cx
[base &b host=localhost]
[other host=acme.com]
$ cx lint input.cx
warn CX-L003: anchor 'b' declared but never referenced [/base]
suggestion: remove the anchor declaration, or add a *b reference
Fixture cli-lint-unused-anchor in conformance/llm/cli.cxd — replayed against this cx when the page was built.
cx lint runs the built-in rule set (CX-L001 and
CX-L003 through CX-L008; the codes are registered in
spec/03-approved/core/code.md
§9.4/§9.5). Exit codes are the CLI convention: 0 = no
findings at or above --fail-on, 1 = findings, 2 = I/O
or parse failure. Per-project configuration lives in a
.cxlint.cx file, discovered by walking up from the
linted file. See lint for the rule list.
How do I diff two CX files semantically?
cx diff a.cx b.cx — the diff is always semantic:
it walks the strict-canonical forms of both inputs, so
attribute order, quote choice, and comment-only changes
produce no delta. There is no --semantic flag because
there is no other mode. Output shape is
--format=unified|json|summary (default unified), with
--color[=always|never|auto] / --no-color for the
unified view. Here the second file re-quotes, re-spaces and
comments the first and changes one value; the diff names the
value alone and exits 1, because there is a change:
input.cx
[server host=localhost port=8080]
prog.cx
[; re-quoted and re-spaced; one value changed]
[server host="acme.com" port=8080]
$ cx diff input.cx prog.cx
@@ /server/@host @@
- host=localhost
+ host=acme.com
Fixture cli-diff-semantic in conformance/llm/cli.cxd — replayed against this cx when the page was built.
See diff for the algorithm and the JSON-output mode used by CI integrations.
How do I run the canonical guide locally?
The guide is generated by the top-level Makefile. From a
devbox shell, devbox run -- make guide renders it into
docs/guide/, and devbox run -- make guide-http renders it
and serves it locally, with the headers the playground's
threaded build needs. make docs regenerates the LLM layer
under docs/llm/ and runs every documentation check.
The generator reads docs-src/canonical/manifest.cxd,
assembles the section files, replays every [fixture] a
page cites against the running cx — a case whose output
moved stops the render, naming it — and writes the rendered
pages. See docgen.
Why doesn't VS Code highlight my CX file?
The CX VS Code extension is not auto-installed, and no
prebuilt .vsix is committed — build one first with
npm run package in tooling/vscode/, then from the
command palette run "Extensions: Install from VSIX…"
and point at the produced file. After
install, run "Developer: Reload Window" to pick up the
grammar. The extension contributes the .cx, .cxs,
and .cxd file associations; if a file is mis-detected,
click the language label in the status bar and switch
manually.
If you are on Neovim, the install script is
tooling/install/install-nvim.sh — it wires
tree-sitter and the LSP. See
editors for Helix and other editors.
Performance
How big a file can cx handle?
Parsing is the default action — cx FILE parses and
evaluates — and there is no configured size cap: the
whole-document path is memory-bound, so the practical
limit is the RAM the parsed tree needs. For inputs too
big to materialize, use the streaming surfaces — the
events pull API at the ABI (cx_events_open /
cx_events_next) and streamed [?for] comprehension
output — which bound resident memory to a chunk. See
streaming-data. The release gate holds streaming
throughput to a ≥200 MB/s floor; that gate does not pass
yet ([?for] measures around the line, [?map] below
it), so treat the floor as the target, not a promise.
cx big.cx reads the whole document, which is fine while the
tree fits in memory; the streamed shape is a comprehension
over the document bound with --data:
input.cx
[users
[user active=true [name Alice] [email a@x.com]]
[user active=false [name Bob] [email b@x.com]]
[user active=true [name Carol] [email c@x.com]]]
prog.cx
[?for [user @active=true [email $e]] [yield $e]]
$ cx --data=input.cx prog.cx
'a@x.com'
'c@x.com'
Fixture program-for-002-active-emails in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
Is parsing single-threaded?
The parser is single-threaded per document — the
bracket grammar is inherently sequential. The
streaming evaluator is single-threaded per stream
but multiple streams can run concurrently, and a [par]
clause on [?map] / [?reduce] parallelises across
bounded worker pools. For analytical workloads on
tabular data, CXCol scans are SIMD-accelerated where
the host CPU supports it.
Why is the playground slower than running cx in the terminal?
The playground is a constrained demonstration surface
bundled into the browser. It carries unavoidable
overhead — Asyncify instrumentation (so [?sleep] can
yield through the JS event loop), pthreads runtime (so
[par] runs real OS threads), and the wasm format
itself. Production CX deployments use the native CLI
(a ~10 MB binary), the language bindings, or libcx as a
shared library (~12 MB) directly —
those paths have no Asyncify overhead, no
SharedArrayBuffer requirement, no bundle-size
pressure. The playground exists to make CX explorable
from any browser without an install step; native CX
is the performance target. See wasm-story for the
full framing, including the three playground delivery
modes (GitHub Pages, file://, make guide-http).
I ran [?map [par]] in the playground and it did not speed up — bug?
Not a bug — your playground is most likely running in
single-thread mode. Three delivery modes: GitHub Pages
and file:// both serve the
single-threaded Asyncify build (no cross-origin
isolation possible without custom HTTP headers, and
browsers gate SharedArrayBuffer behind that). [par]
is honoured (output multiset is correct, sequential is
one valid ordering) but doesn't accelerate. For real
parallel speedup in the playground, run
make guide-http — the V veb server sends the
required cross-origin-isolation headers, the
pthreads-enabled wasm loads, and [par] uses real OS
threads. The playground's adaptive footer tells you
which mode you're in.
How fast is Parquet round-trip vs native CXCol?
There is no maintained benchmark number to quote for the Parquet path. Structurally, Parquet writes pay for compression and the Arrow → Parquet conversion; native CXCol skips compression by default and uses a wire format designed for the CX value model, so it is the cheaper CX-internal path. Rule of thumb: use Parquet for interchange with the broader analytics ecosystem, CXCol for CX-internal pipelines. See cxcol-vs-others for the full decision matrix.
Security
Is [?cx include] safe with untrusted input?
Yes by default — because by default nothing is granted.
CX is deny-by-default: with no --allow-* flags the
evaluation is pure-only, so includes (which ride the
read capability) stay unresolved and no directive can
touch the filesystem or network. That makes the bare
invocation the safe one for untrusted input — an include
reaching outside its directory is left exactly as written:
prog.cx
[config [?cx include='../secrets.cx'] [app name=demo]]
$ cx prog.cx
[config [?cx include=../secrets.cx] [app name=demo]]
Fixture cli-include-ungranted-unresolved in conformance/llm/cli.cxd — replayed against this cx when the page was built.
Trusted input that needs its includes grants the read and
names the root they resolve against:
cx --allow-read --include-root=./conf main.cx.
The full capability model lives at
include-capabilities and in
spec/03-approved/core/security.md.
The cx_features() bitmask exposes what the loaded libcx
supports; the grant set gates what the runtime
permits.
How do I limit resource use?
CX has no environment-variable cap family — there is no
parse-size, depth, or node-count knob. What bounds a run:
the capability grant set (deny-by-default --allow-*)
gates every effect an evaluation can perform, the
streaming surfaces bound resident memory to a chunk,
and per-process ceilings come from the operating system
(ulimit -v 1048576 before cx input.cx caps the address
space at 1 GiB; cgroups and container limits work the same)
like any other process. See limits and security. An
effect the grant set does not hold is refused, as a value:
prog.cx
[?lib 'cx-stdlib/io']
[$io:read-file "/var/log/app.log"]
$ cx prog.cx
[err code=cx-err:CXER0271 message='E_CAP_DENIED: read capability required for io-read-file; none granted (grant via --allow-read)' at='2:1']
Fixture io-001-read-file-cap-denied in deps/cx-core-code/conformance/stdlib/io.cxd — replayed against this cx when the page was built.
Migration
How do I migrate from v0.7.x?
v0.8.0 is the pre-release API/format-stability boundary. [; version-literal-ok ]
The changes are mechanical
renames plus the [?find] → [?for]/CXPath rewrite.
The full diff between v0.7.x and v0.8.0 is documented at [; version-literal-ok ]
from-v07x; the non-mechanical cases (custom directive
names that collide with new core directives) are listed
at the bottom of that section. If you do need to migrate
a v0.7.x codebase, the rename tables there give [; version-literal-ok ]
you everything to do it by hand or with a one-off
project-local script.
What if I am on TypeScript / Java / C# / Ruby / Kotlin / Swift binding?
Those bindings were archived in v0.8.0 — they are still [; version-literal-ok ]
available on v0.7.5 (last release before the archive) [; version-literal-ok ]
and each keeps its own repository, frozen at the ABI it
was archived with (the ships=archived rows of the
repository map). The current binding set is the four
shipped binding repositories bindings names. The path
forward is to migrate to a shipped binding — Layer 1 is
identical across all four, so the migration is mostly
module-import changes plus host-idiomatic adjustments at
Layer 2. (There is no supported pre-reshape line to stay on
— no maintenance releases are cut for tags before the
stability boundary.) Each archived repository's README
carries its archival notes and the pointer to the last
working commit.
The decision to archive was about resource focus, not technical limitation — those bindings worked. The shipped set was chosen for ecosystem reach (a scripting binding for analytics, a services binding, a systems binding, and the native reference). Community-maintained revivals are welcome and the C ABI stays stable for them.
First thirty minutes — quick answers
Working answers to questions adopters ask in the first thirty minutes. The §12.1-§12.8 children above cover deeper operational topics; this section is the beginner-question backstop.
Why does [zip 02134] produce a string?
Leading-zero tokens stay strings unless the prefix is
0x (hex), 0o (octal), or 0b (binary). The rule
is deliberately predictable: leading-zero numerics are
typically identifiers (postal codes, area codes, BIC
codes, account numbers) where the leading zero is
data, not a numeric value. CX's design directly
inverts YAML's "country: NO" footgun.
What is :decimal for?
Arbitrary-precision decimal — for money, scientific
quantities, or any value where IEEE 754 binary floats
would lose precision. 0.1 + 0.2 is
0.30000000000000004 in float; in :decimal it is
0.3. A binding maps :decimal to its host's
arbitrary-precision decimal type, never to a binary
float.
Why brackets instead of braces, indentation, or tags?
Brackets are the lightest balanced delimiter not in
heavy use already. Braces are JSON; mixing braces with
the rest of CX would create constant visual collision.
Indentation breaks under programmatic generation.
Tags double the character count for every construct
and force closing-tag repetition. Brackets also pun
usefully: [name] is the thing called name, which
matches how humans write structured outlines on
whiteboards.
What is logfmt mode?
A CX file made entirely of bare top-level key=value
attributes. Each line reads as one anonymous _ element
carrying that line's attributes, auto-typed like any other
attribute, so a log stream is a CX document — it projects,
queries and hashes like one, with no separate logfmt
parser:
input.cx
ts=2026-05-07T10:30:00Z level=info svc=api req_id=abc123 latency_ms=45
ts=2026-05-07T10:30:01Z level=warn svc=api req_id=def456 latency_ms=210 slow=true
$ cx --from=cx --to=cx input.cx
[_ ts=2026-05-07T10:30:00Z level=info svc=api req_id=abc123 latency_ms=45]
[_ ts=2026-05-07T10:30:01Z level=warn svc=api req_id=def456 latency_ms=210 slow=true]
Fixture logfmt-001-each-line-one-element in conformance/../deps/cx-core-data/conformance/core.cxd — replayed against this cx when the page was built.
Is CX thread-safe?
The C ABI is documented per-symbol with thread-safety
classes. All format converters are stateless and can
run concurrently across threads on disjoint inputs.
The Boehm GC underneath libcx requires
cx_thread_register on each calling thread. The systems
and services bindings handle that automatically
(per-chokepoint registration; locked OS threads); the
scripting binding does NOT — it assumes the interpreter's
global lock and a single calling thread, so keep its libcx
calls on one thread.
Why does my embedded code get garbled?
The bracket-pipe block-content form keeps its bytes but still
reads a [ as CX, so an index like arr[i] inside it opens
an element named i. Use the bracket-hash raw-text form
instead — nothing inside is parsed, and the terminator is
#]. The brackets, the < and the braces come back byte
for byte:
input.cx
[script [# if (x < y) { return [1,2]; } #]]
$ cx --from=cx --to=cx input.cx
[script [# if (x < y) { return [1,2]; } #]]
Fixture 012-rawtext in conformance/../deps/cx-core-data/conformance/core.cxd — replayed against this cx when the page was built.
License and contributing
License: Apache 2.0. Contributing: the project lives at github.com/cx-home/cx — bug reports, doc fixes, and pull requests welcome. For format-design questions or proposed grammar changes, file an issue first; a recorded design decision is a prerequisite for any breaking change. Security reports: through GitHub's private vulnerability reporting at github.com/cx-home/cx/security/advisories/new — do not file a public issue for security-sensitive bugs.