Everything that trips people up, on one page
A compact reference for writing CX. Doubles as a system-prompt preamble for LLMs generating CX code. Optimized for token efficiency.
CX is C-shaped with type suffixes. Bounded primitives and containers first; C's structs, pointers, and heap are there when you reach for them (new/delete on the GC, both backends; malloc natively). One file = one program; no #include required.
.i int (default if omitted) x.i = 42;
.s string name.s = "ada";
.f float pi.f = 3.14;
.d double h.d = 6.626e-34;
.v void (function only; bare return)
n.i = 0; // declares + assigns
n = n + 1; // reassign (no suffix needed)
const.i PI = 3; // not supported — use #define instead
#define PI 3.14159
A nested block is its own scope, as in C — and a declaration inside one constructs every time it runs:
int x; x = 7;
if (ready) { int x; x = 9; } // a DIFFERENT x
// x is 7 again after the brace
while (going) { int c; c = c + 1; } // c is a fresh 0 EVERY pass, not 1,2,3
// x.i = 9 in a block writes the OUTER x
if (cond) { ... } // parens around the WHOLE condition, as in C:
// if cond { ... } <-- ERROR
// if (n % 2) == 0 { ... } <-- ERROR
else if (cond2) { ... }
else { ... }
while (cond) { ... }
break; continue; // both supported
// `for` — limited support; prefer while
// Fixed-size arrays — size must be literal or single #define
#define N 32
array nums.i[N];
nums[0] = 10; // 0-indexed
v.i = nums[i];
array nums.i[8]; // RE-STATING resizes it: `array` is the whole
// statement -- it declares AND redimensions.
// Keeps the elements that still fit AND the
// element type; ->count is the live length.
// (`redim` is retired -- CX-E1048.)
array seed.i[5] = {10, 20, 30}; // initialiser; rest padded with the type's zero
// A DECLARED CONTAINER IS LIVE -- the declaration is the constructor
list xs.i = {1, 2, 3}; // positional elements
map m.i = {alpha: 1}; // KEYED pairs (identifier keys)
sortndx s.i = {3, 1, 2}; // sorts AS IT BUILDS -> reads 1 2 3
xmldoc doc; // already a document: no create call needed
// Linked lists (CX builtin)
list items.s;
listAdd(items, "hello");
n.i = items->count;
listFirst(items); listNext(items); listLast(items); listPrev(items);
listSelect(items, idx); listSet(items, val); listGet(items);
listReset(items);
// Maps (string key -> typed value)
map ages.i;
mapPut(ages, "alice", 30);
v.i = mapGet(ages, "alice");
mapContains(ages, k); mapDelete(ages, k); ages->count;
mapReset(ages); mapNext(ages); mapKey(ages); mapValue(ages);
// Queues (bounded ring — capacity is a RUNTIME expression, so it can be data)
queue jobs.s; // .i / .f / .s, or .StructName
queueInit(jobs, cap, 0); // 0 = REJECT (full push errors), 1 = ROLL (overwrite oldest)
queuePush(jobs, "build"); // add
s.s = queueTake(jobs); // take the OLDEST -> FIFO
s.s = queuePop(jobs); // take the NEWEST -> LIFO
jobs->count; jobs->cap; // live count / capacity
queueClear(jobs); // drop contents, keep capacity + policy
// jobs[0] // CX-E1090 at your line: the queue type has no subscript
// (nor does a sortndx) — position is the container's job
// Each family's KEY KIND — a subscript with the wrong one is CX-E1093 at your line
// list / array / string / pointer -> an INT position map -> a STRING key
// json -> either (the first subscript decides the root) queue / sortndx / xmldoc / memfile / image -> none
// int / float -> none (one number has no slots) `p[0]` on a POINTER is fine: it means *p
// xs["k"] // CX-E1093: list subscripts take an int position … Use a map
// m[0] // CX-E1093: map subscripts take a string key … Use a list or an array
// int n; n[0] // CX-E1090: the int type has no subscript … declare a list or a map
// Drain idiom — reading a queue CONSUMES it, so count is the loop guard
while (jobs->count > 0) { print(queueTake(jobs)); }
// Releasing a container EARLY — one verb, every family (list/map/array/sortndx/queue/json)
listClear(items); // EMPTY it, keep the container
containerDelete(items); // release THIS reference; freed when the last holder drops it
// after this, `items` is dead: using it is CX-E1068 at your line
// a fixed array has no reference -> CX-E1069
// a container PARAM is borrowed (caller owns it) -> CX-E1070
// Heap nodes — `new`/`delete` on CX's GC, BOTH backends (struct: node { val.i; node *next; })
n = new node; // one ZEROED node; size comes from the shape — never a byte count
delete n; // release it AND set n to 0; delete what you new, free what you malloc
// Struct elements: take/pop use an OUT-PARAM (a struct is bytes, not a value)
queue window.Sample; // numeric fields only in v1 (string field = compile error)
queuePush(window, s); // copies the record IN
queueTake(window, out); // copies the OLDEST record OUT into `out`
// Handle metadata — `->` asks about the HANDLE, `[...]` asks about the DATA
doc->count; // elements / children — one name for json, queue, list, map, array, sortndx
jobs->cap; // capacity (queue only — the one family created with one)
doc->valid; // 1 while live, 0 after a free (json + queue keep real liveness)
doc->type; // json: node kind at runtime. Other families: element type, a constant
doc->id; // the raw handle integer — the escape hatch
while (i < doc->count) { ... } // the everyday use: a loop bound
if (jobs->count * 2 >= jobs->cap) ... // capacity is no longer invisible
doc["servers"]->count; // a SUBSCRIPT can be the base too
doc["a"]["b"]->type; // and it chains
// All compile-time: no runtime struct, no tag, no dispatch. `doc->count` is
// exactly the call you'd have written; `xs->type` folds to a constant.
// Read-only — `q->count = 0` is CX-E1046. An unknown field is CX-E1045 and
// names the one you probably meant.
// NOTE `doc->count` derefs a doc handle, so a 3-member object answers 3.
// The retired `jsonSize(doc)` did NOT deref and answered 1 -- the one site in
// the whole rename whose ANSWER moves, so port those by reading them.
// foreach iteration (no loop var; body calls listGet/mapKey)
foreach items {
v.s = listGet(items);
print(v);
}
// json: DECLARE it, then subscript it. The declaration builds the document; the
// FIRST subscript decides the root's kind — a string key makes it an object, an
// int index makes it an array — and every nested level follows the same rule.
// (jsonCreate()/jsonCreateArr() are RETIRED: they picked the kind too early.)
json d;
d["servers"][0]["host"] = "alpha"; // {"servers":[{"host":"alpha"}]}
json ids;
ids[0] = 11; ids[1] = 12; // [11,12]
// A root already decided REFUSES the other kind, loudly, writing nothing —
// a write creates, it never converts.
// _json literal block — write json as itself, checked at COMPILE time
_json cfg { // declares `json cfg`, builds it in place
{ "name": sName, // unquoted value = a CX var, spliced in
"hp": fHp, // a float enters as a number, losslessly
"tags": ["a", "b", 3], // objects + arrays nest to any depth
"meta": { "level": 7 } }
}
// A malformed literal is an error at THIS line, not a bad handle at runtime.
// Builder-call lowering (no string reparse); an expression in value position
// is refused (precompute to a local); keys stay literal. Object OR array top.
// _rules literal block — write rule C as itself, CHECKED at compile time
_rules COMBAT { // declares `string COMBAT`
{ e["hull"] = e["hull"] - 35; // real quotes, not \" escapes
if (e["hull"] < 30) raise(50, e); }// inner braces are the rule body's own
}
ruleExec(ship, COMBAT); // a plain string -- no new runtime surface
// Needs `#pragma rules c`. A syntax error or a call outside the C-rule
// whitelist fails the BUILD at this line, instead of a runtime complaint from a
// rule that then silently never fires. C dialect only (a body must open with
// `{`); no splices; builtins only. The yield stays data -- reassign it at
// runtime and the behaviour changes, exactly as before.
print(...) // varargs, no newline arg suffix
printf("...%d %s\n", n, s) // C-style format
len(s.s) // string length
mid(s, pos, len) // 1-INDEXED substring (PB heritage)
left(s, n) // first n chars
right(s, n) // last n chars
asc(c.s) chr(code.i) // char <-> int
str(n.i) // int to string
vali(s.s) // string to int (NOT val(); use vali)
ucase(s) lcase(s) // case conversion
instr(haystack, needle [, pos]) // 1-indexed find; 0 if not found
random(max) // 0..max-1
elapsed() // ms since program start (NOT ticks)
delay(ms) // sleep (NOT sleep)
function name.i(a.i, b.s) { // .i = returns int
return a + len(b);
}
function shout.v(s.s) { // .v = void; only bare `return;`
print(ucase(s));
return;
}
// Function pointers
fp = &name;
result.i = fp(10, "hi");
#pragma appname "MyApp"
#pragma console on // recommended for stdout programs
#pragma ai_fallback "cxllama, anthropic"
_json cfg { { "port": 7440 } } // json written as itself, checked at compile time
_rules R { { e["hp"] = e["hp"] - 1; } } // rule C written as itself, checked at compile time
int srv = netListen(7440); // loopback; netListenAny() is reachable
if (netPoll(srv, 200) == 1) { // -1 waits forever, 0 polls, N = ms
int conn = netAccept(srv);
if (httpRead(conn) == 1) {
httpRespond(conn, 200, "text/plain", "hi");
}
netClose(conn);
}
cx app.cx --run # a native executable
cx app.cx --target wasm # the SAME source as a web page (needs emsdk; refuses loudly without it)
_json and _rules take two brace levels — the block's, then the content's own.netPoll is the whole blocking model. There is no separate blocking-accept call: netPoll(srv, -1) is one, and netPoll(srv, 0) drops into a render loop.httpServeDir(conn, mount, dir) returns 0 having written nothing when the request is outside the mount — so it sits in an if ladder beside your own routes rather than owning the server.httpAllowOrigin(origin) opens it for one named origin; nothing sends * for you.mid(s, pos, len) is 1-indexed, but arrays are 0-indexed. Mixing them is the #1 bug source.if / while take C parens around the WHOLE condition — both if n % 2 == 0 { } and if (n % 2) == 0 { } error (CX-E0001, CX-E0002). Write if (n % 2 == 0) { }.<= / >= on single-char strings is unreliable — use asc(c) <= asc(c2) instead.byref keyword: a container variable is a heap handle. A passed array additionally becomes dynamic, so re-stating array a.i[n] inside the callee resizes the caller's array and a->count reads the live length. Same on both backends.matA and mata are one variable, and a global A is the same cell as a global a. Silent: no diagnostic, no warning. Single letters collide soonest, which is why matA/matB beat A/B. There is no opt-out.array name.t[X] must be literal or bare #define; expressions like [(N+1)*2] error.arr is reserved — name it nums, data, items, etc.#pragma appname "Greet"
#pragma console on
list names.s;
function greetAll() {
foreach names {
n.s = listGet(names);
print("hello, ", n);
}
}
listAdd(names, "ada");
listAdd(names, "bert");
listAdd(names, "ceci");
greetAll();
#pragma appname "MaxOfArray"
#pragma console on
#define N 6
array data.i[N];
function fillData() {
data[0] = 17; data[1] = 4; data[2] = 42;
data[3] = -8; data[4] = 23; data[5] = 99;
}
function findMax.i() {
best.i = data[0];
i.i = 1;
while (i < N) {
if (data[i] > best) { best = data[i]; }
i = i + 1;
}
return best;
}
fillData();
print("max = ", findMax());
function reverseStr.s(s.s) {
out.s = "";
n.i = len(s);
i.i = n; // start at last char (1-indexed)
while (i >= 1) {
out = out + mid(s, i, 1);
i = i - 1;
}
return out;
}
print(reverseStr("hello"));
| C / Python concept | CX equivalent |
|---|---|
int x; / x = 0 | x.i = 0; |
char *s; / str | s.s = "..."; |
int arr[N]; / lst = [] | array arr.i[N]; or list arr.i; |
dict / HashMap | map name.i; (string keys only) |
malloc / free | new T / delete p; (GC, both backends); malloc itself works natively |
s[i] (0-indexed) | mid(s, i+1, 1) (1-indexed) |
for i in range(n) | i.i = 0; while (i < n) { ... i = i + 1; } |
len(s) (Python) | len(s) (same) |
printf (C) | printf or print |
s.upper() (Py) | ucase(s) |
| pointer arithmetic | use indices into bounded arrays |
struct | supported: dot access, struct pointers, new/delete heap nodes |