CX+AI

CX+AI for C Programmers

What carries over, what is new, and what is different

CX for C programmers

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.


1. What you already know

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.

One habit to bring: write the assignment out. x = x + 1 works on every kind of variable, on both backends, and inside a rule. x++, x-- and x += n are C and work on a plain number; on a json member or a map element (d["k"]++, m["a"] += 2) the native build refuses them today while the VM accepts them, so the written-out form is the one CX+AI's own code and its generated examples teach. A rule's text and a model's reply are read as plain assignment for the same reason.

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 — including everything between the % and the conversion letter, so flags, field widths and precisions all behave: printf("%-20s %8.2f\n", name, total) lines a table up exactly as you expect.

Two differences, and the second one will catch you. %d carries a 64-bit integer — the compiler rewrites the spec, so you never write %lld. And a bare %f prints 3 decimals, not 6: float output honours #pragma decimals, whose default is 3. An explicit precision always wins, so write %.6f for C's default.

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.

2. What is new

Containers, garbage collected

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.

A lookup that misses answers the element type's zero — 0, 0.0 or "" — and does not insert, so mapGet(ages, "nobody") is 0 and ages->count is still 2. There is no "not found" sentinel and no iterator to compare against end(). The cost is that a stored zero and a missing key read alike; when that matters, mapHas(m, k) tests for the KEY and is the only thing that can tell them apart. The same rule covers listGet(l, i) past the end and xs[99] on a short list.

-> asks the container about itself rather than about its contents: c->count, c->cap, c->type, and on a document d->doc->valid. It is resolved at compile time from the declared type and costs nothing at runtime.

Strings that behave like values

s.s = "hello";
t.s = s + " world";
n.i = strlen(t);
printf("%s (%d chars)\n", toupper(t), n);
HELLO WORLD (11 chars)

Concatenation with +, length with strlen, no buffer to size and no strcpy. char * and the C string functions are still there when you want them.

Strings count from 0, as in C: substr(s, 0, n) is the first n bytes, s[i] is a byte, and strstr(s, t) answers the 0-based position or -1 — a position, never a pointer, so >= 0 is the test.

JSON as a type

json cfg;
cfg["screen"]["w"] = 1280;
cfg["screen"]["h"] = 720;

printf("%d x %d\n", cfg["screen"]["w"], cfg["screen"]["h"]);
print(exportDoc(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 allocator

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

An AI surface in the language

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 AI manual, and the Automation & Rules manual beside it.


3. What is different

Five things. None of them is large, and all five bite once.

Types are a suffix, and a variable is declared by using it

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.

Identifiers are case-insensitive

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.

Containers are passed by reference, always

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.

Two backends, one source

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

Arguments evaluate left to right

C leaves the order of a call's arguments unspecified — this is the classic f(g(), h()) question, and the answer in C is "whichever your compiler felt like". CX decides: left to right, on both backends, for user functions and builtins alike. So

printf("rc=%d err=[%s]\n", sqlIndex("t", "name"), sqlError());

reports the error sqlIndex just set, not the one from before it — the same on native and on the VM, and the same under gcc, clang and MSVC. Operands of the string + follow the same rule.

This costs nothing where the order cannot be observed. CX only sequences a call when one of its arguments changes something another one can see; two independent reads are emitted exactly as they always were.

Errors are loud, by policy

CX's rule is that a wrong thing produces a named error at your own line rather than a plausible-looking wrong answer. Calling a metadata read on a typed handle tells you the field to write instead (CX-E1110); a capability a browser tab genuinely cannot provide refuses by name (CX-E5037) rather than returning an empty string. A spelling the language removed is a plain unknown-function error (CX-E1028) — deliberately, because a language that carries a pointer for every name it ever had ends up teaching its own history instead of itself. If you find CX being quietly wrong, that is a defect, not a design.


4. Where to go next