CX+AI

CX+AI Cheatsheet

Everything that trips people up, on one page

CX Language Cheatsheet

A compact reference for writing CX. Doubles as a system-prompt preamble for LLMs generating CX code. Optimized for token efficiency.

Shape

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.

Type suffixes (mandatory on declarations and function returns)

.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)

Variables

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

Control flow

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 (i.i = 0; i < n; i = i + 1) { ... }   // C-style `for`, unchanged from C
// write `x = x + 1`: it works everywhere. `x++` / `x += n` work on a plain number,
// not on a json member or a map element in a native build.

Bounded data structures

// 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
json  d;                          // XML too: d->doc->format = CX_XML, then subscripts

// 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");
v.i = mapGet(ages, "nobody");     // a MISSING key answers the value type's zero (0 / 0.0 / "")
                                  // and does NOT insert -- ->count is unchanged (see gotcha 9)
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 / 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
xs->count;       // elements — queue, list, map, array, sortndx write it straight on the arrow
jobs->cap;       // capacity (queue only — the one family created with one)

// A DOCUMENT puts its own facts one level down, because its TOP-LEVEL arrow
// names belong to the members the program wrote. `doc` is a reserved word.
d->doc->count;   // how many members / children the document has
d->doc->valid;   // 1 while live, 0 after a free
d->doc->type;    // the node kind, at runtime
d->doc->id;      // the raw handle integer — the escape hatch
doc(d, count);   // the same thing, spelled as a call (the fact is a NAME)

d->total;        // ...and a top-level arrow reads the MEMBER named `total`

while (i < d->doc->count) { ... }      // the everyday use: a loop bound
if (jobs->count * 2 >= jobs->cap) ...  // capacity is no longer invisible

d["servers"]->doc->count;              // a SUBSCRIPT can be the base too
d["a"]["b"]->doc->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.
// (Deciding the kind at construction is what made a mismatched first store
//  silent -- so there is no constructor that does it.)
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
// No pragma needed -- the call links the engine. 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.

Builtins (frequently used)

print(...)                        // varargs, no newline arg suffix
printf("...%d %s\n", n, s)        // C-style format -- flags/width/precision all work:
                                  // %-20s %5d %08.3f %+d %.2s %x %e %c
                                  // BUT a bare %f is 3 decimals, not C's 6 (see gotcha 8)
sprintf("...", ...)               // the same formatting, returned as a string
strlen(s.s)                          // string length
substr(s, start, n)               // 0-based substring, like every index in CX
substr(s, -3)                     // a NEGATIVE start counts from the end: the last three
s->len                            // the length is a noun; strlen(s) is the macro over it
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
(int)(s.s)  (float)(s.s)              // string to int / float; unparseable text is 0
                                  // the cast IS the conversion, both ways: (string)n
                                  // makes text of a number; (int)"3.5" is 3 (truncates)
toupper(s)  tolower(s)                // case conversion
strstr(haystack, needle [, pos])  // 0-based find; -1 if not found
random(max)                       // 0..max-1
elapsed()                         // ms since program start (NOT ticks)
delay(ms)                         // sleep (NOT sleep)

Functions

function name.i(a.i, b.s) {       // .i = returns int
    return a + strlen(b);
}
function shout.v(s.s) {           // .v = void; only bare `return;`
    print(toupper(s));
    return;
}
// Function pointers
fp = &name;
result.i = fp(10, "hi");

Pragmas

#pragma appname "MyApp"
#pragma console on               // recommended for stdout programs

Serving, and the web target

_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)

Critical gotchas (these trip everyone)

  1. Everything counts from 0 — strings included. substr(s, 0, n) is the first n characters, and strstr answers -1 for absent: a hit at the first character is 0, so >= 0 is the test and if (strstr(s, t)) is the bug.
  2. 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) { }.
  3. <= / >= on single-char strings is unreliable — use asc(c) <= asc(c2) instead.
  4. Lists/maps/arrays passed as function parameters ARE byref — the callee shares the caller's container and its mutations propagate. No 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.
  5. Identifiers are case-insensitive, always — the scanner lowercases every name, so 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.
  6. Array size in array name.t[X] must be literal or bare #define; expressions like [(N+1)*2] error.
  7. arr is reserved — name it nums, data, items, etc.
  8. A bare %f prints 3 decimals, not C's 6 — float output honours #pragma decimals, whose default is 3. An explicit precision always wins: %.6f for C's default, %.2f for money. print of a float follows the same setting.
  9. A read that finds nothing answers the element type's zero, and never inserts — mapGet on a missing key, listGet(l, i) past the end and xs[99] on a short list all give 0 / 0.0 / "" and leave the container as it was. So a stored zero and a missing key read alike: use mapHas(m, k) when that difference matters.

Worked example 1 — a list-of-strings function

#pragma appname "Greet"
#pragma console on

list names.s;

function greetAll.v() {
    foreach names {
        n.s = listGet(names);
        print("hello, ", n);
    }
}

listAdd(names, "ada");
listAdd(names, "bert");
listAdd(names, "ceci");
greetAll();

Worked example 2 — array + algorithm

#pragma appname "MaxOfArray"
#pragma console on

#define N 6

array data.i[N];

function fillData.v() {
    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());

Worked example 3 — string walk (0-based, like everything)

function reverseStr.s(s.s) {
    out.s = "";
    i.i = strlen(s) - 1;    // the last character sits at length - 1
    while (i >= 0) {
        out = out + substr(s, i, 1);
        i = i - 1;
    }
    return out;
}

print(reverseStr("hello"));

Migrating from the 1-based names — mid, instr, findstring, stringfield, len, ucase, val and the rest are gone (CX-E1028, no hint). The arithmetic lives in one table, in the Language Reference §12 (Migrating from the 1-based names); it is deliberately not repeated here.

When porting from C / Python

C / Python conceptCX equivalent
int x; / x = 0x.i = 0;
char *s; / strs.s = "...";
int arr[N]; / lst = []array arr.i[N]; or list arr.i;
dict / HashMapmap name.i; (string keys only)
malloc / freenew T / delete p; (GC, both backends); malloc itself works natively
s[i] (0-indexed)s[i] — same, 0-indexed, reads/writes one byte. substr(s, i, 1) is the same character as a string
s[i] = cs[i] = c; — a sharer never sees it (copies first if the bytes are shared)
r"raw\text" (Py) / R"(…)" (C++)"""raw\text""" — 3+ quotes open it, a run of the same width closes it
for i in range(n)i.i = 0; while (i < n) { ... i = i + 1; }
Python's lens->len -- the length is a NOUN the handle holds; strlen(s) is the accepted C macro over it
printf (C)printf or print
s.upper() (Py)toupper(s)
pointer arithmeticuse indices into bounded arrays
structsupported: dot access, struct pointers, new/delete heap nodes