What carries over, what is new, and what is different
CX is C, expanded. If you write C, you can read every CX program on this site already, and most of what you know transfers without an asterisk. This page is the short version of what carries over, what is new, and — the part worth reading carefully — the handful of places where CX does something C does not.
It assumes C and nothing else.
Control flow, operators, precedence, arithmetic. if / else if / else, while, do, for, switch / case, break, continue, return. The operators are C's, with C's precedence and C's short-circuit rules. Integer division stays integer division; promotion happens when an operand is floating point. CX did not invent a cleverer arithmetic rule, deliberately.
Functions, in C's own declarator form. This compiles:
int add(int a, int b) {
return a + b;
}
double half(double x) { return x / 2.0; }
printf("%d %f\n", add(20, 22), half(9.0));
42 4.500
Pointers. &x, *p, p->field, pointer arithmetic, arrays of pointers, function pointers, pointers as parameters and as struct fields. A struct field may point at its own type, so linked lists, trees and graphs are written exactly as you would write them in C.
The preprocessor. #define, #include, #ifdef, #if. #define constants are the way to size a fixed array.
printf. The C one, with C's format specifiers.
And when you want raw C, you write raw C. _C{ ... } drops a block of verbatim C into the output. It is a load-bearing part of the design, not an escape valve nobody uses: anything CX has not grown yet, you can still reach.
_C{
printf("straight C\n");
}
One honest limit, and the compiler tells you about it: a_C{}block is lifted into a native stub, so it is native-only and your CX variables are not in scope inside it. Compiling for the register VM warns (CX-W1001) and names the stub. In a browser tab a_C{}program is refused outright — there is no native binary there to hold the C.
list, map, queue, dynamic array, sortndx and json are declarations, not libraries. There is no allocation code, no capacity to guess and no free to forget:
map ages.i;
mapPut(ages, "ada", 36);
mapPut(ages, "grace", 45);
printf("grace is %d, %d entries\n", mapGet(ages, "grace"), ages->count);
grace is 45, 2 entries
The element type comes from the declaration's suffix — map ages.i is string→int, list names.s is a list of strings.
-> asks the container about itself rather than about its contents: c->count, c->cap, c->type, doc->valid. It is resolved at compile time from the declared type and costs nothing at runtime.
s.s = "hello";
t.s = s + " world";
n.i = len(t);
printf("%s (%d chars)\n", ucase(t), n);
HELLO WORLD (11 chars)
Concatenation with +, length with len, no buffer to size and no strcpy. char * and the C string functions are still there when you want them.
mid(s, pos, len) is 1-indexed — BASIC heritage, kept on purpose. Array indices are 0-based, as in C. This is the single most common thing to trip over.
json cfg;
cfg["screen"]["w"] = 1280;
cfg["screen"]["h"] = 720;
printf("%d x %d\n", cfg["screen"]["w"], cfg["screen"]["h"]);
print(jsonExport(cfg));
1280 x 720
{"screen":{"w":1280,"h":720}}
A nested write creates the objects it needs on the way down. A read never does.
new and delete, against CX's own allocatorstruct point { x.i; y.i; }
point *p;
p = new point;
p->x = 3;
p->y = 4;
printf("(%d,%d)\n", p->x, p->y);
delete p;
(3,4)
No byte count appears anywhere in that program. The size comes from the struct's shape, so adding a field cannot leave a stale malloc(16) elsewhere in the file — which is the specific C bug this removes rather than documents. A leak under #pragma checks on reports which struct type leaked, not a byte total.
malloc and free still work and still mean exactly what C means. The discipline is C++'s: delete what you new, free what you malloc. (Two honest limits: malloc/free are native-only on the register VM, and new T[n] for an array of nodes is not built yet.)
CX can call a model at compile time or at run time, and it can take a function body written by a model while the program is running, compile it in-process, and attach it to live state. That is what the register VM is for, and it has its own manual — the CX AI Manual, and the Automation & Rules manual beside it.
Five things. None of them is large, and all five bite once.
count.i = 3;
name.s = "Ada";
ratio.f = 0.75;
.i int, .s string, .f double, .v void. The suffix is needed only on first use. Functions carry it on the return type: function area.f(w.f, h.f). C's own spelling works everywhere too — pick one and be consistent within a file.
Declare the return type explicitly. A function with no suffix returns int, so omitting it on a string- or float-returning function is a real bug rather than a style choice.
The parser lowercases CX identifiers, so myVar, myvar and MYVAR are one variable:
myVar.i = 1;
myvar = myvar + 41;
print(MYVAR);
42
Case is still meaningful inside _C{} blocks, which are C.
A container parameter is the caller's container — there is no byref keyword because there is no other behaviour:
function fill.v(list xs.i) {
listAdd(xs, 99);
}
list nums.i;
listAdd(nums, 1);
fill(nums);
printf("%d items, last=%d\n", nums->count, listGet(nums, 1));
2 items, last=99
An array handed to a user function becomes dynamic at its declaration, and gets the same treatment: writes propagate, a->count reads the live length, and re-stating array a.i[n] inside the callee resizes the caller's array. array is the whole statement — writing it again for a name already declared in the same scope RESIZES that array, because two declarations of one C stack array are two conflicting C declarations, so the array becomes dynamic at its first. (The older redim spelling is retired, CX-E1048.) A fixed array that is never passed and never re-stated stays a raw C array.
For the same reason, assigning one container to another (a = b) is refused rather than silently aliasing two owners onto one object.
--run compiles your program to C and builds it with your C compiler. --runvm runs the same source on CX's register VM. They are expected to produce identical output, and the test suite's main job is checking that they do.
Use native when you want speed — it is C. Use the VM for code that changes while the program runs, and for the browser, where there is no C compiler to call. A natively-compiled program can still hand a piece of itself to the VM at runtime; that is the normal case, not an exotic one.
A few things are native-only and say so at compile time (CX-E1013) instead of behaving differently: malloc/free, _C{} blocks, and the AI calls that need a native HTTP client.
CX's rule is that a wrong thing produces a named error at your own line rather than a plausible-looking wrong answer. A retired spelling tells you what to write instead (CX-E1048) rather than answering "unknown function"; a capability a browser tab genuinely cannot provide refuses by name (CX-E5037) rather than returning an empty string. If you find CX being quietly wrong, that is a defect, not a design.