CX+AI

CX+AI for JavaScript Programmers

JSON you already think in, one equality operator, and no event loop

CX+AI for JavaScript Programmers

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.


What carries over

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.


You would write this — here you write this

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.

A document whose shape you already know

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.

Behaviour that arrives while the program runs

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

One number type, or two

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.


What is new

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.


What is different

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


What is not here

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.


Where to go next