CX · Reference guideFrequently Asked Questions
PlaygroundDownloadsAboutv0.18.0-pre.1

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.