CX+AI

CX Builtins Reference

Every builtin, drawn from the source

GENERATED from DOCS/reference/manuals/CX_Builtins_Reference.html by tests/forge.cx — edit the source, not this page.

AI 4 families · 106 builtins

AI core 48 builtins

ai_ask(prompt)

send a prompt to the configured LLM and return its reply

One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text; an empty prompt returns an empty string without contacting anyone

returns the model's reply, or an empty string on any failure (read aiGetError for the reason)

reply.s = aiAsk("Explain CX in one line.");
ai_async_cancel(id)

accepted for API compatibility; does nothing

A started job runs to completion. Collect it with aiAsyncResult, or ignore it -- there is no cancellation path, and this builtin says so rather than pretending one exists.

idignored

returns void

aiAsyncCancel(id);
ai_async_ready(id)

ask whether a background job has finished

The same reading as aiAsyncStatus. 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.

idthe job id from aiCallAsync

returns 0 while pending, 1 when done, -1 for an unknown id

if (aiAsyncReady(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.

idthe job id from aiCallAsync

returns the model's reply, or an `ERROR: unknown job id N` marker

r.s = aiAsyncResult(id);
ai_async_status(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.

idthe job id from aiCallAsync

returns 0 while pending, 1 when done, -1 for an unknown id

while (aiAsyncStatus(id) == 0) { sleep(1); }
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

aiCacheClear();
ai_cache_get(key)

read a cached reply by key

Counts a hit or a miss either way, so the counters reflect real lookups.

keythe 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 = aiCacheGet("q");
ai_cache_has(key)

test whether a key has a non-empty cached reply

Implemented as a get whose result is tested for length, so it COUNTS AS A LOOKUP in the hit/miss statistics, and a key whose cached value is the empty string answers 0.

keythe cache key to test

returns 1 if the key holds a non-empty value; 0 otherwise

if (aiCacheHas("q")) { ... }
ai_cache_hits()

count cache lookups that found an entry

Reset by aiCacheClear. Note that aiCacheHas performs a lookup, so it moves this counter.

returns the hit count since start or since the last clear

h = aiCacheHits();
ai_cache_miss()

count cache lookups that found nothing

Reset by aiCacheClear. Note that aiCacheHas performs a lookup, so it moves this counter.

returns the miss count since start or since the last clear

m = aiCacheMiss();
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.

keycache key (typically the prompt)
valuethe reply text to store
ttlaccepted for API compatibility and ignored -- there is no expiry

returns void

aiCachePut("q", "a", 60);
ai_cache_size()

count the entries currently in the response cache

returns the number of cached entries

n = aiCacheSize();
ai_cache_stats()

read the cache counters as one formatted line

returns a string of the form `entries=N hits=N misses=N`

print(aiCacheStats());
ai_call(prompt)

send a prompt to the configured LLM and return its reply

The same synchronous operation as aiQuery. One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text

returns the model's reply, or an empty string on failure

r.s = aiCall("...");
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.

promptthe prompt text

returns a positive job id; -1 with no key, no free slot, or on a non-Windows build

id = aiCallAsync("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 aiParseBytecode (or the codeswap path) under this name.

namethe AI function's name
argone integer argument

returns the function's integer result; 0 if the name cannot be read

r = aiCallFunc("clamp", 130);
ai_call_get(id)

collect a background job's reply, waiting if necessary

The same operation as aiAsyncResult. 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.

idthe job id from aiCallAsync

returns the model's reply, or an `ERROR: unknown job id N` marker

r.s = aiCallGet(id);
ai_call_sync(prompt)

send a prompt to the configured LLM and return its reply

The same synchronous operation as aiQuery, spelled to contrast with aiCallAsync. One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text

returns the model's reply, or an empty string on failure

r.s = aiCallSync("...");
ai_chat(prompt)

send a prompt to the configured LLM and return its reply

One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text; an empty prompt returns an empty string without contacting anyone

returns the model's reply, or an empty string on any failure (read aiGetError for the reason)

reply.s = aiChat("Explain CX in one line.");
ai_clear_cache()

empty the response cache and reset its counters

The same operation as aiCacheClear -- a heritage spelling kept for programs written against the beta API.

returns void

aiClearCache();
ai_complete(prompt)

send a prompt to the configured LLM and return its reply

One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text; an empty prompt returns an empty string without contacting anyone

returns the model's reply, or an empty string on any failure (read aiGetError for the reason)

reply.s = aiComplete("Explain CX in one line.");
ai_dump_asm(name)

disassemble a named AI function back to readable text

The counterpart to aiParseBytecode: what the AI wrote, as the runtime holds it.

namethe AI function's name

returns the disassembly text; an empty string if the name cannot be read

print(aiDumpAsm("clamp"));
ai_get_error()

read the reason the last AI operation failed

Set by aiInit and by the query family; an empty string means the last operation reported no error.

returns the error text, or an empty string

print(aiGetError());
ai_get_named(name)

read an integer from a named AI slot

Names must have been DECLARED to the AI subsystem first; calling this with an unknown or undeclared name is a hard, loud failure rather than a silent 0 (FP4). A name bound to a program variable reads and writes that variable directly; a register-only name keeps its value in the slot table.

namethe declared slot name

returns the slot's value

v = aiGetNamed("hp");
ai_has_key()

report whether a usable API key is available for the current provider

True if aiSetKey supplied one, or the provider's environment variable is set and non-empty. ollama and cxllama always answer 1, because they run locally and need no key.

returns 1 if a key is available (or none is needed); 0 otherwise

if (aiHasKey()) { ... }
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 and cxllama run locally and need no key.

returns 1 when a provider and key are available; 0 otherwise, with aiGetError explaining which is missing

if (aiInit() == 0) { print(aiGetError()); }
ai_key(key)

set the API key explicitly instead of reading it from the environment

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.

A heritage spelling of aiSetKey. 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.

keythe API key

returns void

aiKey(k);
ai_last_error()

read the reason the last AI operation failed

The same value aiGetError returns -- a heritage spelling kept for programs written against the beta API.

returns the error text, or an empty string

print(aiLastError());
ai_named_get(name)

read an integer from a named AI slot

The same operation as aiGetNamed -- a heritage spelling kept for programs written against the beta API. Names must have been DECLARED to the AI subsystem first; calling this with an unknown or undeclared name is a hard, loud failure rather than a silent 0 (FP4). A name bound to a program variable reads and writes that variable directly; a register-only name keeps its value in the slot table.

namethe declared slot name

returns the slot's value

v = aiNamedGet("hp");
ai_named_set(name, value)

write an integer into a named AI slot

The same operation as aiSetNamed -- a heritage spelling kept for programs written against the beta API. Names must have been DECLARED to the AI subsystem first; calling this with an unknown or undeclared name is a hard, loud failure rather than a silent 0 (FP4). A name bound to a program variable reads and writes that variable directly; a register-only name keeps its value in the slot table.

namethe declared slot name
valuethe value to store

returns 1

aiNamedSet("hp", 42);
ai_parse_bytecode(name, src)

assemble AI-authored bytecode text into a named callable function

namethe name to register the function under
srcthe bytecode source text

returns the result of the assembler -- non-zero identifies the parsed function

aiParseBytecode("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 = aiPending();
ai_query(prompt)

send a prompt to the configured LLM and return its reply

One implementation behind four names: 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 aiGetError -- so test the result, do not assume it.

promptthe prompt text; an empty prompt returns an empty string without contacting anyone

returns the model's reply, or an empty string on any failure (read aiGetError for the reason)

reply.s = aiQuery("Explain CX in one line.");
ai_result(id)

collect a background job's reply, waiting if necessary

The same operation as aiAsyncResult -- a heritage spelling kept for programs written against the beta API. 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.

idthe job id from aiCallAsync

returns the model's reply, or an `ERROR: unknown job id N` marker

r.s = aiResult(id);
ai_set_async(on)

set the async-preference flag

Stored and readable by the program; the query builtins themselves remain synchronous -- use aiCallAsync/aiResult for a real background request.

onnon-zero to record a preference for asynchronous calls

returns void

aiSetAsync(1);
ai_set_cache(on)

turn the in-process response cache on or off

On by default. While on, aiQuery/aiAsk/aiChat/aiComplete answer a repeated prompt from memory without contacting the provider, keyed by the prompt text itself.

onnon-zero enables the cache, 0 disables it

returns void

aiSetCache(0);
ai_set_fallback(chain)

set a comma-separated chain of providers to try when the primary fails

Tried in order, left to right, only after the configured provider returns an error. 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.

chaincomma-separated provider names, e.g. "openai, groq"

returns void

aiSetFallback("openai, groq");
ai_set_key(key)

set the API key explicitly instead of reading it from the environment

Takes priority over the provider's environment variable. 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.

keythe API key; empty clears it and falls back to the environment

returns void

aiSetKey(k);
ai_set_max_tokens(n)

cap the tokens the model may generate per reply

Values <= 0 are IGNORED and leave the previous setting in place. Default 1024.

nmaximum tokens; must be positive to take effect

returns void

aiSetMaxTokens(4096);
ai_set_model(model)

set the model name sent with each request

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.

modelprovider-specific model identifier; empty clears it

returns void

aiSetModel("claude-sonnet-4");
ai_set_named(name, value)

write an integer into a named AI slot

Names must have been DECLARED to the AI subsystem first; calling this with an unknown or undeclared name is a hard, loud failure rather than a silent 0 (FP4). A name bound to a program variable reads and writes that variable directly; a register-only name keeps its value in the slot table.

namethe declared slot name
valuethe value to store

returns 1

aiSetNamed("hp", 42);
ai_set_provider(provider)

choose the LLM provider for subsequent calls

Accepts anthropic, openai, google, mistral, cohere, xai, deepseek, groq, ollama or cxllama, plus the aliases claude -> anthropic, grok -> xai and gpt -> openai. Switching provider CLEARS any key set by aiSetKey, so set the key after the provider, not before. 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.

providerprovider name or alias; an empty string clears both provider and key

returns void

aiSetProvider("anthropic");
ai_set_system(prompt)

set the system prompt prepended to each request

Sent with every subsequent query until changed; empty clears it. 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.

promptthe system prompt text

returns void

aiSetSystem("Answer in one sentence.");
ai_set_temp(t)

set the sampling temperature sent with each request

Stored as given and passed through to the provider; the runtime does not clamp it. Default 0.7.

ttemperature, in whatever range the provider accepts

returns void

aiSetTemp(0.2);
ai_set_timeout(ms)

set the per-request transport timeout in milliseconds

Values <= 0 are IGNORED and leave the previous setting in place. Default 60000 (60 s).

mstimeout in milliseconds; must be positive to take effect

returns void

aiSetTimeout(15000);
ai_set_url(url)

override the provider's endpoint URL

For a proxy, a self-hosted gateway or a local ollama/cxllama server. Empty restores the provider default. 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.

urlfull endpoint URL; empty restores the default

returns void

aiSetUrl("http://localhost:11434/api/chat");
ai_status(id)

ask whether a background job has finished

The same reading as aiAsyncStatus -- a heritage spelling kept for programs written against the beta API. 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.

idthe job id from aiCallAsync

returns 0 while pending, 1 when done, -1 for an unknown id

s = aiStatus(id);
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.

idthe job id from aiCallAsync

returns 1 once the job is done; -1 for an unknown id

aiWait(id);

AI bytecode & codeswap 30 builtins

clonemarker(src, dst)

copy one marker region's code into another

srcthe marker id to copy from
dstthe 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 = codeSize();
deletecode(marker)

delete a marker region's code

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

markerthe marker id

returns 0

deleteCode(1);
delmarker(marker)

drop a marker region

markerthe marker id

returns void

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 aiQuery, so the cache, provider and fallback chain all apply.

taska description of what the code should do
markerthe marker id to fill

returns non-zero on success

generateCode("clamp hp to 0..100", 1);
getcode()

read back a marker region's current source text

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

returns an empty string

s.s = getCode();
getmarker(…)

look up a marker by name

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.

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

returns 0

m = getMarker();
hasmarker(marker)

test whether a marker id names a live region

markerthe marker id

returns non-zero if the marker exists

if (hasMarker(1)) { ... }
insertcode(marker)

insert code at a marker region

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

markerthe marker id

returns 0

insertCode(1);
markercount(…)

count the live marker regions

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.

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

returns 0

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.

addrthe PC slot to read

returns the slot's opcode field, or the out-of-range sentinel -1 beyond the allocated table

peekCode(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.

addrthe PC slot to read

returns the slot's flags field, or the out-of-range sentinel -1 beyond the allocated table

peekFlags(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.

addrthe PC slot to read

returns the slot's function-id field, or the out-of-range sentinel -1 beyond the allocated table

peekFuncid(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.

addrthe PC slot to read

returns the slot's 64-bit `i` operand, or the out-of-range sentinel 0 beyond the allocated table

peekI(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.

addrthe PC slot to read

returns the slot's `j` operand, or the out-of-range sentinel 0 beyond the allocated table

peekJ(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.

addrthe PC slot to read

returns the slot's `n` operand, or the out-of-range sentinel 0 beyond the allocated table

peekN(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeCode(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeFlags(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeFuncid(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeI(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeJ(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.

addrthe PC slot to write
valuethe value to store

returns void

pokeN(0, 7);
replacecode(marker)

swap a marker region's body for the code most recently installed

markerthe marker id

returns non-zero on success

replaceCode(1);
rt_aifunc_count()

count the registered run-time AI functions

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

returns 0

n = rt_aiFuncCount();
rt_callaifunc()

run a run-time AI function

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time. Always addresses id 0.

returns 0

rt_callAiFunc();
rt_createaifunc(name, prompt)

register an AI-authored function under a name at run time

namethe function name
promptthe task description the AI is asked to implement

returns the new function's id

rt_createAiFunc("clamp", "clamp to 0..100");
rt_destroyaifunc(id)

drop a run-time AI function

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

idthe function id

returns void

rt_destroyAiFunc(id);
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(id)

run a run-time AI function by id

NOT IMPLEMENTED on this backend: the call prints a `not implemented` line on stderr and returns the value below rather than doing anything. It is listed here because the compiler accepts it -- an absence you can see beats one you discover at run time.

idthe function id

returns 0

rt_runAiFunc(id);
setcode(src)

install CX source text as the body of a codeswap marker region

srcthe replacement source text

returns void

setCode(src);

rules & events 24 builtins

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.

typeevent type id to raise
sourcebound-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.

entitythe entity's json handle
schedulehow often it is due
triggera condition rule
promptwhat 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`.

entitythe entity's json handle
preloadthe 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.

entitythe entity's json handle
quitkeythe 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.

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

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

entitythe entity's json handle
schedulehow often it is due
triggera condition rule
fna `function f.v(json e)`

returns void

funcAdd(e, 1, "1", &onTick);
lookupf(entity, name, index)

read a float from an entity's baked persist lane

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 float half of the same one-name `lookup` surface -- see lookupI. There are no F/I variants in CX source: the lanes are already typed by `persist float`/`persist int`, and the read coerces to whatever you assign it to.

entitythe entity's json handle
namethe persist lane's name
indexelement index, for an array lane

returns the lane's value as a float

float th = lookup(e, "reorder");
lookupi(entity, name, index)

read an integer from an entity's baked persist lane

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 native read surface onto the persist bank that `arm` baked -- a reader over the EXISTING bank, not a second copy. In CX you write ONE name, `lookup`, and the value COERCES TO THE SINK exactly like a json subscript; this typed entry point is what an integer sink resolves to.

entitythe entity's json handle
namethe persist lane's name
indexelement index, for an array lane

returns the lane's value as an integer

int n = lookup(e, "critical", i);
oneventfunc(type, gate, fn)

bind a CX function 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. A function pointer cannot live in json, so the binding stores the function's NAME and resolves it through the handler registry.

typethe event type code
gatea condition rule
fnthe handler function

returns void

onEventFunc(1, "1", &onKey);
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.

typethe event type code
gatea condition rule; "1" or empty means always
rulethe 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.

entitythe entity's json handle
schedulehow often it is due
triggera condition rule; the rule runs only when this passes
rulethe 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.

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

taskthe 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();
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.

entitythe entity's json handle
rulethe 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.

entitythe entity's json handle
entrya json rule entry { name, body, promoted }

returns the rule's value as a float, on both paths

v = ruleFire(e, entry);
rulefuncreg(name, fn)

register a CX function under a name so a json-loaded table can reach it

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. Needed when the table came from a FILE rather than from onEventFunc calls: the json holds a name, and this is what a name resolves through. Names are case-insensitive, like every CX identifier.

namethe handler name as the table spells it
fnthe function

returns void

ruleFuncReg("onHit", &onHit);
rulepromote(rules, path)

emit a compilable .cxi from a json array of authored rules

CHECKPOINT PROMOTION, and it is **HOST ONLY -- never rule-callable**: a rule that could promote itself would be capability escalation, so this name must never be on a rule whitelist. Writes one `function <name>.f(json e) <body>` per entry plus a registration block. ALL-OR-NOTHING: it refuses to overwrite a file without the generated marker, compile-checks every body first and writes NOTHING if any fails, refuses name collisions, and writes temp-then-rename so a crash cannot leave half a file. On success it marks each entry `promoted:true` in place.

rulesa json array of { "name": <ident>, "body": "{ ... }" } entries
paththe .cxi file to write

returns 1 on full success (and the array is marked in place); 0 on any failure, with nothing written

rulePromote(rules, "rules_promoted.cxi");

eval 4 builtins

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.

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

namevariable name (case-insensitive, up to 31 chars)
valuefloat 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.

namevariable 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();

Views 2 families · 46 builtins

cxGrid 39 builtins

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.

xleft position in pixels
ytop position in pixels
wwidth in pixels
hheight 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.

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

ggrid 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

ggrid handle
xnew left position in pixels
ynew top position in pixels
wnew width in pixels
hnew 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.

ggrid handle
nnumber 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.

ggrid handle
nnumber 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.

ccolumn index (0-based)
labelcolumn header text (string)
widthcolumn width in pixels (min 24)
typecolumn type 0-5 (TEXT/NUM/CHECK/BAR/COLOR/BADGE)
aligntext 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.

ggrid handle

returns void

gpu_grid_clear(insp);
gpu_grid_set(r, c, text)

set the text of cell (r,c)

rrow index (0-based)
ccolumn index (0-based)
textcell 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.

rrow index (0-based)
ccolumn index (0-based)
vnumeric 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)

rrow index (0-based)
ccolumn 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)

rrow index (0-based)
ccolumn 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_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.

rorigin row index
corigin column index
cspannumber of columns to span (>=1)
rspannumber 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

ggrid handle
titletitle 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.

ggrid handle
font_idfont 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.

ccolumn index (0-based)
font_idfont 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.

rrow index (0-based)
ccolumn index (0-based)
font_idfont 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.

rrow index (0-based)
ccolumn index (0-based)
colorpacked 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.

rrow index (0-based)
ccolumn index (0-based)
colorpacked 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.

rrow index (0-based)
colorpacked 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).

keystyle key name (string)
valuecolour or metric value (int)

returns void

gpu_grid_style(g, "rowh", 30);
gpu_grid_theme(g, name)

apply a colour preset to the grid by name

"light" = white with light-grey zebra; anything else = the default dark palette. Only colours change; heights are untouched.

ggrid handle
nametheme name "light" or "dark" (string)

returns void

gpu_grid_theme(g, "light");
gpu_grid_rownum(g, on)

toggle the automatic row-number gutter on the left

Numbers are 1-based by original row index.

ggrid handle
on1 = 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.

rrow index to turn into a section header
labelsection 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

rsection header row index
folded1 = 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.

rsection header row index
levelnesting 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.

ccolumn index (0-based)
on1 = 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.

rrow index (0-based)
ccolumn index (0-based)
on1 = 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).

colcolumn to sort by (<0 = unsorted)
dirdirection: <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).

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

ggrid handle
on1 = 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.

mxmouse x in pixels
mymouse y in pixels
wheelmouse wheel delta
downmouse button held (1/0)
pressedmouse 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.

ggrid handle

returns void

gpu_grid_draw(g);
gpu_grid_tail(g)

snap the view to the last rows (log tail)

ggrid handle

returns void

gpu_grid_tail(dlog);
gpu_grid_sel(g)

return the selected original row index

ggrid handle

returns int selected original row index, -1 = none

sel.i = gpu_grid_sel(g);
gpu_grid_sortcol(g)

return the current sort column index

ggrid handle

returns int current sort column, -1 = unsorted

gpu_grid_sortcol(g);
gpu_grid_hover(g)

return the hovered original row index

ggrid handle

returns int hovered original row index, -1 = none

gpu_grid_hover(g);
gpu_grid_count(g)

return the grid's row count

ggrid handle

returns int number of rows

gpu_draw_text(60, 744, "rows: " + str(gpu_grid_count(g)), colDim, 16);

GUI 7 builtins

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.

xleft edge of the widget in pixels
ytop edge of the widget in pixels
wwidth in pixels
hheight in pixels
textbutton 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

xleft edge of the widget in pixels
ytop edge of the widget in pixels
wwidth in pixels
hheight in pixels
textlabel 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.

xleft edge of the widget in pixels
ytop edge of the widget in pixels
wwidth in pixels
hheight in pixels
textcheckbox label text
statecurrent 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.

xleft edge of the panel in pixels
ytop edge of the panel in pixels
wwidth in pixels
hheight in pixels
textmulti-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.

xleft edge of the field in pixels
ytop edge of the field in pixels
wwidth in pixels
hheight in pixels
keyimmediate-mode identity/slot key that names the persistent buffer
initialtext 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.

xleft edge of the editor in pixels
ytop edge of the editor in pixels
wwidth in pixels
hheight in pixels
keyimmediate-mode identity/slot key that names the persistent buffer
initialtext 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.

pxdesired font size in pixels

returns void

gpu_gui_fontsize(20);

Game 2 families · 50 builtins

game helpers 46 builtins

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.

x1X of the first vector
y1Y of the first vector (ignored)
z1Z of the first vector (ignored)
x2X of the second vector
y2Y of the second vector (ignored)
z2Z 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.

x1X of the first vector (ignored)
y1Y of the first vector
z1Z of the first vector (ignored)
x2X of the second vector (ignored)
y2Y of the second vector
z2Z 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.

x1X of the first vector (ignored)
y1Y of the first vector (ignored)
z1Z of the first vector
x2X of the second vector (ignored)
y2Y of the second vector (ignored)
z2Z 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.

x1X of the first vector
y1Y of the first vector (ignored)
z1Z of the first vector (ignored)
x2X of the second vector
y2Y of the second vector (ignored)
z2Z 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.

x1X of the first vector (ignored)
y1Y of the first vector
z1Z of the first vector (ignored)
x2X of the second vector (ignored)
y2Y of the second vector
z2Z 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.

x1X of the first vector (ignored)
y1Y of the first vector (ignored)
z1Z of the first vector
x2X of the second vector (ignored)
y2Y of the second vector (ignored)
z2Z 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.

xX of the vector
yY of the vector (ignored)
zZ of the vector (ignored)
sscalar 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.

xX of the vector (ignored)
yY of the vector
zZ of the vector (ignored)
sscalar 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.

xX of the vector (ignored)
yY of the vector (ignored)
zZ of the vector
sscalar 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).

xX of the vector
yY of the vector
zZ 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.

x1X of the first point
y1Y of the first point
z1Z of the first point
x2X of the second point
y2Y of the second point
z2Z 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.

x1X of the first vector
y1Y of the first vector
z1Z of the first vector
x2X of the second vector
y2Y of the second vector
z2Z 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.

x1X of the first vector (ignored)
y1Y of the first vector
z1Z of the first vector
x2X of the second vector (ignored)
y2Y of the second vector
z2Z 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.

x1X of the first vector
y1Y of the first vector (ignored)
z1Z of the first vector
x2X of the second vector
y2Y of the second vector (ignored)
z2Z 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.

x1X of the first vector
y1Y of the first vector
z1Z of the first vector (ignored)
x2X of the second vector
y2Y of the second vector
z2Z 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.

xX of the vector
yY of the vector
zZ 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.

xX of the vector
yY of the vector
zZ 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.

xX of the vector
yY of the vector
zZ 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.

x1X of the start point
y1Y of the start point (ignored)
z1Z of the start point (ignored)
x2X of the end point
y2Y of the end point (ignored)
z2Z of the end point (ignored)
tinterpolation 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.

x1X of the start point (ignored)
y1Y of the start point
z1Z of the start point (ignored)
x2X of the end point (ignored)
y2Y of the end point
z2Z of the end point (ignored)
tinterpolation 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.

x1X of the start point (ignored)
y1Y of the start point (ignored)
z1Z of the start point
x2X of the end point (ignored)
y2Y of the end point (ignored)
z2Z of the end point
tinterpolation 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].

x1X of the first vector
y1Y of the first vector
z1Z of the first vector
x2X of the second vector
y2Y of the second vector
z2Z 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.

camcx_gfx camera handle
txtarget X to follow
tytarget Y to follow
tztarget Z to follow
speedlerp 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.

camcx_gfx camera handle
distdesired distance from the camera target
speedlerp 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.

camcx_gfx camera handle (valid 1..7 for shake state)
intensitymaximum jitter magnitude
durationshake 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.

camcx_gfx camera handle (valid 1..7)
minspeedminimum orbit speed (stored)
maxspeedmaximum orbit speed (stored)
mindistminimum orbit distance (clamps camzoomto)
maxdistmaximum 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).

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

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

objgame object handle from goload
keyproperty 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.

objgame object handle from goload
keyproperty 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.

objgame object handle from goload
keyproperty 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.

objgame object handle from goload
keyproperty name to override
vfloat 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.

objgame object handle from goload
keyproperty name to override
vinteger 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.

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

tmrtimer handle from timercreate
deltatime 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.

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

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

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

tmrtimer 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).

x1X of the first point
y1Y of the first point
z1Z of the first point
x2X of the second point
y2Y of the second point
z2Z 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.

x1X of the first point
y1Y of the first point
z1Z of the first point
x2X of the second point
y2Y of the second point
z2Z of the second point
rangemaximum 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).

camcx_gfx camera handle
wxworld X
wyworld Y
wzworld 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).

camcx_gfx camera handle
wxworld X
wyworld Y
wzworld 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.

x1X of the source point
z1Z of the source point
x2X of the target point
z2Z 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.

c1start color (packed 0xAARRGGBB int)
c2end color (packed 0xAARRGGBB int)
tblend 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.

colbase color (packed 0xAARRGGBB int)
flashcolflash color to blend toward (packed 0xAARRGGBB int)
tflash amount, clamped to 0..1 (0 = base, 1 = full flash)

returns int the blended packed 0xAARRGGBB color

c = colorflash(col, 0xFFFFFFFF, 0.5)

assets 4 builtins

assetclose()

close the open asset bundle

returns void

assetClose();
assetload(name)

load one asset by name from the open bundle

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

paththe bundle or directory to open

returns non-zero on success

assetOpen("game.dat");
blobload(path)

load a file from disk as a blob

paththe file to load

returns a blob handle; 0 on failure

h = blobLoad("data.bin");

Graphics 6 families · 188 builtins

3D graphics 69 builtins

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.

pxcamera (eye) position x
pycamera position y
pzcamera position z
txlook-at target x
tylook-at target y
tzlook-at target z
uxup vector x
uyup vector y
uzup vector z
fovvertical 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.

camcamera handle from gpu_camera_new

returns void

gpu_camera_begin(cam);
gpu_camera_update(cam, mode)

update a camera with a raylib movement mode

Calls raylib UpdateCamera; mode selects the built-in controller.

camcamera handle
mode0=free, 1=orbital, 2=first-person, 3=third-person

returns void

gpu_camera_update(cam, 1);
gpu_camera_setpos(cam, x, y, z)

set a camera's position vector

camcamera handle
xnew position x
ynew position y
znew 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

camcamera handle
xtarget x
ytarget y
ztarget z

returns void

gpu_camera_settarget(cam, 0.0,0.0,0.0);
gpu_camera_posx(cam)

read a camera's position x

camcamera handle

returns camera position.x as float; 0.0 if the handle is invalid

float px = gpu_camera_posx(cam);
gpu_camera_posy(cam)

read a camera's position y

camcamera handle

returns camera position.y as float; 0.0 if the handle is invalid

float py = gpu_camera_posy(cam);
gpu_camera_posz(cam)

read a camera's position z

camcamera handle

returns camera position.z as float; 0.0 if the handle is invalid

float pz = gpu_camera_posz(cam);
gpu_draw_cube(x, y, z, w, h, d, col)

draw a filled 3D cube centered at (x,y,z) (raylib DrawCube)

xcenter x
ycenter y
zcenter z
wwidth (x extent)
hheight (y extent)
ddepth (z extent)
colcolor as 0xRRGGBB / 0xAARRGGBB int

returns void

gpu_draw_cube(0.0,0.0,0.0, 2.0,2.0,2.0, 255);
gpu_draw_cube_wires(x, y, z, w, h, d, col)

draw a wireframe 3D cube centered at (x,y,z) (raylib DrawCubeWires)

xcenter x
ycenter y
zcenter z
wwidth (x extent)
hheight (y extent)
ddepth (z extent)
colline color int

returns void

gpu_draw_cube_wires(0.0,0.0,0.0, 2.0,2.0,2.0, 255);
gpu_draw_sphere(x, y, z, radius, col)

draw a filled 3D sphere at (x,y,z) (raylib DrawSphere)

xcenter x
ycenter y
zcenter z
radiussphere radius
colcolor int

returns void

gpu_draw_sphere(3.0,0.0,0.0, 1.0, 255);
gpu_draw_sphere_wires(x, y, z, radius, col)

draw a wireframe 3D sphere at (x,y,z) (raylib DrawSphereWires)

Uses a fixed 12 rings x 12 slices tessellation.

xcenter x
ycenter y
zcenter z
radiussphere radius
colline color int

returns void

gpu_draw_sphere_wires(3.0,0.0,0.0, 1.0, 255);
gpu_draw_cylinder(x, y, z, rtop, rbot, height, slices, col)

draw a filled 3D cylinder/cone at (x,y,z) (raylib DrawCylinder)

Separate top and bottom radii allow cones (one radius 0) and tapered shapes.

xbase center x
ybase center y
zbase center z
rtoptop radius
rbotbottom radius
heightcylinder height
slicesnumber of radial slices
colcolor int

returns void

gpu_draw_cylinder(0.0,0.0,3.0, 1.0,1.0,2.0, 16, 255);
gpu_draw_cylinder_wires(x, y, z, rtop, rbot, height, slices, col)

draw a wireframe 3D cylinder/cone at (x,y,z) (raylib DrawCylinderWires)

xbase center x
ybase center y
zbase center z
rtoptop radius
rbotbottom radius
heightcylinder height
slicesnumber of radial slices
colline color int

returns void

gpu_draw_cylinder_wires(0.0,0.0,3.0, 1.0,1.0,2.0, 16, 255);
gpu_draw_plane(x, y, z, w, d, col)

draw a flat XZ plane centered at (x,y,z) (raylib DrawPlane)

xcenter x
ycenter y
zcenter z
wsize along x
dsize along z
colcolor int

returns void

gpu_draw_plane(0.0,-1.0,0.0, 10.0,10.0, 128);
gpu_draw_grid(slices, spacing)

draw a reference grid on the XZ plane centered at the origin (raylib DrawGrid)

slicesnumber of grid squares in each direction
spacingdistance 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)

x1start x
y1start y
z1start z
x2end x
y2end y
z2end z
colline 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)

xpoint x
ypoint y
zpoint z
colcolor int

returns void

gpu_draw_point3d(1.0,1.0,1.0, 255);
gpu_draw_triangle3d(x1, y1, z1, x2, y2, z2, x3, y3, z3, col)

draw a filled 3D triangle from three vertices (raylib DrawTriangle3D)

x1vertex 1 x
y1vertex 1 y
z1vertex 1 z
x2vertex 2 x
y2vertex 2 y
z2vertex 2 z
x3vertex 3 x
y3vertex 3 y
z3vertex 3 z
colcolor int

returns void

gpu_draw_triangle3d(0.0,0.0,0.0, 1.0,0.0,0.0, 0.0,1.0,0.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.

camcamera handle the billboard faces
teximage/texture handle (from loadimage)
xworld position x
yworld position y
zworld position z
sizebillboard size in world units
coltint 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.

camcamera handle (gpu_camera_new)
teximage/texture handle (gpu_image_load)
sx,sy,sw,shsource 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,zworld position the quad is centred on
ux,uy,uzthe quad's height (up) axis; normalized here (zero -> world up)
w,hquad size in world units (w across, h along `up`)
rotrotation in degrees about the view axis
coltint packed 0xRRGGBB
alphatint 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).

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

pathmodel 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_cube(w, h, d)

generate a cube mesh and return it as a model handle (raylib GenMeshCube)

Wraps the generated mesh in a model via LoadModelFromMesh.

wwidth (x)
hheight (y)
ddepth (z)

returns 1-based model handle; 0 if the window is not open

int cube = gpu_mesh_cube(2.0,2.0,2.0);
gpu_mesh_sphere(radius, rings, slices)

generate a sphere mesh and return it as a model handle (raylib GenMeshSphere)

radiussphere radius
ringsnumber of horizontal rings
slicesnumber 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_mesh_plane(w, d, resx, resz)

generate a subdivided plane mesh and return it as a model handle (raylib GenMeshPlane)

wsize along x
dsize along z
resxsubdivisions along x
reszsubdivisions along z

returns 1-based model handle; 0 if the window is not open

int pln = gpu_mesh_plane(10.0,10.0, 4,4);
gpu_mesh_cylinder(radius, height, slices)

generate a cylinder mesh and return it as a model handle (raylib GenMeshCylinder)

radiuscylinder radius
heightcylinder height
slicesnumber of radial slices

returns 1-based model handle; 0 if the window is not open

int cyl = gpu_mesh_cylinder(1.0, 3.0, 16);
gpu_model_draw(mdl, x, y, z, scale, col)

draw a model at (x,y,z) with uniform scale (raylib DrawModel)

mdlmodel handle
xposition x
yposition y
zposition z
scaleuniform scale factor
coltint color int

returns void

gpu_model_draw(cube, 0.0,0.0,0.0, 1.0, 255);
gpu_model_draw_wires(mdl, x, y, z, scale, col)

draw a model as wireframe at (x,y,z) with uniform scale (raylib DrawModelWires)

mdlmodel handle
xposition x
yposition y
zposition z
scaleuniform scale factor
coltint color int

returns void

gpu_model_draw_wires(sph, 0.0,0.0,0.0, 1.0, 255);
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)

mdlmodel handle
xposition x
yposition y
zposition z
axrotation axis x
ayrotation axis y
azrotation axis z
anglerotation angle in degrees
sxscale x
syscale y
szscale z
coltint 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_free(mdl)

destroy a model handle immediately (raylib UnloadModel)

Unconditional teardown: drops the ARC bucket then unloads the model and frees the slot. Standalone counterpart to the codegen-emitted decref.

mdlmodel handle to free

returns void

gpu_model_free(m);
gpu_model_decref(mdl)

decrement an owned model's ARC refcount, reclaiming it at zero

Codegen emits this at scope exit for an owned `model` local; the shared CXB_RESOURCE tracer unloads the model when the refcount hits 0. Bounds-checked no-op on an out-of-range handle.

mdlmodel handle

returns void

gpu_model_decref(m);
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.

mdlmodel handle
teximage/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

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. 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).

level0 disables; any positive value requests MSAA. Negatives clamp to 0

returns void

gpuMsaa(4);   // before the window opens
gpu_texture_filter(mode)

set the default sampling filter for model textures

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. 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.

mode0 point, 1 bilinear, 2 trilinear, 3 anisotropic

returns void

gpuTextureFilter(1);
gpu_model_smooth(model, mode)

set the sampling filter for ONE model's textures, now

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. Applies a filter mode to that model's diffuse textures immediately, overriding the global default gpuTextureFilter set. It is TEXTURE FILTERING, not shading -- it does not change how normals are interpolated.

modelthe model
mode0 point, 1 bilinear, 2 trilinear, 3 anisotropic

returns void

gpuModelSmooth(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.

vspathvertex shader file path ("" = default vertex shader)
fspathfragment 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_loadmem(vsblob, fsblob)

load a shader from in-memory source blobs (raylib LoadShaderFromMemory)

Takes vertex and fragment shader source from embed()/blob handles rather than files.

vsblobblob handle holding vertex shader source
fsblobblob handle holding fragment shader source

returns 1-based shader handle; 0 if the window is not open

int sh = gpu_shader_loadmem(vs, fs);
gpu_shader_begin(sh)

begin rendering with a shader (raylib BeginShaderMode)

Subsequent draws use this shader until gpu_shader_end.

shshader handle

returns void

gpu_shader_begin(sh);
gpu_shader_free(sh)

destroy a shader handle immediately (raylib UnloadShader)

Unconditional teardown: drops the ARC bucket then unloads the shader and frees the slot.

shshader handle to free

returns void

gpu_shader_free(sh);
gpu_shader_decref(sh)

decrement an owned shader's ARC refcount, reclaiming it at zero

Codegen emits this at scope exit for an owned `shader` local; the shared tracer unloads the shader at refcount 0. Bounds-checked no-op on an out-of-range handle.

shshader handle

returns void

gpu_shader_decref(sh);
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_set(sh, name, value, uniformtype)

set an integer uniform value on a shader (raylib SetShaderValue)

Looks up the uniform by name; a missing uniform (loc < 0) is a silent no-op. The uniform-type code maps to raylib's SHADER_UNIFORM_* enum.

shshader handle
nameuniform name
valueinteger value to set
uniformtypetype code: 0=float 1=vec2 2=vec3 3=vec4 4=int 5=sampler2d

returns void

gpu_shader_set(sh, "mode", 1, 4);
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.

shshader handle
nameuniform name
fvaluefloat value to set
uniformtypetype 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.

shshader handle
nameuniform name
xvec3 component x
yvec3 component y
zvec3 component z
uniformtypetype 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.

shshader handle
rambient red 0..255
gambient green 0..255
bambient blue 0..255
aambient alpha 0..255

returns void

gpu_light_ambient(sh, 10,10,25, 255);
gpu_light_point(sh, x, y, z, r, g, b, a)

add a point light to a shader and return its index (rlights CreateLight)

Creates a LIGHT_POINT at (x,y,z) with the given RGBA color, registered against the shader.

shshader handle
xlight position x
ylight position y
zlight position z
rcolor red 0..255
gcolor green 0..255
bcolor blue 0..255
acolor 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 pl = gpu_light_point(sh, 5.0,5.0,5.0, 255,0,0,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.

shshader handle
dxlight direction x
dylight direction y
dzlight direction z
rcolor red 0..255
gcolor green 0..255
bcolor blue 0..255
acolor 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_light_pos(sh, lt, x, y, z)

move a light and push the update to its shader (rlights UpdateLightValues)

Sets light lt's position to (x,y,z) and re-uploads its values to the shader. No-op if the shader or light index is invalid.

shshader handle
ltlight index (from gpu_light_point / gpu_light_directional)
xnew light position x
ynew light position y
znew light position z

returns void

gpu_light_pos(sh, pl, 6.0,6.0,6.0);
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.

mdlmodel handle
shshader handle

returns void

gpu_model_shader(cube, sh);
gpu_collide_spheres(x1, y1, z1, r1, x2, y2, z2, r2)

test whether two spheres intersect (raylib CheckCollisionSpheres)

x1sphere 1 center x
y1sphere 1 center y
z1sphere 1 center z
r1sphere 1 radius
x2sphere 2 center x
y2sphere 2 center y
z2sphere 2 center z
r2sphere 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.

min1xbox 1 min x
min1ybox 1 min y
min1zbox 1 min z
max1xbox 1 max x
max1ybox 1 max y
max1zbox 1 max z
min2xbox 2 min x
min2ybox 2 min y
min2zbox 2 min z
max2xbox 2 max x
max2ybox 2 max y
max2zbox 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.

minxbox min x
minybox min y
minzbox min z
maxxbox max x
maxybox max y
maxzbox max z
cx_sphere center x
cy_sphere center y
cz_sphere center z
radiussphere 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)
gpu_model_minx(mdl)

read a model's bounding-box minimum x (raylib GetModelBoundingBox)

mdlmodel 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)

mdlmodel 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)

mdlmodel 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)

mdlmodel 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)

mdlmodel 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)

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

camcamera handle

returns 1-based ray handle; 0 if the window is closed or the camera is invalid

int ray = gpu_ray_mouse(cam);
gpu_ray_x(ray)

read a ray's direction x component

rayray handle

returns ray direction.x as float; 0.0 if the handle is invalid

float rx = gpu_ray_x(ray);
gpu_ray_y(ray)

read a ray's direction y component

rayray handle

returns ray direction.y as float; 0.0 if the handle is invalid

float ry = gpu_ray_y(ray);
gpu_ray_z(ray)

read a ray's direction z component

rayray handle

returns ray direction.z as float; 0.0 if the handle is invalid

float rz = gpu_ray_z(ray);
gpu_collide_ray_sphere(ray, x, y, z, radius)

test whether a ray hits a sphere (raylib GetRayCollisionSphere)

rayray handle (from gpu_ray_mouse)
xsphere center x
ysphere center y
zsphere center z
radiussphere 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.

rayray handle (from gpu_ray_mouse)
minxbox min x
minybox min y
minzbox min z
maxxbox max x
maxybox max y
maxzbox 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

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. Pairs with the camera-begin call; drawing between them is in world space.

returns void

gpuCameraEnd();
gpu_frame_time()

how long the last frame took, in seconds

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. The delta to scale movement by so speed is frame-rate independent.

returns the frame time in seconds

x = x + speed * gpuFrameTime();
gpu_shader_end()

stop drawing through the active shader

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns void

gpuShaderEnd();

window & input 46 builtins

gpu_screen_width()

get the current viewport width in pixels

raylib GetScreenWidth; updates live as the user resizes the window.

returns viewport width, or 0 if no window

cw.i = gpu_screen_width();
gpu_screen_height()

get the current viewport height in pixels

raylib GetScreenHeight; updates live as the user resizes the window.

returns viewport height, or 0 if no window

ch.i = gpu_screen_height();
gpu_window_resized()

return 1 on the first frame after the user resizes the window

Wraps raylib IsWindowResized (auto-clears after the call). Pair with a re-layout so percent-based UI reflows. Always 0 if the window is not resizable.

returns 1 the frame after a resize, else 0

if (gpu_window_resized()) { relayout(); }
gpu_window_fullscreen()

toggle borderless-windowed fullscreen

raylib ToggleBorderlessWindowed; restores the prior windowed size on toggle-off, so one call covers both directions. Re-read screen width/height afterwards.

returns void

function evFullscreen.v(json e) { gpu_window_fullscreen(); }
gpu_window_isfullscreen()

return 1 while borderless-windowed fullscreen is active

Checks raylib FLAG_BORDERLESS_WINDOWED_MODE.

returns 1 if fullscreen, else 0

int f = gpu_window_isfullscreen();
gpu_window_pos(x, y)

move the window's top-left to desktop coordinates (x,y)

raylib SetWindowPosition; lets a docked panel window position itself.

xdesktop x for the window's top-left
ydesktop y for the window's top-left

returns void

gpu_window_pos(0, 40);
gpu_window_size(width, height)

resize the window to width x height, or pass (0,0) to maximize

raylib SetWindowSize; (0,0) maximizes to the monitor work area. Restores from a maximized state first. Requires a resizable window.

widthnew width (0 with height 0 -> maximize)
heightnew height

returns void

gpu_window_size(scrW, gh);
gpu_window_maximize()

maximize the window to fill the monitor work area

Convenience for gpu_window_size(0,0) (raylib MaximizeWindow).

returns void

gpu_window_maximize();
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_window_x()

get the window's top-left x on the desktop

raylib GetWindowPosition().x; useful for docking a second window beside this one.

returns window x position, or 0 if no window

int wx = gpu_window_x();
gpu_window_y()

get the window's top-left y on the desktop

raylib GetWindowPosition().y.

returns window y position, or 0 if no window

int wy = gpu_window_y();
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.

btnbutton index 0=left 1=right 2=middle

returns 1 while held, else 0

down.i = gpu_mouse_button(0);
gpu_key_down(key)

return 1 while a key (raylib keycode) is held down

raylib IsKeyDown.

keyraylib 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_mouse_region(name, x, y, width, height)

register/update a named rectangular hot region and test if the mouse is inside it

(Re)registers the rect each call and returns 1 if the cursor currently sits inside AND the region is not paused. Polling pattern; the table holds up to 64 named regions.

nameregion name key
xrect top-left x
yrect top-left y
widthrect width
heightrect height

returns 1 if the mouse is inside the (unpaused) region, else 0

if (gpu_mouse_region("btn", 10, 10, 80, 30)) { onHover(); }
gpu_mouse_unregion(name)

remove a named mouse region

Frees the region's slot in the table.

nameregion name to remove

returns 1 on removal, 0 if the name was not found

gpu_mouse_unregion("btn");
gpu_mouse_region_at(mx, my)

return the name of the first unpaused region containing (mx,my)

Walks the region table and returns the first matching region's name, or an empty string if none contain the point. Lets a dispatch loop find the hit region without re-registering each name.

mxquery x (e.g. current mouse x)
myquery y (e.g. current mouse y)

returns the matching region name, or an empty string if none

string r = gpu_mouse_region_at(mousex(), mousey());
gpu_mouse_pause(name)

pause a named mouse region so it stops reporting hits

A paused region returns 0 from gpu_mouse_region and is skipped by gpu_mouse_region_at.

nameregion name to pause

returns 1 if the region was found, else 0

gpu_mouse_pause("btn");
gpu_mouse_resume(name)

resume a paused mouse region

Re-enables hit reporting for the named region.

nameregion name to resume

returns 1 if the region was found, else 0

gpu_mouse_resume("btn");
drawimageregionrot(handle, cx, cy, w, h, sx, sy, sw, sh, deg)

draw one atlas cell centred, scaled and rotated

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. The source rectangle picks the cell out of a sprite atlas; (cx, cy) is the drawn CENTRE.

handlethe atlas image
cxcentre x
cycentre y
wdrawn width
hdrawn height
sxsource x in the atlas
sysource y in the atlas
swsource width
shsource height
degclockwise 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

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. (cx, cy) is the CENTRE, and the rotation pivots about that centre. Degrees, clockwise.

handlethe image
cxcentre x
cycentre y
wdrawn width
hdrawn height
degclockwise rotation in degrees

returns void

drawImageRot(spr, 400, 300, 64, 64, 45);
eventpollkeys()

take a snapshot of the keyboard for this frame

STUBBED: answers 0 (no key event) on both backends. Use gpuKeyPressed instead.

returns 0, always

k = gpuKeyPressed();
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();
fontfree(handle)

release a loaded font

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.

handleignored

returns void

fontFree(f);
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.

namefont file name
sizepoint 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.

handleignored

returns void

fontSet(f);
gpu_blend_add()

switch to additive blending for subsequent draws

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. What makes glows and particle flares brighten what is under them.

returns void

gpuBlendAdd();
gpu_blend_normal()

switch back to normal alpha blending

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns void

gpuBlendNormal();
gpu_draw_text(x, y, text, colour, size)

draw a string at a screen position

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. Two forms: four arguments use the default 20-pixel size, five pick the size.

xleft edge in pixels
ytop edge in pixels
textthe string to draw
colourpacked 0xRRGGBB colour
sizepixel height (optional; default 20)

returns void

gpuDrawText(10, 10, "score", 0xFFFFFF, 32);
gpu_frame_end()

finish the frame and present it

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. This is what makes drawing appear; it also swaps the buffers, which is why the flip calls are no-ops.

returns void

gpuFrameEnd();
gpu_frame_poll()

test whether the graphics window is still open

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. The canonical main-loop guard -- `while (gpuFramePoll()) { ... }` -- 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 (gpuFramePoll()) { gpuFrameBegin(); ... gpuFrameEnd(); }
gpu_frame_pollkeys()

take a snapshot of the keyboard for this frame

STUBBED: answers 0 (no key event) on both backends. Listed because the compiler accepts it -- use gpuKeyPressed for a key this frame.

returns 0, always

k = gpuKeyPressed();
gpu_frame_quit()

request that the frame loop stop

ACCEPTED AND DOES NOTHING on both backends. The loop guard is gpuFramePoll, 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 (gpuFramePoll()) { ... }
gpu_image_draw_tint(handle, cx, cy, w, h, colour, alpha)

draw an image centred, scaled, and multiplied by a colour

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. 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.

handlethe image
cxcentre x
cycentre y
wdrawn width
hdrawn height
colourpacked 0xRRGGBB multiplier
alpha0..255 opacity

returns void

gpuImageDrawTint(spr, 400, 300, 64, 64, 0xFF8080, 200);
gpu_key_pressed()

the key pressed this frame

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. Reports one key per frame; 0 when nothing was pressed.

returns the key code, or 0

k = gpuKeyPressed();
gpu_mouse_pause_all()

stop every registered mouse region from responding

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. The way to make a modal dialog swallow clicks without unregistering the regions behind it.

returns void

gpuMousePauseAll();
gpu_mouse_resume_all()

let every registered mouse region respond again

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns void

gpuMouseResumeAll();
gpu_mouse_wheel()

how far the wheel moved since the last frame

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns the wheel delta; 0 if it did not move

dz = gpuMouseWheel();
gpu_mouse_x()

the mouse pointer's x position in the window

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns x in pixels

mx = gpuMouseX();
gpu_mouse_y()

the mouse pointer's y position in the window

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns y in pixels

my = gpuMouseY();
gpu_screen_close()

close the graphics window

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

returns void

gpuScreenClose();
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 gpuFrameEnd.

returns void

gpuFrameEnd();
gpu_screen_openex(x, y, m, w, h)

open the graphics window, ignoring the placement hints

Only `w` and `h` are used: raylib places the window itself, so the x, y and mode arguments are accepted and DISCARDED. The title is fixed to "CX". gpu_screen_open is the plain form and the one to prefer.

xwindow x hint -- ignored
ywindow y hint -- ignored
mmode hint -- ignored
wwindow width in pixels
hwindow height in pixels

returns 1 always -- it does not report failure

gpu_screen_openex(0, 0, 0, 1280, 720);
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.

wnew width in pixels
hnew height in pixels

returns void

gpu_screen_resize(1920, 1080);
gpu_text_width(font, text, size)

measure how wide a string will be drawn

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. Two forms: one argument measures with the default font at size 20; three take a registered font id, the text and an explicit size.

fontregistered font id (3-argument form)
textthe string to measure
sizepixel height (3-argument form)

returns the width in pixels

w = gpuTextWidth("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 gpuScreenFlip: the frame end swaps buffers.

returns void

gpuFrameEnd();
textwidth(text)

measure how wide a string will be drawn

The same measurement as gpuTextWidth 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.

textthe string to measure

returns the width in pixels

w = textWidth("score");

images & textures 42 builtins

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.

xtext top-left x
ytext top-left y
font_idfont handle from gpu_font_load (0/invalid -> default font)
textstring to draw
colortext color 0xRRGGBB
sizepixel size (<=0 -> font base size)

returns void

gpu_draw_text_font(px, py, fntB, "MOUNT EDITOR", colHi, sc(30));
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.

pathfont file path
sizebase 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_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.

blobembed() blob handle holding the font bytes
extformat hint, e.g. ".ttf"
sizebase 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_free(handle)

destroy a font handle now, unloading its GPU glyph atlas

Unconditional free (raylib UnloadFont); drops the resource bucket then reclaims the slot. Standalone counterpart to the ARC decref.

handlefont handle to free

returns void

gpu_font_free(fnt);
gpu_font_decref(handle)

decrement a font handle's ARC refcount for scope-exit reclaim

Codegen emits this at scope exit for an owned `font` local; the shared tracer unloads the font when the refcount reaches 0. Rarely called by hand.

handlefont handle to decref

returns void

gpu_font_decref(fnt);
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).

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

pathimage file to load (PNG/JPG/etc.)
class_nametreatment 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.

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

blobembed() blob handle holding the image bytes
extformat 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.

handletexture handle to free

returns void

if (img > 0) { gpu_image_free(img); }
gpu_image_decref(handle)

decrement a texture handle's ARC refcount for scope-exit reclaim

Wraps cx_gfx_texture_decref; codegen emits it at scope exit for an owned `texture` local, and the shared tracer unloads the texture at refcount 0.

handletexture handle to decref

returns void

gpu_image_decref(img);
gpu_image_width(handle)

get a texture's width in pixels

handletexture 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

handletexture 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).

ndesired slot capacity (clamped 16-8192)

returns void

gpu_image_cap(384);
gpu_image_livecount()

count currently-open GPU texture slots

Wraps cx_gfx_texture_live_count; live-slot introspection for leak/churn tests over the `texture` type.

returns number of live texture slots

int n = gpu_image_livecount();
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.

widthtarget width in pixels
heighttarget 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.

handlerender-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).

colorclear color 0xRRGGBB
alphaclear 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.

handletexture handle
xdestination top-left x
ydestination top-left y
widthdestination width
heightdestination height

returns void

gpu_image_draw(img0, sx0 - dw/2, sy0 - dh/2, dw, dh);
gpu_image_draw_alpha(handle, x, y, alpha)

draw a texture unscaled at (x,y) with an alpha fade

raylib DrawTexture with a white tint at `alpha` (0-255); draws at the texture's native size.

handletexture handle
xtop-left x
ytop-left y
alphaopacity 0-255

returns void

gpu_image_draw_alpha(img, 220, 100, 128);
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.

handletexture handle
dxdestination x
dydestination y
dwdestination width
dhdestination height
sxsource cell x
sysource cell y
swsource cell width
shsource 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.

handleimage handle from gpu_image_load
sx,sy,sw,shSOURCE sub-rectangle in texture pixels (atlas cell / frame); sw<=0 or sh<=0 => the whole texture
dx,dy,dw,dhDEST rectangle in screen pixels (position + scale)
ox,oyrotation pivot, in DEST pixels from the dest top-left (rotate-about-centre: dw/2, dh/2)
rotation_degrotation, clockwise degrees, about (ox,oy)
coltint colour, packed 0xRRGGBB (multiplies the texture)
alphaopacity 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.

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

wimage width in pixels (must be > 0)
himage height in pixels (must be > 0)
colorfill 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.

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

hhandle of the image to crop in place
xleft edge of the crop rectangle
ytop edge of the crop rectangle
wcrop rectangle width
htcrop 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.

srchandle of the source image
xleft edge of the region to copy
ytop edge of the region to copy
wregion width
htregion 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).

hhandle of the image to resize in place
wnew width in pixels; must be > 0 or the call is refused
htnew 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.

dsthandle of the destination image (drawn into)
srchandle of the source image
sxsource region left edge
sysource region top edge
swsource region width
shsource region height
dxdestination x (top-left of the pasted region)
dydestination 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.

hhandle of the image to key
colorcolour 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.

hhandle of the image to fade
aalpha 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.

hhandle of the image to modify
xregion left edge
yregion top edge
wregion width
htregion height
aalpha 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.

hhandle of the image to export
pathoutput 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.

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

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

hhandle of the owned image local being released

returns void

imageDecref(h);
imagew(h)

returns a CPU image's width in pixels

hhandle 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

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

hhandle of the image to sample
xpixel x coordinate (0-based)
ypixel 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();

2D drawing & effects 18 builtins

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.

wwindow width in pixels (<= 0 -> 800)
hwindow height in pixels (<= 0 -> 600)
titlewindow title text
gpu_screen_open(1280, 720, "My Game")
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_colorbackground 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_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.

colorfill color as 0xRRGGBB

returns void

gpu_screen_clear(0);
gpu_draw_rect(x, y, width, height, color)

draw a filled rectangle at (x,y) sized width x height

raylib DrawRectangle.

xtop-left x
ytop-left y
widthrectangle width
heightrectangle height
colorfill color 0xRRGGBB

returns void

gpu_draw_rect(10, 10, 100, 100, rgb(50,50,50));
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).

xtop-left x
ytop-left y
widthrectangle width
heightrectangle height
colorfill color 0xRRGGBB
alphaopacity 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.

x1start x
y1start y
x2end x
y2end y
colorline 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.

xcentre x
ycentre y
radiuscircle radius
colorfill 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.

xcentre x
ycentre y
radiuscircle radius
colorfill color 0xRRGGBB
alphaopacity 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.

xpixel x
ypixel y
colorpixel 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).

x1first vertex x
y1first vertex y
x2second vertex x
y2second vertex y
x3third vertex x
y3third vertex y
colorfill 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). Native-only (no register-VM path).

xleft edge, pixels
ytop edge, pixels
wwidth, pixels
hheight, pixels
roundnesscorner 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.
col0xRRGGBB 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. Native-only.

x,y,w,hrectangle, pixels
roundness0..1 fraction of the short side (raylib semantics, verbatim)
col0xRRGGBB colour
alpha0..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. Native-only.

x,y,w,hrectangle, pixels
roundness0..1 fraction of the short side (raylib semantics, verbatim)
thickoutline thickness, pixels
col0xRRGGBB 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. Native-only.

x,y,w,hrectangle, pixels
roundness0..1 fraction of the short side (raylib semantics, verbatim)
thickoutline thickness, pixels
col0xRRGGBB colour
alpha0..255 opacity

returns void

gpu_draw_roundrect_lines_alpha(x, y, 200, 80, 0.3, 2.0, 0x8fb2ff, 180)
gpu_fx_wormhole(x, y, radius, progress_pct, color, rings)

draw concentric fading rings that open up as progress rises (wormhole effect)

Legacy immediate-mode effect built from raylib DrawCircleLines. progress_pct (0-100, clamped) controls the inner radius so the wormhole opens; rings<=0 defaults to 3. New work should prefer the cx_pfx particle system.

xcentre x
ycentre y
radiusouter radius
progress_pctopening progress 0-100
colorring color 0xRRGGBB
ringsnumber of rings (<=0 -> 3)

returns void

gpu_fx_wormhole(400, 300, 120, 50, rgb(80, 160, 255), 5);
gpu_fx_plasma(x, y, width, height, intensity, color, rings)

draw layered translucent rectangles as a soft energy-field (plasma) effect

Legacy immediate-mode effect: nested rectangles centred on the given rect, fading outward. intensity (0-100, clamped) dials brightness; rings<=0 defaults to 3.

xrect top-left x
yrect top-left y
widthrect width
heightrect height
intensitybrightness 0-100
colorfield color 0xRRGGBB
ringsnumber of layers (<=0 -> 3)

returns void

gpu_fx_plasma(100, 100, 200, 150, 80, rgb(255, 80, 200), 4);
gpu_fx_thruster(img, x, y, angle_deg, size, speed, tint_color, cone_deg, particle_color, density)

draw a rotated flame sprite plus a cone of random particles (thruster effect)

Legacy immediate-mode effect: if `img` is a valid texture it is blitted rotated at (x,y) as the flame core, then `density` random particles are scattered in a cone behind it. Uses a shared non-deterministic xorshift RNG. Prefer cx_pfx for new work.

imgtexture handle for the flame core (0 = none)
xemitter x
yemitter y
angle_degthrust direction in degrees (float)
sizeflame sprite size in pixels
speedmax particle travel distance
tint_colorflame sprite tint 0xRRGGBB
cone_degparticle spread half-angle in degrees
particle_colorparticle color 0xRRGGBB
densityparticle count (<=0 -> 8)

returns void

gpu_fx_thruster(0, 400, 300, 90.0, 32, 40, rgb(255,180,60), 30, rgb(255,120,40), 12);
gpu_fx_glitter(x, y, width, height, color_a, color_b, density, lifetime_ms)

sprinkle static two-color sparkle dots inside a rectangle

Legacy immediate-mode effect alternating color_a/color_b for each dot. density<=0 defaults to 32. lifetime_ms is currently ignored (this is an immediate draw, not a lifetimed particle spawn).

xrect top-left x
yrect top-left y
widthrect width
heightrect height
color_afirst sparkle color 0xRRGGBB
color_bsecond sparkle color 0xRRGGBB
densitynumber of dots (<=0 -> 32)
lifetime_mscurrently unused

returns void

gpu_fx_glitter(50, 50, 200, 120, rgb(255,255,180), rgb(255,200,80), 40, 500);

particles 11 builtins

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.

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

nameeffect/template name to spawn
xspawn x position in pixels
yspawn y position in pixels
angle_degemission direction in degrees
parent_vxemitter x-velocity, added to each particle (scaled by velocityScale)
parent_vyemitter 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.

nameeffect template name (from the loaded effects.json)
xworld X of the emitter
yworld Y of the emitter
zworld Z of the emitter
dxemission direction X (0,0,0 = omnidirectional)
dyemission direction Y
dzemission direction Z
pvxemitter velocity X to inherit (same units as particle motion; 0 = none)
pvyemitter velocity Y to inherit
pvzemitter 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_msmilliseconds 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.

camthe 3D camera handle (from gpu_camera_new)
oxcurrent render-origin X in the particles' space (0 if none)
oycurrent render-origin Y
ozcurrent render-origin Z
scaleparticle-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.

nametemplate name to modify
keyfield name to set
valnew 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.

nametemplate name to query
keyfield 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.

izero-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();

color 2 builtins

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.

rred channel (0-255; masked to 8 bits)
ggreen channel (0-255; masked to 8 bits)
bblue 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.

rred channel (0-255; masked to 8 bits)
ggreen channel (0-255; masked to 8 bits)
bblue channel (0-255; masked to 8 bits)
aalpha channel (0-255; masked to 8 bits)

returns packed 32-bit color integer in 0xAARRGGBB layout

c4.i = rgba(255, 0, 0, 128);

Text processing 2 families · 64 builtins

strings 60 builtins

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.

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

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

sstring 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).

sstring to parse

returns the parsed float (via strtod); 0.0 if the string is empty

assertFloatEqual(3.14, strfloat("3.14"), 0.01);
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 (mid, findstring, 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.

sstring to measure

returns its byte length; 0 for an empty string

n.i = length(name);
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.

ssource string
nhow 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 "".

ssource string
nhow 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);
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".

sstring 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

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

sstring to trim

returns a new string with trailing whitespace removed; leading whitespace stays

line.s = rtrim(raw);
lcase(s)

lowercase a whole string

ASCII only, per byte, via C's tolower() -- bytes outside A-Z pass through unchanged, so accented and non-Latin UTF-8 text is left exactly as it was rather than mangled. `tolower(s)` is the same operation under a C-flavoured name.

sstring to convert

returns a new lowercased string

key.s = lcase(header);
ucase(s)

uppercase a whole string

ASCII only, per byte, via C's toupper(); other bytes pass through unchanged. `toupper(s)` is the same operation under a C-flavoured name.

sstring to convert

returns a new uppercased string

shout.s = ucase(word);
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.

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

sstring 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);
valf(s)

read a float out of the front of a string

Parses as much as looks like a number and stops -- `valf("12.5xyz")` is 12.5. There is NO error channel: text that does not start with a number gives 0.0, which is indistinguishable from the string "0". Where the difference matters, check the text yourself before converting.

sstring to parse

returns the leading number as a float, or 0.0 if there is none

f.f = valf(field);
vali(s)

read an integer out of the front of a string

BASE 10 ONLY, and the same silent-0 contract as valf(): `vali("12abc")` is 12, `vali("abc")` is 0, and `vali("0x1F")` is 0 -- the "0" parses and the "x" stops it. A leading sign and leading whitespace are accepted. `parsehex()` is the one that reads hex.

sstring to parse

returns the leading base-10 integer, or 0 if there is none

n.i = vali(argline);
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.

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

ninteger to render

returns the binary digits; "0" for zero

println(bin(mask));
space(n)

a string of n spaces

nhow many spaces

returns the string, or "" if n <= 0

println(space(indent) + label);
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.

sstring to reverse

returns a new string with the bytes in the opposite order

r.s = reversestring(word);
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.

haystring to search
needlesubstring to count

returns the number of non-overlapping occurrences; 0 if the needle is empty or longer than the haystack

n.i = countstring(csv, ",");
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.

ssource string
oldsubstring to find; empty means "change nothing"
nwtext to put in its place; empty deletes (that is removestring)

returns a new string with every match replaced

out.s = replacestring(path, "\\", "/");
removestring(s, sub)

delete every occurrence of a substring

Exactly replacestring(s, sub, "") -- same non-overlapping, all-occurrences rule.

ssource string
subsubstring to delete; empty means "change nothing"

returns a new string with every occurrence removed

clean.s = removestring(raw, "\r");
findstring(hay, needle, start)

where a substring starts, counting from 1

Returns a 1-INDEXED position, and 0 for "not found" -- so the test is `> 0`, and the result feeds straight into mid(), which is 1-indexed too. `strstr(hay, needle)` is the same search reported the C way (0-indexed, -1 for absent). An optional third argument resumes the search from a 1-indexed position, which is how you walk every occurrence. AN EMPTY NEEDLE RETURNS 0, i.e. "not found" -- deliberately, so a loop over findstring cannot spin. Note this DISAGREES with contains(), which answers 1 for an empty needle; verified on both backends, and it is why "found" should be tested with the builtin you actually mean.

haystring to search
needlesubstring to look for
start1-indexed position to start from; omit for 1

returns the 1-indexed byte position of the first match at or after `start`, or 0 if there is none

p.i = findstring(line, "=");
insertstring(s, ins, pos)

splice text into a string at a 1-indexed position

`pos` is 1-indexed like mid(): pos 1 prepends, pos length+1 appends. It CLAMPS instead of failing -- pos <= 0 prepends, pos past the end appends -- so an out-of-range position is never an error and never silently drops the text.

ssource string
instext to insert
pos1-indexed position the inserted text will start at

returns a new string with `ins` spliced in

out.s = insertstring(num, ",", 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.

sstring to place
widthfield 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.

sstring to place
widthfield width in bytes

returns a string of exactly `width` bytes, or "" if width <= 0

println(rset(str(n), 8));
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. Identical to ucase() -- both spellings exist because C programmers reach for one and BASIC programmers for the other.

sstring 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. Identical to lcase().

sstring to convert

returns a new lowercased string (ASCII letters only; other bytes unchanged)

key.s = tolower(header);
contains(hay, needle)

does a string hold this substring?

Case-SENSITIVE, byte-exact. An empty needle answers 1 (everything contains nothing) -- which is the opposite of findstring(), where an empty needle reports 0 so a search loop cannot spin. Both are verified on both backends; pick the one whose empty-needle answer you want. `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.

haystring to search
needlesubstring to look for

returns 1 if present (or the needle is empty), else 0

if (contains(line, "ERROR")) { ... }
startswith(hay, prefix)

does a string begin with this prefix?

Case-SENSITIVE, byte-exact. An empty prefix answers 1.

haystring to test
prefixprefix 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.

haystring to test
suffixsuffix to look for

returns 1 if `hay` ends with `suffix` (or the suffix is empty), else 0

if (endswith(lcase(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.

afirst string
bsecond string

returns -1 if a sorts before b, 0 if they match case-insensitively, +1 if after

if (strcmpi(cmd, "QUIT") == 0) { ... }
stringfield(s, n, sep)

pull one delimited field out of a string, counting from 1

ONE-indexed: field 1 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. A field number past the end returns "" -- indistinguishable from a genuinely empty field, so count with countstring() first when that matters.

sstring to split
n1-indexed field number
sepseparator; an EMPTY separator makes the whole string field 1

returns the field's text, or "" if n is past the last field or n <= 0

city.s = stringfield(row, 3, ",");
split(s, sep, out)

break a string on a separator into a string list

APPENDS to `out` rather than clearing it, so splitting twice into one list accumulates. 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 appends the whole string as a single field and returns 1. `stringsplit` is the same builtin under its PureBasic-flavoured name.

sstring to split
sepseparator to split on
outan existing string list the fields are APPENDED to

returns the number of fields appended -- always at least 1

list cols.s
n.i = split(row, ",", cols);
mid(s, pos, n)

a substring, counting from 1

ONE-INDEXED, which is the single most common trip-up here: `mid(s, 1)` is the whole string and `mid(s, 0)` is "" -- array subscripts stay 0-indexed, this does not. Two forms: `mid(s, pos)` runs to the end, `mid(s, pos, n)` takes at most n bytes. `substr(s, pos, n)` is the same operation with a C-style 0-indexed start.

ssource string
pos1-indexed byte position to start at; <= 0 yields ""
nhow many bytes to take, clamped to what remains; omit for "to the end"

returns the substring, or "" if pos is past the end or n <= 0

part.s = mid(line, 5, 3);
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 ucase/lcase 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.

sstring to re-case
mode0 upper, 1 lower, 2 title; omit for title-case

returns a new re-cased string

title.s = capitalize(raw);
atof(s)

read a float out of the front of a string (C's name for valf)

The same function as valf, under C's spelling. Parses as much as looks like a number and stops, so atof("12.5xyz") is 12.5. There is NO error channel: text that does not start with a number gives 0.0, indistinguishable from the string "0".

sstring to parse

returns the leading number as a float, or 0.0 if there is none

f.f = atof(field);
atoi(s)

read an integer out of the front of a string (C's name for vali)

The same function as vali, under C's spelling. BASE 10 ONLY: atoi("12abc") is 12, atoi("abc") is 0, and atoi("0x1F") is 0 because the "0" parses and the "x" stops it. A leading sign and leading whitespace are accepted; parsehex is the one that reads hex.

sstring to parse

returns the leading base-10 integer, or 0 if there is none

n.i = atoi(argline);
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.

pathfile to append to; an empty path returns 0
fmtformat 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.

vthe 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, unlike mid(). 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.

sstring to read
idx0-indexed byte position

returns the byte as 0..255, or 0 if idx is out of range

c.i = getc(line, 0);
instr(s, sub)

1-indexed position of one string inside another (BASIC convention)

Returns 1 for a match at the very start and 0 when the needle is absent, which is why the test is `> 0` and not `>= 0`. strstr is the C-convention twin: 0-indexed, -1 for absent.

sthe string to search
subthe string to look for

returns the 1-indexed position, or 0 if `sub` does not occur

if instr(line, "=") > 0 { ... }
itoa(n)

render an integer as a string (C's name for str on an int)

Base 10, no padding and no prefix. The same renderer str reaches for an int argument.

nthe integer to render

returns the number as text

s.s = itoa(count);
len(v)

how many items a value holds -- bytes for a string, elements for 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.

DISPATCHED BY TYPE, on both backends: a string answers its byte count, a list/map/array its element count, and a JSON value its text length or child count. That is the difference from strlen, which is string-only. Container sizes are also readable as `<handle>->count`.

vthe string, list, map, array or JSON value to measure

returns the byte count or the element count

n.i = len(items);
ltoa(n)

render an integer as a string (C's long-to-ascii name for itoa)

Base 10, no padding and no prefix -- the same renderer itoa and str reach for an int argument. CX integers are 64-bit throughout, so ltoa and itoa are the SAME operation and neither is wider than the other. A FLOAT argument is truncated toward zero first, so ltoa(65.9) is 65.

nthe integer to render

returns the number as text

s.s = ltoa(total);
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.

sstring 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, exactly as vali() does for base 10. 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.

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

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

fmtthe 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.000000" or whatever `#pragma decimals` is set to. Use strf when you want to fix the decimal count at the call site instead.

nthe 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`; instr is the 1-indexed BASIC twin that takes a string needle.

sthe string to search
chthe 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.

athe first string
bthe 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.

vthe float to render
ddecimals to show; omit to use `#pragma decimals`

returns the rendered string

label.s = strf(price, 2) + " at " + strf(angle, 6) + " rad";
stri(n)

render an integer as a string (an alias of itoa)

Base 10, no padding and no prefix -- the same renderer itoa and str reach for an int argument. A FLOAT argument is truncated toward zero first, exactly as C's implicit conversion does at a native call site, so stri(65.9) is 65 (NOT str's 65.900).

nthe integer to render

returns the number as text

s.s = stri(count);
stringsplit(s, sep, out)

break a string on a separator into a string list (the other name for split)

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 and a string ending in the separator contributes a final "".

sthe string to break up
septhe separator to split on
outthe string list that receives the fields

returns how many fields were produced

n.i = stringSplit(line, ",", fields);
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.

sthe string to measure

returns the byte count; 0 for an empty string

n.i = strlen(line);
strstr(h, n)

where a substring starts, counting from 0

findstring()'s C-flavoured twin, reporting the C answer: a ZERO-indexed position, and -1 for "not found". So the test is `>= 0`, NOT `> 0` -- a `> 0` test would silently reject a match at the very start of the string. An empty needle reports -1, the same "not found" findstring() gives it.

hstring to search
nsubstring to look for

returns the 0-indexed byte position of the first match, or -1 if absent

if (strstr(line, "=") >= 0) { ... }
substr(s, pos, n)

a substring, counting from 0

mid()'s C-flavoured twin: identical in every way except that `pos` is ZERO-indexed, so `substr(s, 0)` and `mid(s, 1)` are the same text. Same two forms (`substr(s, pos)` to the end, `substr(s, pos, n)` for at most n bytes) and the same clamping. 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.

ssource string
pos0-indexed byte position to start at
nhow many bytes to take, clamped to what remains; omit for "to the end"

returns the substring, or "" if pos is past the end

part.s = substr(line, 4, 3);
val(s)

read an integer out of the front of a string (BASIC's name for vali)

The same function as vali and atoi. Base 10, no error channel: unparseable text gives 0. Use valf when the text may carry a fraction.

sstring to parse

returns the leading base-10 integer, or 0 if there is none

n.i = val(row);

regular expressions 4 builtins

regexcount(subject, pattern)

count how many times a pattern matches

CX's own small regular-expression engine (no POSIX or PCRE dependency), shared by both backends.

subjectthe text to search
patternthe pattern

returns the number of matches

n = regexCount(s, "[0-9]+");
regexextract(subject, pattern)

return the text a pattern matched

CX's own small regular-expression engine (no POSIX or PCRE dependency), shared by both backends.

subjectthe text to search
patternthe pattern

returns the matched text, or an empty string if the pattern does not match

v.s = regexExtract(s, "[0-9]+");
regexmatch(subject, pattern)

test whether a pattern matches anywhere in a string

CX's own small regular-expression engine (no POSIX or PCRE dependency), shared by both backends.

subjectthe text to search
patternthe pattern

returns non-zero if the pattern matches

if (regexMatch(s, "^[a-z]+$")) { ... }
regexreplace(subject, pattern, replacement)

replace what a pattern matches

CX's own small regular-expression engine (no POSIX or PCRE dependency), shared by both backends.

subjectthe text to search
patternthe pattern
replacementthe replacement text

returns a new string with the matches replaced

out.s = regexReplace(s, "[0-9]+", "#");

Numerics 3 families · 46 builtins

math 39 builtins

sin(x)

sine of an angle in radians

xangle in radians

returns the sine, in -1.0 .. 1.0

y.f = amplitude * sin(t);
cos(x)

cosine of an angle in radians

xangle in radians

returns the cosine, in -1.0 .. 1.0

x.f = radius * cos(angle);
tan(x)

tangent of an angle in radians

xangle in radians

returns the tangent; unbounded, and huge near odd multiples of pi/2

asin(x)

arc sine: the angle whose sine is `x`

xa 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`

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

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

ythe vertical component
xthe horizontal component

returns the angle in radians, in -pi .. pi

heading.f = atan2(ty - py, tx - px);
sinh(x)

hyperbolic sine

xany value

returns the hyperbolic sine; overflows to infinity for large `x`

cosh(x)

hyperbolic cosine

xany value

returns the hyperbolic cosine, always >= 1.0

tanh(x)

hyperbolic tangent

xany value

returns the hyperbolic tangent, in -1.0 .. 1.0

log(x)

natural logarithm (base e)

This is C's `log`, NOT base 10 -- `log10` is the base-10 one.

xa positive value; 0 gives -infinity and a negative gives NaN

returns the natural log

log10(x)

base-10 logarithm

xa positive value; 0 gives -infinity and a negative gives NaN

returns the base-10 log

digits.i = 1 + floor(log10(n));
exp(x)

e raised to the power `x`, the inverse of `log`

xthe exponent

returns e**x; overflows to infinity for large `x`

sqrt(x)

square root

xa non-negative value; a negative gives NaN

returns the square root

pow(b, e)

raise `b` to the power `e`

bthe base
ethe exponent; need not be a whole number

returns b**e

area.f = pow(side, 2.0);
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.

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

xthe value to round up

returns the smallest whole number >= x, as a float

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.

xthe 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));
fabs(x)

absolute value of a FLOAT

`abs` is the int form; this one keeps the fraction.

xany value

returns x without its sign

fmin(a, b)

the smaller of two FLOATS

afirst value
bsecond value

returns whichever is smaller

fmax(a, b)

the larger of two FLOATS

afirst value
bsecond value

returns whichever is larger

sign(x)

which side of zero a value is on, as an INT

xany value

returns -1 if x is negative, +1 if positive, 0 if zero (including -0.0)

step.i = sign(target - current);
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.

athe dividend
bthe 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`.

xthe value to constrain
lolower bound, returned when x is below it
hiupper bound, returned when x is above it

returns x, lo or hi, as a float

volume.f = clamp(volume + step, 0.0, 1.0);
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.

athe value at t = 0
bthe value at t = 1
tthe 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.

xthe value to test
lolower bound, counted as inside
hiupper 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

x1first point's x
y1first point's y
x2second point's x
y2second 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.

xthe value to rescale
aLoinput range low
aHiinput range high
bLooutput range low, and the answer when aLo equals aHi
bHioutput range high

returns x expressed in the output range

px.f = remap(value, 0.0, 100.0, left, right);
degrees(r)

convert radians to degrees

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

dan angle in degrees

returns the same angle in radians

y.f = cy + radius * sin(radians(deg));
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.

xany integer

returns x without its sign

cbrt(x)

cube root

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.

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.

xthe value

returns the real cube root of x

r = cbrt(-27.0);   // -3.0
fmod(x, y)

floating-point remainder of x/y

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.

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.

xthe dividend
ythe divisor

returns the remainder of x/y with x's sign

r = fmod(-7.5, 2.0);   // -1.5
hypot(x, y)

the length of the hypotenuse, without overflow

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.

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.

xone leg
ythe other leg

returns sqrt(x*x + y*y)

d = hypot(dx, dy);
log2(x)

base-2 logarithm

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.

A direct bridge to the C library function of the same name; the answer is C's answer, to the bit.

xthe value

returns log base 2 of x

b = log2(1024.0);   // 10.0
max(a, b)

the larger of two INTS

The float form is `fmax`.

afirst value
bsecond value

returns whichever is larger

hp.i = max(0, hp - damage);
min(a, b)

the smaller of two INTS

The float form is `fmin`.

afirst value
bsecond value

returns whichever is smaller

pi(…)

the constant pi

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 value of pi as a double.

returns 3.14159265358979323846

c = 2.0 * pi() * r;
trunc(x)

discard the fractional part, toward zero

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.

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.

xthe value

returns x with its fraction removed, rounded toward zero

n = trunc(-2.7);   // -2.0

hashing 5 builtins

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.

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

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

inputthe 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

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

inputthe bytes to check

returns the checksum as an int

if crc32(block) != stored { print "block corrupt"; }

random 2 builtins

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.

maxexclusive upper bound (optional)

returns a pseudo-random integer

n = random(6) + 1;
randomf()

a pseudo-random float

The same xorshift64* stream as random(), delivered as a float. Not cryptographic.

returns a pseudo-random float

f = randomf();

Containers & algorithms 3 families · 99 builtins

containers 70 builtins

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.

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

athe array to reduce

returns the mean as a float; 0 when empty

arrayadd(handle, value)

append one element to a grown array

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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical.

handlethe grown-array handle
valuethe value to append

returns the new element count; 0 if the handle is not a grown array

arrayAdd(a, 7);
arrayfill(handle)

arrayfill -- reset every cell of a grown array to its element type's zero

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 PAD half of `array d.i[4] = {9};` written for a name already declared -- one statement declares AND redimensions (v3.285.0), so the cells the literal does not name have to read as they would at a first declaration. It takes NO element type: the array records its own, and cx_array_fill_zero reads it, which is why the two backends cannot pad a string cell differently from each other. Emitted only when the initialiser provably leaves a tail (cxc_array_init_needs_pad), so a literal covering every cell costs nothing.

handlethe grown-array handle

returns void

array d.i[4] = {9};   // the compiler emits the pad
arrayflatidx(handle, i0, i1, ...)

turn per-dimension indices into a row-major flat 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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical. Computed by the runtime from the array's own dimension header, so a multi-dimensional subscript needs no compile-time knowledge of the sizes.

handlethe grown-array handle
i0, i1, ...one index per dimension

returns the flat index; 0 if the handle is not a grown array

a[i][j] = v;
arraygetflat(handle, flat)

read a grown array element by flat 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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical. The value comes back typed from the element's stored type.

handlethe grown-array handle
flatrow-major flat index

returns the element; integer 0 if the handle is not a grown array

v = a[i];
arraynew(ndims, d0, d1, ...)

allocate a grown array (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 a DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical.

ndimsnumber of dimensions
d0, d1, ...one size per dimension

returns a grown-array handle

function f(array a.i) { ... }
arrayredim(handle, ndims, d0, d1, ...)

resize a grown array, preserving what fits

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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical. This is what an `array` RE-STATEMENT lowers to (the `redim` spelling was retired v3.285.0).

handlethe grown-array handle
ndimsnumber of dimensions
d0, d1, ...the new sizes

returns void

array a.i[64];   // a re-statement
arraysearch(handle, key)

find a value in a grown array

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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical. Dispatches on the KEY's type -- float, string or integer -- so one call serves every element type.

handlethe grown-array handle
keythe value to find

returns the element's index, or -1 if absent

i = search(a, 42);
arraysetflat(handle, flat, value)

write a grown array element by flat 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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical.

handlethe grown-array handle
flatrow-major flat index
valuethe value to store

returns void

a[i] = v;
arraysort(handle, desc)

sort a grown array 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.

EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for a DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical.

handlethe grown-array handle
descnon-zero to sort descending

returns void

sort(a);
arraytotal(handle)

count a grown array's elements across every dimension

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 DYNAMIC array -- one passed to a user function, which CX promotes to a grown `cx_array` handle so the callee's writes and its own `array` re-statement reach the caller (rule 31). Both backends share one cx_array runtime, so behaviour is identical.

handlethe grown-array handle

returns the total element count; 0 for a handle that is not a grown array

n = a->count;
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.

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

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

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

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

athe array to advance

returns 1 if the cursor landed on an element, 0 once past the end

arrsettype()

declare an array's element type

ACCEPTED AND DOES NOTHING in v3: an array carries its element type in the symbol table from its declaration, so there is nothing to set at run time. Kept so beta-era source still compiles, and listed here so you know it is a no-op rather than assuming it took effect.

returns void

arrSetType(a);
arrsize(array)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count 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.

count an array's elements (RETIRED spelling)

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.

RETIRED since v3.186.0: calling this is CX-E1048, which names the replacement at your own line. Write `a->count` instead -- one field name per idea across every container, resolved from the declared type, and on a fixed array it folds to a compile-time constant. The row stays here because the compiler still recognises the name in order to refuse it helpfully.

arraythe array

returns never returns -- the call is refused at compile time with CX-E1048

n = a->count;
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.

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

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

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

cthe container to overwrite
vthe 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.

cthe container to search
keythe 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.

listthe list
valuethe 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.

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

listthe list
index0-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.

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

lstthe list to read
i0-indexed element; omit to read at the cursor

returns the value of the element

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.

lstthe list to read
i0-indexed element to read

returns the value of the element

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.

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

listthe list
index0-based position to insert at
valuethe 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.

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

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

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

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

lstthe list to position
i0-indexed element to stand on

returns void

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.

lstthe list to write into
i0-indexed element; omit to write at the cursor
vthe value to store

returns void

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.

lstthe list to write into
i0-indexed element to write
vthe value to store

returns void

listsize(list)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count 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.

count the elements in 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. RETIRED as a call since v3.186.0 -- write `xs->count`, which CX-E1048 tells you at your own line if you do not.

listthe list

returns the element count

n = xs->count;
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.

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

mthe map to empty

returns void

mapClear(m);
mapconfig(map, hash_kind, multi)

set a map's hashing and multimap mode right after creation

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 `map m.t [multi] by HASH`. It must run before the first insert.

mapthe map
hash_kindwhich bucket hash to use
multinon-zero for a multimap

returns void (the emitted code discards the pushed 0)

map m.i by hash;
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.

mthe map to test
kthe 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).

mthe map to remove from
kthe key to remove

returns void

mapdestroy(handle)

release a map created by mapCreate

ACCEPTED AND DOES NOTHING: the map is owned by the collector, which reclaims it. Kept so the create/destroy pairing reads naturally in source, and listed here so you know it is a no-op rather than assuming a leak was closed.

handleignored

returns void

mapDestroy(m);
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.

handlethe int handle from mapCreate
keythe 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.

mthe map to test
kthe key to look for

returns 1 if the key is present, 0 if not

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.

handlethe int handle from mapCreate
keythe 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.

mthe map to read

returns the key of the current entry

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.

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

handlethe int handle from mapCreate
keythe string key
valuethe 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).

mthe map to rewind

returns void

mapsize(handle)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count 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.

count the entries in a handle-held map (RETIRED spelling)

RETIRED as a call since v3.186.0: CX-E1048 names the replacement at your own line. Write `m->count` -- one field name per idea across every container. The row stays because the compiler still recognises the name in order to refuse it helpfully.

handlethe int handle from mapCreate

returns never returns -- the call is refused at compile time with CX-E1048

n = m->count;
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.

mthe map to read

returns the value of the current entry

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.

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

cthe array, list, map or JSON array to reduce

returns the smallest element; 0 when empty

oamapget(array, key)

read a value from a grown ARRAY by key

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.

ORDERED-MAP PROMOTION, the PHP/Lua model: a list given keyed access becomes an ordered map. The list stays the sequence -- values live once, in insertion order, and never move -- and a companion key-to-slot index is added beside it. One implementation serves both backends. The array-backed twin of omapGet.

arraythe grown array being used as an ordered map
keythe string key

returns the value at the key's slot; a zero value if the key is absent

v = mapGet(a, "hp");
oamapput(array, key, value)

store a value in a grown ARRAY under a key, appending it 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.

ORDERED-MAP PROMOTION, the PHP/Lua model: a list given keyed access becomes an ordered map. The list stays the sequence -- values live once, in insertion order, and never move -- and a companion key-to-slot index is added beside it. One implementation serves both backends. The array-backed twin of omapPut, for an array promoted to dynamic.

arraythe grown array being used as an ordered map
keythe string key
valuethe value

returns void

mapPut(a, "hp", 10);
omapget(list, key)

read a value from a list by key

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.

ORDERED-MAP PROMOTION, the PHP/Lua model: a list given keyed access becomes an ordered map. The list stays the sequence -- values live once, in insertion order, and never move -- and a companion key-to-slot index is added beside it. One implementation serves both backends.

listthe list being used as an ordered map
keythe string key

returns the value at the key's slot; a zero value if the key is absent

v = mapGet(xs, "hp");
omapput(list, key, value)

store a value in a list under a key, appending it 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.

ORDERED-MAP PROMOTION, the PHP/Lua model: a list given keyed access becomes an ordered map. The list stays the sequence -- values live once, in insertion order, and never move -- and a companion key-to-slot index is added beside it. One implementation serves both backends.

listthe list being used as an ordered map
keythe string key
valuethe value

returns void

mapPut(xs, "hp", 10);
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.

cthe list or array to reverse

returns void

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.

cthe list or array to sort
desc1 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)`.

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

cthe list or array to sort
cmpthe comparator function to order by

returns void

sortfield(c, field, desc)

sort a list of structs in place by one FIELD

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 field to order by is resolved at compile time, so it is named the way you would read it rather than as an offset. An optional trailing 1 sorts descending. In place, like sort.

cthe list of structs to sort
fieldthe struct field to order by
desc1 to sort descending; omit or 0 for ascending

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.

cthe 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).

vthe local whose address is wanted

returns the variable's address as an integer

jsonLoadArray(data, path, varslot(arr));

queues 19 builtins

queuemake(etype, elem_size)

Allocate an uninitialised queue handle (emitted by the declaration)

Never written in CX — the `queue q.i;` declaration emits it, exactly as a list declaration emits its make. The queue has capacity 0 and is usable by nothing except queueInit; every other op reports "no capacity" and stops.

etypeelement type code — 0 int, 1 float, 2 string, 3 struct
elem_sizeper-element byte stride; meaningful ONLY for a struct queue (the compiler bakes the declared struct's size). Ignored for scalars, which always use the runtime's own element width

returns the queue handle (an opaque int; handle 0 is always invalid)

queue jobs.s;   // the compiler emits queuemake for you
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.

qthe queue (declared `queue q.i;`)
capacityring 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).
policy0 = 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`.

qthe queue

returns void

queueClear(impacts)     // per bout: forget last bout's facts
queuefree(q)

Release the ring (emitted at scope exit for a local queue)

Never written in CX — the compiler emits it when a function-local queue goes out of scope.

qthe queue

returns nothing

function f.v() { queue tmp.i; ... }   // queuefree emitted at the closing brace
queuecount(q)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count 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.

how many elements are live right now

The drain-loop guard, so no caller ever has to probe by failing. Reading a queue CONSUMES the element, so `while q->count > 0` is the honest way to walk one.

qthe queue

returns live element count (0 when empty)

while alerts->count > 0 { println(queueTake(alerts)); }
queuemax(q)retired spelling

Not callable from CX since v3.186.0 — write h->cap instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->cap 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.

the capacity set at queueInit

Pairs with queueCount to report fullness (`count/max`) without tracking the bound yourself.

qthe queue

returns capacity

if (q->count == q->cap) { ... }   // full
queuevalid(h)retired spelling

Not callable from CX since v3.186.0 — write h->valid instead; the call form is CX-E1048. It remains in this reference because it is still 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.

hqueue 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); }
queuepushi(q, v)

Push an INT element

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. On a full queue: REJECT errors loudly, ROLL overwrites the oldest.

qthe queue (declared `.i`)
vthe int to add

returns nothing

queue ids.i;  queueInit(ids, 16, 0);  queuePush(ids, 42);
queuepushf(q, v)

Push a FLOAT element

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. On a full queue: REJECT errors loudly, ROLL overwrites the oldest.

qthe queue (declared `.f`)
vthe float to add

returns nothing

queue samples.f;  queuePush(samples, 120.0);
queuepushs(q, v)

Push a STRING element

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. The queue increfs its own copy, so string elements ride the normal string reference counting. On a full queue: REJECT errors loudly, ROLL overwrites the oldest.

qthe queue (declared `.s`)
vthe string to add

returns nothing

queue alerts.s;  queuePush(alerts, "SLOW response");
queuetakei(q)

Remove and return the OLDEST int (FIFO)

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.i`)

returns the oldest int, which is removed from the queue

while ids->count > 0 { n.i = queueTake(ids); }
queuetakef(q)

Remove and return the OLDEST float (FIFO)

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.f`)

returns the oldest float, which is removed from the queue

ms.f = queueTake(samples);
queuetakes(q)

Remove and return the OLDEST string (FIFO)

Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Ownership of the string transfers to the caller. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.s`)

returns the oldest string, which is removed from the queue

msg.s = queueTake(alerts);   // arrival order
queuepopi(q)

Remove and return the NEWEST int (LIFO)

The stack end of the same queue — the ops define the behaviour, there is no mode flag. Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.i`)

returns the newest int, which is removed from the queue

last.i = queuePop(history);   // most recent first
queuepopf(q)

Remove and return the NEWEST float (LIFO)

The stack end of the same queue — the ops define the behaviour, there is no mode flag. Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.f`)

returns the newest float, which is removed from the queue

latest.f = queuePop(samples);
queuepops(q)

Remove and return the NEWEST string (LIFO)

The stack end of the same queue — the ops define the behaviour, there is no mode flag. Emit-selected: you write ONE name (`queuePush`/`queueTake`/`queuePop`) and the declaration's `.suffix` picks this typed entry point at compile time, so the runtime never guesses the element type. Ownership transfers to the caller. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared `.s`)

returns the newest string, which is removed from the queue

while recent->count > 0 { println(queuePop(recent)); }   // newest first
queuepushst(q, src)

Copy a STRUCT element into the queue, by value

Chosen by a declaration whose suffix is a struct name (`queue window.Sample;`). The whole record travels as one element, so fields cannot drift apart the way a queue-per-field pushed in lockstep can. v1 is numeric-fields-only: a string field in a queued struct is refused AT THE DECLARATION, not at runtime.

qthe queue (declared over a struct type)
srcthe struct variable to copy in

returns nothing

queue window.Sample;  struct Sample s;  s.ms = 120.0;  queuePush(window, s);
queuetakest(q, dst)

Copy the OLDEST struct element OUT into your variable (FIFO)

A struct element is NOT expression-position — it is bytes, not a value a call can hand back — so it is copied into a struct you supply. Writing the 1-arg form on a struct queue is a COMPILE error naming the out-param form, never a silent wrong-sized read. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared over a struct type)
dstthe struct variable the oldest element is copied into

returns nothing — the result arrives in `dst`

struct Sample out;  queueTake(window, out);  println(str(out.ms));
queuepopst(q, dst)

Copy the NEWEST struct element OUT into your variable (LIFO)

The stack end of a struct queue. Like queueTake on a struct queue it uses an out-param, and the 1-arg form is a compile error. Empty is a LOUD error naming the op, never a sentinel value — guard the read with `q->count > 0`.

qthe queue (declared over a struct type)
dstthe struct variable the newest element is copied into

returns nothing — the result arrives in `dst`

struct Sample out;  queuePop(window, out);   // most recent reading

Data & serialization 3 families · 122 builtins

json 77 builtins

jsonparse(src)

parse a JSON string into a document handle

Returns a CXJ_DOC wrapper node; call jsonValue to reach the root value. Handles `//` line and block comments (non-standard) and an int64-exact number lane; no \uXXXX escapes.

srcthe JSON source text to parse

returns doc handle on success (test valid with `!= 0`); -1 on parse failure (the sentinel to check with `== -1`)

jh = jsonparse(raw);
jsonparseblob(blob)

parse JSON from an embed() blob handle

Wraps cx_json_parse_buf over the blob's static baked-in bytes.

bloban embed() blob handle whose bytes are the JSON source

returns doc handle on success; -1 on a bad blob handle or parse error

json j = jsonParseBlob(data);
jsonparsemf(mfh)

parse JSON directly from a memfile handle

MFS Layer 1: parses in place over the memory filesystem's byte buffer (no copy).

mfha memfile handle whose contents are the JSON source

returns doc handle on success; -1 on a bad memfile handle

json j = jsonParseMf(mfh);
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.

doca doc/root handle from jsonParse 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.

handleany json handle (a doc or a doc["k"] view)

returns void

jsondecref(doc);
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.

handleany 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).

doca doc handle from jsonParse

returns the root value node handle; 0 if the doc handle is invalid

root = jsonvalue(doc);
jsonsize(node)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count lowers to. The answer moves: this one does not dereference a document handle, so it answers 1 where ->count answers the real member count — port these by reading them, not by renaming. 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.

count the members or elements of a JSON container

Reads child_count directly and does NOT deref a DOC wrapper (a doc reports 1) -- pass a value node, e.g. jsonValue(doc).

nodean array or object value node

returns the child (member/element) count; 0 for an invalid handle or a scalar

assertEqual(3, doc->count);
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.

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

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

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

nodethe 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"]).

objthe object value node to search
keythe 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.

arrthe array value node
idxthe 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).

nodea 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).

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

nodea bool value node

returns 1 for true, 0 for false; 0 for a non-bool or invalid node

jActive.i = jsonbool(jsonmember(jh, "active"));
jsontype(node)retired spelling

Not callable from CX since v3.186.0 — write h->type instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->type 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.

report a JSON node's kind as an int code

Derefs a DOC wrapper to its root. Codes: 0=null 1=bool 2=number 3=string 4=array 5=object.

nodeany json value node (or doc)

returns the kind code 0-5; 0 (null) for a missing/absent/invalid handle

assertEqual(3, j["name"]->type);
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).

nodeany json value node

returns 1 if the node's type is null (incl. missing key/invalid handle); 0 otherwise

assertEqual(1, jsonIsNull(j["nada"]));
jsoncount(node)retired spelling

Not callable from CX since v3.186.0 — write h->count instead; the call form is CX-E1048. It remains in this reference because it is still the runtime target that ->count 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.

how many children does the node this handle names hold?

The lowering target of `doc->count`. Distinct from both neighbours on purpose: `cx_json_size` does not deref, so a DOC handle reports 1 rather than the object's real member count; `cx_json_len` derefs but answers a STRING node with its text length. This one derefs and counts children, answering 0 for a scalar.

nodejson node or doc handle

returns number of members (object) or elements (array); 0 for a scalar

for (i = 0; i < doc->count; i = i + 1) { ... }
jsonvalid(node)retired spelling

Not callable from CX since v3.186.0 — write h->valid instead; the call form is CX-E1048. It remains in this reference because it is still 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.

does this handle still name a live json node?

The lowering target of `doc->valid`. Deliberately NOT equivalent to `doc != 0`: a freed or never-parsed handle is non-zero, which is what made `jsonvar == 0` a footgun in the first place. Derefs a DOC handle first, so it answers for the node the doc wraps. Catches use-after-free, not use-after-free-then-reuse: a pool slot can be recycled by a later parse, and no generation counter is kept today.

nodejson node or doc handle (any integer is safe to pass)

returns 1 when the handle names a live pool node, 0 otherwise

if (doc->valid) { print(doc->count); }
jsonmemberv(node, key)

look up a key on a JSON object, dereferencing a parsed-doc wrapper

The doc-aware read behind `j["key"]`: transparently unwraps a DOC so a parsed doc subscripts without an explicit jsonValue.

nodean object value node or a parsed-doc wrapper
keythe member key string to find

returns the member value node handle; 0 if the key is absent

nAlphaNode = jsonmemberv(e, "bgAlpha");
jsonmembervivify(obj, key, want)

navigate to obj[key] for a WRITE, creating it if absent

The string-key step of a chained write. Returns the level at `key` as a writable container of kind `want`: an absent key is created, a json null is promoted in place, an existing level of the right kind is reused unchanged. A level that already holds the OTHER container kind, or a scalar, is refused loudly with nothing written -- vivification creates, it never converts. Mostly emitted by both backends for `a["x"]["y"] = v`; callable directly.

objjson object (a document handle is deref'd)
keythe member name to navigate to or create
wantCX_JSON_VIV_OBJECT or CX_JSON_VIV_ARRAY -- the kind the NEXT subscript needs, which is what decides the level's own kind

returns the level's handle, or 0 if refused (conflict / non-object obj)

h = jsonMemberVivify(cfg, "servers", 2);   // "servers" as an array
jsonelementvivify(arr, idx, want)

navigate to arr[idx] for a WRITE, creating it if absent

The int-index twin of jsonMemberVivify, and the step that makes `doc["servers"][0]["host"] = "alpha"` build a real array level. An index past the end grows the array, padding the gap with real json nulls; an existing element is reconciled against `want` by the same create-never-convert rule. A negative index is refused loudly.

arrjson array (a document handle is deref'd)
idxzero-based element index; the array grows to reach it
wantCX_JSON_VIV_OBJECT or CX_JSON_VIV_ARRAY -- the kind the NEXT subscript needs

returns the element's handle, or 0 if refused (conflict / negative / non-array)

h = jsonElementVivify(rows, 0, 1);   // element 0 as an object
jsonelementv(node, idx)

fetch an array element by index, dereferencing a parsed-doc wrapper

The doc-aware read behind `j[i]`: unwraps a DOC before indexing.

nodean array value node or a parsed-doc wrapper
idxthe 0-based element index

returns the element node handle; 0 if the index is out of range

json el = jsonElementV(arr, 0);
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.

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

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

nodeany json value node

returns the coerced float value; 0.0 for a null/container/invalid node

float lat = e["lat"];
jsonnumeqint(node, rhs)

null-or-zero int equality for a `json ==/!= int` compare

Option C footgun fix: an array/object is never == a number, so `doc == 0` stops being always-true. Both emitters route ==/!= here; also user-callable.

nodeany json value node
rhsthe int right-hand operand

returns 1 if node is null/absent OR a bool/number/string coercing to rhs; 0 (a container is never equal to a number, and a non-matching value)

if (doc == 0) { print("no doc\n"); }
jsonnumeqflt(node, rhs)

null-or-zero float equality for a `json ==/!= float` compare

Like jsonNumEqInt but for a float rhs; honors #pragma floattolerance. An array/object is never == a number.

nodeany json value node
rhsthe float right-hand operand

returns 1 if node is null/absent OR a bool/number/string within tolerance of rhs; 0 otherwise (containers never match)

if (val == 1.5) { doStuff(); }
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.

nodeany json value node

returns the kind-appropriate length; 0 for null/missing/invalid

int n = len(doc);
jsonreduce(node, opch)

numeric reduce over a json array's elements or an object's

member VALUES (the same members `foreach` visits, and the same values `sum` answers for a map). ALWAYS ANSWERS FLOAT, a deliberate divergence from the int-preserving list/array contract: every element is read through cx_json_as_float, because a json number IS a pool float node. `sum` over `[1,2,3]` is 6.0, not 6. ELEMENT COERCION, measured not assumed (2026-08-03): a number reads as itself; a numeric string "3.5" parses to 3.5 (the SHARED lenient read the rule engine uses); a non-numeric or empty string is 0; true is 1, false and null are 0; and a NESTED array or object element is 0 -- so `minof([1,[2,3]])` is 0, not 1. An EMPTY array or object answers 0.0, following the bulk core's empty contract for every op. A doc handle derefs first, so sum(jsonParse("[1,2]")) and sum(doc["items"]) agree when items is the root.

nodejson array/object node, or a doc handle wrapping one
opchthe op as a character code: '+' sum, '<' minof, '>' maxof, 'a' avg

returns the reduced value as a float; 0.0 for an empty container

total.f = sum(doc["scores"]);
jsonfindnum(node, key, aspred)

LINEAR first-match search for a numeric value among a json

container's elements (distinct from `search`, which is a bsearch over sorted contiguous storage and has no json meaning). Elements are compared as floats through the same coercion jsonreduce documents, within `#pragma floattolerance`; a string element never matches a numeric key.

nodejson array/object node, or a doc handle wrapping one
keythe value to look for
aspred0 -> the 0-based index or -1 when absent (`find`); 1 -> 1/0 membership (`contains`)

returns index, -1, or the 1/0 predicate per aspred

if (contains(doc["ids"], 42)) { print("present"); }
jsonfindstr(node, key, aspred)

the string-key lane of jsonfindnum. Matches only STRING

elements, by exact text; a number element never matches a string key.

nodejson array/object node, or a doc handle wrapping one
keythe text to look for
aspred0 -> index or -1 (`find`); 1 -> 1/0 membership (`contains`)

returns index, -1, or the 1/0 predicate per aspred

idx = find(doc["names"], "ada");
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 -- the same relationship xmlCreate() has to `xmldoc d;`. 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

arrthe array node to append to
vthe 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.

arrthe array node to append to
vthe int value to append

returns void

jsonArrInt(arr, 9007199254740993);
jsonarrstr(arr, v)

append a string element to a JSON array

arrthe array node to append to
vthe string value to append

returns void

jsonArrStr(slots, mods[slotMod[si]].id);
jsonarrbool(arr, v)

append a boolean element to a JSON array

arrthe array node to append to
vthe value; stored as 1 if nonzero else 0

returns void

jsonArrBool(arr, 1);
jsonarrnull(arr)

append a null element to a JSON array

arrthe array node to append to

returns void

jsonArrNull(arr);
jsonarradd(arr, node)

append a node (object/array) as the next array element

Moves the node into the array's tree. Rejects (reports + drops) a node that already belongs to a container -- a json node lives in exactly one parent.

arrthe array node to append to
nodethe object/array node to append (must be freshly built, unparented)

returns void

jsonArrAdd(els, e);
jsonaddnode(obj, key, node)

nest a node (object/array) under an object key

The object counterpart of jsonArrAdd. Moves the node in and sets its key. Rejects an already-parented node.

objthe object node to add to
keythe member key for the nested node
nodethe 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.

objthe object node to add to
keythe member key
vthe 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.

objthe object node to add to
keythe member key
vthe 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.

objthe object node to add to
keythe member key
vthe string value

returns void

jsonAddStr(mE, "axis", "ENERGY");
jsonaddbool(obj, key, v)

append a boolean member to a JSON object

objthe object node to add to
keythe member key
vthe 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

objthe object node to add to
keythe member key

returns void

jsonAddNull(obj, "note");
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.

objthe object node
keythe member key
vthe 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).

objthe object node
keythe member key
vthe 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.

objthe object node
keythe member key
vthe 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 the VALUE (scalar-by-value), not the handle. A nested object/array source is not supported (would alias) and is reported rather than corrupting.

objthe destination object node
keythe destination member key
srcthe source json value node to copy from

returns void

jsonSetNode(dst, "hp", src);
jsonsetelemnum(arr, idx, v)

store a float into arr[idx], growing the array to reach it

The array twin of jsonSetNum. Any gap between the array's current end and `idx` is filled with real json nulls, so a sparse store is expressible rather than dropped. An existing element is overwritten in place.

arrjson array (a document handle is deref'd)
idxzero-based index; negative is refused loudly with no write
vthe float value to store
jsonSetElemNum(scores, 3, 9.5);
jsonsetelemint(arr, idx, v)

store an int64 into arr[idx], growing the array to reach it

The int-exact lane of jsonSetElemNum: an int-born element round-trips past 2^53 without rounding through a double.

arrjson array (a document handle is deref'd)
idxzero-based index; negative is refused loudly with no write
vthe integer value to store
jsonSetElemInt(ids, 0, 9007199254740993);
jsonsetelemstr(arr, idx, v)

store a string into arr[idx], growing the array to reach it

arrjson array (a document handle is deref'd)
idxzero-based index; negative is refused loudly with no write
vthe string value to store
jsonSetElemStr(names, 2, "cherry");
jsonsetelemnode(arr, idx, src)

copy another json value into arr[idx] by its TYPE

The array twin of jsonSetNode, and it exists for the same reason: a json subscript read yields a node HANDLE, so storing it with jsonSetElemNum would write the handle as a number. Scalars copy by value; a nested object/array reports rather than aliasing two parents onto one subtree.

arrjson array (a document handle is deref'd)
idxzero-based index; negative is refused loudly with no write
srcthe json value node to copy
jsonSetElemNode(out, 0, doc["price"]);
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`.

objthe object node
keythe member key
opa one-character op string: "+" "-" "*" "/" "%"
rhsthe 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.

objthe object node
keythe member key
opa one-character op string: "+" "-" "*" "/" "%"
rhsthe 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.

objthe object node
keythe member key
opa one-character op string -- only "+" (concatenate)
rhsthe 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", "+", "!");
jsonexport(node)

serialize a JSON value to a compact string

Machine output (no whitespace); int-born numbers emit exact digits. Derefs a DOC wrapper.

nodeany json value node or doc

returns the compact JSON text as a string handle; empty for an invalid handle

exported = jsonexport(built);
jsonexportpretty(node)

serialize a JSON value to a pretty, 2-space-indented string

Objects/arrays break and indent, empty containers stay on one line, each member/element re-emits any leading comment captured at parse, and a trailing newline is added. Used by jsonBind's flush.

nodeany json value node or doc

returns the pretty-printed JSON text as a string handle; empty for an invalid handle

string s = jsonExportPretty(doc);
jsonload(path)

read a file and parse it into a JSON document

Batteries-included sugar: fread + jsonParse in one call.

paththe 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

jsonExport + fwrite in one call.

paththe destination file path
jthe 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

jsonExportPretty + fwrite in one call.

paththe destination file path
jthe 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.

viewa file-bound json view handle from jsonBind

returns void

jsonFlush(gBindings);
jsondirty(view)

test whether a file-bound JSON view has unsaved changes

viewa 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

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

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

paththe JSON file path to bind
policy0 = READONLY, 1 = LAZY-WRITE
subpathoptional 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");
jsonadd(node, key, value)

append a string value to a JSON node

The same operation as jsonAddStr. It used to be a silent no-op, which is why example programs built documents that came out empty; it is real now.

nodethe array or object node
keythe key, or an empty string for an array append
valuethe string value

returns the result of the underlying add

jsonAdd(arr, "", "sword");
jsondumparray()

render a JSON array back to text

STUBBED: answers an empty string without rendering anything. Use jsonDump, which is the real serialiser -- this name is listed only because the compiler still accepts it.

returns an empty string, always

s.s = jsonDump(doc);
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.

mapthe flat map from parseFullJson
prefixthe dotted key prefix
outa 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.

paththe JSON file to read
mapthe string map to fill

returns non-zero on success; 0 on failure

parseFullJson("save.json", m);
parsejson()

parse a JSON string

STUBBED: answers 0 without parsing anything. Use jsonParse, which is the real parser -- this name is listed only because the compiler still accepts it.

returns 0, always

doc = jsonParse(src);

xml 30 builtins

xmlparse(src)

parses an XML string into a document and returns its handle

Wraps the parsed root element in a DOC node. Supports element tags, attributes, text content, self-closing tags, nested children and sibling iteration; skips XML declarations/comments/DTD; no CDATA/entities.

srcthe XML source text to parse

returns int document handle, or -1 on parse failure

doc  = xmlparse("<book><title>X</title><author>Alice</author></book>");
xmlparsemf(mfh)

parses XML straight out of a memfile handle with no copy

Latches directly onto the memfile's bytes (MFS Layer 1), mirroring jsonparsemf.

mfha memfile handle whose bytes hold the XML source

returns int document handle, or -1 on a bad memfile handle or parse failure

xdoc = xmlparsemf(xmf);
xmlfree(doc)

destroys an XML document now, reclaiming its whole node subtree

The direct/legacy entry point: drops the document's ARC resource bucket then frees the tree. Out-of-range handles are a no-op.

docthe document handle to free

returns void

xmlfree(doc);
xmldecref(doc)

drops one ARC reference on an owned xmldoc, reclaiming it at refcount zero

Codegen-emitted at scope exit for an owned `xmldoc` local (CXB_RESOURCE bucket model); not intended for direct CX use. Out-of-range handles are a no-op.

docthe document handle to decref

returns void

xmldecref(doc);
xmllivecount()

returns the number of currently-open XML document handles

Counts in-use DOC nodes only (not raw pool occupancy); an always-available regression signal for scope-exit auto-free, mirroring mflivecount.

returns int count of live XML documents

int before = xmllivecount();
xmlroot(doc)

return the root element of a document, or 0 if it has none yet

A READ, so it stays quiet: an empty document genuinely has no root, and 0 is the correct answer to a fair question -- not an error, and not something to vivify (a root would need a name this call does not have). One meaning on parsed and built documents alike: the root element. To BUILD a document, add the root to the DOCUMENT itself with xmlAddNode -- appending to an empty document is what makes a root. Do not route a build through xmlRoot(); on a fresh document it answers 0, and every write through that 0 is refused loudly.

docthe document handle

returns the root element node handle, or 0 if there is no root yet

root.i = xmlroot(doc);
xmlname(node)

returns an element node's tag name

nodethe element node handle

returns string tag name, or empty string if the node handle is invalid

assertStringEqual("book", xmlname(root));
xmltext(node)

returns a node's text content

Text is whitespace-trimmed (leading/trailing) at parse time.

nodethe element node handle

returns string text content, or empty string if the node handle is invalid

assertStringEqual("X",     xmltext(kid));
xmlchild(node)

returns a node's first child element

nodethe element node handle

returns int first-child node handle, or 0 if none or the handle is invalid

kid = xmlchild(root);
xmlnext(node)

returns a node's next sibling element

nodethe element node handle

returns int next-sibling node handle, or 0 if none or the handle is invalid

kid = xmlnext(kid);
xmlattr(node, key)

returns the value of a node's named attribute

Linear-scans the node's attribute list for a key match.

nodethe element node handle
keythe attribute name to look up

returns string attribute value, or empty string if missing or the node handle is invalid

assertStringEqual("42",    xmlattr(root2, "id"));
xmladdnode(parent, name)

create a named child element under a parent and return it

The parent may be an element OR a DOCUMENT: appending to an empty document is what makes a root element, so this is how a built document gets its root. There is no separate "add the root" call and no special case in the code. A WRITE, so a dead parent handle is REFUSED LOUDLY on stderr and nothing is appended (FP4) -- it does not silently return 0 and let the next four statements run on the result.

parentthe parent element, or the document handle to add a root to
namethe tag name for the new element

returns the new element node handle, or 0 if the parent handle was dead or the node pool was exhausted (both reported loudly)

doc.i = xmlcreate();
root.i = xmladdnode(doc, "person");
xmlsettext(node, text)

set a node's text content

Replacing text takes ownership of the new string and releases the old. A WRITE, so a dead node handle is REFUSED LOUDLY on stderr and no text is stored (FP4).

nodethe element node handle
textthe text content to set

returns nothing

xmlsettext(kid, "Alice");
xmlsetattr(node, key, val)

set or update a named attribute on a node

If the key already exists its value is replaced; otherwise a new attribute is added. A WRITE, so a dead node handle is REFUSED LOUDLY on stderr and no attribute is set (FP4).

nodethe element node handle
keythe attribute name
valthe attribute value

returns nothing

xmlsetattr(root, "version", "1.0");
xmlexport(node)

serializes a node's subtree back to an XML string

Recursively emits tags, attributes, text and children; elements with no children and no text self-close. Exporting a DOC node emits its element children.

nodethe node (or document) handle to serialize

returns string XML serialization, or empty string if the handle is invalid

xs = xmlexport(xh);
xmladdchild(parent, name)

create a named child element under a node

parentthe node to add under
namethe new element's tag name

returns a handle to the new child element

row = xmlAddChild(doc, "row");
xmladdtext(node, text)

set a node's text content

nodethe element
textthe text to set

returns the result of the underlying set

xmlAddText(row, "hello");
xmlchildren(node)

count or list a node's children (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlChildren(node);
xmlcount(node)

count a node's children (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlCount(node);
xmlcreate()

create a new, empty XML document and return its handle

The document starts with NO root element. Add one by adding a node to the DOCUMENT itself -- appending to an empty document is what makes a root -- and grow the tree from the handle that returns. Do NOT build through xmlRoot(): on a fresh document it correctly answers 0 (there is no root yet), and every write through that 0 is refused loudly. xmlRoot() is for reading a document you already have.

returns the new document handle

doc.i = xmlcreate();
root.i = xmladdnode(doc, "person");
xmlsetattr(root, "version", "1.0");
println(xmlexport(doc));
xmldepth(node)

report how deep a node sits (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlDepth(node);
xmlfind(node)

search for a node by path or name (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlFind(node);
xmlhas(node)

test whether a node or attribute exists (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlHas(node);
xmlparent(node)

walk up to a node's parent (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlParent(node);
xmlprev(node)

walk to a node's previous sibling (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlPrev(node);
xmlremove(node)

remove a node from its parent (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlRemove(node);
xmlsetname(node)

rename a node (not implemented)

NOT IMPLEMENTED, and it says so: the call raises a loud `not implemented` error rather than the silent 0 or no-op it used to give. The name is accepted so that a program using it stops at the call instead of quietly computing on nothing (FP4). Parsing, element creation, text and attributes all work -- it is the traversal and edit half that is missing.

nodethe node

returns never returns normally -- the call raises a loud not-implemented error

xmlSetname(node);
xmlsetvalue(node, value)

set a node's text content

The same operation as xmlAddText, under the name that reads as a setter.

nodethe element
valuethe text to set

returns the result of the underlying set

xmlSetValue(row, "hello");
xmltag(node)

read a node's tag name

NATIVE ONLY -- an alias of the node-name accessor with no register-VM binding, so a VM build refuses it at compile time with CX-E1013.

nodethe element

returns the tag name

s.s = xmlTag(row);
xmlvalue(node)

read a node's text content

nodethe element

returns the node's text

s.s = xmlValue(row);

memory files 15 builtins

embedref(index)

resolve an embedded blob's handle

Turns the compiler's blob index into a run-time handle, cached after the first call.

indexthe blob index the compiler assigned

returns the blob handle

h = embedRef(0);
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.

handlethe embedded blob's handle

returns the blob's length in bytes

n = embedSize(logo);
mfdecref(handle)

drop one owning reference to a memory file

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 at scope exit for an owned `memfile` local (the resource ARC model). You do not normally write this -- mfFree is the explicit release.

handlethe memory file

returns void

// emitted at scope exit
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.

handlethe memory file

returns void

mfFree(h);
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.

handlethe memory file
sthe bytes to insert

returns 1 on success; 0 otherwise

mfInsert(h, "prefix ");
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.

handlethe memory file
paththe file to splice in

returns the number of bytes inserted, or -1 on failure

mfInsertFile(h, "part.txt");
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();
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.

paththe file to read

returns a handle to the new memory file; 0 on failure

h = mfLoad("in.txt");
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();
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.

handlethe memory file

returns the cursor offset in bytes

n = mfPos(h);
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.

handlethe memory file
sthe bytes to write

returns the file's new size in bytes; 0 for a bad handle

mfPuts(h, "hello");
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.

handlethe memory file
paththe file to write

returns 1 on success; 0 on failure

mfSave(h, "out.txt");
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.

handlethe memory file
offsetbyte offset from the start

returns 1 on success; 0 for a bad handle or a negative offset

mfSeek(h, 0);
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.

handlethe memory file

returns the used byte count

n = mfSize(h);
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.

handlethe memory file

returns the buffer's contents

s.s = mfToStr(h);

Input/output & filesystem 2 families · 29 builtins

files & filesystem 18 builtins

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

pathpath 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"); }
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.

pathfile 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");
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".

pathpath 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".

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

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

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

oldpexisting path; empty returns 0
newpnew 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.

srcpfile to read; empty returns 0
dstpfile 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.

srcpfile to move; empty returns 0
dstpdestination path; empty returns 0

returns 1 on success, 0 if both the rename and the copy failed

movefile("out.log", "archive/out.log");
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.

pathdirectory to scan; an empty path means the current directory
patternglob to match entry names against; empty means everything
namesstring 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);
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";
fappend(path, content)

append text to a file, creating it if it does not exist

The append half of fwrite -- fappend(p, c) is exactly fwrite(p, c, 1). It extends the file in place rather than staging a temporary copy, so a short write cannot be rolled back, but it is still reported.

paththe file to append to
contentthe text to add at the end

returns 1 on success, 0 on failure. NOT a byte count

ok.i = fappend("run.log", stamp + " started\n");
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.

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

pathfile to write; an empty path is a no-op returning 0
contentbytes to write; may be empty, which truncates the file to zero
append0 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.

vthe 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, with two CX adjustments: %d carries a 64-bit integer (the compiler rewrites it upstream), and float output honours `#pragma decimals`. sprintf is the same formatting returned as a string instead of printed.

fmtthe format string

returns void

printf("%s: %d%%\n", name, pct);
println(v)

write a value to stdout followed by a newline (the other name for print)

Identical to print; both append the newline.

vthe value to write

returns void

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.

chthe character code to write

returns the code written, as C's putchar does

putc(65);

compression & archives 11 builtins

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.

outdestination archive path to write
srcsource 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.

outdestination archive path to write
srcsource 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.

outdestination archive path to write
srcsource 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.

outdestination archive path to write
srcsource 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.

outdestination archive path to write
srcsource 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.

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

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

outthe archive path to write
formatone of "7z", "zip", "tar", "tar.gz", "tar.bz2", "tar.zst", "tar.xz"
srcthe file or directory to pack

returns 0 on success; -1 on error, including a build without libarchive

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.

inthe archive to read
destdirthe directory to extract into

returns 0 on success; -1 on error, including a build without libarchive

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.

srcthe bytes to encode

returns the base64 text

auth.s = "Basic " + base64Enc(user + ":" + pass);
base64dec(src)

decode base64 text back to bytes

srcbase64 text, with or without padding

returns the decoded bytes; an empty input gives an empty string

Networking 2 families · 32 builtins

network 21 builtins

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.

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

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

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

hosthostname or literal IP address
portTCP 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.

handlea server or connection handle
timeout_ms0 = 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.

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

handlea connection handle
datathe 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.

handlea server or connection handle

returns 0 on success, < 0 on failure (netError() has the detail)

netClose(c);
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.

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

urlthe URL to post to
bodythe request body, sent as-is

returns the response body, or an empty string on failure

reply.s = httpPost(url, jsonDump(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.

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

urlan 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

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

urlan 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

urlthe destination ftp:// or ftps:// URL, including the remote filename
localpaththe 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)

urlthe destination sftp:// URL, including the remote filename
localpaththe 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.

smtpurlthe server URL, e.g. "smtp://mail.example.com:587"
fromthe sender address
tothe recipient address
subjectthe subject line
bodythe 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.

hostthe host name or address
userthe remote user name
passthat user's password
cmdthe 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.

hostthe host name or address
userthe remote user name
keyfilepath to the OpenSSH or PEM private key
cmdthe 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");

http 11 builtins

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.

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

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

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

connthe connection httpRead was called on
namethe 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 "".

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

connthe connection to answer on
statusthe HTTP status code, e.g. 200, 404, 500
ctypethe Content-Type, e.g. "text/plain", "application/json"
bodythe 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.

connthe connection to answer on
status301, 302, 303, 307 or 308
urlthe 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.

connthe connection to answer on
ctypethe 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).

conna connection httpStream opened
datathe 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.

connthe connection httpRead was called on
mountthe URL prefix to serve, e.g. "/" or "/static"
dirthe 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.

originthe 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");

Date & time 1 family · 25 builtins

date & time 25 builtins

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.

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

dweekday number, 0..6 with 0 = Sunday

returns "Sun".."Sat", or the string "?" when d is out of range

println(dayname(0));
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());
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;
day(packed)

the day of the month, 1..31

No argument reads today's; a `date()`-style YYYYMMDD decodes that one.

packeda YYYYMMDD value to decode; omit for today

returns the day of the month, 1..31

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.

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

packeda YYYYMMDD value; omit for today

returns the day of the year, 1..366

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.

t0a baseline from `timer()`/`elapsed()`; omit for since-start

returns milliseconds

t0.i = elapsed(); load(); printf("%d ms\n", elapsed(t0));
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);
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_daya `time()`-style value to decode; omit for now

returns the hour, 0..23

if hour() >= 18 { greet.s = "good evening"; }
microseconds()

the epoch time in MICROSECONDS

A DIFFERENT CLOCK FROM ITS NAME'S NEIGHBOURS: this is `now()` scaled by a million, so it carries epoch time with microsecond UNITS but only one-second RESOLUTION -- the low six digits are always zero. For real microsecond timing use `elapsedus`.

returns seconds since the epoch, times 1000000

milliseconds()

the same monotonic millisecond counter as `timer`

returns milliseconds since the program started

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_daya `time()`-style value to decode; omit for now

returns the minute, 0..59

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.

packeda YYYYMMDD value to decode; omit for the current month

returns the month, 1..12

now()

the current time as SECONDS SINCE THE UNIX EPOCH

The same value as `timestamp`, `seconds` and `ticks` -- four spellings of one clock. Not the one to measure a duration with: `elapsed` and `timer` are monotonic and millisecond-resolution, 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();
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_daya `time()`-style value to decode; omit for now

returns the second, 0..59

seconds()

seconds since the Unix epoch; the same clock as `now`

returns seconds since 1970-01-01 UTC

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.

sseconds to wait

returns void

sleep(1);
sleep_ms(ms)

block this program for `ms` MILLISECONDS

msmilliseconds to wait

returns void

sleep_ms(16);
ticks()

seconds since the Unix epoch; the same clock as `now`

The name suggests a fine-grained counter and it is NOT one: it ticks once per second. `timer` is the millisecond counter.

returns seconds since 1970-01-01 UTC

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();
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));
timestamp()

seconds since the Unix epoch; the same clock as `now`

returns seconds since 1970-01-01 UTC

wallclock()

PROCESSOR time used by this program, in seconds, as a float

Despite the name this is C's `clock()`, so it measures CPU consumed, NOT time passed: a program that spends a minute waiting on the network reports almost nothing. Use `elapsed` or `timer` for real elapsed time.

returns CPU seconds consumed so far

cpu.f = wallclock();
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.

packeda YYYYMMDD value to decode; omit for the current year

returns the year, e.g. 2026

printf("%d\n", year(20260806));

Diagnostics & system 4 families · 64 builtins

debugging & assertions 34 builtins

watchregister(nIdx, sName, sSite, nKind, nTrigger, nRelay, nFn, nIsNative)

declare one watch SITE, at program start

Emitted once per (variable, function) pair the compiler instrumented, from the program's prologue -- his "init at the end when we know all the resolved values" applied to watches: every function is known by then, so a handler reference resolves to a direct value here and never to a name lookup at a write.

nIdxthe site index the emitter assigned (dense, from 0).
sNamethe watched variable, spelled as the author wrote it.
sSitethe function the instrumented writes live in ("main" for module-level statements) -- the site half of the key.
nKindCX_WATCH_INT / CX_WATCH_FLOAT / CX_WATCH_STRING.
nTrigger0 = onchange; N > 0 = report at most once every N ms.
nRelay0 = deliver every trigger; N > 0 = deliver one report per N triggers, carrying how many it stands for. A relay throttles DELIVERY only -- the trigger stays complete.
nFnthe handler: a C address (native) or a bytecode function index (VM). 0 with nIsNative = 1 means "no handler, use the side channel"; the VM passes -1 for that, because 0 is a valid function index there -- the same sentinel split the error door makes for the same reason.
nIsNative1 = nFn is a C address, 0 = a VM function index.
watchRegister(0, "hull", "takedamage", 0, 500, 0, 0, 1);
watchhiti(nIdx, nVal, nCond)

an INT write reached an instrumented site

nIdxthe site index.
nValthe value AFTER the write -- so a condition written over the variable reads the new value, which is what a debugger means by a watch.
nCondthe author's condition, already evaluated by the emitted code (1 when they wrote none). It is WRITTEN as a string and COMPILED as an expression, so a typo in it is a compile error on that line rather than a watch that silently never fires.
watchHitI(0, hull, hull > 5);
watchhitf(nIdx, fVal, nCond)

a FLOAT write reached an instrumented site

nIdxthe site index.
fValthe value after the write.
nCondthe evaluated condition (1 when none was written).
watchHitF(1, angle, angle > 3.0);
watchhits(nIdx, sVal, nCond)

a STRING write reached an instrumented site

nIdxthe site index.
sValthe value after the write.
nCondthe evaluated condition (1 when none was written).
watchHitS(2, name, len(name) > 0);
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.

condthe condition that must hold
msgtext 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.

expectedthe value the code should produce
actualthe value it did produce
msgtext 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.

expectedthe text the code should produce
actualthe text it did produce
msgtext 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.

expectedthe value the code should produce
actualthe value it did produce
msgtext 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.

athe first value
bthe second value
msgtext 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.

expectedthe text the code should produce
actualthe text it did produce
msgtext identifying the assertion in the output

returns void

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();
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.

textthe text to append

returns void

console("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

consoleClear();
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.

titlethe terminal window's title

returns void

consoleAttach("cx trace");
consoledetach()

close the attached terminal and stop streaming

returns void

consoleDetach();
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.

textthe line to append

returns void

consoleLn("tick");
cx_str_peek_ring(handle)

borrow a string's bytes as a C pointer valid for the current statement

AN INTERNAL HELPER, not everyday CX: it hands out a pointer into the string-handle ring buffer, which stays valid only through the surrounding statement. The console family uses it to reach a C string without copying. Storing the pointer is a use-after-free waiting to happen.

handlethe string

returns a C pointer to the bytes, valid for this statement only

// used internally by console()/consoleLn()
debug_render(x, y, w, h)

draw the captured debug buffer as an in-game overlay panel

NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op. The live alternative to the pop-up-on-exit window: pair it with a key toggle to show and hide the log during play. A no-op in a headless build.

xpanel left
ypanel top
wpanel width
hpanel height

returns void

debugRender(10, 10, 400, 200);
debugclose()

stop capturing and send output back to stdout

The Orfeus-era spelling of closeDebug.

returns void

debugClose();
debuglevel(level)

set a verbosity level for debug output

ACCEPTED AND DOES NOTHING: an Orfeus-era hook with no equivalent in the current debug console. Listed so you know it is a no-op rather than assuming it took effect. There is no level filtering yet.

levelignored

returns void

debugLevel(2);
debuglog(text)

log a line into the captured debug buffer

The canonical "log a line" call: the Orfeus-era spelling of consoleLn, writing with a trailing newline. Once consoleAttach has opened a real terminal, these lines stream there LIVE as well.

textthe line to log

returns void

debugLog("entered room " + str(n));
debugopen(title, w, h)

start capturing output into a debug console window

The Orfeus-era spelling of openDebug, same runtime and same forms. 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.

titlewindow title (optional)
wwindow width (optional)
hwindow height (optional)

returns void

debugOpen("trace");
debugprint(text)

write text straight into the captured buffer

The Orfeus-era spelling of console -- no trailing newline.

textthe text to append

returns void

debugPrint("tick");
debugrender(x, y, w, h)

draw the captured debug buffer as an in-game overlay panel

The Orfeus-era spelling of debug_render. NATIVE ONLY -- the register VM cannot drive a window, so a call under `-P pcode=risc` (or in the browser, which runs the same VM) is refused at compile time with CX-E1013 advising a native build, never a silent no-op.

xpanel left
ypanel top
wpanel width
hpanel height

returns void

debugRender(10, 10, 400, 200);
debugtitle(title)

change the debug window's title

ACCEPTED AND DOES NOTHING: an Orfeus-era hook with no equivalent in the current debug console. Listed so you know it is a no-op rather than assuming it took effect. The title is fixed when the window opens.

titleignored

returns void

debugTitle("new");
debugupdate()

refresh the debug console

ACCEPTED AND DOES NOTHING: an Orfeus-era hook with no equivalent in the current debug console. Listed so you know it is a no-op rather than assuming it took effect. The panel redraws on the next frame anyway.

returns void

debugUpdate();
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());
memaudit(…)

dump a memory audit

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.

NOT IMPLEMENTED: a stub that does nothing on both backends. Listed because the compiler accepts it. Use `#pragma checks on` for the real leak walk.

returns void

memAudit();
memused(…)

report process memory in use

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.

NOT IMPLEMENTED: a stub that answers 0 on both backends. Listed because the compiler accepts it -- an absence you can see beats one you discover at run time. Use gcStats for real allocator figures.

returns 0, always

n = memUsed();
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.

titlewindow title (optional)
wwindow width (optional)
hwindow height (optional)

returns void

openDebug("trace", 900, 600);
prtc(ch)

write one CHARACTER to stdout with no newline, by code

The character sibling of prts, taking a numeric code as putc does.

chthe 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`.

vthe float to write

returns void

prti(n)

write an INTEGER to stdout with no newline

The int sibling of prts.

nthe 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

system info 17 builtins

argc()

return the command-line argument count

Reads the captured g_cx_argc set at program start; the count includes argv(0), the program path.

returns number of command-line arguments (including the program path at index 0)

if (argc() > 1) {
argv(n)

return the n-th command-line argument as a string

Reads the captured g_cx_argv; argv(0) is the program path and argv(1) is the first user argument. Returns an empty string for an out-of-range index.

nargument index (0 = program path, 1 = first user argument)

returns the n-th argument as a string; "" if n is out of range

root = argv(1);
argvptr()

the process argument VECTOR itself, as a pointer-sized integer

The raw `char **argv` the process was started with -- the same value C's own main receives, not a copy -- so a CX program can hand its real command line on to a C function through `foreign` natively or `libcall` on the VM. Everything a program wants to READ is already in argc() and argv(n); this is for passing the vector ON. IT IS ALSO WHAT MAKES `int main(int argc, char** argv)` RUN ON BOTH BACKENDS (v3.312.0, D31). Native forwards the C main's own argv; the register VM has no C main to forward from, so the emitter fills that second parameter from here. One value, one meaning, both backends -- and the pointer is REAL rather than a zero standing in for it, which is the difference between "it runs" and "it is right". 0 before cx_rt_init has run, and 0 in any host that started the runtime with no command line: a shared library entered through an export, an embedded VM, or a program whose own main took the entry and has not handed it over. That is the honest answer -- there is no vector to point at.

returns the argv pointer as an integer, or 0 if the process has no vector

p.i = argvptr();   // hand it to a C function that takes char**
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.

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

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

seedany integer; the same seed always replays the same stream

returns void

randomseed(2026);   // this run is now repeatable
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

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.

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

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

cmdthe command line

returns the command's exit code

rc = system("git status");

process control 10 builtins

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

cmdprogram and arguments; spaces separate, double quotes group
cwdworking directory for the child; "" keeps the parent's
envenvironment 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.

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

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

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

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

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

ha handle from procSpawn
sthe 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.

ha 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).

ha handle from procSpawn

returns 1 if released, 0 if refused because the child is still running

if (procAlive(h) == 0) { procClose(h); }

error handling 3 builtins

onerrorinstall(nFn, nIsNative)

install (or uninstall) THE universal error handler

One handler at a time: a second install replaces the first, silently and by design -- a handler STACK is a separate ruling, not a v1 default. Write `onerror(&myErrors);` in CX, never this. The compiler lowers that spelling to this call because the two backends carry a function reference differently, and it is the EMITTER that knows which (rule 20): native passes the handler's C address with nIsNative=1, the register VM passes its bytecode function index with nIsNative=0. The flag is not redundant with the house `cx_vm_func_hook != NULL` probe: a NATIVE program that loads a VM module publishes that hook too, and would then dispatch a C address as if it were a bytecode index.

nFnthe handler: a C function address (native) or a bytecode function index (VM). A NEGATIVE value UNINSTALLS -- which is what `onerror()` with no argument lowers to. Zero is NOT the uninstall sentinel, because 0 is a valid VM function index.
nIsNative1 = nFn is a C address, 0 = nFn is a VM function index.
onerror(&myErrors);   // install
onerror();            // uninstall
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.

nCodethe 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.
sNotethe message, handed to the handler unchanged.
errorRaise(7, "config file had no [server] section");
onerrordefault(nOn)

turn the DEFAULT error handler off (or back on)

The default handler exists WITHOUT any registration -- his "batteries included, like ticks()": every program already reports its failures to the side channel as a structured record, with the code, the message, the source position and the function it happened in. Nothing is written unless $CX_SIDE_CHANNEL names a destination, so stderr is byte-for-byte what it has always been and no golden in the tree moves; under the screen (v3.307.0) the session claims the channel and the record lands in it with no code change. `#pragma onerrordefault off` lowers to this, and turns the record off.

nOn0 = off, 1 = on (the default).
#pragma onerrordefault off

Runtime & VM 1 family · 11 builtins

vm 11 builtins

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

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

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

handlemodule handle returned by vmLoad/vmLoadStr
namefunction 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.

handlemodule handle returned by vmLoad/vmLoadStr
namename of the function to invoke
jsonargjson 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.

handlemodule handle returned by vmLoad/vmLoadStr
nloop-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.

handlemodule handle returned by vmLoad/vmLoadStr

returns void

vmUnload(h);
libcall(fn, ...)

call a foreign function whose arguments and result are integers, pointers or strings

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.

Loads a native shared library at run time -- .dll on Windows, .so on Linux, .dylib on macOS -- and calls into it. Plain C, no JIT and no runtime code generation, so the CALL path works on every platform CX runs on. These all fit the platform's integer-register convention. A signature that MIXES integer and floating-point arguments is routed through a portable ABI thunk -- no assembly and no libffi.

fnthe address from libSym
...the arguments

returns the function's integer result

r = libCall(f, "hello");
libcallf(fn, ...)

call a foreign function whose arguments and result are all floating-point

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.

Loads a native shared library at run time -- .dll on Windows, .so on Linux, .dylib on macOS -- and calls into it. Plain C, no JIT and no runtime code generation, so the CALL path works on every platform CX runs on. A separate entry point because floating-point uses a DIFFERENT register file from integers in both the Win64 and SysV ABIs -- one call shape cannot serve both.

fnthe address from libSym
...the floating-point arguments

returns the function's floating-point result

r = libCallF(f, 2.0);
libclose(lib)

release an open library

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.

Loads a native shared library at run time -- .dll on Windows, .so on Linux, .dylib on macOS -- and calls into it. Plain C, no JIT and no runtime code generation, so the CALL path works on every platform CX runs on.

libthe handle from libOpen

returns void

libClose(lib);
libopen(path)

load a native shared library by path or name

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.

Loads a native shared library at run time -- .dll on Windows, .so on Linux, .dylib on macOS -- and calls into it. Plain C, no JIT and no runtime code generation, so the CALL path works on every platform CX runs on. IN A BROWSER TAB THIS IS REFUSED at the door: emscripten's dlopen loads its own side modules, not a host library, so libOpen stops with CX-E5037 naming the capability rather than handing back a NULL that would surface as a wasm trap five steps later. On Windows a bare name like `msvcrt.dll` is searched on the standard DLL path.

pathlibrary path, or a bare name to search the loader path

returns a library handle; 0 on failure

lib = libOpen("msvcrt.dll");
libsym(lib, name)

resolve a symbol in an open library to a callable address

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.

Loads a native shared library at run time -- .dll on Windows, .so on Linux, .dylib on macOS -- and calls into it. Plain C, no JIT and no runtime code generation, so the CALL path works on every platform CX runs on.

libthe handle from libOpen
namethe exported symbol name

returns the symbol's address; 0 if it is not exported

f = libSym(lib, "puts");