JSON you already think in, one equality operator, and no event loop
You already think in JSON, and you already ship things that run in a browser. CX+AI does both of those and compiles to a native binary as well. This page is the delta.
Everything here was run.
JSON is the thing you think in, and here it is a type. Not a parse-and-hope step at the edge of your program — a declared type with a path syntax that looks like the one you know.
string sent = "{ \"port\": 8080, \"host\": \"localhost\" }";
json cfg = parseDoc(sent);
printf("%s:%d\n", cfg["host"], cfg["port"]);
localhost:8080
And when you know the shape at authoring time, you write it as itself — no quoting, and checked when the file is compiled:
_json cfg {
{ "port": 8080, "host": "localhost" }
}
printf("%s:%d\n", cfg["host"], cfg["port"]);
localhost:8080
Containers are references. Hand a document to a function and the function has your document. No spread, no structured clone, no surprise.
Braces and semicolons, if/else/while, + for concatenation. The surface syntax will not fight you.
It runs in a tab. The same source compiles to WebAssembly and runs in a page — including the graphics, the JSON and the rules engine. There is no separate web build to maintain.
Three things you already do in JavaScript, side by side. The JavaScript is the JavaScript a JavaScript programmer writes, and one of these three is a draw — the page says which.
const cfg = { port: 8080, host: 'localhost' };
console.log(`${cfg.host}:${cfg.port}`);
localhost:8080
_json cfg {
{ "port": 8080, "host": "localhost" }
}
printf("%s:%d\n", cfg["host"], cfg["port"]);
localhost:8080
Call this one a draw, and it is the reason you are the closest audience this language has. Your object literal is already checked when the file is parsed; so is the _json block. Nothing is gained by moving.
The narrow difference shows up one line later, when the document did not come from your own file. JSON.parse hands back a value whose type is whatever arrived. In CX+AI the read states what you want — port.i = cfg["port"] is an int and text.s = cfg["port"] is "8080" — and it is the same document type either way, so the code that reads a compiled-in document and the code that reads a socket's reply are the same code.
// the policy arrives from a config file, an operator, or a model
const policy = 'return total >= 50 ? 0 : 4.95;';
const ship = new Function('total', policy);
for (const total of [18.0, 96.0]) {
console.log(` basket ${total.toFixed(2).padStart(6)} ship ${ship(total).toFixed(2).padStart(4)}`);
}
basket 18.00 ship 4.95
basket 96.00 ship 0.00
_json baskets {
[ { "total": 18.00, "ship": 0 }, { "total": 96.00, "ship": 0 } ]
}
string policy = "{ if (e[\"total\"] >= 50) { e[\"ship\"] = 0; } "
"else { e[\"ship\"] = 4.95; } }";
json b;
foreach baskets {
b = jsonGet(baskets);
ruleExec(b, policy);
printf(" basket %6.2f ship %4.2f\n", b["total"], b["ship"]);
}
basket 18.00 ship 4.95
basket 96.00 ship 0.00
The advantage: what you are agreeing to run. new Function is the careful form — it does not close over your local scope — and it still compiles JavaScript, with the globals of wherever it runs in reach. You start with a whole language and argue it back down, and that argument is yours to make again every time the text comes from somewhere new.
ruleExec runs a rule: assignments and conditions over the document you handed it, and there is nothing else to reach for. You are not narrowing a general language; the small one is what there is. The same text can also be written in the file as a _rules block, in which case a typo in it is an error at that line before the program exists — one engine, two ways in.
console.log(' 7 / 2 =', 7 / 2);
console.log(' 0.1 + 0.2 =', 0.1 + 0.2);
console.log(' 2**53 + 1 =', Math.pow(2, 53) + 1);
7 / 2 = 3.5
0.1 + 0.2 = 0.30000000000000004
2**53 + 1 = 9007199254740992
a.f = 0.1;
b.f = 0.2;
big.i = 9007199254740992;
printf(" 7 / 2 = %d\n", 7 / 2);
printf(" 7.0 / 2 = %.1f\n", 7.0 / 2);
printf(" 0.1 + 0.2 = %.17g\n", a + b);
printf(" 2^53 + 1 = %lld\n", big + 1);
7 / 2 = 3
7.0 / 2 = 3.5
0.1 + 0.2 = 0.30000000000000004
2^53 + 1 = 9007199254740993
Look at the middle row before the last one. The floats are identical doubles and they always will be — CX+AI does not fix 0.1 + 0.2, and any page that tells you a language did is selling something.
The last row is the whole difference: your integers are doubles, so above 253 they stop being able to count, and 9007199254740993 is a number JavaScript cannot hold. Here .i is a 64-bit integer, so an id, a counter or a total in cents is exact where you left it. The price is that you choose: 7 / 2 is 3 until you write 7.0. JavaScript's own answer to this is BigInt — a third type you opt into, and one that throws if you mix it with an ordinary number. Here the exact integer is the ordinary one.
Types are a suffix, and they are load-bearing. n.i, s.s, f.f are not annotations a checker reads and the runtime ignores — they are what the compiler uses to decide how a value is read.
json cfg = parseDoc("{ \"port\": 8080 }");
port.i = cfg["port"]; // read as int -> 8080
text.s = cfg["port"]; // read as string -> "8080"
printf("%d [%s]\n", port, text);
8080 [8080]
That is the opposite trade from TypeScript: fewer places to write a type, and the ones you do write actually do something.
Behaviour can arrive as text and be run safely. A rule is a string CX+AI compiles to bytecode in-process — no eval, no Function, no script tag, and crucially no access to anything except the document it was handed. See Rules that watch your data.
== is C's ==, and there is no second one. There is no ===, because there is nothing for it to be different from. No coercion table, no truthiness ladder, no [] == false.
Integer division truncates, because there is one number type in JavaScript and two here. Both are shown, with what they print, under one number type, or two above.
No event loop and no promises. A call returns when it returns. An AI call is a function that gives you a string:
reply.s = ai_call("hi");
if (reply == "") { printf("no answer: %s\n", ai_get_error()); }
no answer: no provider set
There is an async pair when you want one, but the default is the straight line, and most programs are better for it.
A failure is a value, not a throw. An AI call that fails gives an empty string and a readable reason. A missing JSON key reads as 0 or as empty. You test results rather than wrapping things in try.
Strings count from 0, like arrays. substr(s, 0, 4) is s.slice(0, 4), and strstr(s, t) is indexOf — 0-based, -1 when absent.
No npm. No bundler. No prototype chain, no this, no classes, no closures over mutable captures. No undefined — a thing either has a value or the read gives you the zero of the type you asked for, and ->type tells you which.
Where you would reach for a package there is usually a builtin — more than eight hundred of them. Where there is not, C is one block away and it compiles into the same binary.
The honest list of what you will miss: the ecosystem, first-class functions as a everyday tool, and destructuring. What you get for it: one native binary with nothing to install, the same file running in a tab, and a program whose behaviour can change while it runs without eval and without a security argument.