CX+AI

CX Cheatsheet

Everything that trips people up, on one page

GENERATED from DOCS/reference/manuals/CX_Cheatsheet.html by tests/forge.cx — edit the source, not this 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` — limited support; prefer while

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

Builtins (frequently used)

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)

Functions

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");

Pragmas

#pragma appname "MyApp"
#pragma console on               // recommended for stdout programs
#pragma ai_fallback "cxllama, anthropic"

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. mid(s, pos, len) is 1-indexed, but arrays are 0-indexed. Mixing them is the #1 bug source.
  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.

Worked example 1 — a list-of-strings function

#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();

Worked example 2 — array + algorithm

#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());

Worked example 3 — string walk (note 1-indexed mid)

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"));

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)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 arithmeticuse indices into bounded arrays
structsupported: dot access, struct pointers, new/delete heap nodes