Install, first program, and the same program on both backends
This page takes you from nothing to a running CX program, and then to the same program running two different ways. It is a walkthrough, not a reference — read it once, top to bottom, and you will have written and run CX.
If you would rather not install anything yet, <https://cxai.plus> runs the compiler in your browser. Everything in the "Your first program" section below works there, and you can come back to the install when you want a real binary.
CX ships as a complete folder for Windows, Linux and macOS: the cx binary, the prebuilt runtime library, the headers your programs compile against, an installer, the manuals and the examples. There is no build step and no source to compile — the folder is the install, and you can keep it anywhere.
Download the bundle for your operating system from the project's Releases page, unpack it, and run the installer from inside the unpacked folder:
| Windows | powershell -ExecutionPolicy Bypass -File .\install.ps1 |
| Linux | sh ./install.sh |
| macOS | sh ./install.sh |
What the installer does is worth knowing, because it explains what CX is. CX compiles your program to C and hands that C to a real C compiler, so the installer checks that one is present — gcc on Windows (MinGW-w64) and Linux, clang on macOS via the Xcode Command Line Tools — and offers to install it if it is missing. On Linux and macOS it also makes sure the system libraries CX links against are there. Then it proves the install by running a real build: a missing piece fails at install time, naming the exact package, rather than at your first --run.
Each bundle carries its own INSTALL.md with the exact commands for that release; if it disagrees with this page, believe the bundle.
Graphics is per-platform, and the rule is that windowed rendering only ships for an operating system after a human has watched it render there. Today that means Windows ships with graphics enabled; the Linux and macOS bundles are headless, and a graphics call on them stops with a named error (CX-E5035) rather than pretending to draw.
Put this in hello.cx:
print("Hello from CX");
and run it:
cx hello.cx --run
Hello from CX
There is no main, no #include, and no boilerplate. Statements at the top level of a file are the program. You can write int main() { ... } when you want to — CX accepts it, and the examples that came from C keep it — but you do not have to.
--run compiles and runs in one step. --build compiles and stops, leaving you an executable to keep. cx --help lists everything else.
A CX variable is declared by using it, and its type rides on the name as a suffix:
name.s = "Ada";
count.i = 3;
ratio.f = 0.75;
printf("%s x%d at %f\n", name, count, ratio);
Ada x3 at 0.750
.i is a 64-bit integer, .s a string, .f a double, and .v means "returns nothing" on a function. The suffix is only needed the first time a name appears; after that it is just name. Functions carry the suffix on their return type:
function area.f(w.f, h.f) {
return w * h;
}
printf("%f\n", area(3.0, 2.5));
7.500
If you prefer C's spelling, C's spelling works — int count = 3; and double area(double w, double h) { ... } compile exactly the same. CX is C with things added, not C with things replaced.
This is the part that is not C, and it is the reason CX exists. Lists, maps, queues, dynamic arrays and JSON documents are ordinary declarations, garbage collected, with no allocation code to write:
list names.s;
listAdd(names, "ada");
listAdd(names, "grace");
listAdd(names, "alan");
printf("%d names\n", names->count);
foreach names {
n.s = listGet(names);
print(ucase(n));
}
3 names
ADA
GRACE
ALAN
->count asks the container a question about itself, as distinct from [...] which asks about its contents. It is resolved at compile time from the declared type, so it costs nothing at runtime.
JSON is a first-class type rather than a library. A write creates whatever it needs on the way down — and what it creates is decided by how you index it:
json cfg;
cfg["screen"]["w"] = 1280;
cfg["servers"][0]["host"] = "alpha";
cfg["servers"][1]["host"] = "beta";
printf("%d on %s\n", cfg["screen"]["w"], cfg["servers"][0]["host"]);
print(jsonExport(cfg));
1280 on alpha
{"screen":{"w":1280},"servers":[{"host":"alpha"},{"host":"beta"}]}
Nothing there declares "screen" or "servers" first. A string key makes the level it indexes an object; an int index makes it an array — so a list of records is something you just write, rather than something you have to build, serialise and re-parse before it will nest.
Nothing converts the values on the way out either: the destination decides how a value is read, so cfg["screen"]["w"] into a %d is an int and into a .s would be text. That single idea has its own manual — the Coercion Guide — and it is the one most worth reading next.
CX has two backends, and the same source runs on both:
cx hello.cx --run # compile to a native binary via the C compiler, then run it
cx hello.cx --runvm # run the same program on CX's register VM, in-process
Try it on the list program above. Both print:
3 names
ADA
GRACE
ALAN
They are expected to agree, and the project's test suite exists largely to check that they do, program by program, on all three operating systems.
Why two? The native backend is the one to use when you want speed: it is C, compiled by your C compiler, with C's performance. The VM is there for code that changes while the program is running — a rule loaded from a config file, a function an AI writes and hands to a live program, anything you would otherwise need a plugin system and a recompile for. The VM is also what makes CX run in a browser tab, where there is no C compiler to call.
You do not choose between them once and for all. A single program can compile natively and still hand a piece of itself to the VM at runtime.