What transfers, what the type suffixes replace, and where C's arithmetic shows
You are here because something needs to be fast, or needs to ship as one file, or needs to keep running while it changes. This page is about what transfers and what does not.
Everything here was run.
More than you would expect.
You declare a variable by assigning to it. There is no separate declaration statement to write first.
Containers are handles you pass around, and you never think about ownership. A list, a map or a JSON document handed to a function is the caller's — what the function writes, the caller sees. That is Python's behaviour, and it is CX+AI's for the same reason: it is what people expect and it is what avoids a copy.
foreach walks a container, and it walks the container itself rather than an index you maintain.
JSON is a first-class thing, and this is where CX+AI is closer to Python than to C. A document is a type, a path reads a value, and a value comes out as a value.
string sent = "{ \"port\": 8080, \"host\": \"localhost\" }";
json cfg = parseDoc(sent);
printf("%s:%d\n", cfg["host"], cfg["port"]);
localhost:8080
Three things you already do in Python, side by side. The Python is the Python a Python programmer writes; there is no straw here, and one of the three goes to Python outright.
cfg = {"port": 8080, "host": "localhost"}
print(f"{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. Your dict literal is checked when the module is compiled to bytecode, and the _json block is checked when the file is compiled. Nothing is gained by moving, and a page that told you otherwise would be selling you syntax you already have.
The difference arrives one line later, when the document did not come from your own file. Here is a sender that put the port in quotes — which is most senders, sooner or later:
import json
cfg = json.loads('{"port": "8080"}')
port = cfg["port"]
print(port + 1)
TypeError: can only concatenate str (not "int") to str
string reply = "{ \"port\": \"8080\" }";
json cfg = parseDoc(reply);
port.i = cfg["port"];
text.s = cfg["port"];
printf("%d [%s]\n", port, text);
8080 [8080]
The read is where the type is stated. json.loads hands back whatever the sender sent, and the mismatch surfaces at some later line that expected a number — the int() calls scattered through your parsing layer are there for exactly this. port.i says what you want on the line where you want it.
And it is the same json type either way, so the code that reads a compiled-in document and the code that reads a socket's reply is the same code.
Both languages can do this, and Python's version is the careful one a good Python programmer writes — restricted globals, because you do not hand eval the builtins.
# the policy arrives from a config file, an operator, or a model
policy = "0 if total >= 50 else 4.95"
for total in (18.00, 96.00):
ship = eval(policy, {"__builtins__": {}}, {"total": total})
print(f" basket {total:6.2f} ship {ship:4.2f}")
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. eval runs Python — you start with a whole language and narrow it down with a denylist, and the narrowing is your job every time. ruleExec runs a rule: assignments and conditions over the document you handed it, and nothing else exists to reach for. You are not securing a general language down to a small one; the small one is what there is.
The second half is that the same text can be checked early. Written in the file as a _rules block it is compiled when the file is compiled, so a typo is an error on its line — and it is still an ordinary string, so the two forms meet at one engine.
print("7 / 2 =", 7 / 2)
print("7 // 2 =", 7 // 2)
print("2**64 =", 2**64)
print("2**200 =", 2**200)
7 / 2 = 3.5
7 // 2 = 3
2**64 = 18446744073709551616
2**200 = 1606938044258990275541962092341162602522202993782792835301376
big.i = 9223372036854775807;
printf("7 / 2 = %d\n", 7 / 2);
printf("7.0 / 2 = %.1f\n", 7.0 / 2);
printf("max + 1 = %lld\n", big + 1);
7 / 2 = 3
7.0 / 2 = 3.5
max + 1 = -9223372036854775808
This row goes to Python, and it is not close. A Python integer is as large as it needs to be; 2**200 is an ordinary value you can print. A CX+AI .i is 64 bits, and past that it wraps — silently, the way C does, which is worse than warning. If your program does arbitrary-precision arithmetic, that is a capability you would be giving up, and no amount of speed buys it back.
Now the loop, because it is the same decision seen from the other side. Sum of the multiples of 3 or 5 below 20,000,000, same machine, same afternoon; all three print 93333316666668.
total = sum(i for i in range(n) if i % 3 == 0 or i % 5 == 0)
for (i = 0; i < n; i = i + 1) {
if (i % 3 == 0 || i % 5 == 0) { sum = sum + i; }
}
| time | |
|---|---|
| Python 3.14, the generator above | 1.257 s |
| Python 3.14 + numpy 2.4.3, vectorised | 0.236 s |
| CX+AI 3.1, the loop above | 0.016 s |
Read the middle row first, and then read what it costs. Vectorising is what a Python programmer actually does here, and it takes five sixths of the gap away without leaving Python. But numpy buys that speed by replacing the unbounded integer with a machine one — and np.int64(2**63 - 1) + 1 answers -9223372036854775808, the same wrap as the CX line above. It warns where CX+AI does not; the arithmetic is identical.
So the honest shape of this is not CX+AI against Python. It is fixed-width against unbounded, and you already reach for fixed-width when the loop matters — the middle row is that trade, taken deliberately, by you. What differs is where it sits: a second array library and a second way of writing the loop, or the ordinary integer of the language, in the file you were already in.
The first row is what is left when the shape stops vectorising. A loop whose next step depends on the last has no mask to build, and there the middle row goes away and Python's own answer is the top one.
Types are a suffix, and they are how the language avoids asking you to convert anything. n.i is a 64-bit int, s.s a string, f.f a double.
n.i = 7;
s.s = "seven";
f.f = 0.5;
printf("%d %s %.2f\n", n, s, f);
7 seven 0.50
You will write more type suffixes than you are used to and far fewer conversion calls, because the declared type is what a value is read as:
json cfg = parseDoc("{ \"port\": 8080, \"ratio\": 0.75 }");
port.i = cfg["port"]; // read as int
ratio.f = cfg["ratio"]; // read as float
text.s = cfg["port"]; // read as string -- same field, same document
printf("%d %.2f [%s]\n", port, ratio, text);
8080 0.75 [8080]
No int(), no float(), no str(). The trade is deliberate and it is the subject of the Coercion Guide.
A document can be written as itself. _json is a block, not a string — parsed when the file is compiled, so a malformed one is an error on its own line rather than an exception at three in the morning.
Behaviour can be data. A rule is a string the program compiles and runs while it is running, in-process. There is no Python equivalent that is also safe; see Rules that watch your data.
Braces, not indentation. There is no significant whitespace anywhere.
Integer division truncates, because this is C's arithmetic and CX+AI does not soften it. Both forms are shown, with what they print, under the number, and what it costs above.
If you have been away from C for a while, that is the one that will bite. It is not a bug and it will not be changed: a language that answered differently would make a C programmer wrong about his own craft.
Strings count from 0, like everything else. substr(s, 0, 4) is s[0:4]; there is no second convention for the string cutters.
printf("[%s]\n", substr("Valletta", 0, 4));
[Vall]
A variable's type is fixed where it is declared. A name does not become a different kind of thing later. That is what lets the compiler pick the right read at every use, and it is the price of the conversion calls you are no longer writing.
A string is not a number wearing a coat.
typed.s = "42";
printf("%d\n", (int)typed);
42
int n = "42"; does not compile. Parsing can fail, and a failure that returns 0 is indistinguishable from the number zero — so CX+AI makes you ask, and names the builtin that asks.
No classes. No decorators. No comprehensions. No generators. No pip.
Where you would reach for a package there is usually a builtin — more than eight hundred of them, covering strings, JSON, files, networking, graphics, compression and the AI surface. Where there is not, C is one block away:
_C{
/* raw C, compiled into the same binary */
}
That escape hatch is supported and load-bearing rather than an admission of defeat. CX is C eXpanded: everything C can do is still reachable.
The honest list of what you will miss, in the order you will miss it: comprehensions, keyword arguments, and the standard library's long tail. What you get for it: a single native binary with no runtime to install, the same source running in a browser tab, and a program that can change its own behaviour while it runs.