Behaviour as text, checked at build, changed while it runs
The behaviour of a program is usually compiled into it. Here it can be a string — one you wrote, or one that arrived while the program was running.
One page. Everything here was run.
A rule is a short piece of C that runs against one document. You hand it the document and the rule text; CX+AI compiles that text to register-VM bytecode in-process and runs it.
There is no system(), no popen(), no dlopen() anywhere on that path. Nothing is written to disk, no compiler is invoked, no library is loaded. That is why the same program works in a browser tab with no toolchain installed — and it is why a reply from a model, which is also just a string, can drive it.
There is nothing to turn on. Calling a rule is the instruction: the compiler sees ruleExec in your source and links the rule engine into the binary. A program that uses no rules carries none of it.
#pragma rules c still exists, and is for the one case the compiler cannot see — a program whose rule text arrives at run time from a file, a socket or a model, with no rule call written down anywhere in it. That program says so:
#pragma rules c
_rules is a block, so the body is the C it is — no escaped quotes — and it is checked when this file is compiled. A typo in it is an error on its own line.
_json baskets {
[
{ "ref": "A-1174", "total": 18.00, "ship": 0 },
{ "ref": "A-1176", "total": 96.00, "ship": 0 }
]
}
_rules FLAT {
{
e["ship"] = 4.95;
}
}
json order;
foreach baskets {
order = jsonGet(baskets);
ruleExec(order, FLAT);
printf("%s ship %.2f\n", order["ref"], order["ship"]);
}
A-1174 ship 4.95
A-1176 ship 4.95
e is whichever document the rule is run against. That is the whole interface. Flat rate, both baskets — remember the second number.
Now the same program, same binary, same baskets — and a different policy, which this time is an ordinary string, because that is honestly what a rule arriving from a config file, an operator or a model is.
_json baskets {
[
{ "ref": "A-1174", "total": 18.00, "ship": 0 },
{ "ref": "A-1176", "total": 96.00, "ship": 0 }
]
}
string arrived = "{ if (e[\"total\"] >= 50) { e[\"ship\"] = 0; } else { e[\"ship\"] = 4.95; } }";
json order;
printf("policy is %d characters\n", strlen(arrived));
foreach baskets {
order = jsonGet(baskets);
ruleExec(order, arrived);
printf("%s ship %.2f\n", order["ref"], order["ship"]);
}
policy is 71 characters
A-1174 ship 4.95
A-1176 ship 0.00
Nothing was rebuilt. One variable held different text, and the second basket now ships free. That is the whole idea, and it is worth reading the two transcripts side by side rather than taking the sentence for it.
_rules NAME { ... } declares an ordinary string. It goes through the same frontend that ruleExec uses at run time, just earlier. So:
_rules for a policy you author. You get the build-time check for free, and no escaped quotes.They meet at one frontend, which is why a rule that works one way works the other.
A bad rule does not stop your program. ruleExec prints a complaint and carries on — with the rule silently never firing. That reads like a balance problem or a data problem, not a compile error, and it is the single hardest failure in this system to attribute.
Which is the argument for _rules wherever you can use it: a rule checked at build time cannot fail this way, because it never gets to run time broken.
A rule is a string, so a rule can be kept. Say so, and the program gets a store:
#pragma rules persistence yes
It is a json file — <programname>.rules unless #pragma rulesfile "name" says otherwise — read at startup, before your first line runs. rulePromote(rule) puts a rule in it and makes it live in the same call, so a rule learned in this run is already usable in this run and is there again in the next one.
A rule that does not compile is written to the store as well, marked failed, with the refusal beside it:
{ "name": "r1", "body": "{ system(\"nope\"); }", "status": "failed",
"error": "cx C-rule: system is not allowed ..." }
That is deliberate, and it is the more useful half. The store records what was tried — which is how you see what a model reached for and what the sandbox refused, instead of rediscovering the same refusal next week. A failed entry is reported once at load and never applied.
The file is text. Read it, edit it, delete a line you do not want. ruleStore() hands your program the same array as data, so it can show its own vocabulary. And nothing watches the file: a store is read at startup, so what you read is what runs.
Every rule is written over one parameter, and by default it is called e. If your rules are about ships, say so:
#pragma rules entity ship
Then a rule reads ship["hull"]. The default never changes for a program that says nothing.
One honest edge, because rule text travels: a rule written over ship runs only where the entity is called ship, and it will not tell you when it does not. A name a rule does not have is born as a rule-local document rather than refused, so such a rule writes somewhere harmless and your real document is untouched. That holds for _rules blocks too. If you rename your entity, the rule text is yours to keep in step.
Everything above hands the rule its document. That works while the answer is in that document, and gets awkward the moment it is not: the program has to guess in advance what the policy will want, fetch it, and attach it as another field. Guess wrong and the rule cannot be written — you change the host and rebuild, which is the thing rules exist to avoid.
A rule can write a query instead.
The program offers a container; the rule queries it. Those are two different jobs, and only the first one is the host's:
json orders;
json ctx;
orders = parseDoc("[{\"id\":1,\"customer\":\"ada\",\"total\":250},"
"{\"id\":2,\"customer\":\"bob\",\"total\":90},"
"{\"id\":3,\"customer\":\"ada\",\"total\":410}]");
sqlTable("orders", orders);
_rules ASKS {
{
json rows;
rows = sqlquery("select id, total from orders"
" where customer = 'ada' order by id", 0);
e["found"] = rows->count;
e["first"] = jsonasint(jsonmember(jsonelement(rows, 0), "total"));
}
}
ctx = parseDoc("{\"found\":-1,\"first\":-1}");
ruleExec(ctx, ASKS);
printf("found %d, first %d\n", ctx["found"], ctx["first"]);
found 2, first 250
That is real SQL — joins, group by, aggregates — running over a JSON document that is already in memory. There is no database file and nothing was copied anywhere.
Registration is the boundary. sqlTable is not available inside a rule, and that omission is the point rather than an oversight. A rule that could register a container could grant itself reach over anything it can name. The host offers; the rule queries. Try it and the build stops:
cx C-rule: sqltable is not allowed (line 3, col 23)
Values go in as ?, never as text. Build the arguments as a JSON array — jsonArrStr, jsonArrInt and friends are on the rule whitelist for this reason, not as a convenience:
json p;
p = jsonarr();
jsonarrstr(p, name);
rows = sqlquery("select id from orders where customer = ?", p);
Concatenating a value into the statement text is the injection door, and it matters more here than in ordinary code: the author of a rule is often a model. A rule that could query but could not build an argument array would have string concatenation as its only option.
One spelling to know. Count a rowset with rows->count. jsonSize is refused on a typed handle and would answer 1 for a three-row result if it were not — the refusal is doing real work.
When a statement will not prepare, sqlQuery returns nothing and sqlError() says why, in SQLite's own words. Both are on the whitelist, because a rule that cannot see why it was refused cannot correct itself — and neither can a model rewriting it.
A rule is C, but it is a subset of C — the sandbox is the language boundary rather than a permission list bolted on afterwards. It can read and write the document it was given, do arithmetic, and query the containers you registered; it cannot open a file, call out, register anything further, or reach data you did not offer it.
That is what makes the AI story safe enough to be interesting: a model's reply is text, text becomes behaviour, and the behaviour it becomes can only touch the document it was pointed at. See Ask a model, then act on the answer.
sqlTable offers.