clonemarker(src, dst)copy one marker region's code into another
src | the marker id to copy from |
dst | the marker id to copy to |
returns non-zero on success
cloneMarker(1, 2);
Every builtin, drawn from the source
clonemarker(src, dst)copy one marker region's code into another
src | the marker id to copy from |
dst | the marker id to copy to |
returns non-zero on success
cloneMarker(1, 2);
code_size()report how many bytecode scratch slots are addressable
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. Seeded lazily, so a program that only ever reads still sees a non-zero window.
returns the live capacity of the scratch table (always > 0)
n = code_size();
delmarker(marker)drop a marker region
Marks the id free. Answers whether it actually removed anything, so a caller can tell a delete from a no-op -- it returned nothing until 2026-08-25.
marker | the marker id |
returns 1 if a marker was dropped, 0 if there was nothing at that id
delMarker(1);
generatecode(task, marker)ask the AI to write the body for a marker region
Runs the prompt through the same query path as ai_call, so the cache, provider and fallback chain all apply.
task | a description of what the code should do |
marker | the marker id to fill |
returns non-zero on success
generateCode("clamp hp to 0..100", 1);getcode()read back the code most recently installed by setCode or generateCode
The exact inverse of setCode, and the reason generateCode is usable at all: it lets you LOOK at the bytecode the model wrote before replaceCode commits it to a marker. `generateCode(task, 1); println(getCode()); replaceCode(1);` is the whole review loop. It reads the pending buffer, not a marker -- the call takes no arguments, so there is no region for it to name.
returns the pending code text, or an empty string if nothing is pending
s.s = getCode();
hasmarker(marker)test whether a marker id names a live region
marker | the marker id |
returns non-zero if the marker exists
if (hasMarker(1)) { ... }markercount()count the live marker regions
Walks the marker table and answers how many ids currently hold installed code. Real on BOTH backends since 2026-08-25: it used to bind only on the register VM, to a stub that answered 0 however many markers were installed, while the native side refused it outright.
returns the number of markers with code installed; 0 if none
n = markerCount();
peek_code(addr)read the opcode field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's opcode field, or the out-of-range sentinel -1 beyond the allocated table
peek_code(0);
peek_flags(addr)read the flags field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's flags field, or the out-of-range sentinel -1 beyond the allocated table
peek_flags(0);
peek_funcid(addr)read the function-id field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's function-id field, or the out-of-range sentinel -1 beyond the allocated table
peek_funcid(0);
peek_i(addr)read the 64-bit `i` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's 64-bit `i` operand, or the out-of-range sentinel 0 beyond the allocated table
peek_i(0);
peek_j(addr)read the `j` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's `j` operand, or the out-of-range sentinel 0 beyond the allocated table
peek_j(0);
peek_n(addr)read the `n` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT.
addr | the PC slot to read |
returns the slot's `n` operand, or the out-of-range sentinel 0 beyond the allocated table
peek_n(0);
poke_code(addr, value)write the opcode field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_code(0, 7);
poke_flags(addr, value)write the flags field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_flags(0, 7);
poke_funcid(addr, value)write the function-id field of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_funcid(0, 7);
poke_i(addr, value)write the 64-bit `i` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_i(0, 7);
poke_j(addr, value)write the `j` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_j(0, 7);
poke_n(addr, value)write the `n` operand of a bytecode scratch slot
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. A negative address, or one past the configured cap, is not written.
addr | the PC slot to write |
value | the value to store |
returns void
poke_n(0, 7);
replacecode(marker)swap a marker region's body for the code most recently installed
marker | the marker id |
returns non-zero on success
replaceCode(1);
rt_aifunc_count()count the registered run-time AI functions
The first half of the enumeration pair: it bounds the index that rt_aifunc_name(i) takes, so valid positions are 0 to rt_aifunc_count() - 1. The verbs themselves take a NAME, not a position -- read the name with rt_aifunc_name and act on that.
returns how many run-time AI functions are registered
for (i = 0; i < rt_aifunc_count(); i = i + 1) { println(rt_aifunc_name(i)); }rt_aifunc_name(index)the name of the i-th registered run-time AI function
The second half of the enumeration pair, and the ONLY place an index still means anything in this family: it converts a position into the identity rt_runAiFunc and rt_destroyAiFunc take. Out of range answers the empty string rather than failing, because the loop that reaches it has already checked its own bound -- and "" is not a legal function name, so it stays distinguishable from every real answer.
index | a position in 0 to rt_aifunc_count() - 1 |
returns the function's name, or an empty string if the index is out of range
s = rt_aifunc_name(0);
rt_callaifunc()run the FIRST registered run-time AI function
Takes no arguments -- that is fixed at its binding -- so "the first registered" is the only thing it can mean. Prefer rt_runAiFunc(name), which says which function it means; this exists because the name is published.
returns the first function's result, or -1 if the registry is empty
v = rt_callAiFunc();
rt_createaifunc(name, prompt)register an AI-authored function under a name at run time
name | the function name |
prompt | the task description the AI is asked to implement |
returns 0 on error, 1 if the function was newly registered, 2 if it replaced one of the same name -- NOT a handle (cxtest 152 asserts rc == 1)
rt_createAiFunc("clamp", "clamp to 0..100");rt_destroyaifunc(name)drop a registered run-time AI function by name
The registry is addressed by NAME, like the rest of the AI family (ai_call_func, ai_dump_asm) -- his ruling of 2026-08-24. rt_createAiFunc still returns 0/1/2 for error, new registration, or replaced, and is still not a handle. To ENUMERATE, use the pair rt_aifunc_count() and rt_aifunc_name(i): ask how many, ask what the i-th is called, then act on that name. An index is a position you read a name FROM, never a target you act ON -- which is also why it is safe that destroying compacts the table. It ANSWERS whether anything was dropped, and still records the detail in ai_last_error: 1/0 says WHETHER, the error text says WHICH name and how many were registered. Neither replaces the other -- a caller should not have to interrogate a separate channel to learn that its call did nothing.
name | the function's name; nothing is dropped if no function has it |
returns 1 if a function of that name was dropped, 0 if none was registered
if (rt_destroyAiFunc("triple") == 0) { println("nothing to drop"); }rt_disasm()print the AI subsystem's current disassembly to stdout
The shared implementation the register VM's own rt_disasm uses, so both backends print the same thing.
returns void
rt_disasm();
rt_runaifunc(name)run a registered run-time AI function by name
The registry is addressed by NAME, like the rest of the AI family (ai_call_func, ai_dump_asm) -- his ruling of 2026-08-24. rt_createAiFunc still returns 0/1/2 for error, new registration, or replaced, and is still not a handle. To ENUMERATE, use the pair rt_aifunc_count() and rt_aifunc_name(i): ask how many, ask what the i-th is called, then act on that name. An index is a position you read a name FROM, never a target you act ON -- which is also why it is safe that destroying compacts the table. The function runs with argument 0. To pass an argument, use ai_call_func(name, arg) instead.
name | the function's name, as given to rt_createAiFunc or ai_parse_bytecode |
returns the function's result, or -1 if no function of that name is registered (-1 rather than 0, because 0 is a legitimate result)
v = rt_runAiFunc("triple");setcode(src)install CX source text as the body of a codeswap marker region
src | the replacement source text |
returns void
setCode(src);
raise(type, source)fire an event of the given type from CX code
Calls cx_event_raise: uses `source` as the event context/payload passed to OnEvent handlers (falls back to the shared event ctx when source is 0), then runs every handler registered for that type. It also sets a legacy ctx["source"] field, which is deprecated and warns once.
type | event type id to raise |
source | bound-object / json entity handle passed to handlers as the payload (0 = use the shared event context) |
returns void
raise(50, e);
airule(entity, schedule, trigger, prompt)ask the AI to author a rule and register it on an entity, in one call
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. THE ONE-LINER THE PRODUCT IS ABOUT: an LLM query and a rule registration fused, so a sentence becomes autonomously running behaviour with no recompile. What the AI returns is a rule STRING -- data you can read, edit and save, not opaque code. ANSWERS AT THE CALL SITE -- 1 registered, 0 refused (his ruling 2026-08-25: a door handed rule TEXT can be handed something malformed at RUNTIME, so its caller must be able to learn nothing was registered; a door handed a compiled FUNCTION cannot, which is why funcAdd stays silent). Every refusal has already printed its reason, so the number is an answer and not a diagnosis. aiRule PROPAGATES that answer rather than reporting success for producing text: the authoring loop may yield no valid rule, and the rule it does yield may still fail to compile.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule |
prompt | what the behaviour should do, in plain language |
returns void
aiRule(e, 60, "1", "restock when stock falls below the reorder point");
arm(entity, preload)bake an entity's persist lanes by running a preload rule once
The preload half of the rules ABI: it sizes the entity's typed lanes to the compiler's persist counts, runs `preload` ONCE so its writes land in those lanes, and stamps the source document's revision. Every later ruleExec on that entity then READS the baked lanes instead of recomputing. A fire whose source document has changed re-bakes at the fire boundary. Needs `#pragma rules c`.
entity | the entity's json handle |
preload | the preload rule source |
returns 1 when armed; 0 on failure -- and the entity stays UN-armed, so a later persist read faults loudly rather than reading a half-baked table
arm(e, "persist float th; th = 0.8;");
automationrun(entity, quitkey)tick flat out until a named key on an entity goes non-zero
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The console equivalent of riding a graphics frame loop: hand over control in one line instead of writing the while. A rule flips the key when the work is done.
entity | the entity's json handle |
quitkey | the entity key a rule sets to stop the loop |
returns void
automationRun(e, "done");
automationtick()advance the clock and fire every rule that is due and triggered
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The per-frame call. A graphics program gets this driven for it once the first rule is added.
returns void
automationTick();
eventctx()the json object the dispatcher fills with the current event
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. A gate rule is evaluated against THIS -- `event["key"]`, `event["mx"]` and so on.
returns a json handle to the current event context
e = eventCtx();
eventfire(type)dispatch an event through the registry
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. Every entry registered for the type is considered: its gate rule is evaluated against the event context, and the handler runs only if the gate passes.
type | the event type code |
returns the result of the dispatch
eventFire(1);
eventsettable(tbl)replace the whole event table with a caller-parsed json array
The two-way, AI-authorable load: a program (or an AI) builds the handler table as json and installs it in one call, instead of accumulating it through OnEvent. HANDLER NAMES ARE CASE-INSENSITIVE, like every CX identifier: an entry's `"fn"` refers to a handler registered from a CX function name, and the scanner folds that name, so the table's string is folded to match. `"OnHit"`, `"onhit"` and `"ONHIT"` all name the same handler -- two handlers differing only by case cannot exist, so nothing legitimate is lost. EVERY `"fn"` IS RESOLVED HERE, AT LOAD. A name that matches no registered handler is a loud CX-E5033 and STOPS the run. It is checked at load rather than at first fire because an unresolvable handler used to fail by simply never running -- the quietest failure mode there is, and one that let a run start with a dead handler and look healthy (FP4). Entries with only a `"do"` rule carry no name and are not checked. The registry keeps `tbl` for the process lifetime and increfs it, so a table built inside a function survives that function's scope exit.
tbl | json array indexed by event type; each element an array of entries ({"gate": rule, "fn": handler-name} or {"gate": rule, "do": rule}). |
eventSetTable(jsonLoad("events.json"));eventtable()the json array holding every event binding
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. Indexed by event type (1 keyboard, 2 mouse, 3 timer, 4 window; 4..255 are free for your own). Created on first use.
returns a json handle to the event table
t = eventTable();
funcadd(entity, schedule, trigger, fn)register a compiled CX function on an entity, on the same schedule and trigger
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The fast, heavy path beside the data path -- the function is `function f.v(json e)` and receives the entity. **NATIVE ONLY**: native carries a function value as a C address, the register VM as a bytecode index, so on the VM this hard-errors rather than calling the wrong thing. Windowed games run native.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule |
fn | a `function f.v(json e)` |
returns void
funcAdd(e, 1, "1", &onTick);
oneventrule(type, gate, rule)bind a data-only rule handler to an event type
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. Both the gate and the handler are RULES -- pure data, so the binding is json-native, loadable and AI-authorable.
type | the event type code |
gate | a condition rule; "1" or empty means always |
rule | the handler rule to run when the gate passes |
returns void
onEventRule(1, "event[\"key\"] == 27", "quit = 1");
ruleadd(entity, schedule, trigger, rule)register a rule on an entity, to run on a schedule behind a trigger
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. ANSWERS AT THE CALL SITE -- 1 registered, 0 refused (his ruling 2026-08-25: a door handed rule TEXT can be handed something malformed at RUNTIME, so its caller must be able to learn nothing was registered; a door handed a compiled FUNCTION cannot, which is why funcAdd stays silent). Every refusal has already printed its reason, so the number is an answer and not a diagnosis.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule; the rule runs only when this passes |
rule | the rule source text to run |
returns void
ruleAdd(e, 30, "hp < 20", "hp = hp + 5");
ruleauthmock(text)queue a canned authoring response instead of calling the AI
The AI rule-authoring loop. While the queue is non-empty, ruleAuthor consumes it INSTEAD of contacting a provider -- one push per attempt, FIFO. What makes an authoring test deterministic, offline and free, and equally the seam for feeding rules from a source that is not an LLM.
text | the response to hand back for the next attempt |
returns void
ruleAuthMock("{ speed = speed / 2; }");ruleauthor(task)ask the AI for one rule, validate that it compiles, and retry on a decline
The AI rule-authoring loop. The returned text is a `{ ... }` body over `json e`. A reply that does not compile in the rules subset is DECLINED and retried with the compile error appended, up to a bounded number of extra attempts. Every attempt and decline is recorded for ruleAuthStats.
task | the plain-language request |
returns the validated rule text; the EMPTY string on final failure -- loud, never a silent no-rule
body.s = ruleAuthor("halve speed below 20 hp");ruleauthreset()clear the authoring feed, drain the mock queue and forget the fire warnings
The AI rule-authoring loop. Test isolation, or the start of a fresh authoring session.
returns void
ruleAuthReset();
ruleauthstats()read the authoring loop's evidence feed as a json document
The AI rule-authoring loop. Returns attempts, declines, whether the final attempt succeeded, the last decline's class and reason, and a record per attempt. The class is a stable string -- `pointer`, `call-denied`, `arity`, `struct`, `nonscalar`, `foreach-colon`, `syntax`, `empty`, `other` -- so a test can assert WHY the model was refused, not merely that it was.
returns a NEW json handle you own
s = ruleAuthStats();
rulecachecount()how many distinct rule strings are currently compiled and cached
The cache-effectiveness instrument: compiling N distinct rules M times should cache N. Use it to prove a hot loop is not recompiling.
returns the number of cached rule programs
n = ruleCacheCount();
ruleentity(name, obj)LINK A NAME TO AN OBJECT, so a rule reaches the world without
being handed it (RULEX-325 Part E; his catch: "link an entity with a variable/global"). The entity NAME is the join key the rules already had -- `#pragma rules entity ship` names it, a rule body spells its parameter with it, and a stored key carries it. This says which OBJECT that name currently is. TWO SPELLINGS, ONE MECHANISM (his ruling 2026-09-06). `ruleEntity(obj)` binds the name this program DECLARED; `ruleEntity("name", obj)` binds any name. The one-arg form is a COMPILE-TIME REWRITE -- the compiler supplies the declared name as a literal, exactly as rulec_fixup_raise already turns a rule's `raise(type)` into `raise(type, <entity>)` -- so there is one C function here and no runtime question about which name was meant. **A one-arg call in a program that declared no entity is a NAMED REFUSAL at compile time, never a silent bind to the default `e`** (his ruling, FP4). BINDING AN ALREADY-BOUND NAME REPLACES IT. It does not stack, there is no scope and there is no unbind -- a name is whatever it was last bound to. Said here because it is the only place a reader will look, and because the opposite (a stack) is what a reader coming from a scoping language will assume.
name | the entity name. Case-insensitive, matching the rest of the rule system; empty is refused loudly rather than bound. |
obj | the object -- a json handle, the shape a rule body reads. |
returns 1 when bound, 0 when refused (empty name).
ruleEntity(ship); // the declared name
ruleEntity("world2", other); // any nameruleentityof(name)the object currently linked to a name, or -1
-1 RATHER THAN 0, and the difference is load-bearing: 0 is a legal json handle, so a caller testing `if (ruleEntityOf(n))` on a 0-handle would read a live entity as a missing link. The vtable checks `< 0`.
name | the entity name (case-insensitive). |
returns the linked handle, or -1 when nothing is bound to that name.
e.i = ruleEntityOf("ship"); if (e < 0) { ...no link... }ruleexec(entity, rule)run a rule against an entity and return its value
The explicit one-shot path, beside the registered one. **COMPILE-CACHED**: the same rule string compiles ONCE, content-keyed for the life of the process, and runs cached after that -- so calling it in a loop does not recompile. Member writes to `e` PERSIST on the bound document.
entity | the entity's json handle |
rule | the rule source text |
returns the rule's `return` value as a float; a rule returning a string reads as 0
v = ruleExec(e, "return hp * 2;");
rulefire(entity, entry)run one rule entry, using its compiled form if it has been promoted
The dispatcher that honours promotion. Given a json rule entry `{ name, body, promoted }`: if it is marked promoted AND its name is registered, the COMPILED function runs with no recompile of the text; otherwise the body runs through the cached text path. A promoted mark with no registered function -- promote ran, the rebuild did not -- falls back to text AND warns LOUDLY once per rule name: graceful for the player, visible for the developer.
entity | the entity's json handle |
entry | a json rule entry { name, body, promoted } |
returns the rule's value as a float, on both paths
v = ruleFire(e, entry);
rulepromote(rule | rules, path)promote a rule to a more durable form -- the store, or generated source
TWO FORMS UNDER ONE NAME, picked by how many arguments you give it, because both do the same thing at different distances: they graduate a rule out of the moment it was written in. **`rulePromote(rule)`** appends the rule to the program's rule store AND makes it live in the same call -- the store is durable, so a rule learned in one run is there for the next. It needs `#pragma rules persistence yes`, and the compiler refuses the call without it rather than let a promote go nowhere. A rule that does NOT compile is written to the store anyway, marked `failed`, with the frontend's own sentence beside it: the store records what was TRIED. The return value answers *did this rule work*, never *did anything happen*. **`rulePromote(rules, path)`** emits a compilable `.cxi` from a json array of authored rules -- one `function <name>.f(json e) <body>` per entry plus a registration block. **HOST ONLY, never rule-callable**: a rule that could promote itself would be capability escalation, so this name must never appear on a rule whitelist. ALL-OR-NOTHING -- every body is compile-checked first, a file carrying no generated marker is never overwritten, and if any rule fails nothing is written.
rule | rules | the rule TEXT to store, or the json array of entries to emit |
path | (two-argument form only) the .cxi to write |
returns 1 on success, 0 on failure -- and for the one-argument form a failed rule is still recorded
rulePromote("{ e[\"hull\"] = 100; }");rulestatus()what the last ruleExec() actually did, which its return value
cannot tell you. ruleExec returns the RULE'S OWN value (`{ return hp > 0; }` is how a gate is written), so a rule that applies, a rule that changes nothing, a rule that is refused and a rule that does not compile all answer 0 -- the refusals print, loudly, but a program cannot branch on a printout. This answers the other question, and it is written once per ruleExec. 1 APPLIED the rule ran and WROTE the json it was given. Exactly that: the document's mutation counter moved. Writing the same value again still counts as a write, and a rule that changes something other than `e` -- a persist lane, a global -- does not. 2 NOCHANGE the rule ran and wrote nothing to that document. 3 REFUSED CX declined the rule's FORM or the capability: a blank rule, the retired non-brace DSL (CX-E5027), or a binary built with no `#pragma rules c`. 4 DIDNOTPARSE the C frontend refused the BODY. A DENIED CALL (`system()` in a rule) is here too, with syntax errors: separating them is what ruleAuthor's own stats already do, and a second classifier would be a second answer to one question. 0 NOTRUN nothing has run yet. THE AMBIENT READER IS DELIBERATE, and it is the door the filed `onerror()` arc consumes: a handler reached from somewhere else in the program needs the fact without having been at the call site. Nothing wires it there yet -- that design is still open -- but when it lands, its input is already standing.
returns one of the five values above.
ruleExec(ship, rule); if (ruleStatus() == 4) { println("bad rule"); }rulestore()the program's rule store, as data you can walk
A json ARRAY of entries in the shape ruleFire already reads -- `{ name, body, promoted }` -- plus `phrase`, `status` ("ok" or "failed") and, on a failed one, the `error` the frontend gave. The store is TEXT ON DISK and is meant to be read, edited and curated by hand; this is the same array the running program holds, so an entry promoted a moment ago is already in it. FAILED ENTRIES ARE IN HERE TOO, deliberately: the store records what was TRIED, not only what worked. They are reported once at load and are NOT applied, so walking this array is how a program shows its author what its AI reached for and what the sandbox refused.
returns the entry array's json handle, or 0 if the program declared no store (`#pragma rules persistence yes`).
json s = ruleStore(); foreach s { ... }rulestorecount()how many entries the rule store holds
Loaded entries plus anything promoted so far this run, failed entries included. 0 when no store was declared, and 0 on a first run that has learned nothing yet -- those are the same number and deliberately so: neither is an error, and a program that wants to tell them apart asks ruleStore() for 0.
returns the entry count.
printf("%d rules known\n", ruleStoreCount());rulestoredoc()the whole store as one document
A store is `{ "options": { ... }, "rules": [ ... ] }`, and the vtable takes that DOCUMENT -- it reads both halves from it. `ruleStore()` answers only the rules array and `ruleStoreOptions()` only the options, so without this a program that LOADS a store file could not drive the vtable at all.
returns the document handle, or 0 when no store is open.
ran.i = __vtRun(wish, ruleStoreDoc(), "ship");
rulestoreoptions()the store's `options` object
A store is `{ "options": { ... }, "rules": [ ... ] }` (his ruling 2026-09-06). The options hold the matcher's settings -- `match`, `trigram_min` and the length rule -- so they travel with the rules they govern rather than with whichever program loads them, and so the BAKE can read them at build time.
returns the options object handle, or 0 when no store is open.
json o = ruleStoreOptions();
rulestorereload()read the store file again, now
The store is read ONCE, in main's prologue. A rules file edited by anything else while the program runs -- a person, another process, a deploy -- is invisible until this is called. **Explicit, never watched** (his ruling 2026-09-06: a file watch is a later item): the program says when to look. ⚠ A FAILED RELOAD KEEPS THE OLD TABLE RUNNING and names the reason. This is the whole difference between it and opening twice: OPEN binds an empty store when the file is broken, which is right at startup and catastrophic mid-run, because a typo would silently disarm every rule a running program has. The worst a bad file can cost you here is the edit.
returns 1 when the store was replaced, 0 when it was not (and the old one is still live).
if (!ruleStoreReload()) { ...the previous rules are still running... }ai_set_options(cfg)configure AI from ONE json document, per model
Replaces a scatter of individual setters with a document describing every model the program may reach. `defaults` holds what every section inherits; `models` is an ARRAY of sections, and **its order is the fallback chain** -- an array rather than an object because sequence is load-bearing and json promises no member order. A section carries its own `host` and `port`, so **an endpoint belongs to the model that owns it**. That is the point of the shape rather than a convenience: the older `ai_set_url` applied to every provider the chain walked, and per-section options make that unexpressible. Keys are checked when the document is read, not when a request is sent. A key misspelled inside a section is REFUSED and named; a key that is documented but not yet built is refused as "not yet wired", which is a different sentence on purpose -- the reader of the first is hunting their own typo, the reader of the second is waiting for a feature. Keys in `defaults` are PERMISSIVE: they apply where they mean something and are skipped where they do not, because a mixed local-and-hosted document would otherwise refuse itself.
cfg | a json document (an object with optional `defaults` and `models`) |
returns 1 when the document was accepted and is now in force; 0 when it was refused, with ai_get_error() naming the key and the reason. Nothing is applied on a refusal -- the previous configuration stands, so a bad document cannot leave a program half-configured.
if (ai_set_options(cfg) == 0) { println(ai_get_error()); }ai_async_ready(id)ask whether a background job has finished
WINDOWS ONLY. On Linux and macOS the async family is not implemented: start and wait answer -1, pending answers 0, and result answers the unknown-job marker. Use the synchronous query builtins there.
id | the job id from ai_call_async |
returns 0 while pending, 1 when done, -1 for an unknown id
if (ai_async_ready(id) == 1) { ... }ai_async_result(id)collect a background job's reply, waiting if necessary
BLOCKS until the job finishes if it has not already, then RETIRES the job -- the id is spent and its slot is reusable, so a second call with the same id gets the unknown-job marker. An unknown id answers the string `ERROR: unknown job id N` rather than an empty one, so a caller can test for `ERROR` without having to distinguish failure from a genuinely empty reply. WINDOWS ONLY. On Linux and macOS the async family is not implemented: start and wait answer -1, pending answers 0, and result answers the unknown-job marker. Use the synchronous query builtins there.
id | the job id from ai_call_async |
returns the model's reply, or an `ERROR: unknown job id N` marker
r.s = ai_async_result(id);
ai_cache_clear()empty the response cache and reset its counters
Releases every cached value's reference; the hit and miss counters go back to zero.
returns void
ai_cache_clear();
ai_cache_get(key)read a cached reply by key
Counts a hit or a miss either way, so the counters reflect real lookups.
key | the cache key to look up |
returns the cached reply, or an EMPTY STRING on a miss -- a stored empty string is therefore indistinguishable from absence
v.s = ai_cache_get("q");ai_cache_hits()count cache lookups that found an entry
Reset by ai_cache_clear. Note that ai_cache_get performs a lookup, so it moves this counter.
returns the hit count since start or since the last clear
h = ai_cache_hits();
ai_cache_miss()count cache lookups that found nothing
Reset by ai_cache_clear. Note that ai_cache_get performs a lookup, so it moves this counter.
returns the miss count since start or since the last clear
m = ai_cache_miss();
ai_cache_put(key, value, ttl)store a reply in the response cache under a key
The cache is a PROCESS-LIFETIME owner and takes its own reference on the value, so the entry stays valid after the caller's string goes out of scope. Re-putting an existing key replaces the value. THE TTL ARGUMENT IS ACCEPTED AND IGNORED -- the cache has no clock and entries never expire.
key | cache key (typically the prompt) |
value | the reply text to store |
ttl | accepted for API compatibility and ignored -- there is no expiry |
returns void
ai_cache_put("q", "a", 60);ai_cache_size()count the entries currently in the response cache
returns the number of cached entries
n = ai_cache_size();
ai_cache_stats()read the cache counters as one formatted line
returns a string of the form `entries=N hits=N misses=N`
print(ai_cache_stats());
ai_call(prompt)send a prompt to the configured LLM and return its reply
Cache lookup (when enabled), provider auto-pick if none was set, the request, then the fallback chain if it failed. A successful reply is cached under the prompt text. On any failure the answer is an EMPTY STRING and the reason is readable from ai_get_error -- so test the result, do not assume it.
prompt | the prompt text |
returns the model's reply, or an empty string on failure
r.s = ai_call("...");ai_call_async(prompt)start a background LLM request and return a job id
WINDOWS ONLY. On Linux and macOS the async family is not implemented: start and wait answer -1, pending answers 0, and result answers the unknown-job marker. Use the synchronous query builtins there. Fails FAST with -1 when no API key is available, rather than starting a worker that cannot succeed. At most 32 jobs may be live at once.
prompt | the prompt text |
returns a positive job id; -1 with no key, no free slot, or on a non-Windows build
id = ai_call_async("Summarise this.");ai_call_func(name, arg)run an AI-authored function on the native AI mini-VM
The function must have been created by ai_parse_bytecode (or the codeswap path) under this name.
name | the AI function's name |
arg | one integer argument |
returns the function's integer result; 0 if the name cannot be read
r = ai_call_func("clamp", 130);ai_dump_asm(name)disassemble a named AI function back to readable text
The counterpart to ai_parse_bytecode: what the AI wrote, as the runtime holds it.
name | the AI function's name |
returns the disassembly text; an empty string if the name cannot be read
print(ai_dump_asm("clamp"));ai_get_error()read the reason the last AI operation failed
Set by ai_init and by the query family; an empty string means the last operation reported no error.
returns the error text, or an empty string
print(ai_get_error());
ai_has_key()report whether a usable API key is available for the current provider
True if the model's section in the ai_set_options document carries a "key", or the provider's environment variable is set and non-empty. A provider whose table row carries no environment key -- ollama, kwaai and custom -- always answers 1, because none is needed.
returns 1 if a key is available (or none is needed); 0 otherwise
if (ai_has_key()) { ... }ai_init()check that a provider and a usable key are configured
Auto-picks a provider from the environment if none was set (anthropic, openai, google, xai, deepseek, groq, mistral, cohere, ollama -- first key found wins). Sets the error text on failure. ollama, kwaai and custom carry no environment key in the provider table and so need none.
returns 1 when a provider and key are available; 0 otherwise, with ai_get_error explaining which is missing
if (ai_init() == 0) { print(ai_get_error()); }ai_parse_bytecode(name, src)assemble AI-authored bytecode text into a named callable function
name | the name to register the function under |
src | the bytecode source text |
returns the result of the assembler -- non-zero identifies the parsed function
ai_parse_bytecode("clamp", src);ai_pending()count background jobs that have not finished
WINDOWS ONLY. On Linux and macOS the async family is not implemented: start and wait answer -1, pending answers 0, and result answers the unknown-job marker. Use the synchronous query builtins there.
returns the number of live, unfinished jobs
n = ai_pending();
ai_set_provider(provider)choose the LLM provider for subsequent calls
Accepts anthropic, openai, google, mistral, cohere, xai, deepseek, groq, ollama, kwaai or custom, plus the aliases claude -> anthropic, grok -> xai and gpt -> openai. A key belongs to the model that uses it -- give it as "key" in that model's section of the ai_set_options document, where it cannot travel to a provider you did not name. Stored in a fixed runtime buffer; a value too long to fit is refused LOUDLY with CX-E5025 naming the limit rather than silently truncated.
provider | provider name or alias; an empty string clears both provider and key |
returns void
ai_set_provider("anthropic");ai_usage()what the AI has cost so far, as a json document
Totals across every provider (`in`, `out`, `calls`, `uncounted`), the same four plus `cap` per provider under `providers`, and the call that just happened under `last` (`provider`, `in`, `out`, `counted`). The numbers are the PROVIDER'S OWN accounting, read out of its reply, so they are what you will be billed for rather than an estimate. A reply that carried no usage block adds nothing to the totals and increments `uncounted` instead -- zero is a legal token count, so "the provider did not say" is never reported as "the call was free". Counts are per PROCESS; ai_usage_reset() clears them. Only providers that have been called or capped get a row.
returns a json document handle -- read it with jsonMember/jsonAsInt, or print it with exportDoc
u = ai_usage(); printf("%d tokens in
", jsonAsInt(jsonMember(u, "in")));ai_usage_reset()clear the AI usage counters, keeping the caps
Zeroes every provider's token totals, call count and uncounted count, and forgets the last call. THE CAPS SURVIVE: a cap is a policy your program declared and a counter is a measurement, so a program rolling a daily budget at midnight resets what it counted without disarming what it decided.
returns nothing
ai_usage_reset();
ai_wait(id)block until a background job finishes
WINDOWS ONLY. On Linux and macOS the async family is not implemented: start and wait answer -1, pending answers 0, and result answers the unknown-job marker. Use the synchronous query builtins there. Waits with no timeout.
id | the job id from ai_call_async |
returns 1 once the job is done; -1 for an unknown id
ai_wait(id);
eval(expr)evaluate an arithmetic expression held in a string, at runtime
Returns a FLOAT. Names in the expression come from the eval store (evalSet / evalGet / evalClear), which is separate from the program's own variables. An expression it cannot parse is LOUD -- CX-E5029 -- not a silent 0.
expr | the expression text to evaluate |
returns the value as a float; a parse failure raises CX-E5029
evalSet("x", 3.0); v.f = eval("x * 2 + 1");evalset(name, value)store a named float variable for the expression evaluator
Inserts or updates the value in a small case-insensitive table (max 64 vars, names truncated to 31 chars); these variables are what the eval() expression parser reads. A full table drops the insert silently.
name | variable name (case-insensitive, up to 31 chars) |
value | float value to store |
returns void
evalSet("a", 100);evalget(name)read a named float variable from the expression evaluator
Case-insensitive lookup in the eval variable table; yields 0.0 when the name is undefined.
name | variable name to look up (case-insensitive) |
returns the variable's stored float value; 0.0 if not defined
assertFloatEqual(evalGet("a"), 10.0);evalclear()clear all expression-evaluator variables
Resets the eval variable table to empty (count back to 0).
returns void
evalClear();
gpu_grid_new(x, y, w, h)create a native data-grid widget at x,y,w,h and return its handle
Reuses a free registry slot or grows the grid registry; the grid is a pure data structure until drawn.
x | left position in pixels |
y | top position in pixels |
w | width in pixels |
h | height in pixels |
returns int 1-based grid handle, 0 = failure
g.i = gpu_grid_new(60, 96, 840, 600);
gpu_grid_free(g)unconditionally destroy a grid handle now, freeing all its storage
Drops the ARC bucket directly then reclaims cells, columns, sections and text handles.
g | grid handle |
returns void
gpu_grid_free(g);
gpu_grid_decref(g)ARC-decrement a grid handle, reclaiming it when its refcount reaches 0
Codegen-emitted at scope exit for an owned `datagrid` local; the shared tracer calls the internal reclaim at refcount 0.
g | grid handle |
returns void
gpu_grid_decref(g);
gpu_grid_livecount()return the number of currently-open grid handles
Leak/regression-test signal (mirrors cxmf_live_count); a leaked grid also shows in the generic checks leak walk.
returns int count of live grid handles
int before = gpu_grid_livecount();
gpu_grid_bounds(g, x, y, w, h)reposition and resize an existing grid to x,y,w,h
g | grid handle |
x | new left position in pixels |
y | new top position in pixels |
w | new width in pixels |
h | new height in pixels |
returns void
gpu_grid_bounds(insp, 10, topY, w - 20, iH);
gpu_grid_cols(g, n)set the grid's column count, reallocating cell storage
n must be > 0; also (re)allocates the initial row buffer.
g | grid handle |
n | number of columns |
returns void
gpu_grid_cols(g, 6);
gpu_grid_rows(g, n)set the grid's row count, growing storage and resorting
Grows cell storage as needed, re-applies the active sort, and tails the view if follow mode is on.
g | grid handle |
n | number of rows |
returns void
gpu_grid_rows(g, NROWS);
gpu_grid_col(c, label, width, type, align)define column c with a label, width, type and alignment
type: 0 TEXT, 1 NUMBER, 2 CHECK, 3 BAR, 4 COLOR, 5 BADGE; align: 0 left, 1 centre, 2 right; width is clamped to a minimum of 24px.
c | column index (0-based) |
label | column header text (string) |
width | column width in pixels (min 24) |
type | column type 0-5 (TEXT/NUM/CHECK/BAR/COLOR/BADGE) |
align | text alignment 0 left, 1 centre, 2 right |
returns void
gpu_grid_col(g, 0, "Module", 220, GT_TEXT, 0);
gpu_grid_clear(g)empty all cells and section headers, resetting the grid to zero rows
Clears cell text/numbers/colours, drops section headers, and resets selection and scroll.
g | grid handle |
returns void
gpu_grid_clear(insp);
gpu_grid_set(r, c, text)set the text of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
text | cell text (string) |
returns void
gpu_grid_set(g, i, 0, dName[i]);
gpu_grid_num(r, c, v)set the numeric value of cell (r,c)
Value drives NUMBER display, CHECK on/off, and BAR percentage.
r | row index (0-based) |
c | column index (0-based) |
v | numeric value (float) |
returns void
gpu_grid_num(g, i, 2, dPower[i]);
gpu_grid_getnum(r, c)read back the (possibly edited) numeric value of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
returns float cell value, 0 if the handle or indices are out of bounds
pw.i = gpu_grid_getnum(g, sel, 2);
gpu_grid_gettext(r, c)read back the (possibly edited) text of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
returns string cell text, empty string if the handle or indices are out of bounds
selName = gpu_grid_gettext(g, sel, 0) + " (" + gpu_grid_gettext(g, sel, 1) + ")";gpu_grid_json(g, doc, col, row)fill a grid from a json ARRAY OF OBJECTS, one object per row
Keys map to columns BY POSITION, in the first element's own key order -- which is the DOCUMENT's order, so the column order is visible in the source rather than decided elsewhere. If the grid has no columns yet they are CREATED from those keys (label = the key, type inferred from the first value); if it already has columns they are respected and never relabelled, because this call fills a sheet, it does not restyle one. Rows GROW to fit and are never shrunk, so a summary row added afterwards survives a refill. A VALUE IS MAPPED BY THE COLUMN'S DECLARED TYPE, NEVER BY THE JSON'S, and that is forced rather than chosen: cx_grid's numeric formatter prints a whole number without decimals, so an invoice whose money columns were GT_NUM showed "720" beside "658.80". Into a TEXT column a number is therefore rendered at cx_print_decimals -- the program's own `#pragma decimals`, not a new knob -- and into a GT_NUM column it is stored as a number. THE WHOLE DOCUMENT IS CHECKED BEFORE ONE CELL IS WRITTEN. A refusal leaves the grid exactly as it was, because a half-filled sheet looks filled.
g | the grid handle |
doc | a json handle -- a document or a node; an ARRAY of OBJECTS |
col | the first grid column to write (0 = the leftmost) |
row | the first grid row to write (0 = the top) |
returns the number of rows written; 0 for an empty array or a dead handle
n.i = gridJson(g, doc, 0, 0);
gpu_grid_merge(r, c, cspan, rspan)merge cell (r,c) across cspan columns and rspan rows
Spans are clamped to at least 1 and to the grid bounds; covered cells are flagged and not drawn.
r | origin row index |
c | origin column index |
cspan | number of columns to span (>=1) |
rspan | number of rows to span (>=1) |
returns void
gpu_grid_merge(g, 11, 0, 6, 1);
gpu_grid_title(g, title)set the grid's title-bar text and enable the title bar
g | grid handle |
title | title text (string) |
returns void
gpu_grid_title(g, "MODULE DATABASE (cx_grid demo)");
gpu_grid_font(g, font_id)set the grid's default font
0 = raylib default pixel font; otherwise a loadfont() handle for crisp TTF text.
g | grid handle |
font_id | font handle (0 = default) |
returns void
gpu_grid_font(g, gFont);
gpu_grid_colfont(c, font_id)set the font used by every cell in column c
0 = inherit the grid font; overridden per-cell by gpu_grid_cellfont.
c | column index (0-based) |
font_id | font handle (0 = inherit grid) |
returns void
gpu_grid_colfont(g, 0, gFontBold);
gpu_grid_cellfont(r, c, font_id)set the font of one cell (r,c)
Cell font wins over the column font, which wins over the grid font; 0 = inherit.
r | row index (0-based) |
c | column index (0-based) |
font_id | font handle (0 = inherit column/grid) |
returns void
gpu_grid_cellfont(g, 11, 0, gFontBold);
gpu_grid_color(r, c, color)set the colour of cell (r,c)
Used by BAR tint, COLOR swatch and BADGE pill cells.
r | row index (0-based) |
c | column index (0-based) |
color | packed 0xRRGGBB colour |
returns void
gpu_grid_color(g, i, 3, dTint[i]);
gpu_grid_textcolor(r, c, color)set the per-cell TEXT colour of cell (r,c)
-1 reverts the cell to the grid's default text colour.
r | row index (0-based) |
c | column index (0-based) |
color | packed 0xRRGGBB colour, -1 = grid default |
returns void
gpu_grid_textcolor(g, i, 2, parsehex("ff6b6b"));gpu_grid_rowstyle(r, color)set a per-row background colour override for row r
-1 = none, reverting the row to zebra/normal shading.
r | row index (0-based) |
color | packed 0xRRGGBB background colour, -1 = none |
returns void
gpu_grid_rowstyle(g, 7, parsehex("3a2a14"));gpu_grid_style(key, value)set one named style property of the grid to an int value
Keys: headerbg, rowbg, rowalt, sel, hover, grid, text, accent, titlebg (colours) and rowh, headerh, titleh, footerh, zebra (metrics/flags).
key | style key name (string) |
value | colour or metric value (int) |
returns void
gpu_grid_style(g, "rowh", 30);
gpu_grid_theme(g, name)set the whole look at once, from a preset name or a json
document. A THEME IS A DOCUMENT (his ruling, 2026-08-25: "a json theme for the grid is a good idea too like a style sheet"). The shipped presets -- "light", "dark", "sheet" -- are not cases in a switch; each IS a json document that ships with the runtime, and a name is shorthand for one of them. Pass anything beginning with '{' and it is read as your own theme instead, so a program (or a rule, or a visitor) can restyle a grid by editing data. Keys are exactly gpu_grid_style's keys, and a value may be a json NUMBER or a hex STRING ("#e3ece5" or "e3ece5") -- a human writing a stylesheet writes the hex, a program generating one writes the number, and refusing either would make the door useless to one of them. REFUSES LOUDLY. Until 2026-08-25 any name that was not "light" fell through to dark, so a typo -- or `gridTheme(g, "sheet")` before this existed -- quietly handed back the dark theme and said nothing.
g | the grid handle |
name | a preset name, or a json object of style keys |
gridTheme(g, "sheet");
gpu_grid_sheet(g, doc)style the WHOLE sheet from ONE json document, in layers,
down to a single named cell. Sections, applied in this order so that a later layer wins: "theme" a preset name, exactly gpu_grid_theme's ("light" / "dark") "sheet" an object of gpu_grid_style keys -- the sheet-wide defaults "cols" an ARRAY, one entry per column, in column order "rows" an object keyed by the 1-based row number as text "cells" an object keyed by A1 address ("d9"), the single-cell layer Every section is optional; an empty object in "cols" leaves that column alone. Column keys: "label" "width" "type" "align" "decimals" "font" "edit". Row keys: "bg" "level". Cell keys: "bg" "text" "decimals" "font" "edit". A ROW IS ADDRESSABLE FROM THE END: "-1" is the last row, "d-1" its column D cell. A sheet whose length comes from its data has no fixed row 9, and a document that names one goes silently wrong the day a line item is added. A COLOUR is either "#rrggbb" / "rrggbb" or a plain number; "type" and "align" take either their word ("number", "right") or their number. WHY A DOCUMENT AND NOT MORE CALLS. The look of a sheet is data about the sheet, so it is written as data -- read from a file, sent by a server or chosen by a person, with nothing rebuilt. This composes the single-value doors above rather than reimplementing them (one truth, rule 33). FP4: a section, key, address or value the loader cannot honour raises CX-E5052 naming the offending row and STOPS -- deliberately unlike the 3-arg cx_grid_style it composes, which drops an unknown key in silence. A whole document is exactly where one mistyped row would otherwise vanish.
g | the grid handle |
doc | a json handle -- a document or any object node |
gpu_grid_sheet(g, look);
gpu_grid_types(g, doc)declare how every cell is TYPED from ONE json document:
the sheet's default, a column's, a row's, and a single named cell's. The SIBLING of gpu_grid_sheet and it reads the same way: the same four sections, the same column array, the same row keys, the same A1 cell addresses including a row counted from the end. The style document says how the sheet LOOKS; this one says what each cell IS. { "sheet": "text", "cols": [ "text", "number", "bar" ], "rows": { "3": "badge", "-1": "number" }, "cells": { "c2": "check", "d-1": "number" } } The value at every layer is a bare type NAME, not an object -- there is only one thing to say about a cell here, and a document that says it in one word is the shorter program (FP5). The six types: "text" "number" "check" "bar" "color" "badge" -- the same six a column has always had, now reachable at every layer. "" means INHERIT the layer above, so a document can un-type a cell as well as type one; at the "sheet" layer, which has no layer above it, "" means plain text. PRECEDENCE, and it is resolved at the READ, not at the load: a CELL's type beats its ROW's, which beats its COLUMN's, which beats the SHEET's default. A grid nobody has typed answers "text" everywhere, exactly as it always did. WHAT A TYPE GOVERNS. How the cell is drawn (a number, a check box, a progress bar, a colour swatch, a badge); what clicking it does; what an edit commits into; and how the cell BINDS into a formula's `e` -- a "number" cell binds as a number, every other type binds as its text. A TYPE DOES NOT CONVERT DATA. A cell holds a number and a string in separate stores and the type chooses which one is read, so typing a text-filled column "number" shows the numbers nobody set (0), not the text parsed. This is the long-standing behaviour of a column's type, reachable per cell; set the store you mean with gpu_grid_num / gpu_grid_set. AGAINST A FORMULA CELL: the formula owns the VALUE, the type owns how that value is presented and bound, and a REFUSED formula outranks both -- a cell showing #ERR or #CIRC keeps its marker whatever it is typed, because a marker is not a value and a clean 0 in a number cell would look computed. FP4: a section, address, or type name the loader cannot honour raises CX-E5054 naming the offending row and STOPS. An unknown type name is refused WITH THE LEGAL SET SPELLED OUT, because the alternative is sending a person who typed "colour" to a manual they are not holding.
g | the grid handle |
doc | a json handle -- a document or any object node |
gpu_grid_types(g, kinds);
gpu_grid_formula(g, r, c, text)give a cell a FORMULA: text that is compiled in-process
and re-run whenever the sheet changes, so a total follows its inputs. THE FORMULA IS A RULE (DOCS/design/C_RULES.md) -- a `{ ... }` C body over `json e` whose `return` is the cell's value. It is not compiled with the program: it is text the program can read, show and replace while it runs, which is what lets the person in front of the sheet change the arithmetic. Needs `#pragma rules c`, the same gate every other rule needs. `e` binds the three things a spreadsheet formula asks for: e["this"]["b"] this row's cell in column B e["above"]["d"] column D's cells ABOVE this row, as an array -- the range a subtotal sums: `sum(e["above"]["d"])` e["d7"] any cell by its A1 address (1-based row) e["d-1"] the same, counted from the END -- so a totals formula is the same text whatever number of line items the data has A cell in a NUMBER column binds as a number; any other column binds as its text, which the rule engine reads leniently ("3.50" is 3.5). The result is written to the cell as BOTH a number (gpu_grid_getnum) and the text the sheet paints (gpu_grid_gettext), formatted to that cell's decimals. Passing an EMPTY text removes the formula and leaves the cell a plain one. The sheet is re-evaluated to a fixed point, so a formula may name a cell below it. FP4: a formula that does not compile, or a sheet that does not settle because a value depends on itself, marks the CELL (`#ERR` / `#CIRC`) and reports CX-E5053 -- it never leaves the previous number sitting there looking computed, and it never spins.
g | the grid handle |
r | 0-based row |
c | 0-based column |
text | the rule body, or "" to remove the formula |
gpu_grid_formula(g, 8, 3, "{ return sum(e[\"above\"][\"d\"]); }");gpu_grid_getformula(g, r, c)read a cell's formula text back
The companion to gpu_grid_formula, and the reason it exists: a formula the program cannot show is one the person at the sheet cannot find, let alone edit. Answers "" for a cell that has no formula, so a caller can tell the two apart.
g | the grid handle |
r | 0-based row |
c | 0-based column |
returns the formula's text, or "" when the cell has none
gpu_draw_text(24, 496, gpu_grid_getformula(g, row, 3), ink);
gpu_grid_rownum(g, on)toggle the automatic row-number gutter on the left
Numbers are 1-based by original row index.
g | grid handle |
on | 1 = show gutter, 0 = hide |
returns void
gpu_grid_rownum(top, 1);
gpu_grid_section(r, label)mark row r as a collapsible section header with a label
Rows after it (until the next header) belong to it and hide when it folds; clicking the header toggles it.
r | row index to turn into a section header |
label | section header label (string) |
returns void
gpu_grid_section(g, 0, "WEAPONS");
gpu_grid_fold(r, folded)programmatically fold or unfold the section header at row r
r | section header row index |
folded | 1 = folded (collapsed), 0 = expanded |
returns void
gpu_grid_fold(insp, r, 1);
gpu_grid_rowlevel(r, level)set the nesting depth of the section header at row r
0 = top-level, 1 = sub-section, and so on; folding a parent also hides nested sub-sections. Nesting past depth 15 raises CX-E5025.
r | section header row index |
level | nesting depth (0 = top; negatives clamp to 0) |
returns void
gpu_grid_rowlevel(insp, r, 1);
gpu_grid_coledit(c, on)mark a TEXT/NUM column c as click-to-edit
CHECK/BAR/COLOR cells are always interactive regardless of this flag.
c | column index (0-based) |
on | 1 = editable, 0 = read-only |
returns void
gpu_grid_coledit(g, 0, 1);
gpu_grid_celledit(r, c, on)make one cell (r,c) typeable even if its column is not editable
Used for an editable cell sitting in an otherwise read-only grid.
r | row index (0-based) |
c | column index (0-based) |
on | 1 = typeable, 0 = not |
returns void
gpu_grid_celledit(insp, r, 2, 1);
gpu_grid_sort(col, dir)programmatically set the sort column and direction
dir < 0 sorts descending, otherwise ascending; col < 0 clears the sort. Group-aware when the grid has sections (sections move as whole blocks).
col | column to sort by (<0 = unsorted) |
dir | direction: <0 descending, otherwise ascending |
returns void
gpu_grid_sort(g, 2, 1);
gpu_grid_scrollx(px)set the horizontal scroll offset in pixels
Clamped to 0..(content width - viewport width).
px | horizontal scroll offset in pixels |
returns void
gpu_grid_scrollx(g, 0);
gpu_grid_follow(g, on)toggle follow mode so the view auto-tails to the bottom as rows append
Re-enabled automatically when the user scrolls back to the bottom.
g | grid handle |
on | 1 = follow (auto-tail), 0 = off |
returns void
gpu_grid_follow(dlog, 1);
gpu_grid_update(mx, my, wheel, down, pressed)drive one frame of grid input from the mouse state
Handles wheel/scrollbar scrolling, header sort clicks, column resize, section fold, cell selection and text/number editing.
mx | mouse x in pixels |
my | mouse y in pixels |
wheel | mouse wheel delta |
down | mouse button held (1/0) |
pressed | mouse button pressed this frame (1/0) |
returns void
gpu_grid_update(g, mx, my, wheel, down, pressed);
gpu_grid_draw(g)render the grid this frame using raylib primitives
Draws the title bar, header, visible rows (cells/sections), vertical and horizontal scrollbars and the footer.
g | grid handle |
returns void
gpu_grid_draw(g);
gpu_grid_tail(g)snap the view to the last rows (log tail)
g | grid handle |
returns void
gpu_grid_tail(dlog);
gpu_grid_sel(g)return the selected original row index
g | grid handle |
returns int selected original row index, -1 = none
sel.i = gpu_grid_sel(g);
gpu_grid_selcol(g)the COLUMN the selecting click landed on, the twin of
gpu_grid_sel's row. Answers the column index the last selecting press ACTED on, which inside a horizontal merge is the span's origin rather than the column the pointer was over. -1 means no column, and it is a real state rather than a failure: a press in the row-number gutter or to the right of the last column selects a ROW and no column, and a grid nobody has clicked has neither. It reads what the press already decided and does not hit-test again, so asking costs a field read. Pair it with gpu_grid_sel to tell one gesture from another -- typing into a value column, asking about a formula column -- without re-deriving the geometry, which a caller cannot do anyway: the column boundaries move by the row-number gutter, whose width is not reachable from CX and is not meant to be.
g | the grid handle |
returns the column index (0 = leftmost), or -1 for no column
if (gpu_grid_selcol(g) >= 2) { pick(gpu_grid_sel(g)); }gpu_grid_sortcol(g)return the current sort column index
g | grid handle |
returns int current sort column, -1 = unsorted
gpu_grid_sortcol(g);
gpu_grid_hover(g)return the hovered original row index
g | grid handle |
returns int hovered original row index, -1 = none
gpu_grid_hover(g);
gpu_grid_count(g)return the grid's row count
g | grid handle |
returns int number of rows
gpu_draw_text(60, 744, "rows: " + str(gpu_grid_count(g)), colDim, 16);
gpu_gui_button(x, y, w, h, text)draws an immediate-mode button in a borderless panel and reports whether it was clicked this frame
Meant to be polled every frame inside the draw loop; wraps nuklear's nk_button_label in a mini-window keyed by its coordinates.
x | left edge of the widget in pixels |
y | top edge of the widget in pixels |
w | width in pixels |
h | height in pixels |
text | button label text |
returns int -- 1 if the button was clicked this frame, else 0 (also 0 when there is no window/GL context)
if (gpu_gui_button(20, 60, 120, 32, "Click me")) {gpu_gui_label(x, y, w, h, text)draws a static left-aligned text label in a borderless panel
x | left edge of the widget in pixels |
y | top edge of the widget in pixels |
w | width in pixels |
h | height in pixels |
text | label text to display |
returns void
gpu_gui_label(160, 60, 200, 32, "clicks: " + str(clicks));
gpu_gui_checkbox(x, y, w, h, text, state)draws a labelled checkbox and returns its new on/off state
The passed state seeds the widget each frame; the return value reflects any toggle the user made this frame.
x | left edge of the widget in pixels |
y | top edge of the widget in pixels |
w | width in pixels |
h | height in pixels |
text | checkbox label text |
state | current state; nonzero = checked |
returns int -- the checkbox's new state (1 checked, 0 unchecked; echoes the input state when there is no window)
on = gpu_gui_checkbox(20, 60, 140, 32, "enabled", on);
gpu_gui_console(x, y, w, h, text)draws a bordered multi-line text panel, splitting the text on newlines into rows
A minimal read-only log/console view; each newline-separated line becomes its own row and a trailing carriage return is stripped.
x | left edge of the panel in pixels |
y | top edge of the panel in pixels |
w | width in pixels |
h | height in pixels |
text | multi-line body text to display |
returns void
gpu_gui_console(640, 152, 340, 460, log_text);
gpu_gui_input(x, y, w, h, key, initial)draws a single-line text input field whose buffer persists across frames, keyed by name
On the first call for a given key the field is seeded with initial; later calls return whatever the user has typed. State lives in a 32-slot table with a 255-char buffer.
x | left edge of the field in pixels |
y | top edge of the field in pixels |
w | width in pixels |
h | height in pixels |
key | immediate-mode identity/slot key that names the persistent buffer |
initial | text to seed the buffer on first use of this key |
returns string -- the field's current text (empty string when there is no window or the 32-slot table is full)
string name = gpu_gui_input(20, 60, 200, 32, "name", "");
gpu_gui_editor(x, y, w, h, key, initial)draws a multi-line scrollable text editor box whose buffer persists across frames, keyed by name
Like gpu_gui_input but backed by nuklear's NK_EDIT_BOX with an 8 KB buffer and its own scrollbar, intended as a code/log scratch pad. State lives in an 8-slot table.
x | left edge of the editor in pixels |
y | top edge of the editor in pixels |
w | width in pixels |
h | height in pixels |
key | immediate-mode identity/slot key that names the persistent buffer |
initial | text to seed the buffer on first use of this key |
returns string -- the editor's current text (empty string when there is no window or the 8-slot table is full)
string code = gpu_gui_editor(20, 152, 600, 460, "src", "// type CX code here\nint x = 7;\nprintln(x * 6);\n");
gpu_gui_fontsize(px)sets the GUI widget font size in pixels, before the font atlas is baked
Only takes effect if called before the first widget renders (nuklear bakes its atlas on first use); the value is clamped to 8..64 and defaults to 18.
px | desired font size in pixels |
returns void
gpu_gui_fontsize(20);
vec3addx(x1, y1, z1, x2, y2, z2)return the X component of adding two 3D vectors (x1+x2)
CX has no vec3 value type, so vector ops are split into per-component scalar builtins; call vec3addx/y/z to build the sum vector. Only x1 and x2 are used.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector (ignored) |
returns float x1 + x2
vec3addx(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3addy(x1, y1, z1, x2, y2, z2)return the Y component of adding two 3D vectors (y1+y2)
Component-wise scalar helper; only y1 and y2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float y1 + y2
vec3addy(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3addz(x1, y1, z1, x2, y2, z2)return the Z component of adding two 3D vectors (z1+z2)
Component-wise scalar helper; only z1 and z2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1 + z2
vec3addz(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3subx(x1, y1, z1, x2, y2, z2)return the X component of subtracting two 3D vectors (x1-x2)
Component-wise scalar helper; only x1 and x2 are used.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector (ignored) |
returns float x1 - x2
vec3subx(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3suby(x1, y1, z1, x2, y2, z2)return the Y component of subtracting two 3D vectors (y1-y2)
Component-wise scalar helper; only y1 and y2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float y1 - y2
vec3suby(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3subz(x1, y1, z1, x2, y2, z2)return the Z component of subtracting two 3D vectors (z1-z2)
Component-wise scalar helper; only z1 and z2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1 - z2
vec3subz(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3scalex(x, y, z, s)return the X component of a 3D vector scaled by a scalar (x*s)
Component-wise scalar helper; only x and s are used.
x | X of the vector |
y | Y of the vector (ignored) |
z | Z of the vector (ignored) |
s | scalar multiplier |
returns float x * s
vec3scalex(2.0,3.0,4.0, 10.0)
vec3scaley(x, y, z, s)return the Y component of a 3D vector scaled by a scalar (y*s)
Component-wise scalar helper; only y and s are used.
x | X of the vector (ignored) |
y | Y of the vector |
z | Z of the vector (ignored) |
s | scalar multiplier |
returns float y * s
vec3scaley(2.0,3.0,4.0, 10.0)
vec3scalez(x, y, z, s)return the Z component of a 3D vector scaled by a scalar (z*s)
Component-wise scalar helper; only z and s are used.
x | X of the vector (ignored) |
y | Y of the vector (ignored) |
z | Z of the vector |
s | scalar multiplier |
returns float z * s
vec3scalez(2.0,3.0,4.0, 10.0)
vec3length(x, y, z)return the length (magnitude) of a 3D vector
Computes sqrt(x*x + y*y + z*z).
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float sqrt(x^2 + y^2 + z^2)
vec3length(3.0, 4.0, 0.0)
vec3distance(x1, y1, z1, x2, y2, z2)return the Euclidean distance between two 3D points
Computes sqrt of the summed squared component differences.
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
returns float distance between the two points
vec3distance(0.0,0.0,0.0, 3.0,4.0,0.0)
vec3dot(x1, y1, z1, x2, y2, z2)return the dot product of two 3D vectors
Computes x1*x2 + y1*y2 + z1*z2.
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float x1*x2 + y1*y2 + z1*z2
vec3dot(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3crossx(x1, y1, z1, x2, y2, z2)return the X component of the cross product of two 3D vectors
Computes y1*z2 - z1*y2.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float y1*z2 - z1*y2
vec3crossx(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3crossy(x1, y1, z1, x2, y2, z2)return the Y component of the cross product of two 3D vectors
Computes z1*x2 - x1*z2.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1*x2 - x1*z2
vec3crossy(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3crossz(x1, y1, z1, x2, y2, z2)return the Z component of the cross product of two 3D vectors
Computes x1*y2 - y1*x2.
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float x1*y2 - y1*x2
vec3crossz(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3normx(x, y, z)return the X component of the normalized (unit-length) vector
Computes x/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float x divided by the vector length, or 0.0 if length is 0
vec3normx(3.0,4.0,0.0)
vec3normy(x, y, z)return the Y component of the normalized (unit-length) vector
Computes y/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float y divided by the vector length, or 0.0 if length is 0
vec3normy(3.0,4.0,0.0)
vec3normz(x, y, z)return the Z component of the normalized (unit-length) vector
Computes z/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float z divided by the vector length, or 0.0 if length is 0
vec3normz(0.0,0.0,5.0)
vec3lerpx(x1, y1, z1, x2, y2, z2, t)return the X component of the linear interpolation between two 3D points
Computes x1 + (x2 - x1) * t; t is NOT clamped.
x1 | X of the start point |
y1 | Y of the start point (ignored) |
z1 | Z of the start point (ignored) |
x2 | X of the end point |
y2 | Y of the end point (ignored) |
z2 | Z of the end point (ignored) |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float x1 + (x2 - x1) * t
vec3lerpx(0.0,0.0,0.0, 10.0,0.0,0.0, 0.5)
vec3lerpy(x1, y1, z1, x2, y2, z2, t)return the Y component of the linear interpolation between two 3D points
Computes y1 + (y2 - y1) * t; t is NOT clamped.
x1 | X of the start point (ignored) |
y1 | Y of the start point |
z1 | Z of the start point (ignored) |
x2 | X of the end point (ignored) |
y2 | Y of the end point |
z2 | Z of the end point (ignored) |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float y1 + (y2 - y1) * t
vec3lerpy(0.0,0.0,0.0, 0.0,10.0,0.0, 0.5)
vec3lerpz(x1, y1, z1, x2, y2, z2, t)return the Z component of the linear interpolation between two 3D points
Computes z1 + (z2 - z1) * t; t is NOT clamped.
x1 | X of the start point (ignored) |
y1 | Y of the start point (ignored) |
z1 | Z of the start point |
x2 | X of the end point (ignored) |
y2 | Y of the end point (ignored) |
z2 | Z of the end point |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float z1 + (z2 - z1) * t
vec3lerpz(0.0,0.0,0.0, 0.0,0.0,10.0, 0.5)
vec3angle(x1, y1, z1, x2, y2, z2)return the angle in degrees between two 3D vectors
Computes acos(dot/(|a||b|)) converted to degrees; returns 0.0 if either vector has zero length, and the cosine is clamped to [-1,1].
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float angle between the vectors in degrees, or 0.0 if either is zero-length
vec3angle(1.0,0.0,0.0, 0.0,1.0,0.0)
camfollow(cam, tx, ty, tz, speed)smoothly lerp a camera's target toward a world point
Moves the cx_gfx Camera3D target from its current position toward (tx,ty,tz) by fraction speed each call (speed clamped 0..1); call per-frame for smooth following.
cam | cx_gfx camera handle |
tx | target X to follow |
ty | target Y to follow |
tz | target Z to follow |
speed | lerp fraction per call, clamped to 0..1 |
returns void
camfollow(cam, tx, ty, tz, 0.1)
camzoomto(cam, dist, speed)smoothly move a camera toward a target orbit distance
Slides the camera position along its current view direction so the distance to the target approaches dist by fraction speed (clamped 0..1); honors mindist/maxdist clamps set by camorbitsettings and is a no-op when position equals target.
cam | cx_gfx camera handle |
dist | desired distance from the camera target |
speed | lerp fraction per call, clamped to 0..1 |
returns void
camzoomto(cam, 20.0, 0.1)
camshake(cam, intensity, duration)apply a decaying random positional jitter to a camera
Call per-frame while shaking: undoes the previous frame's offset (so it composes with camfollow/zoom), then adds xorshift-random jitter scaled by intensity and a linear time-decay envelope over duration; intensity or duration <= 0 stops the shake. Only camera handles 1..7 hold shake state.
cam | cx_gfx camera handle (valid 1..7 for shake state) |
intensity | maximum jitter magnitude |
duration | shake duration in seconds |
returns void
camshake(cam, 0.5, 1.0)
camorbitsettings(cam, minspeed, maxspeed, mindist, maxdist)store min/max speed and distance clamps for a camera
Records orbital tuning on the camera's shake-state slot; the min/max distance is later honored by camzoomto. Only camera handles 1..7 hold this state.
cam | cx_gfx camera handle (valid 1..7) |
minspeed | minimum orbit speed (stored) |
maxspeed | maximum orbit speed (stored) |
mindist | minimum orbit distance (clamps camzoomto) |
maxdist | maximum orbit distance (clamps camzoomto; 0 = unset) |
returns void
camorbitsettings(cam, 0.1, 2.0, 5.0, 50.0)
goload(name)load a game object from a JSON definition file and return its handle
Resolves name in order (raw path, data/ships/<name>.json, data/<name>, <name>.json), parses the JSON into a reusable object-pool slot, and reads optional scale plus model/shader assets (assets only load when a window is open). Grows the pool up to #pragma MaxGameObjects; on empty name or no loadable/parseable file it prints an error and exits the process (never returns a sentinel).
name | object name or path used to locate the JSON definition |
returns int object handle (>= 1); on failure it prints an error and exits, so it never returns an invalid handle
int h = goload(tmp);
gofree(obj)free a game object and release its slot for reuse
Releases the object's JSON doc, any loaded model/shader, and its override array, then clears the slot so a later goload can reclaim it. No-op on an invalid handle.
obj | game object handle from goload |
returns void
gofree(h);
gogetf(obj, key)read a float property of a game object
Returns the runtime override for key if one was set via gosetf/goseti, otherwise reads the key from the object's JSON. Returns 0.0 on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns float property value (override first, else JSON member); 0.0 if the handle is invalid
speed = gogetf(obj, "speed")
gogeti(obj, key)read an integer property of a game object
Returns the runtime override for key if one was set, otherwise reads the key from the object's JSON. Returns 0 on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns int property value (override first, else JSON member); 0 if the handle is invalid
hp = gogeti(obj, "health")
gogets(obj, key)read a string property of a game object
Reads the key from the object's JSON (string overrides are not supported — only numeric overrides exist). Returns an empty string on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns string property value from the JSON member; empty string if the handle is invalid
name = gogets(obj, "name")
gosetf(obj, key, v)set a runtime float override on a game object property
Stores an in-memory override for key (read back by gogetf/gogeti) without modifying the underlying JSON; grows the per-object override array up to #pragma MaxGameOverrides. No-op on an invalid handle.
obj | game object handle from goload |
key | property name to override |
v | float value to store |
returns void
gosetf(obj, "speed", 12.5)
goseti(obj, key, v)set a runtime integer override on a game object property
Stores an in-memory override for key (kept as a double, read back by gogeti/gogetf) without touching the JSON. No-op on an invalid handle.
obj | game object handle from goload |
key | property name to override |
v | integer value to store |
returns void
goseti(obj, "health", 100)
timercreate(duration)create a countdown timer and return its handle
Allocates a timer counting down from duration seconds (negative clamped to 0); reuses freed slots and grows the pool up to #pragma MaxGameTimers.
duration | countdown length in seconds (negative treated as 0) |
returns int timer handle (>= 1)
lastTmr = timercreate(1.0);
timerupdate(tmr, delta)advance a timer by subtracting a delta from its remaining time
Decrements the timer's remaining time by delta, floored at 0. No-op on an invalid handle.
tmr | timer handle from timercreate |
delta | time to subtract (e.g. per-frame delta seconds) |
returns void
timerupdate(tmr, 0.016)
timerexpired(tmr)test whether a timer has counted down to zero
Returns 1 once the remaining time reaches 0, else 0; 0 for an invalid handle.
tmr | timer handle from timercreate |
returns int 1 if remaining time <= 0, else 0 (0 for an invalid handle)
timerexpired(timercreate(1000.0))
timerreset(tmr)reset a timer's remaining time back to its full duration
No-op on an invalid handle.
tmr | timer handle from timercreate |
returns void
timerreset(tmr)
timerprogress(tmr)return a timer's elapsed fraction from 0.0 to 1.0
Computes 1 - remaining/duration, clamped to 0..1; a zero-length timer reports 1.0 (instantly done) and an invalid handle returns 0.0.
tmr | timer handle from timercreate |
returns float progress in 0..1 (1.0 for a zero-length timer; 0.0 if the handle is invalid)
p = timerprogress(tmr)
cooldownready(tmr)poll a timer as a cooldown and auto-restart it when ready
Returns 1 the moment the timer reaches 0 and simultaneously reloads it to full duration for the next interval (the weapon/AI cooldown pattern); otherwise 0, and 0 for an invalid handle.
tmr | timer handle from timercreate |
returns int 1 if the cooldown is ready (and it auto-restarts), else 0 (0 for an invalid handle)
if (cooldownready(tmr)) { fire(); }dist3d(x1, y1, z1, x2, y2, z2)return the Euclidean distance between two 3D points
Computes sqrt of the summed squared component differences (same as vec3distance).
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
returns float distance between the two points
dist3d(0.0,0.0,0.0, 3.0,4.0,0.0)
inrange3d(x1, y1, z1, x2, y2, z2, range)test whether two 3D points are within a given range
Compares squared distance against range squared (no sqrt), returning 1 if within range inclusive.
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
range | maximum distance (inclusive) |
returns int 1 if the distance is <= range, else 0
inrange3d(0.0,0.0,0.0, 1.0,0.0,0.0, 2.0)
worldtoscreenx(cam, wx, wy, wz)project a 3D world point to its screen X coordinate
Uses raylib GetWorldToScreen for the given camera; returns 0.0 when no window is open or the camera handle is invalid (headless-safe).
cam | cx_gfx camera handle |
wx | world X |
wy | world Y |
wz | world Z |
returns float screen-space X pixel of the projected point; 0.0 if no window/invalid camera
sx = worldtoscreenx(cam, wx, wy, wz)
worldtoscreeny(cam, wx, wy, wz)project a 3D world point to its screen Y coordinate
Uses raylib GetWorldToScreen for the given camera; returns 0.0 when no window is open or the camera handle is invalid (headless-safe).
cam | cx_gfx camera handle |
wx | world X |
wy | world Y |
wz | world Z |
returns float screen-space Y pixel of the projected point; 0.0 if no window/invalid camera
sy = worldtoscreeny(cam, wx, wy, wz)
headingto(x1, z1, x2, z2)return the yaw heading in degrees from one XZ point to another
Computes atan2(x2-x1, z2-z1) in degrees on the XZ plane, where 0 degrees is +Z increasing toward +X.
x1 | X of the source point |
z1 | Z of the source point |
x2 | X of the target point |
z2 | Z of the target point |
returns float heading in degrees (0 = +Z, increasing toward +X)
h = headingto(x1, z1, x2, z2)
colorlerp(c1, c2, t)linearly interpolate between two packed colors
Blends two 0xAARRGGBB colors channel-wise (alpha, red, green, blue) with t clamped to 0..1, returning the packed result.
c1 | start color (packed 0xAARRGGBB int) |
c2 | end color (packed 0xAARRGGBB int) |
t | blend factor, clamped to 0..1 (0 = c1, 1 = c2) |
returns int the interpolated packed 0xAARRGGBB color
colorlerp(0, 16777215, 0.5)
colorflash(col, flashcol, t)blend a base color toward a flash color by an amount
Alias of colorlerp: t=0 returns col unchanged, t=1 returns full flashcol, with t clamped to 0..1.
col | base color (packed 0xAARRGGBB int) |
flashcol | flash color to blend toward (packed 0xAARRGGBB int) |
t | flash amount, clamped to 0..1 (0 = base, 1 = full flash) |
returns int the blended packed 0xAARRGGBB color
c = colorflash(col, 0xFFFFFFFF, 0.5)
assetclose()close the open asset bundle
returns void
assetClose();
assetload(name)load one asset by name from the open bundle
name | the asset's name within the bundle |
returns a handle to the loaded asset; 0 on failure
h = assetLoad("hero.png");assetopen(path)open an asset bundle for subsequent loads
The resolver works in two modes -- loose files on disk, or a packed datafile -- and this chooses the source. assetLoad then reads through whichever is open.
path | the bundle or directory to open |
returns non-zero on success
assetOpen("game.dat");blobload(path)load a file from disk as a blob
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
path | the file to load |
returns a blob handle; 0 on failure
h = blobLoad("data.bin");gpu_camera_new(px, py, pz, tx, ty, tz, ux, uy, uz, fov)create a perspective Camera3D and return its handle
Fills a camera slot with position/target/up/fov and CAMERA_PERSPECTIVE projection. Returns 0 if no window is open; hard errors (CX-E5025) if the 3-slot camera table is full.
px | camera (eye) position x |
py | camera position y |
pz | camera position z |
tx | look-at target x |
ty | look-at target y |
tz | look-at target z |
ux | up vector x |
uy | up vector y |
uz | up vector z |
fov | vertical field of view in degrees |
returns 1-based camera handle; 0 if the window is not open
int cam = gpu_camera_new(0.0,10.0,10.0, 0.0,0.0,0.0, 0.0,1.0,0.0, 45.0);
gpu_camera_begin(cam)begin 3D rendering through a camera (raylib BeginMode3D)
Enters 3D mode for this camera; also feeds the camera position into every loaded shader's viewPos uniform for correct lighting. All 3D draws must sit between this and gpu_camera_end.
cam | camera handle from gpu_camera_new |
returns void
gpu_camera_begin(cam);
gpu_camera_setpos(cam, x, y, z)set a camera's position vector
cam | camera handle |
x | new position x |
y | new position y |
z | new position z |
returns void
gpu_camera_setpos(cam, 1.0,2.0,3.0);
gpu_camera_settarget(cam, x, y, z)set a camera's look-at target vector
cam | camera handle |
x | target x |
y | target y |
z | target z |
returns void
gpu_camera_settarget(cam, 0.0,0.0,0.0);
gpu_draw_sphere(x, y, z, radius, col)draw a filled 3D sphere at (x,y,z) (raylib DrawSphere)
x | center x |
y | center y |
z | center z |
radius | sphere radius |
col | color int |
returns void
gpu_draw_sphere(3.0,0.0,0.0, 1.0, 255);
gpu_draw_grid(slices, spacing)draw a reference grid on the XZ plane centered at the origin (raylib DrawGrid)
slices | number of grid squares in each direction |
spacing | distance between grid lines |
returns void
gpu_draw_grid(10, 1.0);
gpu_draw_line3d(x1, y1, z1, x2, y2, z2, col)draw a 3D line from (x1,y1,z1) to (x2,y2,z2) (raylib DrawLine3D)
x1 | start x |
y1 | start y |
z1 | start z |
x2 | end x |
y2 | end y |
z2 | end z |
col | line color int |
returns void
gpu_draw_line3d(0.0,0.0,0.0, 1.0,1.0,1.0, 255);
gpu_draw_point3d(x, y, z, col)draw a 3D point at (x,y,z) (raylib DrawPoint3D)
x | point x |
y | point y |
z | point z |
col | color int |
returns void
gpu_draw_point3d(1.0,1.0,1.0, 255);
gpu_draw_billboard(cam, tex, x, y, z, size, col)draw a 2D image as a camera-facing billboard at a 3D position (raylib DrawBillboard)
Reuses an existing 2D image/texture handle from the image registry; always faces the given camera.
cam | camera handle the billboard faces |
tex | image/texture handle (from loadimage) |
x | world position x |
y | world position y |
z | world position z |
size | billboard size in world units |
col | tint color int |
returns void
gpu_draw_billboard(cam, tex, 0.0,2.0,0.0, 1.0, 255);
gpu_draw_billboard_pro(cam, tex, sx,sy,sw,sh, x,y,z, ux,uy,uz, w,h, rot, col, alpha)draw an ORIENTED textured billboard (raylib DrawBillboardPro): a quad
of one texture at a world position, its height axis aligned to `up`, sized (w,h) in world units, spun `rot` degrees about the view axis, tinted col(0xRRGGBB) at `alpha`. Origin is centred on the position. This is the general oriented-quad primitive (strands, tracers, beams, sprites-along-a- path); unlike gpu_draw_billboard it is NOT locked to a camera-facing square. Draws into the CURRENT blend/depth state -- bracket a glow pass with gpu_blend_add/gpu_blend_normal and gpu_depth_write for clean additive sums.
cam | camera handle (gpu_camera_new) |
tex | image/texture handle (gpu_image_load) |
sx,sy,sw,sh | source rect in TEXTURE PIXELS (atlas cell). sw<=0 or sh<=0 selects the whole texture; a non-zero rect flips between frames on one sheet. |
x,y,z | world position the quad is centred on |
ux,uy,uz | the quad's height (up) axis; normalized here (zero -> world up) |
w,h | quad size in world units (w across, h along `up`) |
rot | rotation in degrees about the view axis |
col | tint packed 0xRRGGBB |
alpha | tint alpha 0..255 |
gpu_draw_billboard_pro(cam, tex, 0,0,0,0, x,y,z, dx,dy,dz, 0.4, len, 0, rgb(120,180,255), 200)
gpu_depth_write(on)toggle depth-buffer WRITES (rlgl global state). on=0 stops transparent
billboards/particles from writing depth -- so overlapping additive sprites sum cleanly instead of a near quad punching a rectangular depth hole that rejects the sprites behind it (the transparent-quad self-occlusion artifact). on=1 restores normal depth writes. Pair off/on around a glow pass, and re-enable AFTER the blend batch is flushed (i.e. after gpu_blend_normal).
on | 1 = normal depth writes, 0 = depth writes disabled |
gpu_blend_add(); gpu_depth_write(0); ...draw strands...; gpu_blend_normal(); gpu_depth_write(1);
gpu_model_load(path)load a 3D model from a file and return its handle (raylib LoadModel)
Allocates a model slot with a leak-tracking ARC bucket. A failed load (meshCount==0) prints a CX error and exits (FP4), never a silent invisible model.
path | model file path (.obj, .gltf, etc.) |
returns 1-based model handle; 0 if the window is not open; hard-exits on load failure
model m = gpu_model_load("ship.obj");gpu_mesh_sphere(radius, rings, slices)generate a sphere mesh and return it as a model handle (raylib GenMeshSphere)
radius | sphere radius |
rings | number of horizontal rings |
slices | number of vertical slices |
returns 1-based model handle; 0 if the window is not open
int sph = gpu_mesh_sphere(1.0, 16, 16);
gpu_model_draw_ex(mdl, x, y, z, ax, ay, az, angle, sx, sy, sz, col)draw a model with rotation axis/angle and per-axis scale (raylib DrawModelEx)
mdl | model handle |
x | position x |
y | position y |
z | position z |
ax | rotation axis x |
ay | rotation axis y |
az | rotation axis z |
angle | rotation angle in degrees |
sx | scale x |
sy | scale y |
sz | scale z |
col | tint color int |
returns void
gpu_model_draw_ex(pln, 0.0,0.0,0.0, 0.0,1.0,0.0, 45.0, 1.0,1.0,1.0, 255);
gpu_model_texture(mdl, tex)apply a 2D image as a model's first-material diffuse map (raylib SetMaterialTexture)
Uses an existing image/texture handle from the 2D image registry as MATERIAL_MAP_DIFFUSE on material 0.
mdl | model handle |
tex | image/texture handle (from loadimage) |
returns void
gpu_model_texture(cube, tex);
gpu_model_livecount()count currently-live model slots
Leak-check signal for the `model` resource type: number of in-use slots in the model table.
returns number of live model slots (int)
int n = gpu_model_livecount();
gpu_msaa(level)request multi-sample anti-aliasing at the given level
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). The level is STORED and applied when the window OPENS -- calling it afterwards has no effect on the live window. Any level above 0 turns on raylib's 4x MSAA hint; a negative level clamps to 0 (off).
level | 0 disables; any positive value requests MSAA. Negatives clamp to 0 |
returns void
gpu_msaa(4); // before the window opens
gpu_texture_filter(mode)set the default sampling filter for model textures
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Applies to textures loaded or applied AFTER this call; never calling it leaves every texture exactly as its loader made it. Modes: 0 point (crisp pixel art), 1 bilinear, 2 trilinear, 3 anisotropic.
mode | 0 point, 1 bilinear, 2 trilinear, 3 anisotropic |
returns void
gpu_texture_filter(1);
gpu_model_smooth(model, mode)set the sampling filter for ONE model's textures, now
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Applies a filter mode to that model's diffuse textures immediately, overriding the global default gpu_texture_filter set. It is TEXTURE FILTERING, not shading -- it does not change how normals are interpolated.
model | the model |
mode | 0 point, 1 bilinear, 2 trilinear, 3 anisotropic |
returns void
gpu_model_smooth(m, 1);
gpu_shader_load(vspath, fspath)load a shader from vertex + fragment shader files (raylib LoadShader)
An empty-string path becomes NULL so raylib uses its built-in default for that stage. Also caches the viewPos uniform location so gpu_camera_begin can feed camera position automatically.
vspath | vertex shader file path ("" = default vertex shader) |
fspath | fragment shader file path ("" = default fragment shader) |
returns 1-based shader handle; 0 if the window is not open
shader s = gpu_shader_load("", "");gpu_shader_livecount()count currently-live shader slots
Leak-check signal for the `shader` resource type: number of in-use shader slots.
returns number of live shader slots (int)
int n = gpu_shader_livecount();
gpu_shader_setf(sh, name, fvalue, uniformtype)set a float uniform value on a shader (raylib SetShaderValue)
Looks up the uniform by name; missing uniform is a silent no-op.
sh | shader handle |
name | uniform name |
fvalue | float value to set |
uniformtype | type code: 0=float 1=vec2 2=vec3 3=vec4 4=int 5=sampler2d |
returns void
gpu_shader_setf(sh, "strength", 0.5, 0);
gpu_shader_setvec3(sh, name, x, y, z, uniformtype)set a vec3 uniform value on a shader (raylib SetShaderValue)
Packs (x,y,z) into a float[3] and uploads to the named uniform; missing uniform is a silent no-op.
sh | shader handle |
name | uniform name |
x | vec3 component x |
y | vec3 component y |
z | vec3 component z |
uniformtype | type code: 0=float 1=vec2 2=vec3 3=vec4 4=int 5=sampler2d |
returns void
gpu_shader_setvec3(sh, "tint", 1.0,0.5,0.2, 2);
gpu_light_ambient(sh, r, g, b, a)set a shader's "ambient" vec4 light uniform from an RGBA color
Normalizes r,g,b,a (0..255) to 0..1 and uploads to the shader's "ambient" uniform; no-op if that uniform is absent.
sh | shader handle |
r | ambient red 0..255 |
g | ambient green 0..255 |
b | ambient blue 0..255 |
a | ambient alpha 0..255 |
returns void
gpu_light_ambient(sh, 10,10,25, 255);
gpu_light_directional(sh, dx, dy, dz, r, g, b, a)add a directional light to a shader and return its index (rlights CreateLight)
Creates a LIGHT_DIRECTIONAL travelling along (dx,dy,dz): the light sits at the origin with its target along the direction vector.
sh | shader handle |
dx | light direction x |
dy | light direction y |
dz | light direction z |
r | color red 0..255 |
g | color green 0..255 |
b | color blue 0..255 |
a | color alpha 0..255 |
returns 1-based light index; 0 if the shader handle is invalid; hard errors if the 16-light table is full
int sun = gpu_light_directional(sh, -1.0,-0.5,0.0, 255,240,200,255);
gpu_model_shader(mdl, sh)assign a shader to a model's first material
Sets material[0].shader on the model so it renders with the given shader. No-op if either handle is invalid or the model has no materials.
mdl | model handle |
sh | shader handle |
returns void
gpu_model_shader(cube, sh);
gpu_model_minx(mdl)read a model's bounding-box minimum x (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box min.x as float; 0.0 if the handle is invalid
float bminx = gpu_model_minx(cube);
gpu_model_miny(mdl)read a model's bounding-box minimum y (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box min.y as float; 0.0 if the handle is invalid
float bminy = gpu_model_miny(cube);
gpu_model_minz(mdl)read a model's bounding-box minimum z (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box min.z as float; 0.0 if the handle is invalid
float bminz = gpu_model_minz(cube);
gpu_model_maxx(mdl)read a model's bounding-box maximum x (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box max.x as float; 0.0 if the handle is invalid
float bmaxx = gpu_model_maxx(cube);
gpu_model_maxy(mdl)read a model's bounding-box maximum y (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box max.y as float; 0.0 if the handle is invalid
float bmaxy = gpu_model_maxy(cube);
gpu_model_maxz(mdl)read a model's bounding-box maximum z (raylib GetModelBoundingBox)
mdl | model handle |
returns bounding box max.z as float; 0.0 if the handle is invalid
float bmaxz = gpu_model_maxz(cube);
gpu_ray_mouse(cam)build a pick ray from the mouse position through a camera (raylib GetScreenToWorldRay)
Stores the ray in a round-robin ring of transient slots so per-frame calls never exhaust the table. Returns a 1-based ray handle for use with gpu_ray_x/y/z and the ray-collision tests.
cam | camera handle |
returns 1-based ray handle; 0 if the window is closed or the camera is invalid
int ray = gpu_ray_mouse(cam);
gpu_collide_ray_sphere(ray, x, y, z, radius)test whether a ray hits a sphere (raylib GetRayCollisionSphere)
ray | ray handle (from gpu_ray_mouse) |
x | sphere center x |
y | sphere center y |
z | sphere center z |
radius | sphere radius |
returns 1 if the ray hits the sphere, else 0 (also 0 if the ray handle is invalid)
int rhitS = gpu_collide_ray_sphere(ray, 0.0,0.0,0.0, 1.0);
gpu_collide_ray_box(ray, minx, miny, minz, maxx, maxy, maxz)test whether a ray hits an axis-aligned box (raylib GetRayCollisionBox)
Box given by its min and max corners.
ray | ray handle (from gpu_ray_mouse) |
minx | box min x |
miny | box min y |
minz | box min z |
maxx | box max x |
maxy | box max y |
maxz | box max z |
returns 1 if the ray hits the box, else 0 (also 0 if the ray handle is invalid)
int rhitB = gpu_collide_ray_box(ray, -1.0,-1.0,-1.0, 1.0,1.0,1.0);
gpu_camera_end()end 3D camera mode and go back to screen-space drawing
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Pairs with the camera-begin call; drawing between them is in world space.
returns void
gpu_camera_end();
gpu_shader_end()stop drawing through the active shader
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns void
gpu_shader_end();
gpu_frame_time()how long the last frame took, in seconds
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). The delta to scale movement by so speed is frame-rate independent.
returns the frame time in seconds
x = x + speed * gpu_frame_time();
gpu_font_load(path, size)load a .ttf/.otf font at a base pixel size, returning a font handle
raylib LoadFontEx into a 1-based slot table (32 slots). size<=0 defaults to 20. A full table is a hard CX-E5025 error.
path | font file path |
size | base glyph size in pixels |
returns 1-based font handle, or 0 if no window is open
fnt.i = gpu_font_load("data/fonts/Nunito/static/Nunito-SemiBold.ttf", 34);gpu_draw_text_font(x, y, font_id, text, color, size)draw text at (x,y) using a loaded font handle at a pixel size
raylib DrawTextEx with the font from gpu_font_load; falls back to the built-in default font (DrawText) if font_id is invalid. size<=0 uses the font's loaded base size.
x | text top-left x |
y | text top-left y |
font_id | font handle from gpu_font_load (0/invalid -> default font) |
text | string to draw |
color | text color 0xRRGGBB |
size | pixel size (<=0 -> font base size) |
returns void
gpu_draw_text_font(px, py, fntB, "MOUNT EDITOR", colHi, sc(30));
gpu_font_loadmem(blob, ext, size)load a font from an in-memory embed() blob, returning a font handle
raylib LoadFontFromMemory; `ext` is the format hint (".ttf"/".otf"). Same slot handle as gpu_font_load.
blob | embed() blob handle holding the font bytes |
ext | format hint, e.g. ".ttf" |
size | base glyph size in pixels |
returns 1-based font handle, or 0 on failure/no window
int fnt = gpu_font_loadmem(ttf, ".ttf", 24);
gpu_font_livecount()count currently-loaded font slots
Live-slot introspection for leak/churn tests over the `font` resource type.
returns number of live font slots
int before = gpu_font_livecount() + gpu_model_livecount() + gpu_shader_livecount();
gpu_image_load(path)load an image file into a GPU texture, returning a texture handle
raylib LoadImage + LoadTextureFromImage into a 1-based slot. Requires an open window. Returns 0 on failure (no window, missing/undecodable file, or table full).
path | image file path |
returns 1-based texture handle, or 0 on failure
img.i = gpu_image_load("data/ships/" + shipFile[0] + ".png");gpu_image_load_as(path, class_name)load an image and apply an image-treatment CLASS at load time
The class (see cx_imgclass.h) is a recipe of operations run automatically: pixel-stage fixes (synthesize alpha from brightness, black transparent edges, force opaque, premultiply) then the GL filter (smooth/sharp/mipmaps). Built-in classes: particle, smoke, sprite, texture, background; a data/imageclasses.json section (gpu_image_classes) can add or redefine them.
path | image file to load (PNG/JPG/etc.) |
class_name | treatment class; unknown name -> safe default (smooth) |
returns texture handle usable like gpu_image_load; 0 on failure
tex = gpu_image_load_as("fx/puff.png", "particle")gpu_image_classes(path)load an image-class override file, redefining/adding treatment recipes
The file is a JSON object of the shape { "imageClasses": { "<name>": ["op", "op", ...], ... } } where each op is one of: smooth, sharp, mipmaps, alphaFromLuma, edgeBlack, opaque, premultiply. Each listed class replaces (or adds) a recipe; classes not mentioned keep their built-in defaults. Unknown op names are reported loudly and skipped (FP4). An absent file or missing "imageClasses" section is a silent no-op, so the built-in defaults simply stand.
path | path to the JSON override file (e.g. "data/imageclasses.json") |
returns nothing
gpu_image_classes("data/imageclasses.json")gpu_image_loadmem(blob, ext)load a texture from an in-memory embed() blob, returning a texture handle
raylib LoadImageFromMemory then GPU upload; `ext` is the format hint (".png"/".jpg"). Same handle space as gpu_image_load.
blob | embed() blob handle holding the image bytes |
ext | format hint, e.g. ".png" |
returns 1-based texture handle, or 0 on failure
int img = gpu_image_loadmem(png, ".png");
gpu_image_free(handle)destroy a texture handle now, unloading its GPU texture
Unconditional free (UnloadTexture, or UnloadRenderTexture for render targets); drops the resource bucket then reclaims the slot.
handle | texture handle to free |
returns void
if (img > 0) { gpu_image_free(img); }gpu_image_width(handle)get a texture's width in pixels
handle | texture handle |
returns texture width, or 0 for an invalid handle
iw.i = gpu_image_width(img);
gpu_image_height(handle)get a texture's height in pixels
handle | texture handle |
returns texture height, or 0 for an invalid handle
ih.i = gpu_image_height(img);
gpu_image_cap(n)set the GPU texture-table capacity before the window opens
Raises the maximum texture slots (default 64), clamped to 16..8192. Must be called before the first image load; a no-op once the table is allocated. The underlying C function returns nothing (the I_I sig is only for binding generation).
n | desired slot capacity (clamped 16-8192) |
returns void
gpu_image_cap(384);
gpu_image_new(width, height)create a blank transparent render-target texture of width x height
raylib LoadRenderTexture, cleared to transparent. Draw into it between gpu_image_begin/gpu_image_end, then use the handle like any texture. Returns 0 if no window or non-positive size.
width | target width in pixels |
height | target height in pixels |
returns 1-based texture handle, or 0 on failure
gPlayerComposite = gpu_image_new(COMPW, COMPH);
gpu_image_begin(handle)redirect subsequent draw calls into a render-target texture
raylib BeginTextureMode on the target handle; must be paired with gpu_image_end. No-op for a non-target handle.
handle | render-target texture handle from gpu_image_new |
returns void
gpu_image_begin(comp);
gpu_image_end()stop drawing into the current render-target texture
raylib EndTextureMode.
returns void
gpu_image_end();
gpu_image_clear(color, alpha)clear the active render target to a color at an alpha
raylib ClearBackground; call between gpu_image_begin/gpu_image_end (e.g. alpha=0 for a transparent canvas before compositing).
color | clear color 0xRRGGBB |
alpha | clear alpha 0-255 (0 = transparent) |
returns void
gpu_image_clear(rgb(0, 0, 0), 0);
gpu_image_draw(handle, x, y, width, height)draw a texture at (x,y) scaled to width x height
raylib DrawTexturePro; render-target textures are y-flipped automatically so they present right-side-up.
handle | texture handle |
x | destination top-left x |
y | destination top-left y |
width | destination width |
height | destination height |
returns void
gpu_image_draw(img0, sx0 - dw/2, sy0 - dh/2, dw, dh);
gpu_image_draw_region(handle, dx, dy, dw, dh, sx, sy, sw, sh)draw a sub-rectangle (atlas cell) of a texture, scaled to a destination rect
raylib DrawTexturePro: source cell (sx,sy,sw,sh) mapped onto destination (dx,dy,dw,dh). Render targets are y-flipped automatically.
handle | texture handle |
dx | destination x |
dy | destination y |
dw | destination width |
dh | destination height |
sx | source cell x |
sy | source cell y |
sw | source cell width |
sh | source cell height |
returns void
gpu_image_draw_region(img, 400, 100, 200, 240, 0, 0, gpu_image_width(img) / 2, gpu_image_height(img));
gpu_image_draw_pro(handle, sx,sy,sw,sh, dx,dy,dw,dh, ox,oy, rotation_deg, col, alpha)the general 2D image blit (raylib DrawTexturePro verbatim):
SOURCE sub-rect + DEST rect + pivot + clockwise rotation + TINT. The one draw that supersedes region/rotated/region_rot when colour is needed (tinted strands/bursts, a lightning texture drawn at any angle/hue from ONE asset). Native-only (raylib window); float coords match gpu_draw_billboard_pro.
handle | image handle from gpu_image_load |
sx,sy,sw,sh | SOURCE sub-rectangle in texture pixels (atlas cell / frame); sw<=0 or sh<=0 => the whole texture |
dx,dy,dw,dh | DEST rectangle in screen pixels (position + scale) |
ox,oy | rotation pivot, in DEST pixels from the dest top-left (rotate-about-centre: dw/2, dh/2) |
rotation_deg | rotation, clockwise degrees, about (ox,oy) |
col | tint colour, packed 0xRRGGBB (multiplies the texture) |
alpha | opacity 0..255 |
returns void
gpu_image_draw_pro(bolt, 0,0,0,0, x,y, 8,len, 4,0, angle, 0xff3030, 200)
imageopen(path)loads an image file (PNG/JPG/...) from disk into a CPU image in RAM
Calls raylib LoadImage and normalises the pixels to R8G8B8A8; needs no open window. Returns 0 if the file is missing or fails to decode.
path | filesystem path to the image file to load |
returns cx_int image handle (1-based), or 0 on failure
int sheet = imageOpen("art/ModuleFrames.png");imagenew(w, h, color)creates a blank CPU image of the given size filled with a colour
Colour is packed 0xAARRGGBB (alpha 0 means opaque, as elsewhere). Returns 0 if w or h is <= 0 or the slot allocation fails.
w | image width in pixels (must be > 0) |
h | image height in pixels (must be > 0) |
color | fill colour, packed 0xAARRGGBB |
returns cx_int image handle (1-based), or 0 on invalid size/failure
image h = imageNew(4, 4, 0);
imagecopy(h)duplicates a CPU image into a new independent image
Wraps raylib ImageCopy. Returns 0 if the source handle is invalid.
h | handle of the source image to duplicate |
returns cx_int new image handle (1-based), or 0 on failure
int dup = imageCopy(h);
imagecrop(h, x, y, w, ht)crops a CPU image in place to a rectangular region
Wraps raylib ImageCrop; mutates the existing image. No-op if the handle is invalid.
h | handle of the image to crop in place |
x | left edge of the crop rectangle |
y | top edge of the crop rectangle |
w | crop rectangle width |
ht | crop rectangle height |
returns void
imageCrop(h, 0, 0, 16, 16);
imagecropcopy(src, x, y, w, ht)crops a rectangular region of a CPU image into a new image
Wraps raylib ImageFromImage; leaves the source untouched. Returns 0 if the source handle is invalid.
src | handle of the source image |
x | left edge of the region to copy |
y | top edge of the region to copy |
w | region width |
ht | region height |
returns cx_int new image handle (1-based), or 0 on failure
int part = imageCropCopy(h, 0, 0, 16, 16);
imageresize(h, w, ht)resizes a CPU image in place, resampling its pixels to the new size
Wraps raylib ImageResize (bicubic), so this SCALES the picture rather than cropping or padding it -- the whole image still fills the new rectangle and the aspect ratio is the caller's to preserve. The alpha channel survives. It is the last step of a derivation and mutates the handle you already own; there is deliberately no `imageResizeCopy`, because `imageCopy` composes to give that and a second name for one operation is a forwarder (rule 33).
h | handle of the image to resize in place |
w | new width in pixels; must be > 0 or the call is refused |
ht | new height in pixels; must be > 0 or the call is refused |
returns nothing. A bad handle is a no-op, as everywhere in this family. A non-positive dimension is REFUSED rather than obeyed: a zero-width image is a live handle whose every later read is meaningless, which is the silent-wrong this project does not ship (FP4).
int a = imageOpen("plaque.png");
imageResize(a, 424, 128);
imageSave(a, "plaque_web.png");imageblit(dst, src, sx, sy, sw, sh, dx, dy)copies a rectangular region from one CPU image onto another
Wraps raylib ImageDraw; the source rect (sx,sy,sw,sh) is drawn to the destination at (dx,dy) at the same size. No-op if either handle is invalid.
dst | handle of the destination image (drawn into) |
src | handle of the source image |
sx | source region left edge |
sy | source region top edge |
sw | source region width |
sh | source region height |
dx | destination x (top-left of the pasted region) |
dy | destination y (top-left of the pasted region) |
returns void
imageBlit(dst, src, 0, 0, 16, 16, 0, 0);
imagecolorkey(h, color)makes every pixel of a given colour fully transparent
The white-box alpha bake: wraps raylib ImageColorReplace, swapping the keyed colour for transparent (BLANK). No-op if the handle is invalid.
h | handle of the image to key |
color | colour to make transparent, packed 0xAARRGGBB |
returns void
imageColorKey(sheet, parseHex("ffffff"));imagealpha(h, a)scales the whole image's alpha channel by a/255
Multiplies every pixel's alpha by a/255 in place; a is clamped to 0..255. No-op if the handle is invalid.
h | handle of the image to fade |
a | alpha scale factor, 0..255 (255 = unchanged) |
returns void
imageAlpha(h, 128);
imagerectalpha(h, x, y, w, ht, a)sets the alpha of a rectangular region to a fixed value
Writes alpha = a to every pixel in the region, which is clipped to the image bounds; a is clamped to 0..255. No-op if the handle is invalid.
h | handle of the image to modify |
x | region left edge |
y | region top edge |
w | region width |
ht | region height |
a | alpha value to set, 0..255 |
returns void
imageRectAlpha(h, 0, 0, 16, 16, 128);
imagesave(h, path)exports a CPU image to a file, format chosen by the extension
Wraps raylib ExportImage. No-op if the handle is invalid.
h | handle of the image to export |
path | output file path; extension selects the format (e.g. .png) |
returns void
imageSave(sheet, "art/_alpha/ModuleFrames.png");
imagetotexture(h)uploads a CPU image to the GPU texture registry for drawing
The one bridge from the design-time CPU world to the run-time GPU draw registry (raylib LoadTextureFromImage). Needs an open window/GL context; returns 0 when headless or if the handle is invalid.
h | handle of the CPU image to upload |
returns cx_int GPU texture handle, or 0 if headless/invalid
int tex = imageToTexture(h);
imagefree(h)releases a CPU image and frees its slot immediately
Unconditional destroy that drops the resource bucket (bypassing ARC) then unloads the image; a bare imageFree(h) call still works standalone. No-op if the handle is invalid.
h | handle of the image to release |
returns void
imageFree(sheet);
imagedecref(h)decrements an owned image's ARC refcount, reclaiming the slot at zero
Compiler-emitted at scope exit for an owned `image` local (CXB_RESOURCE bucket ARC); reclaims the slot when the refcount reaches 0. Not normally written by hand.
h | handle of the owned image local being released |
returns void
imageDecref(h);
imagew(h)returns a CPU image's width in pixels
h | handle of the image to measure |
returns cx_int width in pixels, or 0 if the handle is invalid
return imagew(h);
imageh(h)returns a CPU image's height in pixels
h | handle of the image to measure |
returns cx_int height in pixels, or 0 if the handle is invalid
int hgt = imageH(h);
imagegetpixel(h, x, y)reads the packed 0xAARRGGBB colour of a pixel at (x, y)
Reads the R8G8B8A8 pixel and packs it as (a<<24)|(r<<16)|(g<<8)|b. Returns 0 if the handle is invalid or (x,y) is out of range.
h | handle of the image to sample |
x | pixel x coordinate (0-based) |
y | pixel y coordinate (0-based) |
returns cx_int packed 0xAARRGGBB colour, or 0 if out of range/invalid
int c = imageGetPixel(h, 0, 0);
imagelivecount()returns the number of currently-open CPU image slots
Always-available leak introspection (not gated by `checks on`); the CPU image pool grows on leak, so a rising count signals leaked handles.
returns cx_int count of live CPU image slots
int before = imagelivecount();
gpu_screen_close()close the graphics window
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns void
gpu_screen_close();
gpu_frame_quit()request that the frame loop stop
ACCEPTED AND DOES NOTHING on both backends. The loop guard is gpu_frame_poll, which already answers 0 once the window is closed -- so there is nothing for this to set. Listed so you know it is a no-op rather than assuming it ends your loop.
returns void
while (gpu_frame_poll()) { ... }gpu_target_fps(fps)set how the frame loop is PACED
Replaces the hardcoded 60 fps that every CX program shipped with; the default is still 60, so a program that never calls this paces exactly as before. Callable BEFORE or AFTER the screen is open. Before: the choice is stored and applied at open (vsync must be a config flag before InitWindow, so this is the only way to get vsync from the first frame). After: applied immediately, so a settings screen can change pacing live without reopening the window.
fps | > 0 cap the frame rate at this many frames per second. == 0 pace to the DISPLAY instead (vsync, FLAG_VSYNC_HINT). This is the tear-free mode: the refresh rate, whatever it is, sets the rhythm, and the cap is lifted. < 0 a loud CX-E5035 and the run STOPS. There is deliberately NO uncapped mode: the main loop's `deltaMs` floors at 1 ms, so sub-millisecond frames would silently run the simulation up to ~40% fast -- an uncapped mode must arrive WITH a microsecond delta accumulator, not before it. |
returns nothing.
gpu_target_fps(144); // cap at 144 gpu_target_fps(0); // follow the display
gpu_frame_poll()test whether the graphics window is still open
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). The canonical main-loop guard -- `while (gpu_frame_poll()) { ... }` -- so the loop exits cleanly on the close button or Alt+F4. It is the INVERSE of raylib's should-close, not an event pump; the frame begin/end pair does the pumping. Variadic and argument-tolerant for beta-era 1-argument calls.
returns non-zero while the window is open and no close has been requested; 0 once it has
while (gpu_frame_poll()) { gpu_frame_begin(); ... gpu_frame_end(); }gpu_mouse_x()the mouse pointer's x position in the window
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns x in pixels
mx = gpu_mouse_x();
gpu_mouse_y()the mouse pointer's y position in the window
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns y in pixels
my = gpu_mouse_y();
gpu_mouse_wheel()how far the wheel moved since the last frame
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns the wheel delta; 0 if it did not move
dz = gpu_mouse_wheel();
gpu_mouse_button(btn)return 1 while a mouse button is held down
raylib IsMouseButtonDown. Button codes: 0=left, 1=right, 2=middle; other values return 0.
btn | button index 0=left 1=right 2=middle |
returns 1 while held, else 0
down.i = gpu_mouse_button(0);
gpu_capture()take the window's current contents as an image
Returns an ordinary image handle, so every `image*` verb works on it: save it with imageSave, measure it with imageW/imageH, trim it with imageCrop, release it with imageFree. It captures the window THIS program owns -- not another program's window and not the desktop, neither of which has a portable answer. Call it inside the frame, between gpu_frame_begin and gpu_frame_end, so it reads what you have just drawn rather than whatever survived the buffer swap. THE IMAGE IS IN REAL PIXELS, WHICH MAY NOT BE YOUR WINDOW'S NUMBERS. On a high-DPI display the framebuffer is larger than the size you asked for -- a 320x240 window captures 640x480 on a Retina Mac, and everything you drew is at the same scale inside it. Ask the image (imageW/imageH), not the window (gpu_screen_width), when the numbers have to match the picture. Unlike its neighbours here, this REFUSES (CX-E5048) when no window is open instead of answering 0. Zero is an honest width for an unopened screen; it is not an honest picture, and a caller's next move is to save the handle and believe it.
returns an image handle, ready for any image verb
while (gpu_frame_begin(rgb(20,20,30))) { drawWorld(); shot = gpu_capture(); imageSave(shot, "frame.png"); imageFree(shot); gpu_frame_end(); }gpu_key_down(key)return 1 while a key (raylib keycode) is held down
raylib IsKeyDown.
key | raylib keycode (e.g. 65 = A, 32 = space) |
returns 1 while held, else 0
if (gpu_key_down(65) == 1) { marc[sel] = marc[sel] - 2; }gpu_blend_add()switch to additive blending for subsequent draws
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). What makes glows and particle flares brighten what is under them.
returns void
gpu_blend_add();
gpu_blend_normal()switch back to normal alpha blending
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013).
returns void
gpu_blend_normal();
drawimageregionrot(handle, cx, cy, w, h, sx, sy, sw, sh, deg)draw one atlas cell centred, scaled and rotated
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). The source rectangle picks the cell out of a sprite atlas; (cx, cy) is the drawn CENTRE.
handle | the atlas image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
sx | source x in the atlas |
sy | source y in the atlas |
sw | source width |
sh | source height |
deg | clockwise rotation in degrees |
returns void
drawImageRegionRot(atlas, 400, 300, 64, 64, 0, 0, 32, 32, 90);
drawimagerot(handle, cx, cy, w, h, deg)draw an image centred, scaled and rotated
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). (cx, cy) is the CENTRE, and the rotation pivots about that centre. Degrees, clockwise.
handle | the image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
deg | clockwise rotation in degrees |
returns void
drawImageRot(spr, 400, 300, 64, 64, 45);
eventquit(…)signal that the event loop should stop
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
returns void
eventQuit();
fontload(name, size)load a font for later use
ACCEPTED AND DOES NOTHING: there is no font registry yet -- raylib's built-in font is what draws. Kept so beta-era source compiles, and listed so you know it is a no-op rather than assuming a font was loaded. Pass 0 as the font id wherever one is wanted.
name | font file name |
size | point size |
returns 0, always -- the default font id
f = fontLoad("arial.ttf", 20);fontset(handle)select a loaded font for subsequent text
ACCEPTED AND DOES NOTHING: there is no font registry yet -- raylib's built-in font is what draws. Kept so beta-era source compiles, and listed so you know it is a no-op rather than assuming a font was loaded. Pass 0 as the font id wherever one is wanted.
handle | ignored |
returns void
fontSet(f);
gpu_draw_text(x, y, text, colour, size)draw a string at a screen position
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Two forms: four arguments use the default 20-pixel size, five pick the size.
x | left edge in pixels |
y | top edge in pixels |
text | the string to draw |
colour | packed 0xRRGGBB colour |
size | pixel height (optional; default 20) |
returns void
gpu_draw_text(10, 10, "score", 0xFFFFFF, 32);
gpu_image_draw_tint(handle, cx, cy, w, h, colour, alpha)draw an image centred, scaled, and multiplied by a colour
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). The position is the image's CENTRE, not its top-left. Reuses the particle tint blit, so one cached white sprite can be drawn in any colour with true alpha.
handle | the image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
colour | packed 0xRRGGBB multiplier |
alpha | 0..255 opacity |
returns void
gpu_image_draw_tint(spr, 400, 300, 64, 64, 0xFF8080, 200);
gpu_key_pressed()the key pressed this frame
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Reports one key per frame; 0 when nothing was pressed.
returns the key code, or 0
k = gpu_key_pressed();
gpu_screen_flip()swap the front and back buffers
ACCEPTED AND DOES NOTHING: raylib swaps buffers itself when the frame ends, so there is nothing left for this to do. Kept because beta-era source calls it, and listed here so you know it is a no-op rather than assuming it is what makes your drawing appear -- that is gpu_frame_end.
returns void
gpu_frame_end();
gpu_screen_resize(w, h)resize the graphics window
Reopens the screen at the new size with an empty title. Read the live dimensions back with the screen-width and screen-height builtins rather than assuming the request took effect.
w | new width in pixels |
h | new height in pixels |
returns void
gpu_screen_resize(1920, 1080);
gpu_text_width(font, text, size)measure how wide a string will be drawn
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). Two forms: one argument measures with the default font at size 20; three take a registered font id, the text and an explicit size.
font | registered font id (3-argument form) |
text | the string to measure |
size | pixel height (3-argument form) |
returns the width in pixels
w = gpu_text_width("score");screenflip(…)swap the front and back buffers
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
ACCEPTED AND DOES NOTHING, for the same reason as gpu_screen_flip: the frame end swaps buffers.
returns void
gpu_frame_end();
textwidth(text)measure how wide a string will be drawn
The same measurement as gpu_text_width on native. ON THE REGISTER VM there is no window to measure against, so it falls back to a flat EIGHT PIXELS PER CHARACTER -- useful for laying out headless, but not a real measurement.
text | the string to measure |
returns the width in pixels
w = textWidth("score");gpu_screen_clear(color)clear the whole screen to a color
Wraps raylib ClearBackground; tolerates being called outside a BeginDrawing/EndDrawing frame by opening a transient one.
color | fill color as 0xRRGGBB |
returns void
gpu_screen_clear(0);
gpu_draw_rect_alpha(x, y, width, height, color, alpha)draw a filled rectangle with an explicit alpha
Like gpu_draw_rect but overrides the color's alpha with `alpha` (0-255, clamped).
x | top-left x |
y | top-left y |
width | rectangle width |
height | rectangle height |
color | fill color 0xRRGGBB |
alpha | opacity 0-255 |
returns void
gpu_draw_rect_alpha(10, 60, 200, 15, rgb(100, 80, 60), 128);
gpu_draw_line(x1, y1, x2, y2, color)draw a line from (x1,y1) to (x2,y2)
raylib DrawLine.
x1 | start x |
y1 | start y |
x2 | end x |
y2 | end y |
color | line color 0xRRGGBB |
returns void
gpu_draw_line(10, 110, 200, 110, rgb(100, 80, 60));
gpu_draw_circle(x, y, radius, color)draw a filled circle centred at (x,y) with a radius
raylib DrawCircle.
x | centre x |
y | centre y |
radius | circle radius |
color | fill color 0xRRGGBB |
returns void
gpu_draw_circle(msx, msy, r2, colMnt);
gpu_draw_circle_alpha(x, y, radius, color, alpha)draw a filled circle with an explicit alpha
Like gpu_draw_circle but overrides alpha (0-255); used for soft translucent particles.
x | centre x |
y | centre y |
radius | circle radius |
color | fill color 0xRRGGBB |
alpha | opacity 0-255 |
returns void
gpu_draw_circle_alpha(485, 140, 45, rgb(255, 130, 80), 150);
gpu_draw_pixel(x, y, color)draw a single pixel at (x,y)
raylib DrawPixel.
x | pixel x |
y | pixel y |
color | pixel color 0xRRGGBB |
returns void
gpu_draw_pixel(560, 60, rgb(255, 255, 255));
gpu_draw_triangle(x1, y1, x2, y2, x3, y3, color)draw a filled triangle through three points
raylib DrawTriangle, drawn in both windings so it is order-independent (no back-face culling gap).
x1 | first vertex x |
y1 | first vertex y |
x2 | second vertex x |
y2 | second vertex y |
x3 | third vertex x |
y3 | third vertex y |
color | fill color 0xRRGGBB |
returns void
gpu_draw_triangle(340, 330, 430, 330, 385, 255, rgb(210, 210, 70));
gpu_draw_roundrect(x, y, w, h, roundness, col)filled rounded rectangle (raylib DrawRectangleRounded)
Corner smoothness is a fixed internal default of 8 segments/corner (not a parameter -- no consumer needs it; a one-line signature extension if one ever does). Both engines (GAP, 3.5.006.0).
x | left edge, pixels |
y | top edge, pixels |
w | width, pixels |
h | height, pixels |
roundness | corner radius as raylib's 0..1 fraction of the SHORT side, inherited VERBATIM (0 = square corners, 1 = fully rounded / capsule). raylib clamps out-of-range values to [0,1] itself. |
col | 0xRRGGBB colour, drawn opaque. |
returns void
gpu_draw_roundrect(x, y, 200, 80, 0.3, 0x2244aa)
gpu_draw_roundrect_alpha(x,y,w,h, roundness, col, alpha)filled rounded rectangle with an explicit alpha
Twin of gpu_draw_roundrect (family idiom, cf. gpu_draw_rect / gpu_draw_rect_alpha). 8 segments/corner. Both engines.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
col | 0xRRGGBB colour |
alpha | 0..255 opacity (0 = transparent, 255 = opaque) |
returns void
gpu_draw_roundrect_alpha(x, y, 200, 80, 0.3, 0x2244aa, 128)
gpu_draw_roundrect_lines(x,y,w,h, roundness, thick, col)rounded-rectangle OUTLINE (raylib DrawRectangleRoundedLinesEx)
8 segments/corner. Both engines.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
thick | outline thickness, pixels |
col | 0xRRGGBB colour, drawn opaque |
returns void
gpu_draw_roundrect_lines(x, y, 200, 80, 0.3, 2.0, 0x8fb2ff)
gpu_draw_roundrect_lines_alpha(x,y,w,h, roundness, thick, col, alpha)rounded-rectangle outline with an explicit alpha
Twin of gpu_draw_roundrect_lines. The Gigantica shield HUD box (rounded outline + intensity) is the named consumer. 8 segments/corner. Both engines.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
thick | outline thickness, pixels |
col | 0xRRGGBB colour |
alpha | 0..255 opacity |
returns void
gpu_draw_roundrect_lines_alpha(x, y, 200, 80, 0.3, 2.0, 0x8fb2ff, 180)
gpu_screen_open(w, h, title)open the graphics window at w x h and start its world
Idempotent: a second call while a window is open does nothing. w or h <= 0 falls back to 800 x 600. The window is RESIZABLE on the desktop, so the size you ask for is where it STARTS -- read gpu_screen_width()/gpu_screen_height() if your layout must follow it, and gpu_window_resized() to know when it moved. ON THE WEB (a wasm build in a browser tab) whether this size is honoured is decided by your own program text, not by a flag: a program that references gpu_screen_width() or gpu_screen_height() ANYWHERE has said it can lay out against a size it did not pick, so its canvas fills the browser window and this w x h is only where it starts; a program that never asks has hardcoded its world, so its canvas is exactly w x h and the rest of the page is left alone. Desktop behaviour is identical either way.
w | window width in pixels (<= 0 -> 800) |
h | window height in pixels (<= 0 -> 600) |
title | window title text |
gpu_screen_open(1280, 720, "My Game")
gpu_draw_rect(x, y, width, height, color)draw a filled rectangle at (x,y) sized width x height
raylib DrawRectangle.
x | top-left x |
y | top-left y |
width | rectangle width |
height | rectangle height |
color | fill color 0xRRGGBB |
returns void
gpu_draw_rect(10, 10, 100, 100, rgb(50,50,50));
gpu_frame_begin(clear_color)open a frame, clear it to color, and say whether to keep going
Wraps raylib BeginDrawing + ClearBackground and returns 0 when the user has asked to close, so the idiom is `while (gpu_frame_begin(color)) { ... }`. Also fires the per-frame automation + GUI hooks. WHAT COUNTS AS "asked to close": on the desktop, the window's close button or the ESC key (raylib's default exit key). IN A BROWSER TAB (a wasm build) there is no close button, and raylib's web platform never reports one -- so ESC is the only exit, and CX checks it there itself so that the same `while` loop ends on the same key it ends on natively. A program whose only exit is the close button therefore never ends on the web; give it a frame cap or an ESC.
clear_color | background color to clear this frame to (rgb()/0xAARRGGBB) |
returns 1 to draw this frame, 0 once the window should close
while (gpu_frame_begin(rgb(20,20,30))) { ...draw...; gpu_frame_end(); }gpu_frame_end()finish the frame and present it
Runs on both engines (since GAP, 3.5.006.0 -- until then the register VM refused it with CX-E1013). This is what makes drawing appear; it also swaps the buffers, which is why the flip calls are no-ops.
returns void
gpu_frame_end();
mediadraw(scene)draw a scene document, once
Walks the scene's elements in document order and draws each one the renderer knows, reading its place, its size and its look from the element's own fields. The program never names a drawing verb: it writes fields on the document, and this walks them. THE SEVENTEEN TYPES IT DRAWS. Flat: `rect` (plain, rounded or outlined), `circle`, `text`, `line`, `triangle`, `pixel`, `image`. In the world, inside the camera: `cube`, `sphere`, `cylinder`, `plane`, `line3d`, `point3d`, `grid`, `model`, `billboard`, `emitter3d`. And `emitter`, which is flat. `window` and `camera` are ACTED ON rather than drawn. Ask the running renderer with `mediaReport()["types"]` rather than keeping a copy of this list. WHERE AN ELEMENT IS. `->x` `->y` `->w` `->h` are percent of the window, PER AXIS -- x and w of its width, y and h of its height -- unless `->units` is `"absolute"`. `->pos` `->dim` `->target` `->axis` are world coordinates and are NEVER percent: a world has no edges to be a percentage of. `->points` holds the corners of a shape whose place is not one x,y (a line reads four numbers, a triangle six). `->z` is what covers what -- an ordering, not a distance -- and does not yet order the draw; that is the renderer's remaining debt. HOW IT LOOKS. `->color`, `->alfa1` (opacity 0-100), `->tint`, `->round` (corner roundness 0-1), `->outline` (thickness in pixels; absent or 0 means FILLED, and it is what makes every `*_wires` and `*_lines` variant one shape), `->size` (a text's height in pixels), `->hidden`. WHAT IT SHOWS. `->source` is a filename, the bytes, or PARSEDOC's own image document; `->frame` is the part of the source drawn, in the SOURCE's own pixels; `->pivot` is what a rotation turns about, in percent of the element's own box; `->rot` is that rotation; `->scale` sizes a model or a billboard. `->orgw` and `->orgh` are written by the renderer when the source loads. BEHAVIOUR AND EVENTS. `->rules` is CX source as TEXT, compiled at run time, with the element bound as `e`; `->tick` asks for it every frame rather than only when the element changed. `->hover` and `->clicked` are written by the renderer -- `->clicked` is the frame a press BEGAN inside the element, and it is cleared once the rules have seen it. THE WINDOW ELEMENT carries `->fullscreen`, `->maximized`, `->resized`, `->load` (`"lazy"` or `"full"`, the scene's default), and the input the renderer sampled: `->mouse` (percent), `->buttons`, `->wheel`, `->key`, `->char`. THE CAMERA carries `->pos`, `->target` and `->fov`. AN EMITTER carries `->source` (the effects file), `->effect`, `->on`, `->burst`, `->dir` and `->vel`. THE DRAW IS GATED ON `->changed`, through a cached render target priced before it was relied on: at one changed frame in a hundred it costs 12 ms where an immediate loop costs 507, and at every-frame change it is still 13% cheaper. The renderer CLEARS the flag as it acts, because "only the changed items are acted on" is only true if acting is what ends the change. A change walks `->parent` and `->next` in both directions, and the flag is its own visited mark, so a cycle stops on its second arrival. A LIVE EMITTER MAKES EVERY FRAME STALE, and that is correctness rather than caution: its particles move inside the particle system, so nothing in the document changes while the picture does.
scene | a json document whose members are elements (parseDoc reads it, and a document declared `media` is the same document) |
returns how many elements it drew; -1 when the program never declared a media anywhere, so no renderer was linked
media scene = parseDoc(text); mediaDraw(scene); n.i = mediaDraw(scene);
mediarun(scene, maxFrames)hand the window to the renderer and get it back when it closes
THE FRAME IS THE RENDERER'S, which is what "the renderer is invisible" means in practice: a program opens a window, hands over its scene, and writes no drawing verb at all -- not even a thin one. gpu_screen_open(640, 480, "title"); mediaRun(scene, 0); gpu_screen_close(); The frame clears to black and a scene that wants a background says so with an element, not with an argument -- one fewer parameter, and the background stays IN the document with every other visual fact. `mediaDraw` is the STEP this is the LOOP over, and they are two operations rather than two names for one: a check cannot open a window, so the step is what a check drives. IT OWNS THE WHOLE FRAME, which is more than the drawing. Each pass samples the pointer and the keyboard onto the scene, runs the rules, walks the change, updates any particle system the scene is running, draws the world inside the camera and the flat elements on top of it, and spends the flags it acted on. A program that hands over its scene writes none of that.
scene | the scene document |
maxFrames | stop after this many frames; 0 or less means "until the window closes", which is the ordinary case |
returns how many frames it ran -- 0 with no window, which is the honest answer rather than a hang
mediaRun(scene, 0); n.i = mediaRun(scene, 120);
mediareport()what the last walk saw, as a document
One handle rather than a builtin per number: `drawn` how many were drawn, `hidden` how many carried `->hidden`, `changed` how many carried `->changed` once the links had been walked, `unknown` how many drew nothing -- a `->type` the renderer does not know, or one it knows whose source would not load -- `walked` how many elements the propagation reached, `ruled` how many rules ran, `acted` how many elements were acted on rather than drawn (the window, the camera, an emitter), `stale` whether anything changed this frame, `redrew` whether the frame rebuilt the cached target or blitted the one it already had, and `types` the list of `->type` names this renderer draws. `types` IS THE FACE SAYING WHAT IT DRAWS, and it exists so a reader -- a check, a second provider, a person -- can ASK rather than keep a copy. A fixture that names every type the renderer claims cannot fall behind it; one written by hand falls behind the day a family lands. `walked`, `stale` AND `redrew` ARE WHAT MAKE THE RENDERER CHECKABLE. A propagation that terminates and a draw that was correctly skipped both look precisely like nothing happening, and "nothing happening" is not a measurement. `stale` AND `redrew` ARE TWO FACTS, not one spelled twice. `stale` is the question the gate asks -- did anything change? -- and has the same answer with a window and without one. `redrew` is what that answer CAUSED, and is only meaningful where a cached target exists, which means a window. A check that cannot open one reads `stale`. It exists so a scene can be checked WITHOUT a window and without reading pixels: the renderer says what it did instead of leaving a caller to infer it (FP4).
returns a json document; an invalid one when no renderer was linked
json r = mediaReport(); println(str(r["drawn"]));
json t = mediaReport()["types"]; // what this renderer draws
if (r["unknown"]) { println("the scene names a type the renderer does not know"); }
json r = mediaReport(); if (r["redrew"]) { println("the target was rebuilt"); }gpu_pfx_load(filename)loads particle-effect templates from a JSON file and resets the particle pool
Parses each effect under the JSON "effects" object into a named template; the "maxParticles" field sets the pool's hard cap. Reloading discards existing templates and live particles.
filename | path to the effects JSON file |
returns void
gpu_pfx_load("Orfeus/data/effects.json");gpu_pfx_spawn(name, x, y, angle_deg, parent_vx, parent_vy)spawns a burst of particles from a named template at a position
Emits the template's count particles, each with randomized angle, velocity and lifetime; the parent velocity is added scaled by the template's velocityScale. No-op if the name is unknown.
name | effect/template name to spawn |
x | spawn x position in pixels |
y | spawn y position in pixels |
angle_deg | emission direction in degrees |
parent_vx | emitter x-velocity, added to each particle (scaled by velocityScale) |
parent_vy | emitter y-velocity, added to each particle (scaled by velocityScale) |
returns void
gpu_pfx_spawn("sparks", 400.0, 300.0, 0.0, 0.0, 0.0);gpu_pfx_spawn3d(name, x, y, z, dx, dy, dz, pvx, pvy, pvz)emit a WORLD-space (3D) particle burst from a template
Spawns the template's `count` particles at world (x,y,z) with velocity biased along the direction (dx,dy,dz); the template's `angleSpread` is the emission cone half-angle. A near-zero direction bursts omnidirectionally (explosion). Each particle also inherits the emitter's velocity (pvx,pvy,pvz) scaled by the template's `velocityScale` -- so a continuously-emitted plume STREAMS with the moving ship (attached) instead of being left behind as a world-static jet. Pass 0,0,0 for a stationary burst (impacts / explosions). Requires the template's `space` to be `"world"` and a `texture`. Render with gpu_pfx_render3d.
name | effect template name (from the loaded effects.json) |
x | world X of the emitter |
y | world Y of the emitter |
z | world Z of the emitter |
dx | emission direction X (0,0,0 = omnidirectional) |
dy | emission direction Y |
dz | emission direction Z |
pvx | emitter velocity X to inherit (same units as particle motion; 0 = none) |
pvy | emitter velocity Y to inherit |
pvz | emitter velocity Z to inherit |
gpu_pfx_spawn3d("engine", ex,ey,ez, -fx,-fy,-fz, svx,svy,svz);gpu_pfx_update(delta_ms)advances every live particle by a time delta, ageing and moving them
Applies gravity, integrates position by velocity, and kills particles whose age reaches their lifetime.
delta_ms | milliseconds elapsed since the last update |
returns void
gpu_pfx_update(16);
gpu_pfx_render()draws all live particles via the gfx layer, interpolating scale, colour and alpha by age
Renders each particle as a circle, rect, pixel or tinted image per its template, blending colour along the template's ramp and fading alpha over life.
returns void
gpu_pfx_render();
gpu_pfx_render3d(cam, ox, oy, oz, scale)draw all WORLD-space particles as camera-facing billboards
Call INSIDE the active 3D camera pass (between gpu_camera_begin/…_end). Each world template is drawn in ONE internal batch. Screen-space templates are untouched -- render those with gpu_pfx_render (the 2D overlay) as before. Particles live in an application-defined ABSOLUTE space; each is mapped to render space as (pos - origin)*scale (the floating-origin transform). With no floating origin, pass origin (0,0,0) and scale 1. `size` stays in render units.
cam | the 3D camera handle (from gpu_camera_new) |
ox | current render-origin X in the particles' space (0 if none) |
oy | current render-origin Y |
oz | current render-origin Z |
scale | particle-space -> render-unit scale (1 if none) |
gpu_pfx_render3d(cam, gOriginX, gOriginY, gOriginZ, WORLD_SCALE);
gpu_pfx_set(name, key, val)live-tunes a named field of a particle template
Sets one of a fixed set of tunable fields (count, size, speed, randomVelocity, lifetime, randomLifetime, emitW, emitH, angleSpread, scaleStart, scaleEnd); int fields take the value cast to int. No-op if the template or field is unknown.
name | template name to modify |
key | field name to set |
val | new value (cast to int for integer fields) |
returns void
gpu_pfx_set(efx[curE], pField[idx], av);
gpu_pfx_get(name, key)reads a named field of a particle template
Returns one of the same tunable fields gpu_pfx_set accepts, as a float.
name | template name to query |
key | field name to read |
returns float -- the field's current value (0.0 if the template or field name is unknown)
av = gpu_pfx_get(efx[curE], pField[i]);
gpu_pfx_count()returns the number of loaded particle-effect templates
returns int -- count of loaded templates
nfx.i = gpu_pfx_count();
gpu_pfx_name(i)returns the name of the i-th loaded particle-effect template
Lets a tuner enumerate effects straight from the loaded JSON by index.
i | zero-based template index |
returns string -- the template's name (empty string if i is out of range)
efx[fi] = gpu_pfx_name(fi);
gpu_pfx_clear()kills all live particles, keeping the loaded templates
Zeroes the particle pool and resets the recycle cursor; templates remain loaded.
returns void
gpu_pfx_clear();
gpu_collide_spheres(x1, y1, z1, r1, x2, y2, z2, r2)test whether two spheres intersect (raylib CheckCollisionSpheres)
x1 | sphere 1 center x |
y1 | sphere 1 center y |
z1 | sphere 1 center z |
r1 | sphere 1 radius |
x2 | sphere 2 center x |
y2 | sphere 2 center y |
z2 | sphere 2 center z |
r2 | sphere 2 radius |
returns 1 if the spheres intersect, else 0
gpu_collide_spheres(0.0,0.0,0.0, 2.0, 1.0,0.0,0.0, 2.0)
gpu_collide_boxes(min1x, min1y, min1z, max1x, max1y, max1z, min2x, min2y, min2z, max2x, max2y, max2z)test whether two axis-aligned bounding boxes intersect (raylib CheckCollisionBoxes)
Each box is given by its min and max corner.
min1x | box 1 min x |
min1y | box 1 min y |
min1z | box 1 min z |
max1x | box 1 max x |
max1y | box 1 max y |
max1z | box 1 max z |
min2x | box 2 min x |
min2y | box 2 min y |
min2z | box 2 min z |
max2x | box 2 max x |
max2y | box 2 max y |
max2z | box 2 max z |
returns 1 if the boxes intersect, else 0
gpu_collide_boxes(0.0,0.0,0.0, 2.0,2.0,2.0, 1.0,1.0,1.0, 3.0,3.0,3.0)
gpu_collide_box_sphere(minx, miny, minz, maxx, maxy, maxz, cx_, cy_, cz_, radius)test whether a box and a sphere intersect (raylib CheckCollisionBoxSphere)
Box given by min/max corners; sphere by center and radius.
minx | box min x |
miny | box min y |
minz | box min z |
maxx | box max x |
maxy | box max y |
maxz | box max z |
cx_ | sphere center x |
cy_ | sphere center y |
cz_ | sphere center z |
radius | sphere radius |
returns 1 if the box and sphere intersect, else 0
gpu_collide_box_sphere(0.0,0.0,0.0, 2.0,2.0,2.0, 1.0,1.0,1.0, 0.5)
rgb(r, g, b)pack red, green, blue bytes into a 24-bit color integer
Computes ((r&0xFF)<<16)|((g&0xFF)<<8)|(b&0xFF); each channel is masked to its low 8 bits.
r | red channel (0-255; masked to 8 bits) |
g | green channel (0-255; masked to 8 bits) |
b | blue channel (0-255; masked to 8 bits) |
returns packed 24-bit color integer in 0x00RRGGBB layout
colBg.i = rgb(20, 20, 40);
rgba(r, g, b, a)pack red, green, blue, alpha bytes into a 32-bit color integer
Computes ((a&0xFF)<<24)|((r&0xFF)<<16)|((g&0xFF)<<8)|(b&0xFF); each channel is masked to its low 8 bits.
r | red channel (0-255; masked to 8 bits) |
g | green channel (0-255; masked to 8 bits) |
b | blue channel (0-255; masked to 8 bits) |
a | alpha channel (0-255; masked to 8 bits) |
returns packed 32-bit color integer in 0xAARRGGBB layout
c4.i = rgba(255, 0, 0, 128);
strtoi(s)parse the leading base-10 integer out of a string
Forwards to bi_vali, which is strtoll(s, NULL, 10); takes only the leading numeric prefix and yields 0 when there are no leading digits.
s | string to parse |
returns the parsed 64-bit integer (base 10); 0 if the string has no leading numeric prefix
assertEqual(42, strtoi("42"));strint(s)parse the leading base-10 integer out of a string
Exact alias of strtoi (same cx_stub_strtoi/bi_vali/strtoll base-10 parse); the FP1 unification resolved a prior backend divergence where risc treated strint as int->string.
s | string to parse |
returns the parsed 64-bit integer (base 10); 0 if the string has no leading numeric prefix
assertEqual(12345, strint("12345"));strtof(s)parse the leading floating-point number out of a string
Forwards to cx_stub_strtof, which is strtod(s, NULL); returns 0.0 for an empty string.
s | string to parse |
returns the parsed float (via strtod); 0.0 if the string is empty
f = strtof("3.14");strfloat(s)parse the leading floating-point number out of a string
Exact alias of strtof (same cx_stub_strtof/strtod parse).
s | string to parse |
returns the parsed float (via strtod); 0.0 if the string is empty
assertFloatEqual(3.14, strfloat("3.14"), 0.01);trim(s)strip whitespace from both ends
Whitespace is C's isspace(): space, tab, newline, carriage return, vertical tab, form feed. Interior whitespace is untouched -- `trim(" a b ")` is "a b", not "ab".
s | string to trim |
returns a new string with leading and trailing whitespace removed
name.s = trim(fread("name.txt"));ltrim(s)strip whitespace from the LEFT end only
s | string to trim |
returns a new string with leading whitespace removed; trailing whitespace stays
body.s = ltrim(line);
rtrim(s)strip whitespace from the RIGHT end only
The one to reach for when stripping a line ending: it removes "\r\n" as whitespace, so a Windows-terminated line and a Unix one come out the same.
s | string to trim |
returns a new string with trailing whitespace removed; leading whitespace stays
line.s = rtrim(raw);
reversestring(s)reverse a string's BYTES
Bytes, not characters: reversing UTF-8 text that has any multi-byte character in it produces invalid UTF-8. Safe for ASCII, wrong for anything else.
s | string to reverse |
returns a new string with the bytes in the opposite order
r.s = reversestring(word);
hex(n)an integer as UPPERCASE hexadecimal digits
No "0x" prefix and no padding, so 255 gives "FF". A negative number is rendered as its 64-bit two's-complement pattern -- `hex(-1)` is the 16 characters "FFFFFFFFFFFFFFFF", not "-1". Verified on both backends.
n | integer to render |
returns the hex digits; "0" for zero
println("mask=" + hex(flags));bin(n)an integer as binary digits
Leading zeros are trimmed, so 5 gives "101" and 0 gives "0". A negative number is rendered as its 64-bit two's-complement pattern -- `bin(-1)` is 64 '1' characters, not "-1". Verified on both backends.
n | integer to render |
returns the binary digits; "0" for zero
println(bin(mask));
space(n)a string of n spaces
n | how many spaces |
returns the string, or "" if n <= 0
println(space(indent) + label);
left(s, n)the first n bytes of a string
Clamps rather than failing: asking for more than there is gives the whole string back, and a zero or negative count gives "". The original is never modified -- every builtin in this family returns a NEW string.
s | source string |
n | how many bytes to take from the start |
returns the leading n bytes, the whole string if n exceeds its length, or "" if n <= 0
code.s = left(line, 3);
right(s, n)the last n bytes of a string
The mirror of left(), with the same clamping: over-long n gives the whole string, n <= 0 gives "".
s | source string |
n | how many bytes to take from the end |
returns the trailing n bytes, the whole string if n exceeds its length, or "" if n <= 0
ext.s = right(path, 4);
lset(s, width)left-justify a string in a fixed-width field
Pads on the RIGHT with spaces to reach `width`. A string already at or over the width is TRUNCATED to it, keeping the left -- so the result is always exactly `width` bytes, which is what makes it usable for column output.
s | string to place |
width | field width in bytes |
returns a string of exactly `width` bytes, or "" if width <= 0
println(lset(name, 20) + str(score));
rset(s, width)right-justify a string in a fixed-width field
Pads on the LEFT with spaces to reach `width`. Over-long input is truncated to the width keeping the LEFT-hand bytes -- the same truncation as lset(), which is worth knowing because right-justified numbers lose their least significant digits, not their most.
s | string to place |
width | field width in bytes |
returns a string of exactly `width` bytes, or "" if width <= 0
println(rset(str(n), 8));
countstring(hay, needle)how many times a substring occurs
Matches are NON-OVERLAPPING: counting "aa" in "aaaa" gives 2, not 3, because the scan resumes after each hit. Same rule replacestring() uses, so the two always agree on how many replacements will happen.
hay | string to search |
needle | substring to count |
returns the number of non-overlapping occurrences; 0 if the needle is empty or longer than the haystack
n.i = countstring(csv, ",");
startswith(hay, prefix)does a string begin with this prefix?
Case-SENSITIVE, byte-exact. An empty prefix answers 1.
hay | string to test |
prefix | prefix to look for |
returns 1 if `hay` begins with `prefix` (or the prefix is empty), else 0
if (startswith(path, "http://")) { ... }endswith(hay, suffix)does a string end with this suffix?
Case-SENSITIVE, byte-exact. An empty suffix answers 1. The usual way to test a file extension -- lowercase the name first if the check should be case-blind.
hay | string to test |
suffix | suffix to look for |
returns 1 if `hay` ends with `suffix` (or the suffix is empty), else 0
if (endswith(tolower(name), ".json")) { ... }strcmpi(a, b)compare two strings, ignoring ASCII case
Returns a SIGN, not a difference: exactly -1, 0 or +1, so the value is stable across backends and safe to compare against a literal. Ordering is by byte after lowercasing, and a string that is a prefix of the other sorts first. Equality is `== 0`, which is the usual C trap -- a bare `if (strcmpi(a,b))` tests for DIFFERENCE.
a | first string |
b | second string |
returns -1 if a sorts before b, 0 if they match case-insensitively, +1 if after
if (strcmpi(cmd, "QUIT") == 0) { ... }removestring(s, sub)delete every occurrence of a substring
Exactly replacestring(s, sub, "") -- same non-overlapping, all-occurrences rule.
s | source string |
sub | substring to delete; empty means "change nothing" |
returns a new string with every occurrence removed
clean.s = removestring(raw, "\r");
replacestring(s, old, nw)replace every occurrence of a substring
Replaces ALL non-overlapping matches in one pass, left to right, and never re-scans what it wrote -- so replacing "a" with "aa" terminates rather than running away. An empty `old` is a no-op returning a copy, not an insertion at every position.
s | source string |
old | substring to find; empty means "change nothing" |
nw | text to put in its place; empty deletes (that is removestring) |
returns a new string with every match replaced
out.s = replacestring(path, "\\", "/");
insertstring(s, ins, where)splice text into a string at a 0-based position
`where` counts from 0 like every other position in this family: 0 prepends, s->len appends, and a NEGATIVE `where` counts from the end exactly as substr(s, -3) does -- -1 inserts before the last character. OUT OF RANGE CLAMPS, it does not fail: past the end appends, and more tail than the string has prepends. That is substr's answer, taken deliberately rather than re-decided, because two out-of-range rules inside one string family is the seam counting from 0 exists to close.
s | source string |
ins | text to insert |
where | 0-based position the inserted text will start at; negative counts from the end; out of range clamps |
returns a new string with `ins` spliced in
out.s = insertstring(num, ",", 3);
length(s)how many BYTES a string holds
Bytes, not characters: the whole string layer is UTF-8-byte-level, so `length("hello")` with an accented e is 6, not 5. Every other index in this family -- substr, strstr, left -- counts the same bytes, so they agree with each other; none of them is codepoint-aware. `length` is also the polymorphic spelling both backends accept on a container -- but the container form is the RETIRED one (CX-E1048 since v3.186.0). Read a list's or map's size with `<handle>->count` instead. This entry documents the string form, which is not retired.
s | string to measure |
returns its byte length; 0 for an empty string
n.i = length(name);
chr(n)a one-byte string from a byte value
The inverse of asc(). Deliberately refuses to build a string containing an embedded NUL, and takes a BYTE rather than a codepoint, so there is no way to spell a multi-byte UTF-8 character with a single call.
n | byte value, 1..255 |
returns a 1-byte string, or "" if n is 0, negative, or above 255
tab.s = chr(9);
asc(s)the numeric value of a string's FIRST byte
Reads one byte, not one character, so the first byte of a multi-byte UTF-8 character gives that byte's value (a lead byte, >= 0xC0), not the codepoint.
s | string to read |
returns the first byte as 0..255, or 0 for an empty string. An empty string and a string starting with a NUL are indistinguishable here.
code.i = asc(letter);
contains(hay, needle)does a string hold this substring?
Case-SENSITIVE, byte-exact. An empty needle answers 1 -- everything contains nothing -- which now agrees with `strstr(s, "") >= 0`. "VERIFIED ON BOTH BACKENDS" IS WHAT THIS BLOCK USED TO SAY, AND IT WAS NOT TRUE: measured 2026-09-06, `contains(s, "")` answered 1 native and 0 on the register VM. The cause is W1's whole subject -- vb_contains did not call THIS function, it hand-rolled the test over the search core and read its "absent" as "no". Fixed by calling bi_contains, which is what a wrapper is for. `contains` is also the spelling for value-membership in a list or array, and both backends dispatch on the first argument's type. This entry documents the string form.
hay | string to search |
needle | substring to look for |
returns 1 if present (or the needle is empty), else 0
if (contains(line, "ERROR")) { ... }split(s, sep, out)break a string on a separator into a string list
CLEARS `out` FIRST, then fills it, so a second split into the same list holds the second split and nothing else. His ruling 2026-09-17: "yes reset". IT APPENDED UNTIL THEN, and that was documented rather than accidental -- which is why this is a BEHAVIOUR CHANGE and is named as one. The accumulate idiom the old default licensed was used NOWHERE: measured over the whole corpus, NINE sites wrote `listClear(xs)` immediately before a split to defend against it (tests/anvil.cx x6, tests/forge.cx x2, tests/srcdb.cx x1 -- three of the tree's own primary tools) and ZERO relied on it. A default every caller defends against is the wrong default, and a caller that FORGOT to defend got a list holding two splits' worth of fields with no diagnostic (TOOLS-397 F2). A `listClear` before a split is now redundant, never wrong. EMPTY FIELDS ARE KEPT: "a,b,,c" yields four entries, the third being "" -- this is a faithful split, not a tokenizer, so consecutive separators do not collapse. There is always a trailing field, so a string ending in the separator contributes a final "". An empty separator yields the whole string as a single field and returns 1. `stringsplit` is NOT this builtin -- it reads ONE field and returns text. This is the whole-string form and it is the only name for it.
s | string to split |
sep | separator to split on |
out | a string list, CLEARED and then filled with the fields |
returns the number of fields -- always at least 1
list cols.s n.i = split(row, ",", cols);
toupper(s)uppercase a whole string
Despite the C name it takes and returns a STRING, not a character code, and it converts the entire string -- there is no per-character form. ASCII only, per byte, via C's toupper(): bytes outside a-z pass through unchanged, so accented and non-Latin UTF-8 text is left exactly as it was rather than mangled. Codepoint-aware folding is a separate operation and is not this one.
s | string to convert |
returns a new uppercased string (ASCII letters only; other bytes unchanged)
shout.s = toupper(word);
tolower(s)lowercase a whole string
Takes and returns a STRING, not a character code, and converts the entire string -- there is no per-character form. ASCII only, per byte, via C's tolower(); bytes outside A-Z pass through unchanged, so non-Latin text survives intact.
s | string to convert |
returns a new lowercased string (ASCII letters only; other bytes unchanged)
key.s = tolower(header);
capitalize(s, mode)re-case a string, title-case by default
Called with one argument it TITLE-CASES: each word's first letter uppercase, the rest lowercase. A "word" boundary is any run of non-letters, which means digits and apostrophes start a new word too -- "o'brien x2y" becomes "O'Brien X2Y". Verified, on both backends; if that is not what you want, use toupper/tolower on the pieces yourself. The optional mode selects the whole-string forms instead: 0 = uppercase, 1 = lowercase, 2 = title-case. An out-of-range mode falls back to 0 (uppercase), it is not an error.
s | string to re-case |
mode | 0 upper, 1 lower, 2 title; omit for title-case |
returns a new re-cased string
title.s = capitalize(raw);
fprintf(path, fmt)format text and APPEND it to a file
Appends; it never truncates, so repeated calls build a file up (this is the log-writing shape). Formatting honours `#pragma decimals` for floats, and the formatted result spills to the heap when it outgrows the stack buffer, so output length is not capped.
path | file to append to; an empty path returns 0 |
fmt | format string, printf-style |
returns the number of bytes written -- a genuine count, unlike fwrite. 0 on any failure (bad format, unopenable path).
n.i = fprintf("log.txt", "run %d took %f s\n", id, secs);ftoa(v)render a float as a string (C's name for str on a float)
The same renderer str reaches for a float argument, so the decimal count follows `#pragma decimals`. strf(v, d) is the form that takes the decimals per call.
v | the float to render |
returns the number as text
s.s = ftoa(ratio);
getc(s, idx)the byte at a 0-indexed position in a string
ZERO-indexed, like every index in the language. This is also what `s[i]` lowers to on both backends, so the subscript form and the call form are the same operation. Reads one BYTE, not one character. Out of range returns 0 by default. Under `#pragma checks on` it is instead a loud bounds error naming the index and the length -- the lenient 0 is the legacy behaviour, not the safe one.
s | string to read |
idx | 0-indexed byte position |
returns the byte as 0..255, or 0 if idx is out of range
c.i = getc(line, 0);
len(v)how many BYTES a string holds -- or, on a json value, its length by kind
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
DISPATCHED BY TYPE on both backends: a string answers its byte count, and a JSON value answers its text length or its child count depending on the node's kind. **It is RETIRED for list / map / array**: `len(xs)` on a container is refused at compile time with CX-E1048 naming `<handle>->count`, which is the one spelling for a container's length. strlen is the string-only name for the same byte count.
v | the string, list, map, array or JSON value to measure |
returns the string's byte count (0 for ""), or the json value's length by kind; CX-E1048 on a list, map or array
n.i = len(line); // a container asks a different way: xs->count
ord(s)the numeric value of a string's FIRST byte (the other name for asc)
BYTES, NOT CODEPOINTS: a multi-byte UTF-8 character gives that character's lead byte (>= 0xC0), not its Unicode value. chr is the inverse.
s | string to read the first byte of |
returns the byte as 0..255; 0 for an empty string
code.i = ord(key);
parsehex(s)read a hexadecimal string as an integer
A plain base-16 parse: no "0x" prefix is required (one is accepted), case does not matter, and parsing stops at the first non-hex character with no error channel -- unparseable text gives 0, the same silence the base-10 read behind `(int)<s>` has. For a "rrggbb" colour the result is the NATURAL 0xRRGGBB packing, so `parsehex("ff0000")` is 16711680 (red), matching `rgb()` and the draw builtins. This header claimed the opposite until v3.209.20 -- it described the PureBasic era's byte-swapped 0xBBGGRR and said so as the contract, while the implementation had carried a comment stating plainly that the C runtime does NOT swap and that `parsehex("ff0000") must be red, not blue`. Two homes, two truths; the header was the wrong one, and it is the versioned surface a reader sees. Measured on both backends before correcting.
s | hex digits, with or without a leading "0x" |
returns the value, or 0 if the text does not start with a hex digit
col.i = parsehex("ff8800");prts(s)write a STRING to stdout with no newline
The raw piece-by-piece output form: no newline, no formatting, no conversion. prti, prtf and prtc are the int, float and character siblings.
s | the string to write |
returns void
sprintf(fmt)format text. ONE name, TWO shapes, and ARGUMENT 1 chooses
`sprintf(fmt, args...)` returns the formatted STRING. This is CX's form and the one nearly every program wants: nothing to size, nothing to overflow. `fmt` may be a literal or a `string` variable; conversions are libc's (the compiler rewrites `%d` to `%lld` upstream) and honour `#pragma decimals`. `sprintf(buf, fmt, args...)` where `buf` is a `char` buffer is C's form, verbatim: it writes into the buffer, NUL-terminates, and returns the BYTE COUNT. It exists so that C pasted into CX means what it meant in C -- the `Examples/C/` programs are Rosetta-Code source, unedited. Both backends answer identically; the register VM clamps the write to the declared size rather than running off the array, since C's own answer there is undefined. Argument 1's DECLARED TYPE picks the shape, at compile time. Anything that is neither a `char` buffer nor a string -- an int, a float, a container -- is CX-E1065 at the .cx line, because there is no third reading to guess at.
fmt | the format string (CX form), OR the destination buffer (C form) |
returns the formatted string (CX form), or the byte count written (C form)
label.s = sprintf("%s: %d%%", name, pct);
char buf[64]; int n = sprintf(buf, "%d/%d", num, den);str(n)render a number as a string
The compiler picks the int or the float renderer from the argument's type, so str(3) gives "3" and str(3.0) gives "3.000" -- three decimals, because the float form honours `#pragma decimals`, whose default is 3. Use strf when you want to fix the decimal count at the call site instead.
n | the int or float to render |
returns the number as text
line.s = "score: " + str(points);
strchr(s, ch)0-indexed position of a character code in a string, C-style
Takes the character as a NUMERIC CODE, as C does, not as a one-character string. Returns -1 when the character is absent, so the test is `>= 0`; strstr is the same search with a string needle.
s | the string to search |
ch | the character code to look for |
returns the 0-indexed position, or -1 if the character does not occur
at.i = strchr(line, 61);
strcmp(a, b)compare two strings in byte order, C-style
Returns a SIGN, not a boolean: negative if `a` sorts before `b`, 0 if they are equal, positive if after. Comparison is by raw byte value, so it is case-sensitive and not locale-aware. CX's `==` on strings is the readable way to test equality; strcmp is for ordering.
a | the first string |
b | the second string |
returns negative, 0, or positive as `a` sorts before, equal to, or after `b`
if (strcmp(name, pivot) < 0) { ... }strf(v, d)a float as text, with the number of decimals you choose
`strf(v)` renders at the file's `#pragma decimals` setting (default 3), which is what `str()` and `ftoa()` do. `strf(v, d)` overrides it FOR THAT CALL -- the pragma is file-global and last-wins, so this is the only way to write a price at 2 decimals and an angle at 6 in the same program. `d` is C's `%.*f` precision and behaves as C does, including the corner: a NEGATIVE `d` means "precision omitted", which in C is six decimals. There is no upper limit -- ask for 40 and you get 40, correctly, however long the result runs.
v | the float to render |
d | decimals to show; omit to use `#pragma decimals` |
returns the rendered string
label.s = strf(price, 2) + " at " + strf(angle, 6) + " rad";
strlen(s)how many BYTES a string holds (C's name for length, string-only)
Counts bytes, not characters, so a multi-byte UTF-8 string measures longer than it looks. Unlike len, this name is string-only: it does not answer for a list, map or array.
s | the string to measure |
returns the byte count; 0 for an empty string
n.i = strlen(line);
strstr(h, n, start)where a substring starts, counting from 0
THE C answer and now the only one: a ZERO-indexed position, and -1 for "not found". So the test is `>= 0`, NOT `> 0` -- a `> 0` test silently rejects a match at the very start of the string, which is the whole reason the 1-based spelling is leaving. An optional third argument resumes the search from a 0-based position, which is how a program walks every occurrence without rebuilding the haystack. AN EMPTY NEEDLE MATCHES AT `start` -- `strstr(s, "")` is 0 and `strstr(s, "", 3)` is 3. That is C's answer (its strstr hands back a pointer to the haystack) and Python's. It CHANGED in this arc: the 1-based core it used to adapt answered "absent" for an empty needle so a loop could not spin, and at 0 that reasoning inverts -- 0 is a real position, and -1 would make the empty needle the one case where a substring the string demonstrably contains reports absent. A start past the end is -1, empty needle or not.
h | string to search |
n | substring to look for |
start | 0-indexed position to resume from; omit for 0 |
returns the 0-indexed byte position of the first match at or after `start`, or -1 if there is none
if (strstr(line, "=") >= 0) { ... }substr(s, start, n)a substring, counting from 0
THE substring operation -- there is no longer a second, 1-indexed one. Two forms: `substr(s, start)` runs to the end, `substr(s, start, n)` takes at most n bytes, and both clamp rather than fail. A NEGATIVE START COUNTS FROM THE END, which is what makes `right(s, n)` a call to this rather than a function of its own: `substr(s, -5)` is the last five bytes, and asking for more tail than there is gives the whole string. FIXED in v3.209.22 -- and the fix was NOT the one name this note predicted. Until then the compiler did not know substr returned a string, so `"x=" + substr(s, 0)` failed the BUILD on both backends with a type error out of the generated C. The cause was not a missing entry: the classifier consulted a GENERATED table that only ever read `CX_BUILTIN` spec lines, and this family has none, so ~66 hand-bound builtins were invisible to it and a hand list was expected to know them all. The generator now derives the answer from these C prototypes instead (tests/strret_derive.awk), which found four more builtins with the same defect. See tests/bugs/B_strret_two_sources.
s | source string |
start | 0-indexed byte position to start at; negative counts back from the end, and below -length clamps to the start |
n | how many bytes to take, clamped to what remains; omit for "to the end" |
returns the substring, or "" if start is at or past the end
part.s = substr(line, 4, 3);
regexmatch(s, pattern)does `pattern` match anywhere in `s`?
s | the subject text. |
pattern | the regular expression. |
returns 1 on a match, 0 on none. A pattern that cannot be COMPILED does not answer 0 -- it refuses (CX-E5067), because 0 is a legitimate answer and cannot also mean "your pattern is broken".
if (regexMatch(line, "^[A-Z][a-z]+$")) { ... }regexcount(s, pattern)how many non-overlapping matches of `pattern` are in `s`?
s | the subject text. |
pattern | the regular expression. |
returns the count, 0 for none. Agrees with regexExtract's `->count` by construction: both walk the subject with the same one-byte advance past an empty match.
n = regexCount(text, "\\bthe\\b");
regexextract(s, pattern)every match of `pattern` in `s`, with its capture groups, as a json document
The document is an ARRAY of matches. A match with no NAMED groups is an array of groups -- `m[0]` the whole match, `m[1]` the first group. A match from a pattern that names any group is an OBJECT carrying both halves: "0", "1", ... as string keys AND each name as a member, because a json node is an array or an object and cannot be both, and dropping the positions to gain the names would silently break `m[1]` for a pattern that happens to name one group. Which form is in play is decided once from the PATTERN, never per match.
s | the subject text. |
pattern | the regular expression. |
returns a json document handle. No match is an EMPTY ARRAY, not null, so `for (i = 0; i < d->count; i++)` needs no special case first. An unset group is an empty string rather than a missing element, so group N is always at index N.
json d = regexExtract(s, "(\\d+)-(\\d+)"); json m = d[0]; print m[1]; // the first group of the first match
regexreplace(s, pattern, repl)`s` with every non-overlapping match of `pattern` replaced by `repl`
s | the subject text. |
pattern | the regular expression. |
repl | the replacement, inserted LITERALLY -- `$1` and `\1` are those two characters, not a group reference. Building a replacement out of groups is regexExtract's job. |
returns the rewritten text; `s` unchanged when nothing matched.
clean = regexReplace(s, "[^0-9]", "");
sin(x)sine of an angle in radians
x | angle in radians |
returns the sine, in -1.0 .. 1.0
y.f = amplitude * sin(t);
cos(x)cosine of an angle in radians
x | angle in radians |
returns the cosine, in -1.0 .. 1.0
x.f = radius * cos(angle);
tan(x)tangent of an angle in radians
x | angle in radians |
returns the tangent; unbounded, and huge near odd multiples of pi/2
asin(x)arc sine: the angle whose sine is `x`
x | a value in -1.0 .. 1.0; outside that range the result is NaN |
returns the angle in radians, in -pi/2 .. pi/2
acos(x)arc cosine: the angle whose cosine is `x`
x | a value in -1.0 .. 1.0; outside that range the result is NaN |
returns the angle in radians, in 0 .. pi
atan(x)arc tangent: the angle whose tangent is `x`
Takes one argument, so it cannot tell quadrant II from IV -- use `atan2` when you have both components of a direction.
x | any value |
returns the angle in radians, in -pi/2 .. pi/2
atan2(y, x)the angle of the vector (x, y), using both signs to pick a quadrant
NOTE THE ARGUMENT ORDER: `y` FIRST, as in C. It is the usual way to turn a delta into a heading, and the reason it beats `atan(y/x)` is that it is defined when `x` is 0 and it knows which half of the circle you are in.
y | the vertical component |
x | the horizontal component |
returns the angle in radians, in -pi .. pi
heading.f = atan2(ty - py, tx - px);
sinh(x)hyperbolic sine
x | any value |
returns the hyperbolic sine; overflows to infinity for large `x`
cosh(x)hyperbolic cosine
x | any value |
returns the hyperbolic cosine, always >= 1.0
tanh(x)hyperbolic tangent
x | any value |
returns the hyperbolic tangent, in -1.0 .. 1.0
exp(x)e raised to the power `x`, the inverse of `log`
x | the exponent |
returns e**x; overflows to infinity for large `x`
log(x)natural logarithm (base e)
This is C's `log`, NOT base 10 -- `log10` is the base-10 one.
x | a positive value; 0 gives -infinity and a negative gives NaN |
returns the natural log
log10(x)base-10 logarithm
x | a positive value; 0 gives -infinity and a negative gives NaN |
returns the base-10 log
digits.i = 1 + floor(log10(n));
log2(x)base-2 logarithm
A direct bridge to the C library function of the same name; the answer is C's answer, to the bit.
x | the value |
returns log base 2 of x
b = log2(1024.0); // 10.0
sqrt(x)square root
x | a non-negative value; a negative gives NaN |
returns the square root
cbrt(x)cube root
A direct bridge to the C library function of the same name; the answer is C's answer, to the bit. Unlike `pow(x, 1.0/3.0)` this is exact for negative inputs.
x | the value |
returns the real cube root of x
r = cbrt(-27.0); // -3.0
pow(b, e)raise `b` to the power `e`
b | the base |
e | the exponent; need not be a whole number |
returns b**e
area.f = pow(side, 2.0);
hypot(x, y)the length of the hypotenuse, without overflow
A direct bridge to the C library function of the same name; the answer is C's answer, to the bit. Computes sqrt(x*x + y*y) in a way that does not overflow when x or y is large.
x | one leg |
y | the other leg |
returns sqrt(x*x + y*y)
d = hypot(dx, dy);
round(x)round to the nearest whole number and return it as an INT
Ties go to the EVEN neighbour (2.5 gives 2, 3.5 gives 4, -2.5 gives -2), which is a decision, not C's: C's `round` is half-away-from-zero and would answer 3 and -3. The int return is what lets `a[round(x)]` compile.
x | the value to round |
returns the nearest whole number as an int; a value beyond int64's range cannot be represented exactly
idx.i = round(t * (count - 1));
sign(x)which side of zero a value is on, as an INT
x | any value |
returns -1 if x is negative, +1 if positive, 0 if zero (including -0.0)
step.i = sign(target - current);
floor(x)round DOWN to a whole number, towards negative infinity
Returns a FLOAT, unlike `round`, which returns an int -- so `floor(-2.5)` is -3.0, not -2.0.
x | the value to round down |
returns the largest whole number <= x, as a float
ceil(x)round UP to a whole number, towards positive infinity
Returns a FLOAT; `ceil(-2.5)` is -2.0.
x | the value to round up |
returns the smallest whole number >= x, as a float
trunc(x)discard the fractional part, toward zero
A direct bridge to the C library function of the same name; the answer is C's answer, to the bit. Differs from `floor` on negatives: trunc(-2.7) is -2.0, floor(-2.7) is -3.0.
x | the value |
returns x with its fraction removed, rounded toward zero
n = trunc(-2.7); // -2.0
fabs(x)absolute value of a FLOAT
`abs` is the int form; this one keeps the fraction.
x | any value |
returns x without its sign
fmin(a, b)the smaller of two FLOATS
a | first value |
b | second value |
returns whichever is smaller
fmax(a, b)the larger of two FLOATS
a | first value |
b | second value |
returns whichever is larger
fmod(x, y)floating-point remainder of x/y
A direct bridge to the C library function of the same name; the answer is C's answer, to the bit. The result takes the SIGN OF X, which is what distinguishes it from a mathematical modulo.
x | the dividend |
y | the divisor |
returns the remainder of x/y with x's sign
r = fmod(-7.5, 2.0); // -1.5
pi()the constant pi
The value of pi as a double.
returns 3.14159265358979323846
c = 2.0 * pi() * r;
degrees(r)convert radians to degrees
r | an angle in radians |
returns the same angle in degrees
radians(d)convert degrees to radians
Every trig builtin here takes radians, so this is the usual bridge from a human-authored angle.
d | an angle in degrees |
returns the same angle in radians
y.f = cy + radius * sin(radians(deg));
lerp(a, b, t)linear interpolation between `a` and `b`
Computed as a + (b - a) * t, so it is NOT clamped: t below 0 or above 1 extrapolates past the endpoints, which is often what you want.
a | the value at t = 0 |
b | the value at t = 1 |
t | the fraction of the way from a to b |
returns the interpolated value
x.f = lerp(startX, endX, elapsed / duration);
between(x, lo, hi)is `x` inside `lo`..`hi`, endpoints INCLUDED
A macro, so it works for ints and floats alike. It evaluates `x` TWICE -- do not pass a call with side effects.
x | the value to test |
lo | lower bound, counted as inside |
hi | upper bound, counted as inside |
returns 1 if lo <= x <= hi, else 0
if (between(mx, panelX, panelX + panelW)) { ... }distance(x1, y1, x2, y2)straight-line distance between two 2-D points
x1 | first point's x |
y1 | first point's y |
x2 | second point's x |
y2 | second point's y |
returns the Euclidean distance, as a float
if (distance(px, py, ex, ey) < range) { attack(); }remap(x, aLo, aHi, bLo, bHi)rescale `x` from one range onto another, linearly
Not clamped: an `x` outside the input range lands outside the output range in proportion. A ZERO-WIDTH input range answers `bLo` rather than dividing by zero.
x | the value to rescale |
aLo | input range low |
aHi | input range high |
bLo | output range low, and the answer when aLo equals aHi |
bHi | output range high |
returns x expressed in the output range
px.f = remap(value, 0.0, 100.0, left, right);
mod(a, b)floating-point remainder of `a / b`, keeping `a`'s sign
Computed as a - b * trunc(a/b), which is C's `fmod`. A ZERO DIVISOR ANSWERS 0.0 rather than trapping or returning NaN -- the same convention CX applies to integer `a / 0`, extended to floats on purpose.
a | the dividend |
b | the divisor; 0.0 gives 0.0 |
returns the remainder, with the sign of `a`
wrapped.f = mod(angle, 360.0);
clamp(x, lo, hi)pull a value inside `lo`..`hi`, returning a FLOAT
Float is the default because that is what the register VM answers; an inverted range (lo > hi) is not checked and yields `hi`.
x | the value to constrain |
lo | lower bound, returned when x is below it |
hi | upper bound, returned when x is above it |
returns x, lo or hi, as a float
volume.f = clamp(volume + step, 0.0, 1.0);
abs(x)absolute value of an INT
The float form is `fabs`; which one a call reaches is decided from the argument's type at compile time.
x | any integer |
returns x without its sign
max(a, b)the larger of two INTS
The float form is `fmax`.
a | first value |
b | second value |
returns whichever is larger
hp.i = max(0, hp - damage);
min(a, b)the smaller of two INTS
The float form is `fmin`.
a | first value |
b | second value |
returns whichever is smaller
md5(input)MD5 digest of a string, as 32 lowercase hex characters
A CHECKSUM, NOT A SECURITY PRIMITIVE: MD5 is broken for anything that has to resist a deliberate collision. Use it to notice accidental change (a cache key, a file-identity check), and reach for `sha256` otherwise.
input | the bytes to hash |
returns the digest as 32 lowercase hex characters
key.s = md5(url);
sha1(input)SHA-1 digest of a string, as 40 lowercase hex characters
Also collision-broken; keep it for compatibility with something that already speaks SHA-1, not for new security work.
input | the bytes to hash |
returns the digest as 40 lowercase hex characters
sha256(input)SHA-256 digest of a string, as 64 lowercase hex characters
The one to reach for by default.
input | the bytes to hash |
returns the digest as 64 lowercase hex characters
if (sha256(readFile(f)) != expected) { print "tampered"; }sha512(input)SHA-512 digest of a string, as 128 lowercase hex characters
input | the bytes to hash |
returns the digest as 128 lowercase hex characters
crc32(input)CRC-32 checksum of a string, as an INTEGER
The standard (zip/gzip) polynomial. Unlike the four digests above this returns a number, not hex -- it is a 32-bit error-detecting checksum for spotting corruption, and it is trivial to forge.
input | the bytes to check |
returns the checksum as an int
if (crc32(block) != stored) { print "block corrupt"; }randomf()a pseudo-random float
The same xorshift64* stream as random(), delivered as a float. Not cryptographic.
returns a pseudo-random float
f = randomf();
random(max)a pseudo-random integer
Drawn from the runtime's xorshift64* generator -- fast, and NOT cryptographic. The stream is seeded from real entropy by default; `#pragma randomseed N` or randomSeed(n) makes a run reproducible.
max | exclusive upper bound (optional) |
returns a pseudo-random integer
n = random(6) + 1;
arraverage(a)the mean of an array (the long name for arrAvg)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Identical to arrAvg and to avg over an array; always float.
a | the array to reduce |
returns the mean as a float; 0 when empty
arravg(a)the mean of an array (the array-specific name for avg)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The same core as avg, and always float.
a | the array to reduce |
returns the mean as a float; 0 when empty
arrfirst(a)move an array cursor to the first element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The array twin of listFirst -- the same one-cursor walk, over a dynamic array.
a | the array to position |
returns 1 if the array has an element to stand on, 0 if it is empty
arrget(a)read the array element the cursor is standing on
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The array twin of the cursor form of listGet. Pair it with arrFirst and arrNext.
a | the array to read |
returns the value of the element
arrmax(a)the largest element of an array (the array-specific name for maxof)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The same core as maxof. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
a | the array to reduce |
returns the largest element; 0 when empty
arrmin(a)the smallest element of an array (the array-specific name for minof)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The same core as minof. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
a | the array to reduce |
returns the smallest element; 0 when empty
arrnext(a)advance an array cursor to the next element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The array twin of listNext, and the loop condition in the same way.
a | the array to advance |
returns 1 if the cursor landed on an element, 0 once past the end
arrsum(a)add up every element of an array (the array-specific name for sum)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The same core as sum, reached through the array-only spelling. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
a | the array to reduce |
returns the total -- int if every element was an int, otherwise float; 0 when empty
average(c)the mean of a container (the other name for avg)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Identical to avg, and always float.
c | the array, list, map or JSON array to reduce |
returns the mean as a float; 0 when empty
avg(c)the mean of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. ALWAYS FLOAT, unlike sum/minof/maxof, because a mean rarely is one. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the mean as a float; 0 when empty
fill(c, v)set every element of a container to one value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Writes in place, through the same element-ownership path a normal assignment uses, so replacing strings does not leak.
c | the container to overwrite |
v | the value to write into every slot |
returns void
fill(grid, 0);
find(c, key)the index of the first element equal to a value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
LINEAR and first-match, so it works on unsorted data -- arraysearch is the binary search that needs the container sorted first. Ints compare exactly, floats within the default tolerance, strings by content.
c | the container to search |
key | the value to look for |
returns the 0-indexed position of the first match, or -1 if there is none
at.i = find(names, "ada");
listadd(list, value)append a value to the end of a list
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
value | the value to append |
returns void
listAdd(xs, 42);
listclear(list)remove every element, leaving an empty list
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
returns void
listClear(xs);
listdelete(list, index)remove the element at a position, closing the gap
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
index | 0-based position to remove |
returns void
listDelete(xs, 0);
listfirst(lst)move a list cursor to the first element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
CX containers carry ONE internal cursor, so these walk a list without an index variable and without a second handle -- and because there is only one cursor per container, two interleaved walks of the same list tread on each other. Returns 0 for an empty list, which is what ends the loop before it starts.
lst | the list to position |
returns 1 if the list has an element to stand on, 0 if it is empty
if (listFirst(items)) { repeat { use(listGet(items)); } while (listNext(items)); }listget(lst, i)read a list element -- at the cursor, or at an index
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
TWO FORMS, told apart by the argument count: listGet(lst) reads where the cursor stands, listGet(lst, i) reads element `i`. listGetAt is the always-indexed spelling for when you want no ambiguity.
lst | the list to read |
i | 0-indexed element; omit to read at the cursor |
returns the value of the element; an index outside 0..count-1 answers the element type's zero (0, 0.0 or "") and leaves the list untouched -- a read never grows it
line.s = rtrim(listGet(rows, i)); // a call may be another call's argument
listgetat(lst, i)read a list element by index, always (never the cursor)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The unambiguous half of listGet: always the indexed form, so a one-argument mistake cannot become a silent cursor read.
lst | the list to read |
i | 0-indexed element to read |
returns the value of the element; an index outside 0..count-1 answers the element type's zero (0, 0.0 or "") and leaves the list untouched
name.s = listGetAt(names, 0);
listindex(lst)where a list cursor is standing, 0-indexed
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Answers -1 when the cursor is not on an element -- after listReset, or once a walk has run off the end.
lst | the list to ask |
returns the 0-indexed cursor position, or -1 if the cursor is not on an element
listinsert(list, index, value)insert a value at a position, shifting the rest along
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
index | 0-based position to insert at |
value | the value to insert |
returns void
listInsert(xs, 0, 42);
listlast(lst)move a list cursor to the LAST element
The starting point for a backwards walk with listPrev, as listFirst is for listNext.
lst | the list to position |
returns 1 if the list has an element to stand on, 0 if it is empty
listnext(lst)advance a list cursor to the next element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
CX containers carry ONE internal cursor, so these walk a list without an index variable and without a second handle -- and because there is only one cursor per container, two interleaved walks of the same list tread on each other. It is the loop condition: it returns 0 once the cursor has run past the end.
lst | the list to advance |
returns 1 if the cursor landed on an element, 0 once past the end
listprev(lst)step a list cursor back one element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The mirror of listNext, for walking backwards from listLast.
lst | the list to step back |
returns 1 if the cursor landed on an element, 0 once before the start
listreset(lst)put a list cursor back before the first element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Leaves the CONTENTS alone -- it rewinds the walk, it does not clear anything. listClear is the one that empties.
lst | the list to rewind |
returns void
listselect(lst, i)put a list cursor on element `i`, 0-indexed
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The random-access way to position the cursor, where listFirst/listNext walk. Returns nothing, so check with listIndex if the index might be out of range: an out-of-range select leaves the cursor off any element, and the listGet that follows answers the element type's zero rather than failing.
lst | the list to position |
i | 0-indexed element to stand on |
returns void
listSelect(rows, 2); v.s = listGet(rows);
listset(lst, i, v)write a list element -- at the cursor, or at an index
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
TWO FORMS, told apart by the argument count: listSet(lst, v) writes where the cursor stands, listSet(lst, i, v) writes element `i`. listSetAt is the always-indexed spelling. THE INDEXED FORM GROWS THE LIST when it has to: writing element 5 of a two-element list makes it six long and pads slots 2..4 with the element type's zero, the same as the subscript store `lst[5] = v`. That is the opposite of a READ, which never grows anything.
lst | the list to write into |
i | 0-indexed element; omit to write at the cursor |
v | the value to store |
returns void
listSet(rows, 5, "f"); // a 2-element list becomes 6 long, slots 2..4 zero-filled
listsetat(lst, i, v)write a list element by index, always (never the cursor)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The unambiguous half of listSet, matching listGetAt.
lst | the list to write into |
i | 0-indexed element to write |
v | the value to store |
returns void
listsort(list)sort a list in place
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list. The same implementation as `sort(xs)`; prefer that spelling.
list | the list |
returns void
sort(xs);
mapclear(m)remove every entry, leaving an empty map
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared map runtime both backends use, so behaviour is identical native and VM. The map itself survives and is immediately reusable -- the bucket array is kept, only the entries go. String and struct values are released as they are dropped. A map parameter is byref by default, so a call inside a function empties the caller's map. Note this is NOT mapReset, which only rewinds the iteration cursor and removes nothing.
m | the map to empty |
returns void
mapClear(m);
mapcontains(m, k)does a map hold this key? (the other name for mapHasKey)
Tests for the KEY, not the value. mapHas and mapHasKey are the same function. This is the only way to tell a MISSING key from a key holding the value type's zero -- a plain read answers 0 / 0.0 / "" for both.
m | the map to test |
k | the key to look for |
returns 1 if the key is present, 0 if not
mapcreate()create a map and return it as a plain int handle
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved.
returns an int handle to a new integer-valued map
m = mapCreate();
mapdelete(m, k)delete one entry from a map, by key
A key that is not present is not an error -- the map is simply unchanged. This is the ONE name for the operation: `mapRemove` was retired at v3.237.0 (one verb per op).
m | the map to remove from |
k | the key to remove |
returns void
mapget(handle, key)read a value by key from a handle-held map
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved -- but the ANSWER is the same one: a missing key reads as the value type's zero, `0` / `0.0` / `""`, on both backends, and a read never inserts, so probing a map cannot grow it. Because a stored zero and a missing key read alike, use mapHas when that difference matters.
handle | the int handle from mapCreate |
key | the string key |
returns the value, or 0 if the key is absent or the handle is zero -- so a stored 0 and a missing key read alike
v = mapGet(m, "hp");
maphas(m, k)does a map hold this key? (the other name for mapHasKey)
Tests for the KEY, not the value. This is the only way to tell a MISSING key from a key holding the value type's zero -- a plain read answers 0 / 0.0 / "" for both.
m | the map to test |
k | the key to look for |
returns 1 if the key is present, 0 if not
if (mapHas(totals, cat)) { mapPut(totals, cat, mapGet(totals, cat) + n); } else { mapPut(totals, cat, n); }maphaskey(handle, key)test whether a handle-held map contains a key
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved. This is the way to tell a stored 0 from an absent key.
handle | the int handle from mapCreate |
key | the string key |
returns non-zero if the key is present; 0 otherwise (including a zero handle)
if (mapHasKey(m, "hp")) { ... }mapkey(m)the KEY of the map entry the cursor is standing on
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Only meaningful between a mapNext that returned 1 and the one that returns 0. With the cursor off an entry -- before the first mapNext, after the last, or on an empty map -- it answers "".
m | the map to read |
returns the key of the current entry; "" when the cursor is not standing on one
mapReset(m); while (mapNext(m)) { printf("%-12s %d\n", mapKey(m), mapValue(m)); }mapnext(m)advance a map cursor to the next entry
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Maps carry the same single cursor as lists, over their entries in HASH order -- which is UNSPECIFIED by contract and is NOT insertion order. Do not write code that depends on it; put a sortndx over the map when you need a defined order. Read the entry with mapKey and mapValue once this returns 1.
m | the map to advance |
returns 1 if the cursor landed on an entry, 0 once past the end
mapReset(m); while (mapNext(m)) { print mapKey(m) + "=" + mapValue(m); }mapput(handle, key, value)store a value under a key in a handle-held map
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved.
handle | the int handle from mapCreate |
key | the string key |
value | the integer value |
returns void; a zero handle is ignored
mapPut(m, "hp", 10);
mapreset(m)put a map cursor back before the first entry
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Maps carry the same single cursor as lists, over their entries in HASH order -- which is UNSPECIFIED by contract and is NOT insertion order. Do not write code that depends on it; put a sortndx over the map when you need a defined order. This rewinds the walk; it does not remove anything (use mapClear to empty the map).
m | the map to rewind |
returns void
mapvalue(m)the VALUE of the map entry the cursor is standing on
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The companion to mapKey, over the same cursor position. With the cursor off an entry it answers the value type's zero -- and because a stored zero reads the same way, let mapNext's own 1/0 end the walk rather than the value.
m | the map to read |
returns the value of the current entry; the value type's zero when the cursor is not standing on one
maxof(c)the largest element of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the largest element; 0 when empty
top.i = maxof(scores);
minof(c)the smallest element of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the smallest element; 0 when empty
reverse(c)reverse the order of a container, in place
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
IN PLACE, like sort: the container itself is rearranged. Pairing it with sort is the other way to get a descending order. An empty or single-element container is left as it is rather than refused.
c | the list or array to reverse |
returns void
listSort(rows); reverse(rows); // descending
search(c, key)binary-search a SORTED list or array for a value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
NEEDS THE CONTAINER SORTED FIRST -- it is a binary search, so on unsorted data it will miss values that are there. `find` is the linear first-match that works on any order; use that unless the container is already sorted and large.
c | the SORTED list or array to search |
key | the value to look for |
returns the 0-indexed position, or -1 if the value is not found
sort(ids); at.i = search(ids, wanted);
sort(c, desc)sort a list or array in place, ascending by default
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
IN PLACE: it rearranges the container rather than returning a sorted copy, so the original order is gone. An optional second argument of 1 sorts descending. Elements compare by their natural order -- numeric for numbers, byte order for strings. sortCmp is the form that takes your own comparator.
c | the list or array to sort |
desc | 1 to sort descending; omit or 0 for ascending |
returns void
sort(scores, 1);
sortarray(array)sort an array in place (compiler-emitted)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for an array sort. Prefer the CX spelling `sort(a)`.
array | the array to sort |
returns void
sort(a);
sortcmp(c, cmp)sort a list or array in place using a CX function as the comparator
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The comparator is one of your own functions, and it follows C ordering: return negative if the first argument sorts first, 0 if they tie, positive if the second sorts first. In place, like sort.
c | the list or array to sort |
cmp | the comparator function to order by |
returns void
sum(c)add up every element of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the total -- int if every element was an int, otherwise float; 0 when empty
total.i = sum(scores);
varslot(v)the address of a fixed-size local, as an integer slot value
An ESCAPE HATCH, not everyday CX. Its live consumer is the native-only three-argument form of jsonLoadArray, which needs an array's address to fill it in place. Prefer passing the array directly -- an array handed to a function is byref already (rule 31).
v | the local whose address is wanted |
returns the variable's address as an integer
jsonLoadArray(data, path, varslot(arr));
sortndxadd(ndx, value)insert a value into a sorted index in order
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
value | the element to insert |
returns void; the insert is silently skipped if the handle is not a live sortndx
sortNdxAdd(s, 42);
sortndxcompact(ndx)reclaim the slots left by removals
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
returns void
sortNdxCompact(s);
sortndxdelete(ndx, key)sortndxDelete -- delete a value from a sorted index
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM. Deletion marks the slot rather than compacting; call sortNdxCompact to reclaim. Spelled `delete(<sortndx>, <value>)` or `sortndxDelete(...)` in CX -- one op under two names, the same way `listDelete` maps to the runtime's `remove_at`. The older `remove` spelling is retired (CX-E1048) under the one-verb-per-operation standard.
ndx | the sortndx handle |
key | the value to remove |
returns non-zero if something was deleted; 0 otherwise
sortndxDelete(s, 42); // or the generic verb: delete(s, 42);
sortndxfirst(ndx)move the cursor to the first element in order
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
returns non-zero if there is a first element
if (sortNdxFirst(s)) { ... }sortndxget(ndx)read the element at the sorted index's cursor
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM. The value comes back typed from the index's own element type -- int, float, string or struct.
ndx | the sortndx handle |
returns the element at the cursor; a zero value if the cursor is not on one
foreach s { print(sortNdxGet(s)); }sortndxnext(ndx)advance the cursor to the next element in order
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
returns non-zero while an element remains
while (sortNdxNext(s)) { ... }sortndxsearch(ndx, key)find a value's position in a sorted index by binary search
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
key | the value to look for |
returns the element's index, or -1 if it is not present (or the handle is not a live sortndx)
i = sortNdxSearch(s, 42);
sortndxsize(ndx)count the elements in a sorted index
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a `sortndx` declaration and the operations on it. Both backends call the same shared cx_sortndx core, so an ordering is identical native and VM.
ndx | the sortndx handle |
returns the element count; 0 if the handle is not a live sortndx
n = s->count;
sqltable(name, doc)offer a CX container to SQL under a table name
REGISTRATION IS THE BOUNDARY. A query -- including one written by a rule or by a model -- can reach only what the program deliberately offered here, so the set of registered names is the whole of what SQL can see. Registering costs nothing until SQL actually runs: the engine is opened lazily. The registered name IS the SQL table name, so it reads back in the query text. Columns are derived from the data -- the union of the keys of the leading rows, because ragged rows are normal here -- and a key missing from a row reads as SQL NULL. A query that must tell "absent" from "null" asks for the hidden `<column>_set` companion, per query. A duplicate name is REFUSED, never a silent rebind.
name | the SQL table name |
doc | a json array of objects |
returns 1 on success, 0 on refusal (the reason is in sqlError())
sqlTable("orders", orders);sqlindex(table, column)build a sortndx index over one column of a registered table
The index is a sortndx of { key, row } pairs: the column's value, and the json node handle of the row it came from. The planner then answers an equality on that column by binary search instead of walking every row. WHETHER IT WAS USED IS READ OFF THE PLAN, never asserted -- `EXPLAIN QUERY PLAN select ...` prints "VIRTUAL TABLE INDEX 0:" for a scan and "INDEX 1:" for an index hit. Building it is a DELIBERATE ACT, not something the engine does behind the program's back: an index costs memory and time nobody asked for, and the pattern this layer formalises is load the data, build the index you need, then refine locally. If the document is MUTATED afterwards the index is not silently trusted -- the planner sees the revision has moved and falls back to a scan, so the answer stays correct and only the speed is lost. Rebuild to get the speed back. Cut 1 indexes NUMERIC columns. A text column is refused by name and still queries perfectly well; it simply scans.
table | a name previously given to sqlTable |
column | the column to index |
returns 1 when built, 0 on refusal (the reason is in sqlError())
sqlIndex("orders", "total");sqlclose(alias)detach a database file opened by sqlOpen
`DETACH`, and the engine's own refusal when the alias names nothing. Closing is rarely necessary -- the connection is torn down with the program -- but ten is the engine's limit on attached files, so a program that walks many databases must let go of each one.
alias | the alias given to sqlOpen (or "disk" for its default) |
returns 1 when detached, 0 on refusal (the reason is in sqlError())
sqlClose("disk");sqlerror()what the last SQL call objected to, or "" when it was happy
The one place a failure is explained, whether it came from the engine, from the table module, or from registration. Handing this text back to a model is what turns a refused statement into a corrected one.
returns the message, or an empty string
if (rows == 0) println(sqlError());
sqlquery(sql, params)run one SQL statement over the registered containers
Returns a json ARRAY OF OBJECTS keyed by the statement's own column names -- self-describing, printable, and the shape a demo can put on screen. An empty result is an empty array, not an error. `?` parameters take their values from `params`, a json array, in order. A string binds as TEXT through the engine's own parameter machinery and is never concatenated into the statement, so a hostile value stays data and cannot become syntax. A statement that will not prepare is a loud error naming what the engine objected to (FP4) and the call answers 0 -- which is also the text an AI authoring loop re-asks with, exactly as a rule that will not compile is. EXPLAIN needs no second builtin: `EXPLAIN QUERY PLAN select ...` is SQL, so it comes back through here as an ordinary rowset. Stage A is READ ONLY. An insert/update/delete against a registered container is refused by name rather than half-supported.
sql | the statement |
params | a json array of `?` values (omit when there are none) |
returns a json array handle, or 0 on error (see sqlError())
json rows = sqlQuery("select * from orders where total > ?", p);sqlopen(path, alias)attach a database FILE beside the in-memory engine
THE ENGINE COULD ALWAYS DO THIS; WHAT WAS MISSING WAS THE DOOR. The amalgamation is compiled with no SQLITE_OMIT_ATTACH, so `ATTACH DATABASE ? AS disk` has worked from a CX program since stage A shipped -- measured on both backends before this builtin was written. Stage A's "never a file" is a true statement about this file's own code and was never one about the engine it opens. The path is BOUND, never spliced: a program names its database with a value it computed, which is what "never hardcode paths" requires of the language that has to obey it too. The alias is not bindable -- SQL has no parameter in that position -- so it is validated as a plain identifier instead, because it becomes part of the query text the program then writes ("select * from disk.t"). A quoted alias would be SAFE and unusable; both are refused. After it succeeds, EVERYTHING ELSE IS ALREADY SQL. A registered container and a disk table join in one statement; `INSERT INTO disk.t SELECT ... FROM <container>` saves the container; `SELECT` reloads it into a json array that sqlTable can offer straight back. No save verb and no load verb are added, because both already exist and are spelled in SQL. A `.db` is NOT a document format: parseDoc takes TEXT and a database is a binary meant not to fit in memory (his ruling, packet D2). Those spellings refuse by name and point here. IN WASM THERE IS NO ENGINE AT ALL -- measured: the module carries zero `sqlite` and zero `cxtable` symbols -- and a browser file would live in MEMFS and die with the tab, so this refuses there by name rather than pretending.
path | the database file; it is created if it does not exist |
alias | the schema name to reach it by (default "disk") |
returns 1 when attached, 0 on refusal (the reason is in sqlError())
if (!sqlOpen(dbPath)) { println(sqlError()); }queueinit(q, capacity, policy)give a declared queue its capacity and overflow policy
The ring is allocated ONCE here and never grows -- that is the point over a list for a per-frame fact queue. Re-initialising with the SAME capacity+policy clears and reuses the queue (so a per-bout setup call is harmless). Re-initialising with a DIFFERENT capacity or policy is a LOUD ERROR, not a silent resize -- use queueClear to empty a queue.
q | the queue (declared `queue q.i;`) |
capacity | ring size. ANY RUNTIME EXPRESSION -- this is the whole point over an array dimension, which must be a literal or #define: the bound can come from json. Must be > 0 (loud otherwise). |
policy | 0 = REJECT (push on full is a loud error -- a dropped item is a lost fact), 1 = ROLL (push on full overwrites the OLDEST, giving a "last N" sliding window; count pins at capacity). |
returns void
queueInit(impacts, lookup(cfg, "maxImpact"), 0)
queueclear(q)drop every pending element, keeping capacity and policy
This is the DATA op; queueInit is the SHAPE op. One name each. THE per-bout op -- it is what replaces the game's `impN = 0`.
q | the queue |
returns void
queueClear(impacts) // per bout: forget last bout's facts
queuevalid(h)typed handles use ->Write h->valid — on a declared container the call form is refused with CX-E1110, and on a handle held in a plain int it is refused too, as CX-E1028 (his ruling of 2026-09-04 ended the int-held call form; a container is declared as what it is). This name survives on the user surface for the one question the arrow cannot ask — whether a RAW HANDLE NUMBER that no variable names is still live — and it is also the runtime target that ->valid lowers to. This holds in #pragma rules c rule text too since v3.187.0 — for one release rule text was the single place the call form stayed legal, because -> could not be written there at all.
is this handle a live queue?
The lowering target of `q->valid`. Unlike every other reader here it does NOT terminate on a bad handle: the others are entitled to treat one as a caller bug, whereas here the bad handle IS the answer being asked for.
h | queue handle (any integer -- an out-of-range or freed one is fine) |
returns 1 when `h` names a live queue, 0 otherwise
if (q->valid) { print(q->count); }jsonfree(doc)destroy a JSON document and reclaim its node-pool subtree
The unconditional (non-ARC) free; `doc` must be a root/DOC handle. Idempotent on 0 / -1.
doc | a doc/root handle from parseDoc or a json declaration |
returns void
jsonfree(doc);
jsondecref(handle)decrement a JSON handle's owning-doc refcount (ARC)
At refcount 0 the whole doc subtree (and its strings) is reclaimed. Codegen-emitted at scope-exit/overwrite; also user-callable. Safe no-op on a freed/rootless handle.
handle | any json handle (a doc or a doc["k"] view) |
returns void
jsondecref(doc);
jsonlivecount()undocumented
jsonincref(handle)increment a JSON handle's owning-doc refcount (ARC)
Codegen-emitted on a json-handle copy/alias so a shared doc isn't freed early; also user-callable.
handle | any json handle (a doc or a doc["k"] view) |
returns void
jsonincref(doc);
jsonvalue(doc)unwrap a parsed document to its root value node
DOC-only: always returns the doc wrapper's first child. Use it before the raw jsonMember/jsonElement/jsonSize accessors (which do not deref a doc).
doc | a doc handle from parseDoc |
returns the root value node handle; 0 if the doc handle is invalid
root = jsonvalue(doc);
jsonfirst(node)start a walk over a json container's members
Seeds the container's cursor at its first member and returns that member's handle, or 0 if there are none. Takes the CONTAINER on every call, never the element it hands back. A parsed document is deref'd first, so this walks the document's members rather than the wrapper.
node | json array/object (or a document handle wrapping one) |
returns the first member's handle, 0 when empty or not a container
h = jsonFirst(items); while (h != 0) { print(jsonAsStr(jsonGet(items))); h = jsonNext(items); }jsonnext(node)advance a walk started by jsonFirst
Takes the SAME container handle jsonFirst was given -- not the handle it returned. Returns the next member's handle, or 0 at the end.
node | the container being walked (a document handle is deref'd) |
returns the next member's handle, 0 when the walk is finished
h = jsonNext(items);
jsonget(node)read the member the walk is currently on
Takes the container, and returns the member handle the cursor sits on -- the accessor a `foreach` body uses to reach the current element. 0 before the walk starts or after it ends.
node | the container being walked (a document handle is deref'd) |
returns the current member's handle, 0 when the cursor is not on one
foreach items { total = total + jsonAsInt(jsonGet(items)); }jsonkey(node)the KEY of the member this container's cursor is on
jsonGet's twin: same argument (the CONTAINER being walked, not the member), same both-backend binding, so one `foreach` body can read a name and a value without changing what it passes. foreach doc { println(jsonKey(doc) + " = " + str(jsonGet(doc))); } WITHOUT IT A JSON OBJECT COULD NOT BE ENUMERATED IN CX AT ALL (D27, filed 2026-08-19 by the page-renderer arc, fixed v3.307.0). The walk could reach every value and never learn a single name; `mapKey(doc)` -- which the foreach refusal CX-E0044 pointed at -- refused natively and answered a silent 0 on the register VM, so the language's own advice named the one thing that does not work.
node | the container being walked (a document handle is deref'd) |
returns the member's key, or "" for an ARRAY element (which has no name) and for a cursor that is not on anything
k = jsonKey(doc);
jsonmember(obj, key)look up a key on a JSON object
Linear scan of the object's members by key. Raw form -- does NOT deref a DOC wrapper (use jsonValue first or the doc-aware jsonMemberV / j["key"]).
obj | the object value node to search |
key | the member key string to find |
returns the member value node handle; 0 if the key is absent
nameV = jsonmember(root, "name");
jsonelement(arr, idx)fetch an array element by index
Walks the child list to the given 0-based index. Raw form -- does NOT deref a DOC wrapper.
arr | the array value node |
idx | the 0-based element index |
returns the element node handle; 0 if the index is out of range
assertFloatEqual(10.0, jsonnumber(jsonelement(arrRoot, 0)));
jsonstring(node)read the text of a JSON string node
Raw accessor: returns the node's stored string handle with no coercion or DOC-deref (a number/bool/container reads empty -- use jsonAsStr to coerce).
node | a string value node |
returns the string handle; the empty string for a non-string or invalid node
jName.s = jsonstring(jsonmember(jh, "name"));
jsonnumber(node)read the numeric value of a JSON number node as a float
Raw accessor returning the node's numv (the double mirror, kept in sync with the int64 lane); no coercion or DOC-deref (a bool/string reads 0.0 -- use jsonAsFloat to coerce).
node | a number value node |
returns the numeric value as a float; 0.0 for a non-number or invalid node
jAge.f = jsonnumber(jsonmember(jh, "age"));
jsonbool(node)read the boolean value of a JSON bool node
Raw accessor returning the node's stored 0/1; no coercion or DOC-deref.
node | a bool value node |
returns 1 for true, 0 for false; 0 for a non-bool or invalid node
jActive.i = jsonbool(jsonmember(jh, "active"));
jsonisnull(node)test whether a JSON node is null or absent
True for both an explicit JSON null and a missing/invalid handle (both report type 0).
node | any json value node |
returns 1 if the node's type is null (incl. missing key/invalid handle); 0 otherwise
assertEqual(1, jsonIsNull(j["nada"]));
jsonasstr(node)coerce a JSON value to a string
Derefs a DOC. string -> its text; number -> exact int64 digits or %g; bool -> "true"/"false"; object/array/null -> empty. Read-coercion behind `string x = json[...]`; also user-callable.
node | any json value node |
returns the coerced string handle; empty for an object/array/null/invalid node
printf("%s ", jsonAsStr(jsonGet(tags)));jsonasint(node)coerce a JSON value to an int
Derefs a DOC. int-born number -> exact int64; bool -> 0/1; numeric string -> strtoll; float number -> truncated. Read-coercion behind `int x = json[...]`; also user-callable.
node | any json value node |
returns the coerced int value; 0 for a null/container/invalid node
int ri = e["hull"];
jsonasfloat(node)coerce a JSON value to a float
Derefs a DOC. bool -> 0.0/1.0; numeric string -> strtod; number -> its value. Read-coercion behind `float x = json[...]`; also user-callable.
node | any json value node |
returns the coerced float value; 0.0 for a null/container/invalid node
float lat = e["lat"];
jsonlen(node)length of a JSON value dispatched by node kind
The data makes the choice: string -> text length, array/object -> child count, number/bool -> its text-form length, null/missing -> 0. `len()`/`length()`/`strlen()` on a json route here; also user-callable.
node | any json value node |
returns the kind-appropriate length; 0 for null/missing/invalid
int n = len(doc);
jsonundecided()create a new document whose ROOT KIND is not yet decided (a DOC wrapping a json null)
What a bare `json d;` declaration mints on both backends, and the constructor form of that declaration -- and, since 3.3.003.0 folded xml onto json, the declaration an XML document is built from as well (`d->doc->format = CX_XML`). It commits to nothing: the FIRST subscript decides, a string key making the root an object and an int index making it an array, exactly as a json null nested one level deep has been promoted since v3.264. A root that has already been decided -- by a parse, or by an earlier subscript -- refuses the other kind loudly instead, because a write creates and never converts.
returns an ARC-owned DOC handle (freed once at scope exit) whose value node is a json null, so an untouched document exports as `null`.
json d; d["servers"][0]["host"] = "alpha"; \ the declaration is the constructor
jsonobjroot()create a document whose root is an empty OBJECT -- the object-literal root
Emitted by both backends as the root of a `json j = {k: v}` object literal, where the literal itself states the root's kind so there is nothing left for a subscript to decide. NOT a general constructor: a bare `json d;` mints an UNDECIDED root instead, and the CX spelling that used to reach this C function is retired to CX-E1048 because committing the root's kind at construction is what made an int-indexed store into it vanish.
returns a new doc handle whose root value is an empty object; 0 on node-pool exhaustion
json ship = {name: "Falcon", hull: 100};jsonarrroot()create a document whose root is an empty ARRAY -- the array-literal root
The array twin of jsonArrRoot's sibling, emitted as the root of a `_json h { [ ... ] }` literal. Same reasoning: the literal states the kind. The retired CX spelling committed an array root, which silently filed a KEYED store at an index and threw the key away.
returns a new doc handle whose root value is an empty array; 0 on node-pool exhaustion
_json ids { [1, 2, 3] }jsonarr()create a new empty JSON array node
A bare array node (not a doc wrapper) for the nested-build API; append with jsonArr* / jsonArrAdd, then nest via jsonAddNode/jsonArrAdd.
returns a fresh empty array node handle; 0 on pool exhaustion
json arr = jsonArr();
jsonobj()create a fresh empty json OBJECT node (unparented) for nesting
The object twin of jsonArr() -- the build-API asymmetry-closer. Unlike a json ROOT it is NOT a DOC wrapper and NOT an ARC creator, so it never double-frees when moved into another container's tree.
returns an object-node handle whose parent is 0, ready to fill with jsonAdd-family calls and then MOVE into a parent via jsonAddNode/jsonArrAdd (freed exactly once with its owning root).
o = jsonObj();
jsonarrnum(arr, v)append a float number element to a JSON array
arr | the array node to append to |
v | the float value to append |
returns void
foreach scores { jsonArrNum(arr, listGet(scores)); }jsonarrint(arr, v)append an int64-exact number element to a JSON array
Stores the value exactly (int-born node), avoiding the double rounding that corrupts integers past 2^53.
arr | the array node to append to |
v | the int value to append |
returns void
jsonArrInt(arr, 9007199254740993);
jsonarrstr(arr, v)append a string element to a JSON array
arr | the array node to append to |
v | the string value to append |
returns void
jsonArrStr(slots, mods[slotMod[si]].id);
jsonarrbool(arr, v)append a boolean element to a JSON array
arr | the array node to append to |
v | the value; stored as 1 if nonzero else 0 |
returns void
jsonArrBool(arr, 1);
jsonarrnull(arr)append a null element to a JSON array
arr | the array node to append to |
returns void
jsonArrNull(arr);
jsonarradd(arr, node)append a node (object/array) as the next array element
Appends the node. A node from ANOTHER document -- or a whole document -- arrives as a deep copy and the source keeps its own; an unowned build part (a jsonObj()/jsonArr() not yet placed) moves in as it is; a node already in this array's document is refused (CX-E1140), because a node lives in exactly one container. To move one within a document, remove it and then add it.
arr | the array node to append to |
node | the object/array node to append (must be freshly built, unparented) |
returns void
jsonArrAdd(els, e);
jsonarrnode(arr, src)append a json VALUE to an array, copying by the source node's TYPE
The append twin of jsonSetElemNode, and the reason both exist: a json subscript read yields a node HANDLE, so appending it with jsonArrNum stores that handle as a number. Distinct from jsonArrAdd, which MOVES a node and refuses one already owned by this document.
arr | a json ARRAY node (or a document whose root is one) |
src | the value node to copy -- string, number, bool, null, blob, or a whole object/array, which is deep-copied |
jsonArrNode(out, row["name"]);
jsonarrdel(arr, idx)remove element `idx` from a json ARRAY and free its subtree
THE REMOVE SIDE OF A BUILD API THAT ONLY HAD AN ADD SIDE. Eight builtins append an element and none removed one, so an indexed json set could not delete a key -- the one thing an AVL tree does that the indexed answer to the same problem could not. Index is ZERO-BASED, which is now the whole language rather than a convention with an exception in it; the elements after `idx` shift down by one, exactly as an array delete reads. IT BUMPS THE DOCUMENT REVISION, and that is not bookkeeping. An sqlIndex baked over this array re-bakes because the counter moved, so a deleted row stops answering an indexed equality query. A delete that reported success and left the counter still would be worse than no delete at all -- the program would believe it had removed something.
arr | json array (a document handle is deref'd); an object or a scalar is refused loudly, because "delete element 0 of this number" has no honest answer |
idx | zero-based; negative or past the last element is refused loudly and nothing changes |
returns 1 when an element was removed, 0 on any refusal (never fatal, FP4)
if (jsonArrDel(rows, 3)) { printf("%d left\n", rows->count); }jsonaddnode(obj, key, node)nest a node (object/array) under an object key
The object counterpart of jsonArrAdd, and the same rule under a key: a deep copy when the node comes from another document, the node itself when it is an unowned build part, and CX-E1140 when it is already in this object's document.
obj | the object node to add to |
key | the member key for the nested node |
node | the object/array node to nest (must be unparented) |
returns void
jsonAddNode(doc, "slots", slots);
jsonaddnum(obj, key, v)append a float member to a JSON object
Pure append (part of the build API) -- allows ordered duplicates while constructing; use jsonSet* for update-or-append semantics.
obj | the object node to add to |
key | the member key |
v | the float value |
returns void
jsonaddnum(built, "count", 42.0);
jsonaddint(obj, key, v)append an int64-exact member to a JSON object
Int-born node stored exactly (no double rounding past 2^53). Emitters route here when the RHS is statically int.
obj | the object node to add to |
key | the member key |
v | the int value |
returns void
jsonAddInt(obj, "id", 9007199254740993);
jsonaddstr(obj, key, v)append a string member to a JSON object
Pure append (build API); use jsonSetStr for update-or-append.
obj | the object node to add to |
key | the member key |
v | the string value |
returns void
jsonAddStr(mE, "axis", "ENERGY");
jsonaddbool(obj, key, v)append a boolean member to a JSON object
obj | the object node to add to |
key | the member key |
v | the value; stored as 1 if nonzero else 0 |
returns void
jsonaddbool(built, "active", 1);
jsonaddnull(obj, key)append a null member to a JSON object
obj | the object node to add to |
key | the member key |
returns void
jsonAddNull(obj, "note");
jsonrootname(node)the element name an xml export uses for the document root
Defaults to "root"; a document parsed FROM xml remembers the name it had, so xml -> json -> xml round-trips it instead of renaming the user's root.
node | any json node handle |
returns the root element name
s = jsonRootName(doc);
jsonsetrootname(node, name)name the element an xml export uses for the document root
node | any json node handle; the owning document is what changes |
name | the element name |
d->doc->format = CX_XML; jsonSetRootName(d, "catalog");
jsonsetnum(obj, key, v)set an object member to a float, updating in place or appending
mapPut / `obj["k"]=v` semantics: updates the existing key or appends if absent -- never a shadowing duplicate. Codegen target of a float subscript write; also user-callable.
obj | the object node |
key | the member key |
v | the float value to store |
returns void
jsonSetNum(obj, "speed", 3.5);
jsonsetint(obj, key, v)set an object member to an int64-exact number, update-or-append
Int64-exact lane of jsonSetNum (mapPut semantics).
obj | the object node |
key | the member key |
v | the int value to store |
returns void
jsonsetint(gCgiFonts, sFont, nH);
jsonsetstr(obj, key, v)set an object member to a string, update-or-append
String lane of the mapPut/subscript-write setters.
obj | the object node |
key | the member key |
v | the string value to store |
returns void
jsonsetstr(gCgiVars, sVar, sVal);
jsonsetnode(obj, key, src)copy a JSON value node into obj[key] by the source's type
For `dst["k"] = src[...]`: a subscript read yields a node handle, so this stores by the source's TYPE, not the handle. A single value copies by value; an object or array from ANOTHER document arrives as a whole deep copy and the source keeps its node; one already in the destination's document is refused (CX-E1140) -- a same-document move is remove, then add.
obj | the destination object node |
key | the destination member key |
src | the source json value node to copy from |
returns void
jsonSetNode(dst, "hp", src);
jsonrmwnum(obj, key, op, rhs)fused read-modify-write of an object member in one key walk (float lane)
Computes obj[key] = obj[key] op rhs with a single key lookup. Missing key reads 0.0 and creates. Deliberate div/mod unification: '/' is C float (x/0=inf), '%' is fmod with mod-0=0. An unknown op reports and writes nothing (FP4). User-callable and the auto-fusion target for `e[k] = e[k] op v`.
obj | the object node |
key | the member key |
op | a one-character op string: "+" "-" "*" "/" "%" |
rhs | the float right-hand operand |
returns the new float value (assignment reads as its RHS); on a bad op returns the unchanged member value with no write
float v = jsonRmwNum(e, "hp", "+", 5.0);
jsonrmwint(obj, key, op, rhs)fused read-modify-write of an object member in one key walk (int64-exact lane)
Int-exact when the member is int-born (or missing) and op is + - * % ; '/' always falls to the float lane (div/mod unification). Missing key reads 0 and creates.
obj | the object node |
key | the member key |
op | a one-character op string: "+" "-" "*" "/" "%" |
rhs | the int right-hand operand |
returns the new int value; on a bad op returns the unchanged value with no write
int v = jsonRmwInt(e, "hp", "-", 3);
jsonrmwstr(obj, key, op, rhs)fused read-modify-write string concat of an object member in one key walk
Computes obj[key] = obj[key] + rhs (the read coerces to text). '+' is the only string RMW op; any other reports and writes nothing. Missing key concats from "" and creates.
obj | the object node |
key | the member key |
op | a one-character op string -- only "+" (concatenate) |
rhs | the string to concatenate |
returns the new concatenated string; on a bad op returns the unchanged member text with no write
string s = jsonRmwStr(e, "log", "+", "!");
jsonload(path)read a file and parse it into a JSON document
Batteries-included sugar: fread + parseDoc in one call. A PATH door rather than a format one, so it reads whatever the file holds -- json, xml, csv or plain text -- and the document remembers which in its ->doc->format.
path | the file path to read |
returns the parsed doc handle; -1 on a parse error (an empty/missing file parses as failure)
json doc = jsonLoad("data/eventsrules.json");jsonsave(path, j)serialize a JSON value (compact) and write it to a file
exportDoc + fwrite in one call.
path | the destination file path |
j | the json value/doc to serialize |
returns the fwrite status int (nonzero/bytes on success, 0 on write failure)
jsonSave("data/lastloadout.json", doc);jsonsavepretty(path, j)serialize a JSON value (pretty) and write it to a file
exportDoc(d, CX_JSON, 1) + fwrite in one call.
path | the destination file path |
j | the json value/doc to serialize |
returns the fwrite status int (nonzero/bytes on success, 0 on write failure)
jsonSavePretty("out.json", doc);jsonflush(view)write a file-bound JSON view's document to disk now if it is dirty
Manual flush point for a jsonBind LAZY-WRITE view (the exit hook is the backstop). No-op on an unbound handle or a clean doc.
view | a file-bound json view handle from jsonBind |
returns void
jsonFlush(gBindings);
jsondirty(view)test whether a file-bound JSON view has unsaved changes
view | a file-bound json view handle from jsonBind |
returns 1 if the bound doc has pending changes; 0 if clean or the handle is unbound/invalid
if (jsonDirty(view)) { jsonFlush(view); }jsonbound(view)test whether a JSON handle is a file-bound view
view | any json handle |
returns 1 if this json is a jsonBind view; 0 otherwise (unbound/invalid)
if (jsonBound(j)) { jsonFlush(j); }jsonunbind(view)release a file-bound JSON view
Decrements the shared doc's refcount; on the last view it flushes any pending changes then frees the whole doc. A still-shared doc just detaches this view's subtree.
view | a file-bound json view handle from jsonBind |
returns void
jsonUnbind(view);
jsonbind(path, policy, subpath)bind a JSON file to a policy-tagged view (shared doc, disjoint views)
One shared refcounted document per path with disjoint policy-tagged sub-views. policy 0=READONLY (a write is an FP4 error), 1=LAZY-WRITE (writes mark dirty, flushed at checkpoint/exit). The optional subpath (default "" = whole file) selects an existing top-level section. Overlapping views are rejected.
path | the JSON file path to bind |
policy | 0 = READONLY, 1 = LAZY-WRITE |
subpath | optional top-level member to view; omitted/empty = the whole file |
returns a json view node handle; 0 on error (missing RO file, overlapping/duplicate view, subpath not found, parse failure)
gRules = jsonBind("data/eventsrules.json", JSON_READONLY, "rules");exportdoc(node, format, options)write a document out as text: its own format, or another lens
The document's own `->doc->format` decides WHICH text: json by default, and xml, csv or the bytes back unchanged when `parseDoc` read one of those or the program set `d->doc->format`. There is no second export verb for ANY of them -- the conversion is explicit because you asked for text, not because you remembered a different function name. WRITING THROUGH A LENS NAMES IT, exactly as reading through one does: `exportDoc(d, CX_CSV)` links the csv lens. Two dependencies are closed for you rather than left as a rule to remember -- an image is written by the BINARY writer and a hex dump is the same document as text, so naming either brings `CX_BINARY` with it. See `parseDoc` for the rule and the measured cost of each lens.
node | the document or node to write out. |
format | optional; the LENS to write through. Omitted (CX_AUTO) means the document's own `->doc->format`, which is what the old name always did. Naming one converts: a json array out as CX_CSV, a tree out as CX_XML, any document out as CX_JSON. |
options | optional; that lens's own options, the same json document `parseDoc` takes -- EXCEPT for CX_JSON, where it is `pretty`: 0 (the default) is compact machine output on one line, 1 is 2-space indented with a trailing newline. The format argument decides which of the two it is, so there is no ambiguity; the cost is that CX_JSON has no room for a second option. IGNORED for an XML document, and deliberately: the xml exporter's output is a fixed point (export, re-read, export is byte-identical), and indentation would put whitespace inside element text, where it is CONTENT. |
returns the serialised text; the empty string for a dead handle (a READ never refuses -- see B_xml_write_through_dead_handle).
s.s = exportDoc(cfg); // {"a":1} -- its own format
s.s = exportDoc(cfg, CX_JSON, 1); // indented, for a person to read
s.s = exportDoc(img); // an image back, byte-identical
s.s = exportDoc(msg, CX_EMAIL); // RFC-822 again, attachments re-encoded
s.s = exportDoc(form, CX_FORM); // a=1&b=x+y, a repeated key per element
s.s = exportDoc(rows, CX_CSV); // a json array out as csvparsedoc(src, format, options)read a document of ANY format into the one json class
THE ONE DOOR. Written `parseDoc(src)`, a program never says what it was handed: a leading `{` or `[` is json, `<` is xml, and ANYTHING ELSE IS TEXT -- a document whose root is one string, which exports back as the bytes that came in. AUTO never refuses, so there is no outcome where a caller is given nothing and no reason. A DOCUMENT THAT OPENS WITH `<` IS ASKED TWO MORE QUESTIONS, because the file has already answered them and nobody names a format for a file they were handed. A root element of `svg` is read in ORDERED mode, where children keep document order instead of folding by name -- svg is painter's order, so the folded reading draws a different picture. A `<!DOCTYPE`, or a root element of `html`, is read TOLERANTLY: unclosed `p`/`li`/`td` closed by the rule the browsers share, void elements closed, `script` and `style` bodies kept verbatim, names folded, entities decoded. Both are the XML LENS IN ANOTHER MODE rather than parsers of their own, so the mapping never differs. CSV, BINARY AND HEX ARE NEVER SNIFFED, on purpose and for one reason: each of them is a claim about text that is ALREADY TRUE of text that means something else. A csv IS text that happens to contain commas, so a prose paragraph with one comma would become a one-column table. EVERY document is bytes, so sniffing binary would make nothing else reachable. And a bare hex string is also an ordinary word -- `deadbeef` is text, and answering with four bytes would be a guess. A wrong document is worse than no document, because a program acts on it. Ask for them: `parseDoc(s, CX_CSV)`, `parseDoc(s, CX_BINARY)`, `parseDoc(s, CX_HEX)`. CX SOURCE READS AS ROWS, AND THEY ARE THE RECORD'S OWN ROWS. `CX_CX` answers `{"lang", "count", "tokens": [[kind, text, subtype], ...]}` -- the same triple the compiler's journal stores, from the SAME mapping (compiler/srctok.c), so the lens and the record cannot drift. Measured across every CX file in the tree: 857 files, 708,797 rows, no disagreement. The reader is the compiler's own scanner and weighs about 811 KB, so a program ASKS for it with `#pragma lens cx` and one that does not is refused by name rather than handed an empty document. There is no WRITER: the standard form spells a name by its kind, which needs the compiler's classification of every name rather than a token stream, so `exportDoc(d, CX_CX)` says so instead of guessing. A FORM BODY IS A DOCUMENT TOO -- `a=1&b=x+y` is `{"a": "1", "b": "x y"}`, a repeated key is an array, and a space is `+` because this is FORM encoding and not URI encoding. AN IMAGE IS THE SAME DOCUMENT WITH ITS HEADER READ. `CX_IMAGE` recognises png, jpeg, bmp and gif, adds `width`, `height`, `channels` and `format`, and DECODES NOTHING -- `#bytes` is the whole file, which is what makes writing it back byte-identical a property of the design rather than of a re-encoder. `channels` is the header's SAMPLES PER PIXEL: a palette png stores one and means three, and knowing which would require reading the palette. Pixels stay a blob, which is his ruling -- a filename or a blob, as SQL does. The graphics family's `gpu_` names are untouched and remain the way to DRAW one. BINARY AND HEX ARE ONE LENS WEARING TWO COATS. Both answer the same document -- `{"#bytes": <blob>, "size": <n>, "mime": <string>}` -- so `bytes -> hex -> bytes` is the identity by construction. In memory a blob is NOT encoded; base64 appears only when a document carrying one is written as JSON TEXT, because json has no bytes type. Written through `CX_BINARY` the bytes go out as themselves, and through `CX_HEX` as the canonical `hexdump -C` form. An EXPLICIT format that will not parse is the refusal this parser already had: an invalid handle, `->valid` 0, the reason on stderr. The document remembers what it was read as in `->doc->format`, and `exportDoc` writes that format back out. `src` may also be a memfile or an embed() blob -- the emitter picks the row from the argument's type, so all three sources are this one spelling. WHICH LENSES YOUR PROGRAM ACTUALLY CARRIES, because it is not all of them and a caller has to know the rule. THE COMPILER LINKS THE LENSES YOUR PROGRAM NAMES: write `CX_IMAGE` anywhere in the source and the image lens is in your binary; never mention it and it is not. Naming the constant IS the asking -- there is no second thing to say. json, xml and text are always linked, which is why those three are also the only ones a sniff can reach. Measured, one small program per lens: binary +512 bytes, image +2,560, hex +3,072, form +3,584, email +6,144, csv +15,360 -- and cx +833,536, because the CX lens IS the compiler's own reader. That last number is the whole reason this is not simply "link everything". IF THE FORMAT IS DECIDED AT RUN TIME -- read from a file, a header, argv -- then nothing in the source names it, so say so once with `#pragma lens image` (or csv, email, binary, hex, form, cx). A lens that was not linked REFUSES and names that line; it never hands back an empty document.
src | the document's text |
format | CX_JSON | CX_XML | CX_CSV | CX_TEXT | CX_EMAIL | CX_SVG | CX_HTML | CX_BINARY | CX_HEX | CX_IMAGE | CX_FORM | CX_CX; omit it and let the document say what it is -- but the last SIX never answer a sniff, so they are only ever reached by being named |
options | a json document of per-lens options, or 0. csv takes `delim`, `header`, `widths` and `names`; xml takes `ordered`; html takes `tolerant`, which is TRUE by default -- pass `{"tolerant": false}` to turn the recovery rules off so a malformed page is refused rather than repaired. A lens that does not read options REFUSES them rather than ignoring them, so an option can never look as though it took effect |
returns a json document handle; an invalid one when an EXPLICIT format will not parse
json cfg = parseDoc(fread("config.json"));
json order = parseDoc(x, CX_XML); // order["@id"], order["item"][0]
json page = parseDoc(html, CX_HTML); // unclosed tags closed for you
json logo = parseDoc(fread("logo.svg")); // ordered, by its root element
json sheet = parseDoc(fread("quarter.csv"), CX_CSV);
json note = parseDoc(s, CX_TEXT); // one string, whatever it holds
json msg = parseDoc(fread("m.eml"), CX_EMAIL); // msg["headers"]["subject"]
json blob = parseDoc(fread("f.bin"), CX_BINARY); // blob["size"], blob["mime"]
json dump = parseDoc("48 65 6c 6c 6f", CX_HEX); // the same document as text
json img = parseDoc(fread("logo.png"), CX_IMAGE); // img["width"]
json form = parseDoc("a=1&b=x+y", CX_FORM); // a space is `+`, not %20
json rows = parseDoc(fread("main.cx"), CX_CX); // the token rowsjsonadd(node, key, value)add a value to a JSON node, picking the type for you
The arity-3 door onto the jsonAdd* family: it dispatches on the VALUE, so a string goes to jsonAddStr, a whole number to jsonAddInt and a fractional one to jsonAddNum. Before 3.1.051.0 it was wired to jsonAddStr alone, so passing a number produced a C compiler diagnostic quoting a runtime header rather than a CX error -- the call now simply works. Reach for the specific name when you want the type pinned.
node | the array or object node |
key | the key, or an empty string for an array append |
value | the string value |
returns the result of the underlying add
jsonAdd(arr, "", "sword");
jsonloadarray(map, prefix, out)fill a container from a flattened JSON map's indexed keys
Reads `<prefix>.0`, `<prefix>.1`, ... out of a map produced by parseFullJson. THE LIST FORM WORKS ON BOTH BACKENDS. There is also a native-only form taking `varslot(arr)` to fill a fixed array in place; the register VM REFUSES that form by name rather than silently filling nothing.
map | the flat map from parseFullJson |
prefix | the dotted key prefix |
out | a list (both backends), or varslot(array) on native only |
returns the number of elements loaded; 0 on failure
jsonLoadArray(m, "items", xs);
parsefulljson(path, map)read a JSON file and flatten it into a string map with dotted-path keys
The whole document becomes one flat map: `player.name` = "Alice", `items.0` = "sword", and a `items._count` entry giving each array's length. Useful when you want lookups by path rather than a walked tree.
path | the JSON file to read |
map | the string map to fill |
returns non-zero on success; 0 on failure
parseFullJson("save.json", m);mfnew()create an empty memory file
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
returns a handle; 0 if allocation failed
h = mfNew();
mffree(handle)release a memory file immediately
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. Idempotent: a 0 or already-freed handle is ignored.
handle | the memory file |
returns void
mfFree(h);
mfdecref(handle)drop one owning reference to a memory file
EMITTED BY THE COMPILER at scope exit for an owned `memfile` local (the resource ARC model). You do not normally write this -- mfFree is the explicit release.
handle | the memory file |
returns void
// emitted at scope exit
mflivecount()count the memory files currently live
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. The regression-test signal for a leaked handle, and visible in the `#pragma checks on` leak walk.
returns the number of live handles
n = mfLiveCount();
mfsize(handle)the number of bytes a memory file holds
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
handle | the memory file |
returns the used byte count
n = mfSize(h);
mfpos(handle)the current cursor position
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
handle | the memory file |
returns the cursor offset in bytes
n = mfPos(h);
mfseek(handle, offset)move the cursor to a byte offset
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. A NEGATIVE offset is refused rather than clamped.
handle | the memory file |
offset | byte offset from the start |
returns 1 on success; 0 for a bad handle or a negative offset
mfSeek(h, 0);
mfputs(handle, s)write bytes at the cursor, overwriting or extending
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. The string's bytes are COPIED immediately, so the source may go out of scope straight after.
handle | the memory file |
s | the bytes to write |
returns the file's new size in bytes; 0 for a bad handle
mfPuts(h, "hello");
mfinsert(handle, s)insert bytes at the cursor, shifting the rest along
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. The in-place byte shift -- unlike mfPuts, nothing is overwritten.
handle | the memory file |
s | the bytes to insert |
returns 1 on success; 0 otherwise
mfInsert(h, "prefix ");
mftostr(handle)read the whole buffer back as a string
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
handle | the memory file |
returns the buffer's contents
s.s = mfToStr(h);
mfload(path)load a disk file into a NEW memory file
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
path | the file to read |
returns a handle to the new memory file; 0 on failure
h = mfLoad("in.txt");mfsave(handle, path)write a memory file's whole buffer to disk
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file.
handle | the memory file |
path | the file to write |
returns 1 on success; 0 on failure
mfSave(h, "out.txt");
mfinsertfile(handle, path)splice a disk file into a memory file at the cursor
The memory filesystem: a growable byte buffer with a cursor, addressed by an integer handle (0 means none). The same core both backends use, and the seam json and xml latch onto when they parse FROM memory rather than from a file. The `#include` primitive of the beta era, kept because it is the natural way to assemble a document from parts.
handle | the memory file |
path | the file to splice in |
returns the number of bytes inserted, or -1 on failure
mfInsertFile(h, "part.txt");
embedsize(handle)the byte length of an embedded blob
`embed` itself is compile-time codegen -- the bytes are baked into the binary -- so only the size and the reference are run-time operations.
handle | the embedded blob's handle |
returns the blob's length in bytes
n = embedSize(logo);
embedref(index)resolve an embedded blob's handle
Turns the compiler's blob index into a run-time handle, cached after the first call.
index | the blob index the compiler assigned |
returns the blob handle
h = embedRef(0);
fappend(path, content)add text to the END of a file, creating it if it is not there
The append form of fwrite, and exactly `fwrite(path, content, 1)`: the same staged write, the same read-only refusal, the same answer. A named function rather than a macro over the three-argument call so ONE spec row binds it on both backends (GAP, 3.5.006.0); the fixed append flag is the whole of what it adds.
path | file to append to; an empty path is a no-op returning 0 |
content | bytes to add at the end |
returns 1 on success, 0 on failure. NOT a byte count.
fappend("log.txt", str(datetime()) + " started\n");fdelete(path)delete a file. CX's portable delete
This is the one to reach for instead of shelling out: `system("del x")` is cmd.exe-only and `system("rm x")` is POSIX-only, so either one silently does nothing on the other platforms. Wraps C's remove(), so on POSIX it will also unlink an empty directory; removedir() is the explicit spelling for that.
path | file to delete; an empty path returns 0 |
returns 1 if the file was deleted, 0 if it was not there or could not be removed
fdelete("scratch.tmp");fileexists(path)does anything exist at this path?
stat-based, so a DIRECTORY counts as existing. Use direxists() to tell the two apart, and note that fread() returning "" does not mean the file is missing. LINKS ARE FOLLOWED, on every OS. This answers about the file the path NAMES, not about a symbolic link or Windows junction pointing at it -- matching POSIX stat(), which is what CX has always done on macOS and Linux. Windows answered about the LINK until v3.204.1, because MSVCRT stat() reports the reparse point; see cx_path_stat in cx_builtins_string.c. A link whose target is missing or unreadable therefore reports ABSENT, not "a zero-byte thing that exists".
path | path to test; an empty path returns 0 |
returns 1 if any filesystem object exists at path, else 0
if (fileexists("save.json")) { s.s = fread("save.json"); }direxists(path)is this path a directory?
LINKS ARE FOLLOWED, on every OS. This answers about the file the path NAMES, not about a symbolic link or Windows junction pointing at it -- matching POSIX stat(), which is what CX has always done on macOS and Linux. Windows answered about the LINK until v3.204.1, because MSVCRT stat() reports the reparse point; see cx_path_stat in cx_builtins_string.c. A link whose target is missing or unreadable therefore reports ABSENT, not "a zero-byte thing that exists".
path | path to test; empty returns 0 |
returns 1 if path exists AND is a directory, else 0 (a plain file gives 0)
if (!direxists("out")) { makedir("out"); }filesize(path)a file's size in bytes
LINKS ARE FOLLOWED, on every OS. This answers about the file the path NAMES, not about a symbolic link or Windows junction pointing at it -- matching POSIX stat(), which is what CX has always done on macOS and Linux. Windows answered about the LINK until v3.204.1, because MSVCRT stat() reports the reparse point; see cx_path_stat in cx_builtins_string.c. A link whose target is missing or unreadable therefore reports ABSENT, not "a zero-byte thing that exists".
path | file to measure |
returns the byte size, or -1 if the path is empty, does not exist, or is a directory. -1 rather than 0, so an empty file (0) stays distinguishable from a missing one.
n.i = filesize("data.bin");makedir(path)create one directory
Creates a SINGLE level: it is mkdir, not `mkdir -p`, so creating "a/b/c" needs a call per level. Created 0755 on POSIX.
path | directory to create; empty returns 0 |
returns 1 on success, 0 on failure -- INCLUDING when the directory already exists. Guard with direxists() rather than treating 0 as fatal.
if (!direxists("out")) { makedir("out"); }removedir(path)remove an EMPTY directory
Refuses a non-empty directory; there is no recursive delete builtin, so clearing a tree means dirlist() + fdelete() per entry, then this.
path | directory to remove; empty returns 0 |
returns 1 on success, 0 on failure (missing, not a directory, or not empty)
removedir("scratch");renamefile(oldp, newp)rename a file, or move it within one filesystem
A thin rename(): it does NOT cross devices, and on Windows it fails when the destination already exists. movefile() is the one that handles both.
oldp | existing path; empty returns 0 |
newp | new path; empty returns 0 |
returns 1 on success, 0 on failure
renamefile("draft.txt", "final.txt");copyfile(srcp, dstp)copy a file's bytes to a new path
Binary-safe, streamed in 16KB blocks, so file size is not bounded by memory. The destination is TRUNCATED if it exists. Copies content only -- permission bits and timestamps are not carried over.
srcp | file to read; empty returns 0 |
dstp | file to write; empty returns 0 |
returns 1 on success, 0 on failure (unreadable source, unwritable destination, or a short write partway through -- in which case the destination is left partially written, not removed)
copyfile("save.json", "save.bak");movefile(srcp, dstp)move a file, across devices if need be
Tries rename() first, which is atomic and instant when source and destination share a filesystem. If that fails it falls back to copyfile() + delete the source, which is how a move onto another volume succeeds. The fallback is not atomic: an interrupted cross-device move can leave both copies.
srcp | file to move; empty returns 0 |
dstp | destination path; empty returns 0 |
returns 1 on success, 0 if both the rename and the copy failed
movefile("out.log", "archive/out.log");tempdir()the system temporary directory, ready to concatenate
ALWAYS ends with a path separator, so `tempdir() + "scratch.tmp"` is a valid path with no separator handling at the call site. The separator matches the directory's own style: backslash if the environment's path contains one, else forward slash. Resolved from TMP, then TEMP, then TMPDIR, falling back to "/tmp" -- which covers Windows and POSIX without a platform branch.
returns the temp directory path, separator-terminated
f.s = tempdir() + "cx_scratch.txt";
dirlist(path, pattern, names)list a directory's entries into a string list
Appends to `names` (it does not clear it first), so listing two directories into one list accumulates. "." and ".." are always skipped. The pattern is a tiny CASE-INSENSITIVE glob: `*` matches any run including empty, `?` matches exactly one character; there are no character classes. An empty pattern means `*`. Entry NAMES are returned, not paths -- join them to `path` yourself. Files and subdirectories are both listed, with no marker distinguishing them; call direxists() on the joined path to tell.
path | directory to scan; an empty path means the current directory |
pattern | glob to match entry names against; empty means everything |
names | string list the matches are APPENDED to |
returns the number of entries appended; 0 if the directory cannot be opened
list files.s
n.i = dirlist("data", "*.json", files);fread(path)read an entire file into a string
Reads in one shot, sized by a 64-bit tell, so files over 2GB are not truncated. Binary-safe: the result is a counted string, so embedded NULs survive.
path | file to read |
returns the file's contents; an EMPTY STRING if the path is empty, the file cannot be opened, or the file is zero bytes. A missing file and an empty file are indistinguishable in the return value -- use fileexists() when the difference matters.
s.s = fread("config.json");fwrite(path, content, append)write a whole file, replacing whatever was there
The write is STAGED: content goes to "<path>.tmp" and is renamed over the target only once it is fully written and closed, so a disk-full or short write leaves the original file intact rather than truncated. A read-only target is refused (probed with fopen "r+b", which neither truncates nor creates, so the answer honours ACLs and read-only mounts). On POSIX the original file's permission bits are re-applied after the rename, because the staged file would otherwise arrive with umask's bits -- a 0600 file came back 0644 before that.
path | file to write; an empty path is a no-op returning 0 |
content | bytes to write; may be empty, which truncates the file to zero |
append | 0 replaces the file, 1 appends to it (the CX `fappend` spelling) |
returns 1 on success, 0 on failure. NOT a byte count.
ok.i = fwrite("out.txt", "hello\n");print(v)write a value to stdout followed by a NEWLINE
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Takes any printable value and renders it the way CX renders it, so it needs no format string. printf is the format-string form and prt* the newline-free one.
v | the value to write |
returns void
print "loaded " + str(n) + " rows";
printf(fmt)write formatted text to stdout, C-style
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The conversions are libc's -- and so is everything between the `%` and the conversion letter. FLAGS, WIDTH and PRECISION all work: `%-20s` left-justifies in a 20-column field, `%8s` right-justifies in 8, `%5d` pads a number to 5 columns, `%05d` pads it with zeros, `%+d` forces the sign, `%11.2f` gives 11 columns with 2 decimals, `%.2s` truncates a string to 2 bytes. That is what makes columnar output possible without a padding helper. TWO CX ADJUSTMENTS: `%d` carries a 64-bit integer (the compiler rewrites it upstream), and float output honours `#pragma decimals` -- so a BARE `%f` prints 3 decimals, not C's 6. Write `%.6f` when you want C's. sprintf is the same formatting returned as a string instead of printed.
fmt | the format string |
returns void
printf("%-20s %5d %11.2f\n", item, qty, price); // name, then two numeric columnsprintln(v)write a value to stdout followed by a newline (the other name for print)
Identical to print; both append the newline.
v | the value to write |
returns void
println(toupper(name)); // any expression, including another call
putc(ch)write one character to stdout, by numeric code
Takes the character as a CODE, not a one-character string, and adds no newline. It is the raw single-byte counterpart to print.
ch | the character code to write |
returns the code written, as C's putchar does
putc(65);
stringsplit(s, sep, n)pull one delimited field out of a string, counting from 0
ZERO-indexed, like every other index in the language: field 0 is the text before the first separator. Nothing is allocated for the fields you did not ask for, so this is the cheap way to read one column; use split() when you want them all. THE FIELD NUMBER IS THE LAST ARGUMENT, not the middle one: the separator sits beside the string it separates, the way `split(s, sep, out)` already reads. Written the other way round it is a TYPE error rather than a wrong answer -- the separator and the number land in each other's slots and the compiler refuses the call on both backends. A field number past the end returns "" -- indistinguishable from a genuinely empty field, because an empty field is a real value ("a,,c" has one at 1) and no return could mean "absent" and nothing else. Count with countstring() first when that matters.
s | string to split |
sep | separator; an EMPTY separator makes the whole string field 0 |
n | 0-indexed field number |
returns the field's text, or "" if n is past the last field or n < 0
city.s = stringsplit(row, ",", 2);
targz(out, src)create a gzip-compressed tar (.tar.gz) archive from a file or directory
Format-specific alias over bi_archivecreate/cx_archive_create; recursively walks the source with libarchive and stores paths relative to the source root.
out | destination archive path to write |
src | source file or directory to pack (directories are walked recursively) |
returns 0 on success; -1 on error (also -1 when the runtime was built without libarchive)
w1 = tarGz("R:/Temp/arctest/w.tgz", "R:/Temp/arctest/src");tarbz2(out, src)create a bzip2-compressed tar (.tar.bz2) archive from a file or directory
Alias over bi_archivecreate with format "tar.bz2"; same recursive libarchive packing as targz.
out | destination archive path to write |
src | source file or directory to pack (directories are walked recursively) |
returns 0 on success; -1 on error (also -1 when built without libarchive)
w2 = tarBz2("R:/Temp/arctest/w.tbz", "R:/Temp/arctest/src");tarxz(out, src)create an xz-compressed tar (.tar.xz) archive from a file or directory
Alias over bi_archivecreate with format "tar.xz"; same recursive libarchive packing as targz.
out | destination archive path to write |
src | source file or directory to pack (directories are walked recursively) |
returns 0 on success; -1 on error (also -1 when built without libarchive)
w3 = tarXz("R:/Temp/arctest/w.txz", "R:/Temp/arctest/src");zipcreate(out, src)create a ZIP archive from a file or directory
Alias over bi_archivecreate with format "zip"; uses libarchive's zip writer.
out | destination archive path to write |
src | source file or directory to pack (directories are walked recursively) |
returns 0 on success; -1 on error (also -1 when built without libarchive)
w4 = zipCreate("R:/Temp/arctest/w.zip", "R:/Temp/arctest/src");sevenzip(out, src)create a 7z archive from a file or directory
Alias over bi_archivecreate with format "7z"; same recursive libarchive packing as the other archive builtins.
out | destination archive path to write |
src | source file or directory to pack (directories are walked recursively) |
returns 0 on success; -1 on error (also -1 when built without libarchive)
w5 = sevenZip("R:/Temp/arctest/w.7z", "R:/Temp/arctest/src");compress(s)zstd-compress a string and return the compressed BYTES
The result is binary, not text: keep it in a string, measure it with `length`, and do not print it. Level 19 (strong) is fixed. A build without zstd returns an empty string rather than failing to link, so an empty result means either an error or an unsupported build.
s | the bytes to compress |
returns the compressed payload, or an empty string on error
packed.s = compress(readFile(path));
decompress(s)the inverse of `compress`
Needs no size hint: the zstd frame carries the original length. Input that is not a zstd frame gives an empty string rather than garbage.
s | a payload produced by `compress` |
returns the original bytes, or an empty string on error or non-zstd input
archivecreate(out, format, src)pack a file or directory into an archive
A directory source is walked recursively and stored with paths relative to it. The named wrappers (`zip`, `targz`, ...) are this function with the format fixed, so use one of those unless the format is a variable.
out | the archive path to write |
format | one of "7z", "zip", "tar", "tar.gz", "tar.bz2", "tar.zst", "tar.xz" |
src | the file or directory to pack |
returns 0 on success; -1 on error. A runtime built WITHOUT libarchive does not return at all: it refuses with CX-E5047, naming the package and the rebuild (the -1-for-no-libarchive contract was retired 2026-08-25 -- a silent -1 is the FP4 class its siblings already refuse)
archiveCreate("out/release.zip", "zip", "build/dist");archiveextract(in, destdir)unpack an archive into a directory
The format is detected from the archive itself, not from the filename.
in | the archive to read |
destdir | the directory to extract into |
returns 0 on success; -1 on error. A runtime built WITHOUT libarchive does not return at all: it refuses with CX-E5047, naming the package and the rebuild (contract retired 2026-08-25).
base64enc(src)encode bytes as base64 text (RFC 4648, with "=" padding)
The standard alphabet, NOT the URL-safe one: the output can contain "+" and "/" and needs escaping before it goes in a URL.
src | the bytes to encode |
returns the base64 text
auth.s = "Basic " + base64Enc(user + ":" + pass);
base64dec(src)decode base64 text back to bytes
src | base64 text, with or without padding |
returns the decoded bytes; an empty input gives an empty string
netlisten(port)listen for TCP connections on a port, LOOPBACK only
Loopback is the default because a listening socket is an attack surface: this server is reachable from programs on this machine and from nothing else, which is what a demo, a test and a local tool all actually want. Reaching beyond the machine is a deliberate, differently-spelled act (see netListenAny), so exposure is never something a program does by accident. The result is a SERVER handle: pass it to netPoll to ask whether a connection is waiting and to netAccept to take one. It is not itself readable or writable.
port | TCP port to bind (1-65535; below 1024 needs privilege) |
returns a server handle > 0, or < 0 on failure -- netError() then has a NAMED reason ("the port is already in use"), never a bare errno
srv.i = netListen(7000);
netlistenany(port)listen for TCP connections on a port, on EVERY interface
The explicit counterpart to netListen: this one is reachable from other machines. A distinct name rather than a flag argument is the point -- exposing a port beyond this machine should be visible at a glance and greppable across a codebase. On Windows the first such bind in a program's life may raise a one-time firewall prompt; that is machine configuration, not a program error.
port | TCP port to bind (1-65535; below 1024 needs privilege) |
returns a server handle > 0, or < 0 on failure (netError() has the detail)
srv.i = netListenAny(7000);
netaccept(server)take the next incoming connection on a server handle
BLOCKS until a client arrives. To wait with a deadline -- or not to wait at all -- ask netPoll first: a zero timeout never blocks, and netPoll(srv, -1) blocks exactly as this does. The returned connection is independent of the server: closing it leaves the server listening, and closing the server does not close connections already accepted from it.
server | a handle from netListen or netListenAny |
returns a connection handle > 0, or < 0 on failure (netError() has the detail)
c.i = netAccept(srv);
netconnect(host, port)open a TCP connection to a host and port
The host may be a name ("localhost", "build01.local") or a literal address ("127.0.0.1"); resolution covers IPv4 and IPv6. The attempt is BOUNDED by netTimeout(ms) -- the same knob the HTTP client uses -- so a peer that is switched off costs the timeout rather than the operating system's own retry policy, which can run to tens of seconds. Set netTimeout(1000) before sweeping a peer list.
host | hostname or literal IP address |
port | TCP port to connect to |
returns a connection handle > 0, or < 0 on failure -- netError() names the condition ("connection refused", "host not found", "timed out")
c.i = netConnect("127.0.0.1", 7000);netpoll(handle, timeout_ms)ask whether a handle is ready, waiting at most timeout_ms
The tier's one waiting primitive, deliberately the C mechanism named plainly (select on one handle). "Ready" means the next operation will not block: on a SERVER handle a connection is waiting, so netAccept returns at once; on a CONNECTION handle bytes are waiting (or the peer has closed), so netRead returns at once. timeout_ms = 0 polls and returns immediately -- the form that drops into a frame loop. timeout_ms < 0 waits forever, which makes netPoll(srv, -1) followed by netAccept exactly a blocking-accept server. A program holding several connections polls each with 0 and sleeps once, rather than paying the timeout per handle.
handle | a server or connection handle |
timeout_ms | 0 = poll, < 0 = wait forever, > 0 = wait up to this long |
returns 1 ready, 0 timed out, < 0 on failure
ready.i = netPoll(srv, 100);
netread(handle)read the bytes waiting on a connection
BLOCKS until at least one byte arrives or the peer closes (netPoll first to avoid that). Returns up to one buffer's worth: TCP is a byte STREAM, not a message queue, so one netWrite by the peer may arrive as two netReads and two may arrive as one -- a protocol that needs message boundaries must put them in the bytes (a length prefix, or a newline). BINARY-SAFE: CX strings carry their length, so the result may contain embedded NUL bytes and survives netWrite -> netRead unchanged. Printing such a value stops at the first NUL -- that is print's contract, not a truncation of the data.
handle | a connection handle |
returns the bytes read; "" if the peer closed cleanly OR on error -- netError() distinguishes them (empty after a clean close)
msg.s = netRead(c);
netwrite(handle, data)write bytes to a connection
Writes ALL of the data or fails: partial writes are retried internally, so this never reports success having sent half a message. Binary-safe -- the length comes from the CX string, so embedded NUL bytes are sent like any other byte.
handle | a connection handle |
data | the bytes to send |
returns the number of bytes written (always the full length on success), or < 0 on failure -- a peer that vanished mid-write is reported as "connection reset", never as a short write
n.i = netWrite(c, "hello");
netclose(handle)close a connection, or stop a server listening
Closing a connection sends the peer an orderly end-of-stream, which its next netRead sees as "". Closing a server handle stops new connections arriving and leaves already-accepted connections untouched. The handle is invalid afterwards: using it again is a program defect, not a network condition, and is refused loudly (CX-E5038) rather than read as an empty message.
handle | a server or connection handle |
returns 0 on success, < 0 on failure (netError() has the detail)
netClose(c);
mailfetch(url, user, pass, max)read a mailbox over IMAP(S) and answer it as json
THE VERB AND THE LENS ARE ONE DOOR: every element of the array that comes back is already `parseDoc(message, CX_EMAIL)`, so a caller never holds a message as bytes and never asks a second question to turn it into a record. A message that will not parse is SKIPPED rather than fatal -- refusing one must not lose the other nine hundred.
url | the mailbox, e.g. "imaps://mail.example.com/INBOX" |
user | the account name |
pass | the password or app token. CREDENTIALS ARE ARGUMENTS AND NEVER URL PARTS: a URL reaches logs, error text and neterror(), and a password that rides in one leaks without anybody deciding to. |
max | take the NEWEST `max` messages; 0 is every one. A cap takes the TAIL, because a mailbox is read to see what ARRIVED. |
returns a json ARRAY of message documents. An unreachable mailbox still answers an array -- empty and unreachable are different facts, and the second is read off `neterror()`, as with every verb here.
json box = mailFetch(url, user, pass, 20);
foreach (json m in box) { println(m["headers"]["subject"]); }httpget(url)GET a URL and return the response body
An EMPTY string means the request failed -- but an empty body is also a legitimate 204, so check `httpStatus()` rather than the string when the difference matters.
url | the URL to fetch |
returns the response body, or an empty string on failure
body.s = httpGet("https://example.com/api/items");httppost(url, body)POST a body to a URL and return the response body
Sends `body` verbatim, with no Content-Type set and no form encoding: build the payload (JSON, form text) yourself.
url | the URL to post to |
body | the request body, sent as-is |
returns the response body, or an empty string on failure
reply.s = httpPost(url, exportDoc(doc));
httpstatus()the HTTP status code of the last httpGet or httpPost
Per-thread. 0 means no request has run on this thread, or the transport failed before a status came back (a DNS or connect error) -- in which case `netError()` has the reason.
returns the status code, or 0
if (httpStatus() != 200) { print netError(); }neterror()a readable description of the last network failure
Per-thread, and EMPTY after a call that succeeded, so it can be tested as well as printed.
returns the message, or an empty string if the last call was clean
nettimeout(ms)set the default TRANSFER timeout for the network builtins
Without it a stalled server hangs the program with no recourse, so the default is 60000 ms. It returns the PREVIOUS value, which is what makes save-and-restore around one risky call a one-liner. Passing 0 means "no transfer timeout" -- a separate 15-second CONNECT timeout still applies and is not adjustable.
ms | the new transfer timeout in milliseconds; 0 disables it |
returns the timeout that was in force before this call
prev.i = netTimeout(5000); body.s = httpGet(u); netTimeout(prev);
ftpget(url)download a file over ftp or ftps and return its contents
The same transport as `httpGet`, chosen by the URL scheme, so the timeout and error reporting are identical.
url | an ftp:// or ftps:// URL naming a file |
returns the file's contents, or an empty string on failure
data.s = ftpGet("ftp://host/pub/readme.txt");sftpget(url)download a file over SSH (sftp) and return its contents
url | an sftp:// URL naming a file |
returns the file's contents, or an empty string on failure
ftplist(url)list a remote directory
The URL must name a DIRECTORY and end with a slash; without the trailing slash the server is being asked for a file instead.
url | an ftp:// URL ending in "/" |
returns the listing as text, or an empty string on failure
entries.s = ftpList("ftp://host/pub/");ftpput(url, localpath)upload a local file over ftp or ftps
url | the destination ftp:// or ftps:// URL, including the remote filename |
localpath | the local file to read and send |
returns 0 on success, a negative number on failure
if (ftpPut("ftp://host/in/out.csv", "R:/tmp/out.csv") < 0) { print netError(); }sftpput(url, localpath)upload a local file over SSH (sftp)
url | the destination sftp:// URL, including the remote filename |
localpath | the local file to read and send |
returns 0 on success, a negative number on failure
emailsend(smtpurl, from, to, subject, body)send one email through an SMTP server
`body` is the message text only; no headers are synthesised beyond the addresses and subject, and there are no attachments.
smtpurl | the server URL, e.g. "smtp://mail.example.com:587" |
from | the sender address |
to | the recipient address |
subject | the subject line |
body | the message text |
returns 0 on success, a negative number on failure
emailSend("smtp://mail:25", "cx@host", "me@host", "build", log);sshexec(host, user, pass, cmd)run a command on a remote host over SSH and return its stdout
PORT 22 IS FIXED and so is the 15-second timeout; there is no port argument. Only stdout comes back -- stderr and the command's exit status do not, so a command that fails looks like one that printed nothing. `sshExecKey` is the key-based form and is the better habit.
host | the host name or address |
user | the remote user name |
pass | that user's password |
cmd | the command line to run remotely |
returns the command's stdout, or an empty string on failure
out.s = sshExec("build01", "ci", pw, "uname -a");sshexeckey(host, user, keyfile, cmd)the same remote exec, authenticated with a PRIVATE KEY FILE
Same fixed port 22, same 15-second timeout, same stdout-only result.
host | the host name or address |
user | the remote user name |
keyfile | path to the OpenSSH or PEM private key |
cmd | the command line to run remotely |
returns the command's stdout, or an empty string on failure
out.s = sshExecKey("build01", "ci", "/home/ci/.ssh/id_ed25519", "uname -a");httpread(conn)read ONE HTTP request from a connection and parse it
Reads until the request is complete -- the blank line that ends the headers, plus however many body bytes Content-Length declared -- so a request split across several TCP segments arrives whole. Each read is bounded by netTimeout(ms), the same knob netConnect and httpGet use, so a client that connects and says nothing costs that timeout rather than the program. The parsed request belongs to THIS connection: httpMethod, httpPath, httpHeader and httpBody all take the same handle back, and asking them about a connection whose request has not been read is refused loudly (CX-E5039) rather than answered with the previous one.
conn | a connection handle from netAccept (or netConnect) |
returns 1 a request was read and parsed; 0 the peer closed without sending one (an ordinary outcome -- browsers open speculative connections and drop them); < 0 malformed or timed out, and netError() names which
if (httpRead(c) > 0) { println(httpPath(c)); }httpmethod(conn)the method of the request httpRead parsed on this connection
Returned exactly as the client sent it, which for every browser and every HTTP client in practice means upper case ("GET", "POST", "OPTIONS"). It is NOT upper-cased here: HTTP methods are case-sensitive by specification, so folding one would invent a request the client did not make.
conn | the connection httpRead was called on |
returns the method, e.g. "GET"
if (httpMethod(c) == "POST") { body.s = httpBody(c); }httppath(conn)the request target of the request httpRead parsed
The second field of the request line, verbatim: "/send", "/poll?since=4", "/". Verbatim matters -- the query string is still attached (splitting it is N5) and the path is NOT decoded, so a program that routes on it compares the same bytes the client sent. Routing is a plain CX `if` on this value; there is no route table builtin, because a language with string comparison does not need one.
conn | the connection httpRead was called on |
returns the request target, e.g. "/send"
if (httpPath(c) == "/hello") { httpRespond(c, 200, "text/plain", "hi"); }httpheader(conn, name)one header of the request httpRead parsed, by name
The name is matched case-INSENSITIVELY, because HTTP header names are case-insensitive and a program that had to guess whether this client wrote "Content-Type" or "content-type" would be wrong half the time. The value has its surrounding whitespace trimmed. A header the client did not send returns "" -- which is also what a header sent EMPTY returns; the two are worth distinguishing only in tests, and the raw request line is not kept for that.
conn | the connection httpRead was called on |
name | the header name, in any case ("Content-Type", "origin") |
returns the header value, or "" if the request did not carry it
ctype.s = httpHeader(c, "content-type");
httpbody(conn)the body of the request httpRead parsed
Exactly Content-Length bytes, BINARY-SAFE: CX strings carry their length, so a POSTed body with embedded NUL bytes arrives whole. A request with no body (every GET, in practice) returns "".
conn | the connection httpRead was called on |
returns the request body
msg.s = httpBody(c);
httprespond(conn, status, ctype, body)answer a request with a status and a body, and finish
Writes a complete response -- status line, Content-Type, Content-Length, Connection: close, the CORS header if httpAllowOrigin set one, and the body -- in ONE netWrite, so a browser never sees a half-formed reply. The status number is accompanied by its standard reason phrase for the codes a small server actually answers with; an unrecognised code is sent as-is with a generic phrase rather than being rejected, because HTTP allows it and this tier does not exist to police the caller's status codes. Binary-safe: Content-Length comes from the CX string, so a body with embedded NUL bytes is sent whole.
conn | the connection to answer on |
status | the HTTP status code, e.g. 200, 404, 500 |
ctype | the Content-Type, e.g. "text/plain", "application/json" |
body | the response body (may be "") |
returns the number of bytes written, or < 0 on failure (netError() has the detail)
httpRespond(c, 200, "application/json", "{\"ok\":true}");httpredirect(conn, status, url)answer a request by sending the client somewhere else
The one answer httpRespond cannot give: a redirect is a status AND a `Location:` header, and httpRespond writes no header the caller chooses. A 302 sent through it arrives with no destination, so the browser stays where it is -- measured on the wire, which is why this exists (v3.302.0). Writes the whole response in ONE netWrite: the status line with its reason phrase, `Location:`, the CORS header if httpAllowOrigin set one, and a short text body naming the destination. The body is what a client that does not follow redirects sees, and a bare redirect with an empty body tells such a reader nothing. THREE REFUSALS, each by name (FP4): - a status outside 301/302/303/307/308 -- a `Location:` on a 200 is ignored by every client, so accepting one would send a response that looks like a redirect and is not; - an empty url -- `Location:` with no value is a malformed header, and the client's behaviour on it is not defined; - a url containing CR or LF -- that is HEADER INJECTION, and it is the one failure here with a security consequence: a newline in a header value lets whoever supplied the url append headers, or a whole second response, to something a program believed it controlled. Refused unconditionally, never sanitised, because silently rewriting a caller's url is the FP4 shape this language forbids.
conn | the connection to answer on |
status | 301, 302, 303, 307 or 308 |
url | the destination, absolute or site-relative |
returns the number of bytes written, or < 0 on failure (netError() has the detail)
httpRedirect(c, 302, "https://example.com/cx.zip");
httpstream(conn, ctype)answer with headers only and hold the connection open
The other half of the tier's receive story: instead of one body and a close, the response has NO Content-Length and stays open, so the program can push data as it appears. Pass "text/event-stream" and the connection is a Server-Sent Events stream a browser reads with `new EventSource(url)` -- which is why httpEvent exists beside this. Any other content type gives a plain open-ended response. The program owns the stream from here: write to it with httpEvent (framed) or netWrite (raw), and end it with netClose. Nothing times it out.
conn | the connection to answer on |
ctype | the Content-Type, e.g. "text/event-stream" |
returns the number of header bytes written, or < 0 on failure
httpStream(c, "text/event-stream");
httpevent(conn, data)send one Server-Sent Event on a streaming connection
Frames the data the way SSE requires -- `data: ` before each line, a blank line after the last -- which is the whole of what this does that netWrite does not. A multi-line message is emitted as several `data:` lines, one per line, so the browser reassembles it with the newlines intact; sending it as a single line with embedded newlines would end the event early and deliver the remainder as a second one. Call httpStream(conn, "text/event-stream") first. An SSE frame is text by specification: a NUL byte in the data would truncate the event at the browser, so it is refused by name rather than sent (base64 or JSON-escape binary payloads).
conn | a connection httpStream opened |
data | the event data |
returns the number of bytes written, or < 0 on failure
httpEvent(c, "hello");
httpservedir(conn, mount, dir)answer this request from a directory of files, safely
Requests whose target begins with `mount` are answered from the file of the same name under `dir`; everything else is left alone for the program to route, which is what makes this compose with a plain CX `if` ladder rather than replacing it. `mount` of "/" serves the whole directory at the root. A request for a DIRECTORY is answered with `index.html` inside it -- so "/" is the site's index, the web's convention. A directory with no index.html is 404: there is no directory listing, deliberately, because a listing hands a stranger the names of every file a program never meant to advertise. CONTAINMENT IS THE POINT. The target is decoded ONCE, backslashes are read as separators (Windows would), `.` and `..` are resolved, and the result is checked against the RESOLVED root -- so `..`, `%2e%2e`, `..\`, a doubled `....//`, an absolute path and a symlink out of the tree all end at the same 404 as a file that simply is not there. A blocked path and a missing one are answered identically ON PURPOSE: a distinct error would tell a stranger which of their guesses about your filesystem was right. LINKS ARE FOLLOWED AND THEN JUDGED, not refused: a symbolic link or Windows junction that lands INSIDE the served directory is an ordinary file and is served; one that lands outside it is that same 404. That is one guarantee on every OS -- the same request gets the same answer -- rather than "the attack happens to be blocked here". (v3.203.0 resolved links on POSIX only, so a junction inside the root served a file outside it on Windows; v3.204.0 closed that. Recorded because the contract is what a reader trusts.) Only GET is served; another method under `mount` gets 405, and a file larger than this tier reads in one response gets 413 -- both named, never a silent 404. Content-Type comes from the file extension (html, css, js, mjs, wasm, json, png, jpg, gif, svg, ico, txt, and application/octet-stream for anything else). `.wasm` as `application/wasm` is why the table exists at all: a browser refuses to stream-compile a WebAssembly module served as anything else.
conn | the connection httpRead was called on |
mount | the URL prefix to serve, e.g. "/" or "/static" |
dir | the directory to serve it from, e.g. "www" |
returns > 0 a response was sent (the byte count); 0 the request is not under `mount` and NOTHING was written, so route it yourself; < 0 the response could not be written, and netError() says why
if (httpServeDir(c, "/", "www") == 0) { httpRespond(c, 404, "text/plain", "no"); }httpalloworigin(origin)allow a browser page from another origin to read replies
OFF BY DEFAULT, and that default is the point (user's ruling, 2026-07-29). A browser refuses to let a page READ a response from a different origin unless the server says it may, so a CX server that never calls this cannot be read by any foreign page -- by construction, not by configuration. Calling it is the explicit, greppable, one-line act that opens that door, the same shape as netListenAny being a different name rather than a flag. Name a specific origin ("http://localhost:8000") in preference to "*": with "*", ANY website open in the user's browser can call this server and read what comes back, for as long as the program runs. Takes effect on every httpRespond and httpStream after it; "" turns it off again.
origin | the origin to allow ("http://localhost:8000"), "*" for any, "" for none |
returns the origin that was in force before this call, so it can be restored
httpAllowOrigin("http://localhost:8000");monthname(m)the three-letter English name of a month
ONE-based: 1 is "Jan", 12 is "Dec". Always English and always three letters; there is no locale or long-form option.
m | month number, 1..12 |
returns "Jan".."Dec", or the string "?" when m is out of range. NOT an empty string -- a "?" in the output is visible, which is the point (FP4).
println(monthname(month()) + " " + str(year()));
dayname(d)the three-letter English name of a weekday
ZERO-based, and starting on Sunday: 0 is "Sun", 6 is "Sat". Note the index base differs from monthname()'s -- both match what the date builtins hand you.
d | weekday number, 0..6 with 0 = Sunday |
returns "Sun".."Sat", or the string "?" when d is out of range
println(dayname(0));
datetime()the current date and time packed as YYYYMMDDHHMMSS
Local time, one int carrying both halves, so it sorts chronologically and survives being written to a file as a plain number.
returns now as YYYYMMDDHHMMSS
logline.s = str(datetime()) + " " + msg;
sleep(s)block this program for `s` SECONDS
NOTE THE UNITS: seconds, not milliseconds -- `sleep(100)` waits a minute and forty seconds. `sleep_ms` and `delay` are the millisecond forms.
s | seconds to wait |
returns void
sleep(1);
sleep_ms()sleep_ms -- block this program for `ms` MILLISECONDS; the same wait as delay
now()the current time as SECONDS SINCE THE UNIX EPOCH
THE one epoch clock: `timestamp`, `seconds` and `ticks` were three more spellings of this same value and are gone (BUGS-138, rule 33). Not the one to measure a duration with: `elapsed`, `timer` and `elapsedus` are monotonic, this one has a one-second grain and can jump when the system clock is corrected.
returns seconds since 1970-01-01 UTC
stamp.i = now();
date()today's date packed as the integer YYYYMMDD
Local time. 6 August 2026 is 20260806, so the value sorts chronologically and `year(date())` / `month(date())` / `day(date())` take it apart.
returns today as YYYYMMDD
printf("%d\n", date());wallclock()elapsed WALL-CLOCK seconds since the program started, as a
float with sub-millisecond precision. The name is the truth and the old contract was not: it claimed CPU time ("a program that spends a minute waiting reports almost nothing"), and it measured 303.000 ms across delay(300). It was C's `clock()`, which is CPU time on POSIX and WALL time on Windows -- one builtin meaning two different things depending on the host, which is the case CX has to decide (BUGS-138 rider 119). CX decides WALL, on every platform, because that is what its name says and what all four callers in the tree use it for: they are deadlines on a child process, and a CPU-time deadline never fires. The same monotonic clock as `timer` and `elapsedus`, in float seconds.
returns seconds since the program started
fStart.f = wallclock(); ... if (wallclock() - fStart > 2.5) ...
timer()a MONOTONIC millisecond counter, relative to program start
The same counter `milliseconds` returns and the one `elapsed(t0)` subtracts from, so `elapsed(timer())` is about 0. Monotonic means it never goes backwards when the system clock is adjusted -- which is why durations are measured with this and not with `now`.
returns milliseconds since the program started
t0.i = timer(); work(); printf("%d ms\n", elapsed(t0));milliseconds()the same monotonic millisecond counter as timer
One counter under two names: `timer` and `milliseconds` both read cx_timer_ms, the clock `elapsed(t0)` subtracts from.
returns milliseconds since the program started
t0.i = milliseconds();
elapsedus()MICROseconds since the program started, monotonic
The high-resolution twin of `elapsed`, for measurements too short to show up in milliseconds. Takes no baseline argument -- subtract two readings.
returns microseconds since program start
a.i = elapsedus(); f(); printf("%d us\n", elapsedus() - a);dayofweek(packed)which day of the week a date falls on, 0..6
ZERO IS SUNDAY, so Monday is 1 and Saturday is 6. No argument asks about today; a `date()`-style YYYYMMDD asks about that date.
packed | a YYYYMMDD value; omit for today |
returns 0 for Sunday through 6 for Saturday
if (dayofweek() == 0) { print "weekend"; }dayofyear(packed)which day of the year a date falls on, 1..366
ONE-BASED: 1 January is 1, not 0. No argument asks about today.
packed | a YYYYMMDD value; omit for today |
returns the day of the year, 1..366
year(packed)the four-digit year
Called with NO argument it reads today's; called with a `date()`-style YYYYMMDD value it decodes that one. A value that is not a plausible packed date (below 10000101) is treated as "no argument" and answers about today.
packed | a YYYYMMDD value to decode; omit for the current year |
returns the year, e.g. 2026
printf("%d\n", year(20260806));month(packed)the month number, 1..12
No argument reads today's; a `date()`-style YYYYMMDD decodes that one. ONE-BASED, so January is 1, not 0.
packed | a YYYYMMDD value to decode; omit for the current month |
returns the month, 1..12
day(packed)the day of the month, 1..31
No argument reads today's; a `date()`-style YYYYMMDD decodes that one.
packed | a YYYYMMDD value to decode; omit for today |
returns the day of the month, 1..31
elapsed(t0)milliseconds of real time, monotonic
With NO argument it answers the milliseconds since the program started; with a baseline it answers the milliseconds since that baseline, which is the form to time a section with. The baseline comes from `timer()` or from a bare `elapsed()` -- they read the same counter. Monotonic, so a system clock adjustment cannot make a duration come out negative.
t0 | a baseline from `timer()`/`elapsed()`; omit for since-start |
returns milliseconds
t0.i = elapsed(); load(); printf("%d ms\n", elapsed(t0));hour(seconds_of_day)the hour of the day, 0..23
Called with NO argument it reads the clock; called with a seconds-since- midnight value (what `time()` returns) it decodes that instead. Local time.
seconds_of_day | a `time()`-style value to decode; omit for now |
returns the hour, 0..23
if (hour() >= 18) { greet.s = "good evening"; }minute(seconds_of_day)the minute within the hour, 0..59
No argument reads the clock; a seconds-since-midnight value decodes that.
seconds_of_day | a `time()`-style value to decode; omit for now |
returns the minute, 0..59
second(seconds_of_day)the second within the minute, 0..59
No argument reads the clock; a seconds-since-midnight value decodes that.
seconds_of_day | a `time()`-style value to decode; omit for now |
returns the second, 0..59
time()the time of day as SECONDS SINCE MIDNIGHT
Local time, 0 to 86399. This is a time OF DAY, not a timestamp -- `now` is the epoch one -- and it is what `hour`, `minute` and `second` decode.
returns seconds since local midnight
secs.i = time();
closedebug()stop capturing and send output back to stdout
The debug console CAPTURES print and println into a buffer instead of, or as well as, stdout. Two name families reach the same runtime -- `openDebug`/`console` and the Orfeus-era `debugOpen`/`debugPrint` -- so a program can mix them freely.
returns void
closeDebug();
debug_render(x, y, w, h)draw the captured debug buffer as an in-game overlay panel
Runs on both engines (since GAP, 3.5.006.0 -- it was native-only and the register VM refused it with CX-E1013). The live alternative to the pop-up-on-exit window: pair it with a key toggle to show and hide the log during play, inside a frame loop that has a window open. Does nothing in a headless build (`-t` or `#pragma console off`), on either engine.
x | panel left |
y | panel top |
w | panel width |
h | panel height |
returns void
debug_render(10, 10, 400, 200);
consoleattach(title)open a real terminal window and stream output to it LIVE
Unlike the captured buffer, this shows output AS IT HAPPENS, which is what you want for a demo where AI reasoning should scroll past while the game runs. Per platform: a console on Windows; on Linux a log plus `tail -f` in your terminal; on macOS the same through Terminal.app. NOT behind the headless guard -- it pulls in no graphics dependency, so even a headless build can use it.
title | the terminal window's title |
returns void
consoleAttach("cx trace");consoledetach()close the attached terminal and stop streaming
returns void
consoleDetach();
console(text)write text straight into the captured buffer
The debug console CAPTURES print and println into a buffer instead of, or as well as, stdout. Two name families reach the same runtime -- `openDebug`/`console` and the Orfeus-era `debugOpen`/`debugPrint` -- so a program can mix them freely. BYPASSES the sink check, so it works whether or not openDebug was called -- which is what makes it usable as a plain trace call. No trailing newline.
text | the text to append |
returns void
console("tick");consoleln(text)write a line into the captured buffer
The debug console CAPTURES print and println into a buffer instead of, or as well as, stdout. Two name families reach the same runtime -- `openDebug`/`console` and the Orfeus-era `debugOpen`/`debugPrint` -- so a program can mix them freely. Appends a trailing newline.
text | the line to append |
returns void
consoleLn("tick");console_clear()empty the captured buffer
The debug console CAPTURES print and println into a buffer instead of, or as well as, stdout. Two name families reach the same runtime -- `openDebug`/`console` and the Orfeus-era `debugOpen`/`debugPrint` -- so a program can mix them freely.
returns void
console_clear();
splitparameters(tokens, divisor, valuetakers)turn the command line into PARAMETERS, in one call
Reads `args` and answers a json ARRAY with one entry per parameter, each carrying his three results: `flag` (true/false), `name`, and `option` (optional). Nothing is segregated -- a positional argument is an entry with `flag: false`, so the result is the whole command line in order. THERE IS NO SEPARATOR ARGUMENT: the OS already split the line before the program started, so this never parses a quote and quoting stays the OS's business (his ruling -- "if we agree os already splits it its an uneeded option"). THREE STATES, KEPT DISTINCT on his explicit ruling ("yes keep them distinct"): a flag ABSENT is simply not in the array; a flag with NO option (`-v`) has no `option` key at all; a flag with an EMPTY option (`--out=`) has the key, holding "". Collapsing any two would make one readout mean two things. THE LONGEST TOKEN WINS, so with `"-,--"` the parameter `--out` is the flag `out` and not the flag `-out`. The divisor splits every parameter, not only flags, so `make CC=gcc` reads as name `CC` option `gcc`. FP4: a `valuetakers` flag sitting LAST, with nothing after it to consume, is reported by name on stderr and left with no option -- never given an empty one, which would be indistinguishable from the legitimate `--out=`.
tokens | comma-separated flag prefixes, e.g. "-,--" |
divisor | splits name from option inside one parameter, e.g. "=" |
valuetakers | optional: comma-separated flags, WITH their token ("--out,--in"), whose value is the NEXT parameter |
returns a json array of {flag, name, option?} entries, one per parameter
json p = SplitParameters("-,--", "=", "--out,--in");opendebug(title, w, h)start capturing output into a debug console window
The debug console CAPTURES print and println into a buffer instead of, or as well as, stdout. Two name families reach the same runtime -- `openDebug`/`console` and the Orfeus-era `debugOpen`/`debugPrint` -- so a program can mix them freely. Three forms: no arguments opens with the default title at 800x600, one sets the title, three set title, width and height. The window pops up when the program exits. In a HEADLESS build (`cx -t`) this is a no-op and output keeps flowing to stdout, so no gui link is dragged in.
title | window title (optional) |
w | window width (optional) |
h | window height (optional) |
returns void
openDebug("trace", 900, 600);assert(cond, msg)fail the run if a condition is not true
The foundation of the CX test suites: a false condition prints a failure line and counts against the run. assertEqual is the form that reports both the expected and the actual value, which is almost always the more useful failure message.
cond | the condition that must hold |
msg | text identifying the assertion in the output |
returns void
assert(count > 0, "parser produced tokens");
assertequal(expected, actual, msg)fail the run unless two values are equal, reporting both
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
TYPE-DISPATCHED, and on BOTH backends the same way: a string pair compares by content, and a pair where EITHER side is a float compares as floats. That second rule is the one that matters -- comparing an int literal against a float that happens to be 100.5 must not truncate and pass.
expected | the value the code should produce |
actual | the value it did produce |
msg | text identifying the assertion in the output |
returns void
assertEqual(3, len(parts), "three fields");
assertequalstr(expected, actual, msg)fail the run unless two strings have the same contents (the other name for assertStringEqual)
Identical to assertStringEqual; both reach the same string comparison.
expected | the text the code should produce |
actual | the text it did produce |
msg | text identifying the assertion in the output |
returns void
assertfloatequal(expected, actual, msg)fail the run unless two floats are equal within tolerance
Compares with a tolerance rather than exactly, which is what makes it usable on computed floats where the last bits will not match.
expected | the value the code should produce |
actual | the value it did produce |
msg | text identifying the assertion in the output |
returns void
assertnotequal(a, b, msg)fail the run unless two INTEGERS differ
INT ONLY, deliberately: there are no float or string siblings, because a native-only float form would be a divergence between the backends rather than a feature.
a | the first value |
b | the second value |
msg | text identifying the assertion in the output |
returns void
assertstringequal(expected, actual, msg)fail the run unless two strings have the same contents
Compares by content, not by handle. assertEqualStr is the same function under another name.
expected | the text the code should produce |
actual | the text it did produce |
msg | text identifying the assertion in the output |
returns void
gcstats()report the collector's live-object and allocation counters
The instrument behind the leak checks: what the allocator currently holds, by bucket.
returns the statistics as reported by the collector
print(gcStats());
prtc(ch)write one CHARACTER to stdout with no newline, by code
The character sibling of prts, taking a numeric code as putc does.
ch | the character code to write |
returns void
prtf(v)write a FLOAT to stdout with no newline
The float sibling of prts; decimals follow `#pragma decimals`.
v | the float to write |
returns void
prti(n)write an INTEGER to stdout with no newline
The int sibling of prts.
n | the integer to write |
returns void
prtn(…)write a newline to stdout
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The line-ending half of the prt* family, for when a line has been built with prts/prti/prtf.
returns void
cpucount()how many logical processors the MACHINE has
Every one, whether or not this process may run on them; `cpucount_process` is the count you can actually use. Falls back to 1 where the platform cannot be asked, so it never answers 0.
returns the logical processor count, at least 1
workers.i = cpucount();
cpucount_process()how many logical processors THIS PROCESS may run on
Reads the affinity mask, so a pinned or containerised process gets the smaller, truthful number. Falls back to `cpucount` if the mask cannot be read, and so is also never 0. This is the one to size a thread pool with.
returns the usable processor count, at least 1
memtotal()total physical RAM, in BYTES
returns the byte count, or 0 if the platform cannot be asked
memfree()physical RAM currently available, in BYTES
A snapshot, and it moves; treat it as a reading, not a reservation.
returns the byte count, or 0 if the platform cannot be asked
printf("%d MB free\n", memfree() / 1048576);osname()which operating system this build is running on
ONE OF A FIXED, LOWERCASE SET: "windows", "macos", "linux", "bsd", "web" (a wasm build -- browser or node), or "unix" for anything else. Decided at COMPILE time from the platform macros, so it names the build's target, and it is safe to compare with `==`. ALWAYS EQUAL TO THE COMPILE-TIME `CX_OS_NAME`, on every target. That was a promise this line already made and could not keep across a cross-compile until v3.271.0, when the preprocessor's constants started describing the TARGET rather than the compiling host (SWEEP_2026-08.md row 245).
returns the platform name
if (osname() == "windows") { sep.s = "\\"; }hostname()this machine's network name
returns the host name, or an empty string if the system will not say
username()the name of the user this process is running as
Read from the environment (USERNAME on Windows, USER then LOGNAME elsewhere), so it reflects the environment rather than the OS's account database -- an empty environment gives an empty string, not an error.
returns the user name, or an empty string
argc()how many command-line arguments the program has, C-identical
The program path counts, exactly as it does in C, so a bare invocation answers 1 and `argc() == args->count + 1`. It READS the `args` document rather than the process vector, which is what makes it move when a program patches its own command line -- and what let every existing call site keep working unchanged when the document arrived. THE ONE EXCEPTION IS STATED RATHER THAN SMOOTHED OVER: a runtime started with no vector at all -- a `--shared` library, or a `_CM{}` program that has not called cx_rt_init -- answers 0, because claiming 1 would claim a program path that does not exist.
returns 1 + the number of user arguments; 0 when there is no command line
if (argc() < 2) { println("usage: report <file>"); return 1; }argv(n)the n-th command-line argument as a string, C-identical
`argv(0)` is the PROGRAM PATH and `argv(1)` is the first user argument, so `argv(n)` is `args[n-1]`. That one-apart relationship is the whole C convention and it is why no `progpath()` builtin was added: argv(0) already is that, and a second name for one datum is what rule 33 refuses. Out of range answers "" -- and because it reads the `args` document, an element a program has written is what comes back, whatever type it was written as.
n | 0 for the program path, 1..argc()-1 for the arguments |
returns the argument as a string; "" when n is out of range
path.s = argv(1);
delay(ms)sleep for the given number of milliseconds
Windows uses Sleep(), POSIX uses nanosleep(); a value of 0 or less is a no-op.
ms | milliseconds to sleep (<= 0 does nothing) |
returns void
delay(16);
diskfree(path)free space, in BYTES, on the filesystem that holds `path`
The disk-side twin of memfree(), and deliberately in the same units: bytes, not megabytes, so the two compose without a scale factor in between. Reports the space available to the CALLING user -- on POSIX that is f_bavail rather than f_bfree, the difference being the root-reserved margin, and a program asking "can I write this file" wants the smaller, honest number. `path` need not be a directory: any existing path on the target volume works, and "." is the ordinary spelling for "wherever I am". FP4, and the one place this does NOT copy memfree: that builtin answers a silent 0 when the OS call fails, which is indistinguishable from a genuinely full disk. -1 is a value no real filesystem can report, so a failure to MEASURE and a measurement OF zero stay separable by the caller.
path | a path on the filesystem to measure; "" is rejected (-1) |
returns free bytes >= 0, or -1 if the path cannot be interrogated (missing, unreadable, or not a mounted filesystem)
bytes.i = diskfree(".");randomseed(seed)pin the random stream, so the run replays
Random is RANDOM BY DEFAULT since v3.215.0: an unseeded program seeds from real entropy at startup and prints different numbers every run. This builtin is how you get the other behaviour deliberately -- a golden test, a bug report, a level you can regenerate. Called anywhere, it wins from that point on, over both the entropy default and `#pragma randomseed N`; the pragma is the same thing placed before the program's first statement. EVERY DISTINCT SEED IS A DISTINCT STREAM. It was not always: the state used to be `seed | 1`, which forced it odd and so gave `randomseed(42)` and `randomseed(43)` the identical stream, as it did 0 and 1 -- half of every seed a caller could type was unreachable, silently. Seeds are now taken as written, with ONE documented exception: xorshift64* has no zero state (0 maps to 0 forever), so seed 0 is remapped to a fixed nonzero constant. A caller who passes that constant's bit pattern as a negative integer gets seed 0's stream -- the whole of the collision, and stated rather than hidden. Seeds the int forms and randomf() alike: one stream, so a seeded program is reproducible whichever spelling it draws from.
seed | any integer; the same seed always replays the same stream |
returns void
randomseed(2026); // this run is now repeatable
getenv(name)read an environment variable
NO ERROR CHANNEL: an unset variable and one set to "" both give an empty string, so a program that must tell them apart has to arrange its own sentinel. The name is passed to the C library verbatim, so case sensitivity is the platform's -- Windows ignores it, POSIX does not.
name | the variable to read |
returns the value, or an empty string if it is unset
home.s = getenv("HOME");cxtune()read or set a runtime tunable by name
The run-time face of the `#pragma` capacity settings (buffer seeds and caps). Prefer the pragma where the value is known at compile time -- it costs nothing at run time.
returns the tunable's value
cxTune();
exec(cmd)run a shell command and return its exit code
Passes the string to the C library's system(). In a browser tab there is no shell, so the call is REFUSED with CX-E5037 naming the capability rather than returning a fabricated status -- the same answer on both backends.
cmd | the command line |
returns the command's exit code
rc = exec("git status");system(cmd)run a shell command and return its exit code
The same operation as exec, under its C name. In a browser tab it is refused with CX-E5037 rather than pretending to succeed.
cmd | the command line |
returns the command's exit code
rc = system("git status");procspawn(cmd, cwd, env)start a program as a CHILD of this one and keep the parentage
NO SHELL IS INVOLVED, and that is a contract clause rather than an omission. A shell in between turns "there is no such program" into "a shell started fine and exited 1", which would make NEVER-STARTED -- one of the five states this family exists to serve -- all but unreachable. Without one, the OS itself refuses and the reason is real. The command line is split into argv on rules both platforms honour: spaces separate, double quotes group. Someone who genuinely wants a shell asks for one by name (`cmd /c ...`, `/bin/sh -c ...`), which is then an honest child like any other. stdout, stderr and stdin are each a private pipe, and the two output streams stay SEPARATE -- a screen that merges them can never tell a program's answer from its complaint. THE ENVIRONMENT IS THE CALLER'S TO SET, not this family's to guess. Until v3.308.0 the third argument was a per-session TEMP DIRECTORY and the runtime decided, on the caller's behalf, that it meant TMP, TEMP and TMPDIR. That was one caller's policy living inside the mechanism -- so the mechanism could serve exactly one policy, and a second need (routing a child's side channel into its session) had nowhere to go. The general form costs nothing, subsumes the special case exactly, and moves the decision to the only place that can make it (FP7: the same power, one fewer moving part).
cmd | program and arguments; spaces separate, double quotes group |
cwd | working directory for the child; "" keeps the parent's |
env | environment overrides for the child, "KEY=VALUE;KEY=VALUE". Applied ON TOP of the parent's environment: a key named here replaces any inherited one (never sits behind it), and a key not named is inherited unchanged. "" inherits everything. The parent process's own environment is never modified, so two spawns cannot race over it. A fragment with no '=' is skipped rather than guessed at. |
returns a handle > 0, or 0 if the child NEVER STARTED -- in which case procError() says why, in the OS's own words
h = procSpawn("cx prog.cx --run", ".", "TMP=" + d + ";TEMP=" + d);procerror()why the last procSpawn failed, in the operating system's words
Latched at the failure and not cleared by anything else, so a caller can ask after the fact. "" when no spawn has failed. This is the `errno/reason` the lifecycle contract's NEVER-STARTED state is required to carry: a state with no reason is a shrug with a timestamp.
returns the failure text, or "" if the last spawn succeeded
if (h == 0) { println("never started: " + procError()); }procpid(h)the child's operating-system process id
For the RUNNING state's payload and for a human who wants to look with their own tools. It is NOT how liveness is decided -- a pid is a number that outlives its process and gets reused, which is what makes a `ps | grep` a guess.
h | a handle from procSpawn |
returns the OS process id
println("running, pid " + str(procPid(h)));procalive(h)ask the OS, through our own parent handle, whether it still runs
THE ONE QUESTION THIS FAMILY EXISTS TO ANSWER. Windows: WaitForSingleObject on the process handle we hold. POSIX: waitpid(WNOHANG) on our own child. Neither can be fooled by a quiet log or a busy process table, and both are answering about THE process we started rather than about a pid that looks like it. It also REAPS: the moment the child is gone its exit code is latched, so procExit() has a real answer afterwards and no zombie is left behind.
h | a handle from procSpawn |
returns 1 while the child is running, 0 once it is not
if (procAlive(h) == 0) { rc = procExit(h); }procexit(h)the child's exit code, once it has one
ONLY MEANINGFUL AFTER procAlive() HAS ANSWERED 0, and the reason is worth the sentence: before then this returns -1, which is also a legitimate exit code on Windows. That ambiguity is deliberate and undisguised. A caller that reads an exit code without first asking whether the child has finished is inferring, and inference is the failure this whole family is a refusal of.
h | a handle from procSpawn |
returns the exit code, or -1 if the child has not been seen to exit
if (procAlive(h) == 0) { println("rc=" + str(procExit(h))); }procout(h)take whatever the child has written to STDOUT and not yet been read
Never blocks: "" means "nothing waiting right now", never "the program is finished" and never "the program is stuck". Bytes still in the pipe after the child exits are still delivered, so a caller drains once more after seeing procAlive() == 0 rather than losing the last thing the program said.
h | a handle from procSpawn |
returns the bytes available now, or "" if none are
chunk = procOut(h); if (len(chunk) > 0) { record(chunk); }procerr(h)the same, for STDERR, which is a SEPARATE stream and stays separate
Kept apart from stdout all the way through: a merged capture cannot tell a program's output from its diagnostics, and every instrument downstream then inherits that confusion.
h | a handle from procSpawn |
returns the bytes available now, or "" if none are
e = procErr(h); if (len(e) > 0) { record_stderr(e); }procin(h, s)write to the child's STDIN
The control half of the contract: a program waiting on input can be answered rather than killed. No newline is added -- what you pass is what it reads.
h | a handle from procSpawn |
s | the bytes to write |
returns bytes written, or -1 if the pipe is gone (the child closed it or ended)
procIn(h, "yes" + chr(10));
prockill(h)end the child AND EVERYTHING IT STARTED
Tree-kill, the Invoke-Cx discipline, because a shell that spawned a compiler that spawned a linker leaves two survivors when only the shell is killed -- and survivors are how a "stopped" run keeps holding a lock. Windows: the child is created inside a Job Object and the JOB is terminated. POSIX: the child gets its own session with setsid(), so one killpg reaches the whole group. KILLED is a state the CALLER records: this function does not label anything, it only ends it. Who killed it and when is knowledge the killer has.
h | a handle from procSpawn |
returns 1 if the kill was issued, 0 if the child had already finished
if (procKill(h) == 1) { note("killed by the operator"); }procclose(h)release the pipes and handles this process holds for a child
REFUSES WHILE THE CHILD IS ALIVE, and answers 0 to say so. Closing a live child's handles would throw away the parentage that is the only honest source of its state -- the caller would be left with a pid and a guess, which is exactly the position this family removes. Kill it or wait for it, then close. The handle is not recycled afterwards: a later call on a closed handle stays LOUD rather than silently addressing a stranger's process (cx_queue.c keeps its slots for the same reason, and records the same cost).
h | a handle from procSpawn |
returns 1 if released, 0 if refused because the child is still running
if (procAlive(h) == 0) { procClose(h); }onerroraddrule(sGate, sRule)add a RULE to the universal error door
THE ARM EVERY SIBLING DOOR ALREADY HAD. `onevent` carries cx_event_add_rule beside cx_event_add_func and automation carries cx_auto_add_rule beside cx_auto_add_func; this door had only the function arm, which is the asymmetry the on* dogma audit found by SHAPE (DOCS/design/SCREEN_SERVER.md, finding A1) with no knowledge that the arm had ever been intended. His ruling, 2026-08-25: "onerror gains the rule arm; resumption stays refused; it was supposed to but got lost in the chat." Write `onerror("code >= 1000", MYRULE);` in CX -- the TWO-argument form is the rule arm. `onerror(&h)` is still the function arm, `onerror()` still uninstalls, and both are unchanged. RULES ACCUMULATE; A HANDLER REPLACES. A second onerror(&h) replaces the first by design (above); registered rules are a LIST, like events and automation, because rules are data you collect. The two arms of one door therefore behave differently, deliberately. GATE AND RULE ARE BOTH COMPILED AT REGISTRATION, and that is FORCED by the return value rather than chosen: a door that stored the strings and compiled them at fire time could not answer "registered" honestly. It is automation's shape already (cx_auto_add_rule compiles once), not a new one. A gate of "" or "1" is stored as no gate at all -- always fires, nothing compiled. RESUMPTION IS STILL REFUSED -- his ruling, and the no-resumption position above is unchanged and not weakened by this arm. A rule runs, it returns, the program still stops with a non-zero exit. A rule MAY mutate the doc and the annotation is visible to the function handler that runs after it; it cannot change whether execution continues. This is said here as well as in the design because a rule engine that can write to `e` LOOKS like it could change the outcome. ORDER AT A RAISE: the stderr report -> the default side-channel record -> every registered rule, in registration order, behind its gate -> the installed function handler. Rules and the handler COMPOSE; neither suppresses the other. FP2: a program that registers no rule pays one count test on a path that was already about to print and exit. Nothing on any hot path learns this exists.
sGate | a rule evaluated against the error doc; "" or "1" always fires. |
sRule | the rule to run when the gate fires. |
returns 1 registered, 0 refused -- and every refusal has ALREADY printed its own reason (cx_rule_compile for a malformed gate or rule, cx_grow_capped for the registry cap), so the number never has to carry the diagnosis. His standing dogma: a verb reporting through an error channel answers at its call site too.
onerror("code >= 1000", LOGIT);errorraise(nCode, sNote)raise a USER error checkpoint
Runs the installed handler with (nCode, sNote), then stops the program with a non-zero exit. This is what `#error-check <n> <note>` lowers to, and the directive is the spelling to write -- it is checked at compile time and it prefixes the note with the source location for free. Call this form directly when the message has to be BUILT at run time; that is the whole difference between them.
nCode | the checkpoint number, CX_USER_CODE_MIN..CX_USER_CODE_MAX (1..999). CX's own codes are 1000 and up, so a handler can always tell the two apart; see the band note in cx_limits.h. |
sNote | the message, handed to the handler unchanged. |
errorRaise(7, "config file had no [server] section");
hostcall(pin, arg)call the host function wired behind one of this chip's pins
Written BY A CHIP, not by a host: it is the reverse crossing. The name is the LINKS MEMBER's, never the host function's, so a chip is written against the pins its datasheet declares and the host may wire any function of the right shape behind them -- the chip stays generic and the injection specialises it. One json in, one json out (his rule). Nothing is checked here: the socket already refused any target whose signature was not `j:j`, so by the time a call is possible there is nothing left but the name.
pin | the links member's name, as the host wired it |
arg | one json document handle |
returns the json handle the host function answered, or 0 after a refusal
r = hostCall("onscore", doc);chipfn(handle, ordinal)one export's address, by the ordinal its datasheet gave it
Emitted by `H->name(args)`, which lowers to libCall/libCallf over this. The symbols were resolved ONCE at socket time into a table on the instance, so this is an array index and a call costs no name lookup -- the bargain part 0 of CHIPS.md measured and set. IT ANSWERS AN ADDRESS RATHER THAN CALLING, so the marshalling stays in the ONE core libCall already uses for every argument family and both return families. A chip call and a library call are then the same call (rule 33).
handle | an instance handle. |
ordinal | the export's index in the datasheet the host compiled against. |
returns the function's address, or 0 -- having NAMED the chip and the ordinal on stderr, because one step later there is only an address.
float a = Math1->area(2.5);
libopen(path)open a native shared library and answer a handle to it
path | the library to load -- a full path, or a bare name the platform loader searches for ("msvcrt.dll", "libSystem.dylib"). |
returns the library handle, or 0 when it could not be loaded.
int h = libOpen("C:/tools/mathchip.dll");libsym(handle, name)resolve one symbol in an open library to its address
handle | a handle from libOpen. |
name | the exported symbol's name, spelled exactly as the library exports it (a CX function exports its own lowercased name). |
returns the address as an integer, or 0 when the library exports no such name -- which is also what a closed or never-opened handle answers.
int fn = libSym(h, "cx_probe_pure");
libclose(handle)close a library opened by libOpen
Every address it handed out becomes stale the moment it closes.
handle | a handle from libOpen; 0 is a no-op, as C's free(NULL) is. |
libClose(h);
libcall(fn)call a function in an open library and read an integer back
Takes the address plus however many arguments the callee takes, and picks the platform call shape from what it is given: all-float goes to the float registers, a mix of integer and float through the ABI thunk, everything else through the integer registers. A string is passed as its bytes, an array as its buffer, a list or a map is refused (neither has a C representation).
fn | an address from libSym. |
returns whatever the callee returned, as an integer; 0 for a null address.
int n = libCall(fn_strlen, "hello");
libcallf(fn)call a function in an open library and read a float back
libcall's twin: same arguments, same call shapes, and the return read as a double rather than as an integer.
fn | an address from libSym. |
returns whatever the callee returned, as a float; 0.0 for a null address.
float r = libCallf(fn_pow, 2.0, 10.0);
vmload(path)compile and load a whole CX file onto an embedded VM, returning a module handle
Reads the file, compiles it via the embedded frontend (requires `#pragma rules c`), admission-gated on the Layer-2 VM-clean verdict (rejects any native-only construct), and prepares a dedicated persistent VM. Errors are loud (empty path, open/size failure, OOM, compile/admission failure, module-table full, or a build without CX_RULES_C).
path | path to the .cx module file to load |
returns module handle (>= 0) on success; -1 on any error
h = vmLoad("plugin.cx");vmloadstr(src)compile and load a CX module from an in-memory source string
Same admission gate, capabilities, and handle registry as vmLoad, but the source is a CX string (AI-authored, embed-bundled, or network-fetched) with no temp-file round-trip; both share the vmload_core implementation.
src | CX module source text to compile |
returns module handle (>= 0) on success; -1 on any error
h = vmLoadStr(modsrc);
vmhas(handle, name)test whether a loaded module defines a named function
Linear-scans the module's captured function names; returns 0 for an invalid or already-unloaded handle.
handle | module handle returned by vmLoad/vmLoadStr |
name | function name to look for |
returns 1 if the module defines that function, else 0
vmHas(h, "bump")
vmcall(handle, name, jsonarg)call a named function in a loaded module and return its value as a float
Passes the json entity `jsonarg` as arg 0 (the rule ABI, so results flow back through the entity) and runs under the module's loop budget with the module VM published as the rule VM. A string return, an invalid handle, a missing function, or budget exhaustion all yield 0.
handle | module handle returned by vmLoad/vmLoadStr |
name | name of the function to invoke |
jsonarg | json entity handle passed as arg 0 and used as the result channel |
returns the function's return value coerced to float; 0 on a string return or on any error
vmCall(h, "ki", e);
vmbudget(handle, n)set a loaded module's per-call loop-iteration budget (runaway cage)
Host-only tuning (not in the rule capability whitelist, so a module cannot raise its own cage): n>0 sets a finite cap, n==0 uncaps, n<0 is a loud error leaving the budget unchanged. Takes effect on the next vmCall.
handle | module handle returned by vmLoad/vmLoadStr |
n | loop-iteration budget; 0 = uncapped, negative = rejected |
returns void
vmBudget(h, 2000000000);
vmunload(handle)free a loaded module and release its handle slot
Frees the module's VM, parsed program, and library-callback array; the slot becomes reusable. No-op on an invalid or already-unloaded handle.
handle | module handle returned by vmLoad/vmLoadStr |
returns void
vmUnload(h);