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.
prompt | the 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.
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.
id | the 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.
id | the 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.
id | the 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.
key | the cache key to look up |
returns the cached reply, or an EMPTY STRING on a miss -- a stored empty string is therefore indistinguishable from absence
v.s = 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.
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.
key | cache key (typically the prompt) |
value | the reply text to store |
ttl | accepted for API compatibility and ignored -- there is no expiry |
returns void
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.
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.
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.
name | the AI function's name |
arg | one 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.
id | the 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.
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.
prompt | the 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.
prompt | the 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.
name | the 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.
name | the 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.
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.
name | the 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.
name | the declared slot name |
value | the value to store |
returns 1
aiNamedSet("hp", 42);ai_parse_bytecode(name, src)
assemble AI-authored bytecode text into a named callable function
name | the name to register the function under |
src | the bytecode source text |
returns the result of the assembler -- non-zero identifies the parsed function
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.
prompt | the 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.
id | the 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.
on | non-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.
on | non-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.
chain | comma-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.
key | the 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.
n | maximum 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.
model | provider-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.
name | the declared slot name |
value | the 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.
provider | provider 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.
prompt | the 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.
t | temperature, 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).
ms | timeout 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.
url | full 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.
id | the 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.
id | the 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
src | the marker id to copy from |
dst | the marker id to copy to |
returns non-zero on success
cloneMarker(1, 2);
code_size()
report how many bytecode scratch slots are addressable
Native AOT has no live bytecode -- the program IS native C -- so this family works over a growable scratch table, one entry per PC slot, which the register VM path delegates to as well so both backends agree. Writes grow the table on demand (capped by `#pragma MaxCodeScratch`); READS NEVER GROW IT. Seeded lazily, so a program that only ever reads still sees a non-zero window.
returns the live capacity of the scratch table (always > 0)
n = 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.
returns 0
deleteCode(1);
delmarker(marker)
drop a marker region
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.
task | a description of what the code should do |
marker | the marker id to fill |
returns non-zero on success
generateCode("clamp hp to 0..100", 1);getcode()
read back 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
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.
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.
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.
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.
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.
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.
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.
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.
addr | the PC slot to write |
value | the 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.
addr | the PC slot to write |
value | the 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.
addr | the PC slot to write |
value | the 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.
addr | the PC slot to write |
value | the 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.
addr | the PC slot to write |
value | the 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.
addr | the PC slot to write |
value | the value to store |
returns void
pokeN(0, 7);
replacecode(marker)
swap a marker region's body for the code most recently installed
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
name | the function name |
prompt | the 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.
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.
returns 0
rt_runAiFunc(id);
setcode(src)
install CX source text as the body of a codeswap marker region
src | the 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.
type | event type id to raise |
source | bound-object / json entity handle passed to handlers as the payload (0 = use the shared event context) |
returns void
raise(50, e);
airule(entity, schedule, trigger, prompt)
ask the AI to author a rule and register it on an entity, in one call
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. THE ONE-LINER THE PRODUCT IS ABOUT: an LLM query and a rule registration fused, so a sentence becomes autonomously running behaviour with no recompile. What the AI returns is a rule STRING -- data you can read, edit and save, not opaque code.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule |
prompt | what the behaviour should do, in plain language |
returns void
aiRule(e, 60, "1", "restock when stock falls below the reorder point");
arm(entity, preload)
bake an entity's persist lanes by running a preload rule once
The preload half of the rules ABI: it sizes the entity's typed lanes to the compiler's persist counts, runs `preload` ONCE so its writes land in those lanes, and stamps the source document's revision. Every later ruleExec on that entity then READS the baked lanes instead of recomputing. A fire whose source document has changed re-bakes at the fire boundary. Needs `#pragma rules c`.
entity | the entity's json handle |
preload | the preload rule source |
returns 1 when armed; 0 on failure -- and the entity stays UN-armed, so a later persist read faults loudly rather than reading a half-baked table
arm(e, "persist float th; th = 0.8;");
automationrun(entity, quitkey)
tick flat out until a named key on an entity goes non-zero
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The console equivalent of riding a graphics frame loop: hand over control in one line instead of writing the while. A rule flips the key when the work is done.
entity | the entity's json handle |
quitkey | the entity key a rule sets to stop the loop |
returns void
automationRun(e, "done");
automationtick()
advance the clock and fire every rule that is due and triggered
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The per-frame call. A graphics program gets this driven for it once the first rule is added.
returns void
automationTick();
eventctx()
the json object the dispatcher fills with the current event
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. A gate rule is evaluated against THIS -- `event["key"]`, `event["mx"]` and so on.
returns a json handle to the current event context
e = eventCtx();
eventfire(type)
dispatch an event through the registry
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. Every entry registered for the type is considered: its gate rule is evaluated against the event context, and the handler runs only if the gate passes.
returns the result of the dispatch
eventFire(1);
eventsettable(tbl)
replace the whole event table with a caller-parsed json array
The two-way, AI-authorable load: a program (or an AI) builds the handler table as json and installs it in one call, instead of accumulating it through OnEvent. HANDLER NAMES ARE CASE-INSENSITIVE, like every CX identifier: an entry's `"fn"` refers to a handler registered from a CX function name, and the scanner folds that name, so the table's string is folded to match. `"OnHit"`, `"onhit"` and `"ONHIT"` all name the same handler -- two handlers differing only by case cannot exist, so nothing legitimate is lost. EVERY `"fn"` IS RESOLVED HERE, AT LOAD. A name that matches no registered handler is a loud CX-E5033 and STOPS the run. It is checked at load rather than at first fire because an unresolvable handler used to fail by simply never running -- the quietest failure mode there is, and one that let a run start with a dead handler and look healthy (FP4). Entries with only a `"do"` rule carry no name and are not checked. The registry keeps `tbl` for the process lifetime and increfs it, so a table built inside a function survives that function's scope exit.
tbl | json array indexed by event type; each element an array of entries ({"gate": rule, "fn": handler-name} or {"gate": rule, "do": rule}). |
eventSetTable(jsonLoad("events.json"));eventtable()
the json array holding every event binding
The event registry IS json, which is what makes it two-way: load a binding set from a file, edit a binding while the program runs, save it back, or let an AI author it. Indexed by event type (1 keyboard, 2 mouse, 3 timer, 4 window; 4..255 are free for your own). Created on first use.
returns a json handle to the event table
t = eventTable();
funcadd(entity, schedule, trigger, fn)
register a compiled CX function on an entity, on the same schedule and trigger
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls. The fast, heavy path beside the data path -- the function is `function f.v(json e)` and receives the entity. **NATIVE ONLY**: native carries a function value as a C address, the register VM as a bytecode index, so on the VM this hard-errors rather than calling the wrong thing. Windowed games run native.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule |
fn | a `function f.v(json e)` |
returns void
funcAdd(e, 1, "1", &onTick);
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.
entity | the entity's json handle |
name | the persist lane's name |
index | element 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.
entity | the entity's json handle |
name | the persist lane's name |
index | element 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.
type | the event type code |
gate | a condition rule |
fn | the 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.
type | the event type code |
gate | a condition rule; "1" or empty means always |
rule | the handler rule to run when the gate passes |
returns void
onEventRule(1, "event[\"key\"] == 27", "quit = 1");
ruleadd(entity, schedule, trigger, rule)
register a rule on an entity, to run on a schedule behind a trigger
The automation engine, and the reason CX exists: an entity is a json document, a rule is a string, and behaviour is DATA you can load, edit, save and let an AI write. Registered behaviour runs on a SCHEDULE and behind a TRIGGER, so nothing polls.
entity | the entity's json handle |
schedule | how often it is due |
trigger | a condition rule; the rule runs only when this passes |
rule | the rule source text to run |
returns void
ruleAdd(e, 30, "hp < 20", "hp = hp + 5");
ruleauthmock(text)
queue a canned authoring response instead of calling the AI
The AI rule-authoring loop. While the queue is non-empty, ruleAuthor consumes it INSTEAD of contacting a provider -- one push per attempt, FIFO. What makes an authoring test deterministic, offline and free, and equally the seam for feeding rules from a source that is not an LLM.
text | the response to hand back for the next attempt |
returns void
ruleAuthMock("{ speed = speed / 2; }");ruleauthor(task)
ask the AI for one rule, validate that it compiles, and retry on a decline
The AI rule-authoring loop. The returned text is a `{ ... }` body over `json e`. A reply that does not compile in the rules subset is DECLINED and retried with the compile error appended, up to a bounded number of extra attempts. Every attempt and decline is recorded for ruleAuthStats.
task | the plain-language request |
returns the validated rule text; the EMPTY string on final failure -- loud, never a silent no-rule
body.s = ruleAuthor("halve speed below 20 hp");ruleauthreset()
clear the authoring feed, drain the mock queue and forget the fire warnings
The AI rule-authoring loop. Test isolation, or the start of a fresh authoring session.
returns void
ruleAuthReset();
ruleauthstats()
read the authoring loop's evidence feed as a json document
The AI rule-authoring loop. Returns attempts, declines, whether the final attempt succeeded, the last decline's class and reason, and a record per attempt. The class is a stable string -- `pointer`, `call-denied`, `arity`, `struct`, `nonscalar`, `foreach-colon`, `syntax`, `empty`, `other` -- so a test can assert WHY the model was refused, not merely that it was.
returns a NEW json handle you own
s = ruleAuthStats();
rulecachecount()
how many distinct rule strings are currently compiled and cached
The cache-effectiveness instrument: compiling N distinct rules M times should cache N. Use it to prove a hot loop is not recompiling.
returns the number of cached rule programs
n = ruleCacheCount();
ruleexec(entity, rule)
run a rule against an entity and return its value
The explicit one-shot path, beside the registered one. **COMPILE-CACHED**: the same rule string compiles ONCE, content-keyed for the life of the process, and runs cached after that -- so calling it in a loop does not recompile. Member writes to `e` PERSIST on the bound document.
entity | the entity's json handle |
rule | the rule source text |
returns the rule's `return` value as a float; a rule returning a string reads as 0
v = ruleExec(e, "return hp * 2;");
rulefire(entity, entry)
run one rule entry, using its compiled form if it has been promoted
The dispatcher that honours promotion. Given a json rule entry `{ name, body, promoted }`: if it is marked promoted AND its name is registered, the COMPILED function runs with no recompile of the text; otherwise the body runs through the cached text path. A promoted mark with no registered function -- promote ran, the rebuild did not -- falls back to text AND warns LOUDLY once per rule name: graceful for the player, visible for the developer.
entity | the entity's json handle |
entry | a json rule entry { name, body, promoted } |
returns the rule's value as a float, on both paths
v = ruleFire(e, entry);
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.
name | the handler name as the table spells it |
fn | the 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.
rules | a json array of { "name": <ident>, "body": "{ ... }" } entries |
path | the .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");
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.
x | left position in pixels |
y | top position in pixels |
w | width in pixels |
h | height in pixels |
returns int 1-based grid handle, 0 = failure
g.i = gpu_grid_new(60, 96, 840, 600);
gpu_grid_free(g)
unconditionally destroy a grid handle now, freeing all its storage
Drops the ARC bucket directly then reclaims cells, columns, sections and text handles.
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.
returns void
gpu_grid_decref(g);
gpu_grid_livecount()
return the number of currently-open grid handles
Leak/regression-test signal (mirrors cxmf_live_count); a leaked grid also shows in the generic checks leak walk.
returns int count of live grid handles
int before = gpu_grid_livecount();
gpu_grid_bounds(g, x, y, w, h)
reposition and resize an existing grid to x,y,w,h
g | grid handle |
x | new left position in pixels |
y | new top position in pixels |
w | new width in pixels |
h | new height in pixels |
returns void
gpu_grid_bounds(insp, 10, topY, w - 20, iH);
gpu_grid_cols(g, n)
set the grid's column count, reallocating cell storage
n must be > 0; also (re)allocates the initial row buffer.
g | grid handle |
n | number of columns |
returns void
gpu_grid_cols(g, 6);
gpu_grid_rows(g, n)
set the grid's row count, growing storage and resorting
Grows cell storage as needed, re-applies the active sort, and tails the view if follow mode is on.
g | grid handle |
n | number of rows |
returns void
gpu_grid_rows(g, NROWS);
gpu_grid_col(c, label, width, type, align)
define column c with a label, width, type and alignment
type: 0 TEXT, 1 NUMBER, 2 CHECK, 3 BAR, 4 COLOR, 5 BADGE; align: 0 left, 1 centre, 2 right; width is clamped to a minimum of 24px.
c | column index (0-based) |
label | column header text (string) |
width | column width in pixels (min 24) |
type | column type 0-5 (TEXT/NUM/CHECK/BAR/COLOR/BADGE) |
align | text alignment 0 left, 1 centre, 2 right |
returns void
gpu_grid_col(g, 0, "Module", 220, GT_TEXT, 0);
gpu_grid_clear(g)
empty all cells and section headers, resetting the grid to zero rows
Clears cell text/numbers/colours, drops section headers, and resets selection and scroll.
returns void
gpu_grid_clear(insp);
gpu_grid_set(r, c, text)
set the text of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
text | cell text (string) |
returns void
gpu_grid_set(g, i, 0, dName[i]);
gpu_grid_num(r, c, v)
set the numeric value of cell (r,c)
Value drives NUMBER display, CHECK on/off, and BAR percentage.
r | row index (0-based) |
c | column index (0-based) |
v | numeric value (float) |
returns void
gpu_grid_num(g, i, 2, dPower[i]);
gpu_grid_getnum(r, c)
read back the (possibly edited) numeric value of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
returns float cell value, 0 if the handle or indices are out of bounds
pw.i = gpu_grid_getnum(g, sel, 2);
gpu_grid_gettext(r, c)
read back the (possibly edited) text of cell (r,c)
r | row index (0-based) |
c | column index (0-based) |
returns string cell text, empty string if the handle or indices are out of bounds
selName = gpu_grid_gettext(g, sel, 0) + " (" + gpu_grid_gettext(g, sel, 1) + ")";gpu_grid_merge(r, c, cspan, rspan)
merge cell (r,c) across cspan columns and rspan rows
Spans are clamped to at least 1 and to the grid bounds; covered cells are flagged and not drawn.
r | origin row index |
c | origin column index |
cspan | number of columns to span (>=1) |
rspan | number of rows to span (>=1) |
returns void
gpu_grid_merge(g, 11, 0, 6, 1);
gpu_grid_title(g, title)
set the grid's title-bar text and enable the title bar
g | grid handle |
title | title text (string) |
returns void
gpu_grid_title(g, "MODULE DATABASE (cx_grid demo)");
gpu_grid_font(g, font_id)
set the grid's default font
0 = raylib default pixel font; otherwise a loadfont() handle for crisp TTF text.
g | grid handle |
font_id | font handle (0 = default) |
returns void
gpu_grid_font(g, gFont);
gpu_grid_colfont(c, font_id)
set the font used by every cell in column c
0 = inherit the grid font; overridden per-cell by gpu_grid_cellfont.
c | column index (0-based) |
font_id | font handle (0 = inherit grid) |
returns void
gpu_grid_colfont(g, 0, gFontBold);
gpu_grid_cellfont(r, c, font_id)
set the font of one cell (r,c)
Cell font wins over the column font, which wins over the grid font; 0 = inherit.
r | row index (0-based) |
c | column index (0-based) |
font_id | font handle (0 = inherit column/grid) |
returns void
gpu_grid_cellfont(g, 11, 0, gFontBold);
gpu_grid_color(r, c, color)
set the colour of cell (r,c)
Used by BAR tint, COLOR swatch and BADGE pill cells.
r | row index (0-based) |
c | column index (0-based) |
color | packed 0xRRGGBB colour |
returns void
gpu_grid_color(g, i, 3, dTint[i]);
gpu_grid_textcolor(r, c, color)
set the per-cell TEXT colour of cell (r,c)
-1 reverts the cell to the grid's default text colour.
r | row index (0-based) |
c | column index (0-based) |
color | packed 0xRRGGBB colour, -1 = grid default |
returns void
gpu_grid_textcolor(g, i, 2, parsehex("ff6b6b"));gpu_grid_rowstyle(r, color)
set a per-row background colour override for row r
-1 = none, reverting the row to zebra/normal shading.
r | row index (0-based) |
color | packed 0xRRGGBB background colour, -1 = none |
returns void
gpu_grid_rowstyle(g, 7, parsehex("3a2a14"));gpu_grid_style(key, value)
set one named style property of the grid to an int value
Keys: headerbg, rowbg, rowalt, sel, hover, grid, text, accent, titlebg (colours) and rowh, headerh, titleh, footerh, zebra (metrics/flags).
key | style key name (string) |
value | colour or metric value (int) |
returns void
gpu_grid_style(g, "rowh", 30);
gpu_grid_theme(g, name)
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.
g | grid handle |
name | theme 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.
g | grid handle |
on | 1 = show gutter, 0 = hide |
returns void
gpu_grid_rownum(top, 1);
gpu_grid_section(r, label)
mark row r as a collapsible section header with a label
Rows after it (until the next header) belong to it and hide when it folds; clicking the header toggles it.
r | row index to turn into a section header |
label | section header label (string) |
returns void
gpu_grid_section(g, 0, "WEAPONS");
gpu_grid_fold(r, folded)
programmatically fold or unfold the section header at row r
r | section header row index |
folded | 1 = folded (collapsed), 0 = expanded |
returns void
gpu_grid_fold(insp, r, 1);
gpu_grid_rowlevel(r, level)
set the nesting depth of the section header at row r
0 = top-level, 1 = sub-section, and so on; folding a parent also hides nested sub-sections. Nesting past depth 15 raises CX-E5025.
r | section header row index |
level | nesting depth (0 = top; negatives clamp to 0) |
returns void
gpu_grid_rowlevel(insp, r, 1);
gpu_grid_coledit(c, on)
mark a TEXT/NUM column c as click-to-edit
CHECK/BAR/COLOR cells are always interactive regardless of this flag.
c | column index (0-based) |
on | 1 = editable, 0 = read-only |
returns void
gpu_grid_coledit(g, 0, 1);
gpu_grid_celledit(r, c, on)
make one cell (r,c) typeable even if its column is not editable
Used for an editable cell sitting in an otherwise read-only grid.
r | row index (0-based) |
c | column index (0-based) |
on | 1 = typeable, 0 = not |
returns void
gpu_grid_celledit(insp, r, 2, 1);
gpu_grid_sort(col, dir)
programmatically set the sort column and direction
dir < 0 sorts descending, otherwise ascending; col < 0 clears the sort. Group-aware when the grid has sections (sections move as whole blocks).
col | column to sort by (<0 = unsorted) |
dir | direction: <0 descending, otherwise ascending |
returns void
gpu_grid_sort(g, 2, 1);
gpu_grid_scrollx(px)
set the horizontal scroll offset in pixels
Clamped to 0..(content width - viewport width).
px | horizontal scroll offset in pixels |
returns void
gpu_grid_scrollx(g, 0);
gpu_grid_follow(g, on)
toggle follow mode so the view auto-tails to the bottom as rows append
Re-enabled automatically when the user scrolls back to the bottom.
g | grid handle |
on | 1 = follow (auto-tail), 0 = off |
returns void
gpu_grid_follow(dlog, 1);
gpu_grid_update(mx, my, wheel, down, pressed)
drive one frame of grid input from the mouse state
Handles wheel/scrollbar scrolling, header sort clicks, column resize, section fold, cell selection and text/number editing.
mx | mouse x in pixels |
my | mouse y in pixels |
wheel | mouse wheel delta |
down | mouse button held (1/0) |
pressed | mouse button pressed this frame (1/0) |
returns void
gpu_grid_update(g, mx, my, wheel, down, pressed);
gpu_grid_draw(g)
render the grid this frame using raylib primitives
Draws the title bar, header, visible rows (cells/sections), vertical and horizontal scrollbars and the footer.
returns void
gpu_grid_draw(g);
gpu_grid_tail(g)
snap the view to the last rows (log tail)
returns void
gpu_grid_tail(dlog);
gpu_grid_sel(g)
return the selected original row index
returns int selected original row index, -1 = none
sel.i = gpu_grid_sel(g);
gpu_grid_sortcol(g)
return the current sort column index
returns int current sort column, -1 = unsorted
gpu_grid_sortcol(g);
gpu_grid_hover(g)
return the hovered original row index
returns int hovered original row index, -1 = none
gpu_grid_hover(g);
gpu_grid_count(g)
return the grid's row count
returns int number of rows
gpu_draw_text(60, 744, "rows: " + str(gpu_grid_count(g)), colDim, 16);
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.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector (ignored) |
returns float x1 + x2
vec3addx(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3addy(x1, y1, z1, x2, y2, z2)
return the Y component of adding two 3D vectors (y1+y2)
Component-wise scalar helper; only y1 and y2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float y1 + y2
vec3addy(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3addz(x1, y1, z1, x2, y2, z2)
return the Z component of adding two 3D vectors (z1+z2)
Component-wise scalar helper; only z1 and z2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1 + z2
vec3addz(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3subx(x1, y1, z1, x2, y2, z2)
return the X component of subtracting two 3D vectors (x1-x2)
Component-wise scalar helper; only x1 and x2 are used.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector (ignored) |
returns float x1 - x2
vec3subx(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3suby(x1, y1, z1, x2, y2, z2)
return the Y component of subtracting two 3D vectors (y1-y2)
Component-wise scalar helper; only y1 and y2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float y1 - y2
vec3suby(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3subz(x1, y1, z1, x2, y2, z2)
return the Z component of subtracting two 3D vectors (z1-z2)
Component-wise scalar helper; only z1 and z2 are used.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1 - z2
vec3subz(5.0,5.0,5.0, 1.0,2.0,3.0)
vec3scalex(x, y, z, s)
return the X component of a 3D vector scaled by a scalar (x*s)
Component-wise scalar helper; only x and s are used.
x | X of the vector |
y | Y of the vector (ignored) |
z | Z of the vector (ignored) |
s | scalar multiplier |
returns float x * s
vec3scalex(2.0,3.0,4.0, 10.0)
vec3scaley(x, y, z, s)
return the Y component of a 3D vector scaled by a scalar (y*s)
Component-wise scalar helper; only y and s are used.
x | X of the vector (ignored) |
y | Y of the vector |
z | Z of the vector (ignored) |
s | scalar multiplier |
returns float y * s
vec3scaley(2.0,3.0,4.0, 10.0)
vec3scalez(x, y, z, s)
return the Z component of a 3D vector scaled by a scalar (z*s)
Component-wise scalar helper; only z and s are used.
x | X of the vector (ignored) |
y | Y of the vector (ignored) |
z | Z of the vector |
s | scalar multiplier |
returns float z * s
vec3scalez(2.0,3.0,4.0, 10.0)
vec3length(x, y, z)
return the length (magnitude) of a 3D vector
Computes sqrt(x*x + y*y + z*z).
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float sqrt(x^2 + y^2 + z^2)
vec3length(3.0, 4.0, 0.0)
vec3distance(x1, y1, z1, x2, y2, z2)
return the Euclidean distance between two 3D points
Computes sqrt of the summed squared component differences.
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
returns float distance between the two points
vec3distance(0.0,0.0,0.0, 3.0,4.0,0.0)
vec3dot(x1, y1, z1, x2, y2, z2)
return the dot product of two 3D vectors
Computes x1*x2 + y1*y2 + z1*z2.
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float x1*x2 + y1*y2 + z1*z2
vec3dot(1.0,2.0,3.0, 4.0,5.0,6.0)
vec3crossx(x1, y1, z1, x2, y2, z2)
return the X component of the cross product of two 3D vectors
Computes y1*z2 - z1*y2.
x1 | X of the first vector (ignored) |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector (ignored) |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float y1*z2 - z1*y2
vec3crossx(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3crossy(x1, y1, z1, x2, y2, z2)
return the Y component of the cross product of two 3D vectors
Computes z1*x2 - x1*z2.
x1 | X of the first vector |
y1 | Y of the first vector (ignored) |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector (ignored) |
z2 | Z of the second vector |
returns float z1*x2 - x1*z2
vec3crossy(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3crossz(x1, y1, z1, x2, y2, z2)
return the Z component of the cross product of two 3D vectors
Computes x1*y2 - y1*x2.
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector (ignored) |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector (ignored) |
returns float x1*y2 - y1*x2
vec3crossz(1.0,0.0,0.0, 0.0,1.0,0.0)
vec3normx(x, y, z)
return the X component of the normalized (unit-length) vector
Computes x/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float x divided by the vector length, or 0.0 if length is 0
vec3normx(3.0,4.0,0.0)
vec3normy(x, y, z)
return the Y component of the normalized (unit-length) vector
Computes y/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float y divided by the vector length, or 0.0 if length is 0
vec3normy(3.0,4.0,0.0)
vec3normz(x, y, z)
return the Z component of the normalized (unit-length) vector
Computes z/|v|; returns 0.0 when the vector length is zero.
x | X of the vector |
y | Y of the vector |
z | Z of the vector |
returns float z divided by the vector length, or 0.0 if length is 0
vec3normz(0.0,0.0,5.0)
vec3lerpx(x1, y1, z1, x2, y2, z2, t)
return the X component of the linear interpolation between two 3D points
Computes x1 + (x2 - x1) * t; t is NOT clamped.
x1 | X of the start point |
y1 | Y of the start point (ignored) |
z1 | Z of the start point (ignored) |
x2 | X of the end point |
y2 | Y of the end point (ignored) |
z2 | Z of the end point (ignored) |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float x1 + (x2 - x1) * t
vec3lerpx(0.0,0.0,0.0, 10.0,0.0,0.0, 0.5)
vec3lerpy(x1, y1, z1, x2, y2, z2, t)
return the Y component of the linear interpolation between two 3D points
Computes y1 + (y2 - y1) * t; t is NOT clamped.
x1 | X of the start point (ignored) |
y1 | Y of the start point |
z1 | Z of the start point (ignored) |
x2 | X of the end point (ignored) |
y2 | Y of the end point |
z2 | Z of the end point (ignored) |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float y1 + (y2 - y1) * t
vec3lerpy(0.0,0.0,0.0, 0.0,10.0,0.0, 0.5)
vec3lerpz(x1, y1, z1, x2, y2, z2, t)
return the Z component of the linear interpolation between two 3D points
Computes z1 + (z2 - z1) * t; t is NOT clamped.
x1 | X of the start point (ignored) |
y1 | Y of the start point (ignored) |
z1 | Z of the start point |
x2 | X of the end point (ignored) |
y2 | Y of the end point (ignored) |
z2 | Z of the end point |
t | interpolation factor (0 = start, 1 = end; unclamped) |
returns float z1 + (z2 - z1) * t
vec3lerpz(0.0,0.0,0.0, 0.0,0.0,10.0, 0.5)
vec3angle(x1, y1, z1, x2, y2, z2)
return the angle in degrees between two 3D vectors
Computes acos(dot/(|a||b|)) converted to degrees; returns 0.0 if either vector has zero length, and the cosine is clamped to [-1,1].
x1 | X of the first vector |
y1 | Y of the first vector |
z1 | Z of the first vector |
x2 | X of the second vector |
y2 | Y of the second vector |
z2 | Z of the second vector |
returns float angle between the vectors in degrees, or 0.0 if either is zero-length
vec3angle(1.0,0.0,0.0, 0.0,1.0,0.0)
camfollow(cam, tx, ty, tz, speed)
smoothly lerp a camera's target toward a world point
Moves the cx_gfx Camera3D target from its current position toward (tx,ty,tz) by fraction speed each call (speed clamped 0..1); call per-frame for smooth following.
cam | cx_gfx camera handle |
tx | target X to follow |
ty | target Y to follow |
tz | target Z to follow |
speed | lerp fraction per call, clamped to 0..1 |
returns void
camfollow(cam, tx, ty, tz, 0.1)
camzoomto(cam, dist, speed)
smoothly move a camera toward a target orbit distance
Slides the camera position along its current view direction so the distance to the target approaches dist by fraction speed (clamped 0..1); honors mindist/maxdist clamps set by camorbitsettings and is a no-op when position equals target.
cam | cx_gfx camera handle |
dist | desired distance from the camera target |
speed | lerp fraction per call, clamped to 0..1 |
returns void
camzoomto(cam, 20.0, 0.1)
camshake(cam, intensity, duration)
apply a decaying random positional jitter to a camera
Call per-frame while shaking: undoes the previous frame's offset (so it composes with camfollow/zoom), then adds xorshift-random jitter scaled by intensity and a linear time-decay envelope over duration; intensity or duration <= 0 stops the shake. Only camera handles 1..7 hold shake state.
cam | cx_gfx camera handle (valid 1..7 for shake state) |
intensity | maximum jitter magnitude |
duration | shake duration in seconds |
returns void
camshake(cam, 0.5, 1.0)
camorbitsettings(cam, minspeed, maxspeed, mindist, maxdist)
store min/max speed and distance clamps for a camera
Records orbital tuning on the camera's shake-state slot; the min/max distance is later honored by camzoomto. Only camera handles 1..7 hold this state.
cam | cx_gfx camera handle (valid 1..7) |
minspeed | minimum orbit speed (stored) |
maxspeed | maximum orbit speed (stored) |
mindist | minimum orbit distance (clamps camzoomto) |
maxdist | maximum orbit distance (clamps camzoomto; 0 = unset) |
returns void
camorbitsettings(cam, 0.1, 2.0, 5.0, 50.0)
goload(name)
load a game object from a JSON definition file and return its handle
Resolves name in order (raw path, data/ships/<name>.json, data/<name>, <name>.json), parses the JSON into a reusable object-pool slot, and reads optional scale plus model/shader assets (assets only load when a window is open). Grows the pool up to #pragma MaxGameObjects; on empty name or no loadable/parseable file it prints an error and exits the process (never returns a sentinel).
name | object name or path used to locate the JSON definition |
returns int object handle (>= 1); on failure it prints an error and exits, so it never returns an invalid handle
int h = goload(tmp);
gofree(obj)
free a game object and release its slot for reuse
Releases the object's JSON doc, any loaded model/shader, and its override array, then clears the slot so a later goload can reclaim it. No-op on an invalid handle.
obj | game object handle from goload |
returns void
gofree(h);
gogetf(obj, key)
read a float property of a game object
Returns the runtime override for key if one was set via gosetf/goseti, otherwise reads the key from the object's JSON. Returns 0.0 on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns float property value (override first, else JSON member); 0.0 if the handle is invalid
speed = gogetf(obj, "speed")
gogeti(obj, key)
read an integer property of a game object
Returns the runtime override for key if one was set, otherwise reads the key from the object's JSON. Returns 0 on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns int property value (override first, else JSON member); 0 if the handle is invalid
hp = gogeti(obj, "health")
gogets(obj, key)
read a string property of a game object
Reads the key from the object's JSON (string overrides are not supported — only numeric overrides exist). Returns an empty string on an invalid handle.
obj | game object handle from goload |
key | property name to read |
returns string property value from the JSON member; empty string if the handle is invalid
name = gogets(obj, "name")
gosetf(obj, key, v)
set a runtime float override on a game object property
Stores an in-memory override for key (read back by gogetf/gogeti) without modifying the underlying JSON; grows the per-object override array up to #pragma MaxGameOverrides. No-op on an invalid handle.
obj | game object handle from goload |
key | property name to override |
v | float value to store |
returns void
gosetf(obj, "speed", 12.5)
goseti(obj, key, v)
set a runtime integer override on a game object property
Stores an in-memory override for key (kept as a double, read back by gogeti/gogetf) without touching the JSON. No-op on an invalid handle.
obj | game object handle from goload |
key | property name to override |
v | integer value to store |
returns void
goseti(obj, "health", 100)
timercreate(duration)
create a countdown timer and return its handle
Allocates a timer counting down from duration seconds (negative clamped to 0); reuses freed slots and grows the pool up to #pragma MaxGameTimers.
duration | countdown length in seconds (negative treated as 0) |
returns int timer handle (>= 1)
lastTmr = timercreate(1.0);
timerupdate(tmr, delta)
advance a timer by subtracting a delta from its remaining time
Decrements the timer's remaining time by delta, floored at 0. No-op on an invalid handle.
tmr | timer handle from timercreate |
delta | time to subtract (e.g. per-frame delta seconds) |
returns void
timerupdate(tmr, 0.016)
timerexpired(tmr)
test whether a timer has counted down to zero
Returns 1 once the remaining time reaches 0, else 0; 0 for an invalid handle.
tmr | timer handle from timercreate |
returns int 1 if remaining time <= 0, else 0 (0 for an invalid handle)
timerexpired(timercreate(1000.0))
timerreset(tmr)
reset a timer's remaining time back to its full duration
No-op on an invalid handle.
tmr | timer handle from timercreate |
returns void
timerreset(tmr)
timerprogress(tmr)
return a timer's elapsed fraction from 0.0 to 1.0
Computes 1 - remaining/duration, clamped to 0..1; a zero-length timer reports 1.0 (instantly done) and an invalid handle returns 0.0.
tmr | timer handle from timercreate |
returns float progress in 0..1 (1.0 for a zero-length timer; 0.0 if the handle is invalid)
p = timerprogress(tmr)
cooldownready(tmr)
poll a timer as a cooldown and auto-restart it when ready
Returns 1 the moment the timer reaches 0 and simultaneously reloads it to full duration for the next interval (the weapon/AI cooldown pattern); otherwise 0, and 0 for an invalid handle.
tmr | timer handle from timercreate |
returns int 1 if the cooldown is ready (and it auto-restarts), else 0 (0 for an invalid handle)
if (cooldownready(tmr)) { fire(); }dist3d(x1, y1, z1, x2, y2, z2)
return the Euclidean distance between two 3D points
Computes sqrt of the summed squared component differences (same as vec3distance).
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
returns float distance between the two points
dist3d(0.0,0.0,0.0, 3.0,4.0,0.0)
inrange3d(x1, y1, z1, x2, y2, z2, range)
test whether two 3D points are within a given range
Compares squared distance against range squared (no sqrt), returning 1 if within range inclusive.
x1 | X of the first point |
y1 | Y of the first point |
z1 | Z of the first point |
x2 | X of the second point |
y2 | Y of the second point |
z2 | Z of the second point |
range | maximum distance (inclusive) |
returns int 1 if the distance is <= range, else 0
inrange3d(0.0,0.0,0.0, 1.0,0.0,0.0, 2.0)
worldtoscreenx(cam, wx, wy, wz)
project a 3D world point to its screen X coordinate
Uses raylib GetWorldToScreen for the given camera; returns 0.0 when no window is open or the camera handle is invalid (headless-safe).
cam | cx_gfx camera handle |
wx | world X |
wy | world Y |
wz | world Z |
returns float screen-space X pixel of the projected point; 0.0 if no window/invalid camera
sx = worldtoscreenx(cam, wx, wy, wz)
worldtoscreeny(cam, wx, wy, wz)
project a 3D world point to its screen Y coordinate
Uses raylib GetWorldToScreen for the given camera; returns 0.0 when no window is open or the camera handle is invalid (headless-safe).
cam | cx_gfx camera handle |
wx | world X |
wy | world Y |
wz | world Z |
returns float screen-space Y pixel of the projected point; 0.0 if no window/invalid camera
sy = worldtoscreeny(cam, wx, wy, wz)
headingto(x1, z1, x2, z2)
return the yaw heading in degrees from one XZ point to another
Computes atan2(x2-x1, z2-z1) in degrees on the XZ plane, where 0 degrees is +Z increasing toward +X.
x1 | X of the source point |
z1 | Z of the source point |
x2 | X of the target point |
z2 | Z of the target point |
returns float heading in degrees (0 = +Z, increasing toward +X)
h = headingto(x1, z1, x2, z2)
colorlerp(c1, c2, t)
linearly interpolate between two packed colors
Blends two 0xAARRGGBB colors channel-wise (alpha, red, green, blue) with t clamped to 0..1, returning the packed result.
c1 | start color (packed 0xAARRGGBB int) |
c2 | end color (packed 0xAARRGGBB int) |
t | blend factor, clamped to 0..1 (0 = c1, 1 = c2) |
returns int the interpolated packed 0xAARRGGBB color
colorlerp(0, 16777215, 0.5)
colorflash(col, flashcol, t)
blend a base color toward a flash color by an amount
Alias of colorlerp: t=0 returns col unchanged, t=1 returns full flashcol, with t clamped to 0..1.
col | base color (packed 0xAARRGGBB int) |
flashcol | flash color to blend toward (packed 0xAARRGGBB int) |
t | flash amount, clamped to 0..1 (0 = base, 1 = full flash) |
returns int the blended packed 0xAARRGGBB color
c = colorflash(col, 0xFFFFFFFF, 0.5)
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.
px | camera (eye) position x |
py | camera position y |
pz | camera position z |
tx | look-at target x |
ty | look-at target y |
tz | look-at target z |
ux | up vector x |
uy | up vector y |
uz | up vector z |
fov | vertical field of view in degrees |
returns 1-based camera handle; 0 if the window is not open
int cam = gpu_camera_new(0.0,10.0,10.0, 0.0,0.0,0.0, 0.0,1.0,0.0, 45.0);
gpu_camera_begin(cam)
begin 3D rendering through a camera (raylib BeginMode3D)
Enters 3D mode for this camera; also feeds the camera position into every loaded shader's viewPos uniform for correct lighting. All 3D draws must sit between this and gpu_camera_end.
cam | camera handle from gpu_camera_new |
returns void
gpu_camera_begin(cam);
gpu_camera_update(cam, mode)
update a camera with a raylib movement mode
Calls raylib UpdateCamera; mode selects the built-in controller.
cam | camera handle |
mode | 0=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
cam | camera handle |
x | new position x |
y | new position y |
z | new position z |
returns void
gpu_camera_setpos(cam, 1.0,2.0,3.0);
gpu_camera_settarget(cam, x, y, z)
set a camera's look-at target vector
cam | camera handle |
x | target x |
y | target y |
z | target z |
returns void
gpu_camera_settarget(cam, 0.0,0.0,0.0);
gpu_camera_posx(cam)
read a camera's position x
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
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
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)
x | center x |
y | center y |
z | center z |
w | width (x extent) |
h | height (y extent) |
d | depth (z extent) |
col | color 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)
x | center x |
y | center y |
z | center z |
w | width (x extent) |
h | height (y extent) |
d | depth (z extent) |
col | line 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)
x | center x |
y | center y |
z | center z |
radius | sphere radius |
col | color int |
returns void
gpu_draw_sphere(3.0,0.0,0.0, 1.0, 255);
gpu_draw_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.
x | center x |
y | center y |
z | center z |
radius | sphere radius |
col | line 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.
x | base center x |
y | base center y |
z | base center z |
rtop | top radius |
rbot | bottom radius |
height | cylinder height |
slices | number of radial slices |
col | color 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)
x | base center x |
y | base center y |
z | base center z |
rtop | top radius |
rbot | bottom radius |
height | cylinder height |
slices | number of radial slices |
col | line 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)
x | center x |
y | center y |
z | center z |
w | size along x |
d | size along z |
col | color 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)
slices | number of grid squares in each direction |
spacing | distance between grid lines |
returns void
gpu_draw_grid(10, 1.0);
gpu_draw_line3d(x1, y1, z1, x2, y2, z2, col)
draw a 3D line from (x1,y1,z1) to (x2,y2,z2) (raylib DrawLine3D)
x1 | start x |
y1 | start y |
z1 | start z |
x2 | end x |
y2 | end y |
z2 | end z |
col | line color int |
returns void
gpu_draw_line3d(0.0,0.0,0.0, 1.0,1.0,1.0, 255);
gpu_draw_point3d(x, y, z, col)
draw a 3D point at (x,y,z) (raylib DrawPoint3D)
x | point x |
y | point y |
z | point z |
col | color int |
returns void
gpu_draw_point3d(1.0,1.0,1.0, 255);
gpu_draw_triangle3d(x1, y1, z1, x2, y2, z2, x3, y3, z3, col)
draw a filled 3D triangle from three vertices (raylib DrawTriangle3D)
x1 | vertex 1 x |
y1 | vertex 1 y |
z1 | vertex 1 z |
x2 | vertex 2 x |
y2 | vertex 2 y |
z2 | vertex 2 z |
x3 | vertex 3 x |
y3 | vertex 3 y |
z3 | vertex 3 z |
col | color 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.
cam | camera handle the billboard faces |
tex | image/texture handle (from loadimage) |
x | world position x |
y | world position y |
z | world position z |
size | billboard size in world units |
col | tint color int |
returns void
gpu_draw_billboard(cam, tex, 0.0,2.0,0.0, 1.0, 255);
gpu_draw_billboard_pro(cam, tex, sx,sy,sw,sh, x,y,z, ux,uy,uz, w,h, rot, col, alpha)
draw an ORIENTED textured billboard (raylib DrawBillboardPro): a quad
of one texture at a world position, its height axis aligned to `up`, sized (w,h) in world units, spun `rot` degrees about the view axis, tinted col(0xRRGGBB) at `alpha`. Origin is centred on the position. This is the general oriented-quad primitive (strands, tracers, beams, sprites-along-a- path); unlike gpu_draw_billboard it is NOT locked to a camera-facing square. Draws into the CURRENT blend/depth state -- bracket a glow pass with gpu_blend_add/gpu_blend_normal and gpu_depth_write for clean additive sums.
cam | camera handle (gpu_camera_new) |
tex | image/texture handle (gpu_image_load) |
sx,sy,sw,sh | source rect in TEXTURE PIXELS (atlas cell). sw<=0 or sh<=0 selects the whole texture; a non-zero rect flips between frames on one sheet. |
x,y,z | world position the quad is centred on |
ux,uy,uz | the quad's height (up) axis; normalized here (zero -> world up) |
w,h | quad size in world units (w across, h along `up`) |
rot | rotation in degrees about the view axis |
col | tint packed 0xRRGGBB |
alpha | tint alpha 0..255 |
gpu_draw_billboard_pro(cam, tex, 0,0,0,0, x,y,z, dx,dy,dz, 0.4, len, 0, rgb(120,180,255), 200)
gpu_depth_write(on)
toggle depth-buffer WRITES (rlgl global state). on=0 stops transparent
billboards/particles from writing depth -- so overlapping additive sprites sum cleanly instead of a near quad punching a rectangular depth hole that rejects the sprites behind it (the transparent-quad self-occlusion artifact). on=1 restores normal depth writes. Pair off/on around a glow pass, and re-enable AFTER the blend batch is flushed (i.e. after gpu_blend_normal).
on | 1 = normal depth writes, 0 = depth writes disabled |
gpu_blend_add(); gpu_depth_write(0); ...draw strands...; gpu_blend_normal(); gpu_depth_write(1);
gpu_model_load(path)
load a 3D model from a file and return its handle (raylib LoadModel)
Allocates a model slot with a leak-tracking ARC bucket. A failed load (meshCount==0) prints a CX error and exits (FP4), never a silent invisible model.
path | model file path (.obj, .gltf, etc.) |
returns 1-based model handle; 0 if the window is not open; hard-exits on load failure
model m = gpu_model_load("ship.obj");gpu_mesh_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.
w | width (x) |
h | height (y) |
d | depth (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)
radius | sphere radius |
rings | number of horizontal rings |
slices | number of vertical slices |
returns 1-based model handle; 0 if the window is not open
int sph = gpu_mesh_sphere(1.0, 16, 16);
gpu_mesh_plane(w, d, resx, resz)
generate a subdivided plane mesh and return it as a model handle (raylib GenMeshPlane)
w | size along x |
d | size along z |
resx | subdivisions along x |
resz | subdivisions 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)
radius | cylinder radius |
height | cylinder height |
slices | number 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)
mdl | model handle |
x | position x |
y | position y |
z | position z |
scale | uniform scale factor |
col | tint 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)
mdl | model handle |
x | position x |
y | position y |
z | position z |
scale | uniform scale factor |
col | tint 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)
mdl | model handle |
x | position x |
y | position y |
z | position z |
ax | rotation axis x |
ay | rotation axis y |
az | rotation axis z |
angle | rotation angle in degrees |
sx | scale x |
sy | scale y |
sz | scale z |
col | tint color int |
returns void
gpu_model_draw_ex(pln, 0.0,0.0,0.0, 0.0,1.0,0.0, 45.0, 1.0,1.0,1.0, 255);
gpu_model_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.
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.
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.
mdl | model handle |
tex | image/texture handle (from loadimage) |
returns void
gpu_model_texture(cube, tex);
gpu_model_livecount()
count currently-live model slots
Leak-check signal for the `model` resource type: number of in-use slots in the model table.
returns number of live model slots (int)
int n = gpu_model_livecount();
gpu_msaa(level)
request multi-sample anti-aliasing at the given level
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).
level | 0 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.
mode | 0 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.
model | the model |
mode | 0 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.
vspath | vertex shader file path ("" = default vertex shader) |
fspath | fragment shader file path ("" = default fragment shader) |
returns 1-based shader handle; 0 if the window is not open
shader s = gpu_shader_load("", "");gpu_shader_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.
vsblob | blob handle holding vertex shader source |
fsblob | blob 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.
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.
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.
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.
sh | shader handle |
name | uniform name |
value | integer value to set |
uniformtype | type 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.
sh | shader handle |
name | uniform name |
fvalue | float value to set |
uniformtype | type code: 0=float 1=vec2 2=vec3 3=vec4 4=int 5=sampler2d |
returns void
gpu_shader_setf(sh, "strength", 0.5, 0);
gpu_shader_setvec3(sh, name, x, y, z, uniformtype)
set a vec3 uniform value on a shader (raylib SetShaderValue)
Packs (x,y,z) into a float[3] and uploads to the named uniform; missing uniform is a silent no-op.
sh | shader handle |
name | uniform name |
x | vec3 component x |
y | vec3 component y |
z | vec3 component z |
uniformtype | type code: 0=float 1=vec2 2=vec3 3=vec4 4=int 5=sampler2d |
returns void
gpu_shader_setvec3(sh, "tint", 1.0,0.5,0.2, 2);
gpu_light_ambient(sh, r, g, b, a)
set a shader's "ambient" vec4 light uniform from an RGBA color
Normalizes r,g,b,a (0..255) to 0..1 and uploads to the shader's "ambient" uniform; no-op if that uniform is absent.
sh | shader handle |
r | ambient red 0..255 |
g | ambient green 0..255 |
b | ambient blue 0..255 |
a | ambient alpha 0..255 |
returns void
gpu_light_ambient(sh, 10,10,25, 255);
gpu_light_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.
sh | shader handle |
x | light position x |
y | light position y |
z | light position z |
r | color red 0..255 |
g | color green 0..255 |
b | color blue 0..255 |
a | color alpha 0..255 |
returns 1-based light index; 0 if the shader handle is invalid; hard errors if the 16-light table is full
int 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.
sh | shader handle |
dx | light direction x |
dy | light direction y |
dz | light direction z |
r | color red 0..255 |
g | color green 0..255 |
b | color blue 0..255 |
a | color alpha 0..255 |
returns 1-based light index; 0 if the shader handle is invalid; hard errors if the 16-light table is full
int sun = gpu_light_directional(sh, -1.0,-0.5,0.0, 255,240,200,255);
gpu_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.
sh | shader handle |
lt | light index (from gpu_light_point / gpu_light_directional) |
x | new light position x |
y | new light position y |
z | new 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.
mdl | model handle |
sh | shader 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)
x1 | sphere 1 center x |
y1 | sphere 1 center y |
z1 | sphere 1 center z |
r1 | sphere 1 radius |
x2 | sphere 2 center x |
y2 | sphere 2 center y |
z2 | sphere 2 center z |
r2 | sphere 2 radius |
returns 1 if the spheres intersect, else 0
gpu_collide_spheres(0.0,0.0,0.0, 2.0, 1.0,0.0,0.0, 2.0)
gpu_collide_boxes(min1x, min1y, min1z, max1x, max1y, max1z, min2x, min2y, min2z, max2x, max2y, max2z)
test whether two axis-aligned bounding boxes intersect (raylib CheckCollisionBoxes)
Each box is given by its min and max corner.
min1x | box 1 min x |
min1y | box 1 min y |
min1z | box 1 min z |
max1x | box 1 max x |
max1y | box 1 max y |
max1z | box 1 max z |
min2x | box 2 min x |
min2y | box 2 min y |
min2z | box 2 min z |
max2x | box 2 max x |
max2y | box 2 max y |
max2z | box 2 max z |
returns 1 if the boxes intersect, else 0
gpu_collide_boxes(0.0,0.0,0.0, 2.0,2.0,2.0, 1.0,1.0,1.0, 3.0,3.0,3.0)
gpu_collide_box_sphere(minx, miny, minz, maxx, maxy, maxz, cx_, cy_, cz_, radius)
test whether a box and a sphere intersect (raylib CheckCollisionBoxSphere)
Box given by min/max corners; sphere by center and radius.
minx | box min x |
miny | box min y |
minz | box min z |
maxx | box max x |
maxy | box max y |
maxz | box max z |
cx_ | sphere center x |
cy_ | sphere center y |
cz_ | sphere center z |
radius | sphere radius |
returns 1 if the box and sphere intersect, else 0
gpu_collide_box_sphere(0.0,0.0,0.0, 2.0,2.0,2.0, 1.0,1.0,1.0, 0.5)
gpu_model_minx(mdl)
read a model's bounding-box minimum x (raylib GetModelBoundingBox)
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)
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)
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)
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)
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)
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.
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
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
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
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)
ray | ray handle (from gpu_ray_mouse) |
x | sphere center x |
y | sphere center y |
z | sphere center z |
radius | sphere radius |
returns 1 if the ray hits the sphere, else 0 (also 0 if the ray handle is invalid)
int rhitS = gpu_collide_ray_sphere(ray, 0.0,0.0,0.0, 1.0);
gpu_collide_ray_box(ray, minx, miny, minz, maxx, maxy, maxz)
test whether a ray hits an axis-aligned box (raylib GetRayCollisionBox)
Box given by its min and max corners.
ray | ray handle (from gpu_ray_mouse) |
minx | box min x |
miny | box min y |
minz | box min z |
maxx | box max x |
maxy | box max y |
maxz | box max z |
returns 1 if the ray hits the box, else 0 (also 0 if the ray handle is invalid)
int rhitB = gpu_collide_ray_box(ray, -1.0,-1.0,-1.0, 1.0,1.0,1.0);
gpu_camera_end()
end 3D camera mode and go back to screen-space drawing
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.
x | desktop x for the window's top-left |
y | desktop 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.
width | new width (0 with height 0 -> maximize) |
height | new 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.
btn | button 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.
key | raylib keycode (e.g. 65 = A, 32 = space) |
returns 1 while held, else 0
if (gpu_key_down(65) == 1) { marc[sel] = marc[sel] - 2; }gpu_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.
name | region name key |
x | rect top-left x |
y | rect top-left y |
width | rect width |
height | rect 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.
name | region 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.
mx | query x (e.g. current mouse x) |
my | query 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.
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.
name | region 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.
handle | the atlas image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
sx | source x in the atlas |
sy | source y in the atlas |
sw | source width |
sh | source height |
deg | clockwise rotation in degrees |
returns void
drawImageRegionRot(atlas, 400, 300, 64, 64, 0, 0, 32, 32, 90);
drawimagerot(handle, cx, cy, w, h, deg)
draw an image centred, scaled and rotated
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.
handle | the image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
deg | clockwise rotation in degrees |
returns void
drawImageRot(spr, 400, 300, 64, 64, 45);
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.
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.
name | font file name |
size | point size |
returns 0, always -- the default font id
f = fontLoad("arial.ttf", 20);fontset(handle)
select a loaded font for subsequent text
ACCEPTED AND DOES NOTHING: there is no font registry yet -- raylib's built-in font is what draws. Kept so beta-era source compiles, and listed so you know it is a no-op rather than assuming a font was loaded. Pass 0 as the font id wherever one is wanted.
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.
x | left edge in pixels |
y | top edge in pixels |
text | the string to draw |
colour | packed 0xRRGGBB colour |
size | pixel height (optional; default 20) |
returns void
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.
handle | the image |
cx | centre x |
cy | centre y |
w | drawn width |
h | drawn height |
colour | packed 0xRRGGBB multiplier |
alpha | 0..255 opacity |
returns void
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.
x | window x hint -- ignored |
y | window y hint -- ignored |
m | mode hint -- ignored |
w | window width in pixels |
h | window 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.
w | new width in pixels |
h | new height in pixels |
returns void
gpu_screen_resize(1920, 1080);
gpu_text_width(font, text, size)
measure how wide a string will be drawn
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.
font | registered font id (3-argument form) |
text | the string to measure |
size | pixel 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.
text | the 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.
x | text top-left x |
y | text top-left y |
font_id | font handle from gpu_font_load (0/invalid -> default font) |
text | string to draw |
color | text color 0xRRGGBB |
size | pixel size (<=0 -> font base size) |
returns void
gpu_draw_text_font(px, py, fntB, "MOUNT EDITOR", colHi, sc(30));
gpu_font_load(path, size)
load a .ttf/.otf font at a base pixel size, returning a font handle
raylib LoadFontEx into a 1-based slot table (32 slots). size<=0 defaults to 20. A full table is a hard CX-E5025 error.
path | font file path |
size | base glyph size in pixels |
returns 1-based font handle, or 0 if no window is open
fnt.i = gpu_font_load("data/fonts/Nunito/static/Nunito-SemiBold.ttf", 34);gpu_font_loadmem(blob, ext, size)
load a font from an in-memory embed() blob, returning a font handle
raylib LoadFontFromMemory; `ext` is the format hint (".ttf"/".otf"). Same slot handle as gpu_font_load.
blob | embed() blob handle holding the font bytes |
ext | format hint, e.g. ".ttf" |
size | base glyph size in pixels |
returns 1-based font handle, or 0 on failure/no window
int fnt = gpu_font_loadmem(ttf, ".ttf", 24);
gpu_font_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.
handle | font 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.
handle | font 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).
returns 1-based texture handle, or 0 on failure
img.i = gpu_image_load("data/ships/" + shipFile[0] + ".png");gpu_image_load_as(path, class_name)
load an image and apply an image-treatment CLASS at load time
The class (see cx_imgclass.h) is a recipe of operations run automatically: pixel-stage fixes (synthesize alpha from brightness, black transparent edges, force opaque, premultiply) then the GL filter (smooth/sharp/mipmaps). Built-in classes: particle, smoke, sprite, texture, background; a data/imageclasses.json section (gpu_image_classes) can add or redefine them.
path | image file to load (PNG/JPG/etc.) |
class_name | treatment class; unknown name -> safe default (smooth) |
returns texture handle usable like gpu_image_load; 0 on failure
tex = gpu_image_load_as("fx/puff.png", "particle")gpu_image_classes(path)
load an image-class override file, redefining/adding treatment recipes
The file is a JSON object of the shape { "imageClasses": { "<name>": ["op", "op", ...], ... } } where each op is one of: smooth, sharp, mipmaps, alphaFromLuma, edgeBlack, opaque, premultiply. Each listed class replaces (or adds) a recipe; classes not mentioned keep their built-in defaults. Unknown op names are reported loudly and skipped (FP4). An absent file or missing "imageClasses" section is a silent no-op, so the built-in defaults simply stand.
path | path to the JSON override file (e.g. "data/imageclasses.json") |
returns nothing
gpu_image_classes("data/imageclasses.json")gpu_image_loadmem(blob, ext)
load a texture from an in-memory embed() blob, returning a texture handle
raylib LoadImageFromMemory then GPU upload; `ext` is the format hint (".png"/".jpg"). Same handle space as gpu_image_load.
blob | embed() blob handle holding the image bytes |
ext | format hint, e.g. ".png" |
returns 1-based texture handle, or 0 on failure
int img = gpu_image_loadmem(png, ".png");
gpu_image_free(handle)
destroy a texture handle now, unloading its GPU texture
Unconditional free (UnloadTexture, or UnloadRenderTexture for render targets); drops the resource bucket then reclaims the slot.
handle | texture handle to free |
returns void
if (img > 0) { gpu_image_free(img); }gpu_image_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.
handle | texture handle to decref |
returns void
gpu_image_decref(img);
gpu_image_width(handle)
get a texture's width in pixels
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
returns texture height, or 0 for an invalid handle
ih.i = gpu_image_height(img);
gpu_image_cap(n)
set the GPU texture-table capacity before the window opens
Raises the maximum texture slots (default 64), clamped to 16..8192. Must be called before the first image load; a no-op once the table is allocated. The underlying C function returns nothing (the I_I sig is only for binding generation).
n | desired slot capacity (clamped 16-8192) |
returns void
gpu_image_cap(384);
gpu_image_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.
width | target width in pixels |
height | target height in pixels |
returns 1-based texture handle, or 0 on failure
gPlayerComposite = gpu_image_new(COMPW, COMPH);
gpu_image_begin(handle)
redirect subsequent draw calls into a render-target texture
raylib BeginTextureMode on the target handle; must be paired with gpu_image_end. No-op for a non-target handle.
handle | render-target texture handle from gpu_image_new |
returns void
gpu_image_begin(comp);
gpu_image_end()
stop drawing into the current render-target texture
raylib EndTextureMode.
returns void
gpu_image_end();
gpu_image_clear(color, alpha)
clear the active render target to a color at an alpha
raylib ClearBackground; call between gpu_image_begin/gpu_image_end (e.g. alpha=0 for a transparent canvas before compositing).
color | clear color 0xRRGGBB |
alpha | clear alpha 0-255 (0 = transparent) |
returns void
gpu_image_clear(rgb(0, 0, 0), 0);
gpu_image_draw(handle, x, y, width, height)
draw a texture at (x,y) scaled to width x height
raylib DrawTexturePro; render-target textures are y-flipped automatically so they present right-side-up.
handle | texture handle |
x | destination top-left x |
y | destination top-left y |
width | destination width |
height | destination height |
returns void
gpu_image_draw(img0, sx0 - dw/2, sy0 - dh/2, dw, dh);
gpu_image_draw_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.
handle | texture handle |
x | top-left x |
y | top-left y |
alpha | opacity 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.
handle | texture handle |
dx | destination x |
dy | destination y |
dw | destination width |
dh | destination height |
sx | source cell x |
sy | source cell y |
sw | source cell width |
sh | source cell height |
returns void
gpu_image_draw_region(img, 400, 100, 200, 240, 0, 0, gpu_image_width(img) / 2, gpu_image_height(img));
gpu_image_draw_pro(handle, sx,sy,sw,sh, dx,dy,dw,dh, ox,oy, rotation_deg, col, alpha)
the general 2D image blit (raylib DrawTexturePro verbatim):
SOURCE sub-rect + DEST rect + pivot + clockwise rotation + TINT. The one draw that supersedes region/rotated/region_rot when colour is needed (tinted strands/bursts, a lightning texture drawn at any angle/hue from ONE asset). Native-only (raylib window); float coords match gpu_draw_billboard_pro.
handle | image handle from gpu_image_load |
sx,sy,sw,sh | SOURCE sub-rectangle in texture pixels (atlas cell / frame); sw<=0 or sh<=0 => the whole texture |
dx,dy,dw,dh | DEST rectangle in screen pixels (position + scale) |
ox,oy | rotation pivot, in DEST pixels from the dest top-left (rotate-about-centre: dw/2, dh/2) |
rotation_deg | rotation, clockwise degrees, about (ox,oy) |
col | tint colour, packed 0xRRGGBB (multiplies the texture) |
alpha | opacity 0..255 |
returns void
gpu_image_draw_pro(bolt, 0,0,0,0, x,y, 8,len, 4,0, angle, 0xff3030, 200)
imageopen(path)
loads an image file (PNG/JPG/...) from disk into a CPU image in RAM
Calls raylib LoadImage and normalises the pixels to R8G8B8A8; needs no open window. Returns 0 if the file is missing or fails to decode.
path | filesystem path to the image file to load |
returns cx_int image handle (1-based), or 0 on failure
int sheet = imageOpen("art/ModuleFrames.png");imagenew(w, h, color)
creates a blank CPU image of the given size filled with a colour
Colour is packed 0xAARRGGBB (alpha 0 means opaque, as elsewhere). Returns 0 if w or h is <= 0 or the slot allocation fails.
w | image width in pixels (must be > 0) |
h | image height in pixels (must be > 0) |
color | fill colour, packed 0xAARRGGBB |
returns cx_int image handle (1-based), or 0 on invalid size/failure
image h = imageNew(4, 4, 0);
imagecopy(h)
duplicates a CPU image into a new independent image
Wraps raylib ImageCopy. Returns 0 if the source handle is invalid.
h | handle of the source image to duplicate |
returns cx_int new image handle (1-based), or 0 on failure
int dup = imageCopy(h);
imagecrop(h, x, y, w, ht)
crops a CPU image in place to a rectangular region
Wraps raylib ImageCrop; mutates the existing image. No-op if the handle is invalid.
h | handle of the image to crop in place |
x | left edge of the crop rectangle |
y | top edge of the crop rectangle |
w | crop rectangle width |
ht | crop rectangle height |
returns void
imageCrop(h, 0, 0, 16, 16);
imagecropcopy(src, x, y, w, ht)
crops a rectangular region of a CPU image into a new image
Wraps raylib ImageFromImage; leaves the source untouched. Returns 0 if the source handle is invalid.
src | handle of the source image |
x | left edge of the region to copy |
y | top edge of the region to copy |
w | region width |
ht | region height |
returns cx_int new image handle (1-based), or 0 on failure
int part = imageCropCopy(h, 0, 0, 16, 16);
imageresize(h, w, ht)
resizes a CPU image in place, resampling its pixels to the new size
Wraps raylib ImageResize (bicubic), so this SCALES the picture rather than cropping or padding it -- the whole image still fills the new rectangle and the aspect ratio is the caller's to preserve. The alpha channel survives. It is the last step of a derivation and mutates the handle you already own; there is deliberately no `imageResizeCopy`, because `imageCopy` composes to give that and a second name for one operation is a forwarder (rule 33).
h | handle of the image to resize in place |
w | new width in pixels; must be > 0 or the call is refused |
ht | new height in pixels; must be > 0 or the call is refused |
returns nothing. A bad handle is a no-op, as everywhere in this family. A non-positive dimension is REFUSED rather than obeyed: a zero-width image is a live handle whose every later read is meaningless, which is the silent-wrong this project does not ship (FP4).
int a = imageOpen("plaque.png");
imageResize(a, 424, 128);
imageSave(a, "plaque_web.png");imageblit(dst, src, sx, sy, sw, sh, dx, dy)
copies a rectangular region from one CPU image onto another
Wraps raylib ImageDraw; the source rect (sx,sy,sw,sh) is drawn to the destination at (dx,dy) at the same size. No-op if either handle is invalid.
dst | handle of the destination image (drawn into) |
src | handle of the source image |
sx | source region left edge |
sy | source region top edge |
sw | source region width |
sh | source region height |
dx | destination x (top-left of the pasted region) |
dy | destination y (top-left of the pasted region) |
returns void
imageBlit(dst, src, 0, 0, 16, 16, 0, 0);
imagecolorkey(h, color)
makes every pixel of a given colour fully transparent
The white-box alpha bake: wraps raylib ImageColorReplace, swapping the keyed colour for transparent (BLANK). No-op if the handle is invalid.
h | handle of the image to key |
color | colour to make transparent, packed 0xAARRGGBB |
returns void
imageColorKey(sheet, parseHex("ffffff"));imagealpha(h, a)
scales the whole image's alpha channel by a/255
Multiplies every pixel's alpha by a/255 in place; a is clamped to 0..255. No-op if the handle is invalid.
h | handle of the image to fade |
a | alpha scale factor, 0..255 (255 = unchanged) |
returns void
imageAlpha(h, 128);
imagerectalpha(h, x, y, w, ht, a)
sets the alpha of a rectangular region to a fixed value
Writes alpha = a to every pixel in the region, which is clipped to the image bounds; a is clamped to 0..255. No-op if the handle is invalid.
h | handle of the image to modify |
x | region left edge |
y | region top edge |
w | region width |
ht | region height |
a | alpha value to set, 0..255 |
returns void
imageRectAlpha(h, 0, 0, 16, 16, 128);
imagesave(h, path)
exports a CPU image to a file, format chosen by the extension
Wraps raylib ExportImage. No-op if the handle is invalid.
h | handle of the image to export |
path | output file path; extension selects the format (e.g. .png) |
returns void
imageSave(sheet, "art/_alpha/ModuleFrames.png");
imagetotexture(h)
uploads a CPU image to the GPU texture registry for drawing
The one bridge from the design-time CPU world to the run-time GPU draw registry (raylib LoadTextureFromImage). Needs an open window/GL context; returns 0 when headless or if the handle is invalid.
h | handle of the CPU image to upload |
returns cx_int GPU texture handle, or 0 if headless/invalid
int tex = imageToTexture(h);
imagefree(h)
releases a CPU image and frees its slot immediately
Unconditional destroy that drops the resource bucket (bypassing ARC) then unloads the image; a bare imageFree(h) call still works standalone. No-op if the handle is invalid.
h | handle of the image to release |
returns void
imageFree(sheet);
imagedecref(h)
decrements an owned image's ARC refcount, reclaiming the slot at zero
Compiler-emitted at scope exit for an owned `image` local (CXB_RESOURCE bucket ARC); reclaims the slot when the refcount reaches 0. Not normally written by hand.
h | handle of the owned image local being released |
returns void
imageDecref(h);
imagew(h)
returns a CPU image's width in pixels
h | handle of the image to measure |
returns cx_int width in pixels, or 0 if the handle is invalid
return imagew(h);
imageh(h)
returns a CPU image's height in pixels
h | handle of the image to measure |
returns cx_int height in pixels, or 0 if the handle is invalid
int hgt = imageH(h);
imagegetpixel(h, x, y)
reads the packed 0xAARRGGBB colour of a pixel at (x, y)
Reads the R8G8B8A8 pixel and packs it as (a<<24)|(r<<16)|(g<<8)|b. Returns 0 if the handle is invalid or (x,y) is out of range.
h | handle of the image to sample |
x | pixel x coordinate (0-based) |
y | pixel y coordinate (0-based) |
returns cx_int packed 0xAARRGGBB colour, or 0 if out of range/invalid
int c = imageGetPixel(h, 0, 0);
imagelivecount()
returns the number of currently-open CPU image slots
Always-available leak introspection (not gated by `checks on`); the CPU image pool grows on leak, so a rising count signals leaked handles.
returns cx_int count of live CPU image slots
int before = imagelivecount();
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.
w | window width in pixels (<= 0 -> 800) |
h | window height in pixels (<= 0 -> 600) |
title | window title text |
gpu_screen_open(1280, 720, "My Game")
gpu_frame_begin(clear_color)
open a frame, clear it to color, and say whether to keep going
Wraps raylib BeginDrawing + ClearBackground and returns 0 when the user has asked to close, so the idiom is `while (gpu_frame_begin(color)) { ... }`. Also fires the per-frame automation + GUI hooks. WHAT COUNTS AS "asked to close": on the desktop, the window's close button or the ESC key (raylib's default exit key). IN A BROWSER TAB (a wasm build) there is no close button, and raylib's web platform never reports one -- so ESC is the only exit, and CX checks it there itself so that the same `while` loop ends on the same key it ends on natively. A program whose only exit is the close button therefore never ends on the web; give it a frame cap or an ESC.
clear_color | background color to clear this frame to (rgb()/0xAARRGGBB) |
returns 1 to draw this frame, 0 once the window should close
while (gpu_frame_begin(rgb(20,20,30))) { ...draw...; gpu_frame_end(); }gpu_screen_clear(color)
clear the whole screen to a color
Wraps raylib ClearBackground; tolerates being called outside a BeginDrawing/EndDrawing frame by opening a transient one.
color | fill color as 0xRRGGBB |
returns void
gpu_screen_clear(0);
gpu_draw_rect(x, y, width, height, color)
draw a filled rectangle at (x,y) sized width x height
raylib DrawRectangle.
x | top-left x |
y | top-left y |
width | rectangle width |
height | rectangle height |
color | fill color 0xRRGGBB |
returns void
gpu_draw_rect(10, 10, 100, 100, rgb(50,50,50));
gpu_draw_rect_alpha(x, y, width, height, color, alpha)
draw a filled rectangle with an explicit alpha
Like gpu_draw_rect but overrides the color's alpha with `alpha` (0-255, clamped).
x | top-left x |
y | top-left y |
width | rectangle width |
height | rectangle height |
color | fill color 0xRRGGBB |
alpha | opacity 0-255 |
returns void
gpu_draw_rect_alpha(10, 60, 200, 15, rgb(100, 80, 60), 128);
gpu_draw_line(x1, y1, x2, y2, color)
draw a line from (x1,y1) to (x2,y2)
raylib DrawLine.
x1 | start x |
y1 | start y |
x2 | end x |
y2 | end y |
color | line color 0xRRGGBB |
returns void
gpu_draw_line(10, 110, 200, 110, rgb(100, 80, 60));
gpu_draw_circle(x, y, radius, color)
draw a filled circle centred at (x,y) with a radius
raylib DrawCircle.
x | centre x |
y | centre y |
radius | circle radius |
color | fill color 0xRRGGBB |
returns void
gpu_draw_circle(msx, msy, r2, colMnt);
gpu_draw_circle_alpha(x, y, radius, color, alpha)
draw a filled circle with an explicit alpha
Like gpu_draw_circle but overrides alpha (0-255); used for soft translucent particles.
x | centre x |
y | centre y |
radius | circle radius |
color | fill color 0xRRGGBB |
alpha | opacity 0-255 |
returns void
gpu_draw_circle_alpha(485, 140, 45, rgb(255, 130, 80), 150);
gpu_draw_pixel(x, y, color)
draw a single pixel at (x,y)
raylib DrawPixel.
x | pixel x |
y | pixel y |
color | pixel color 0xRRGGBB |
returns void
gpu_draw_pixel(560, 60, rgb(255, 255, 255));
gpu_draw_triangle(x1, y1, x2, y2, x3, y3, color)
draw a filled triangle through three points
raylib DrawTriangle, drawn in both windings so it is order-independent (no back-face culling gap).
x1 | first vertex x |
y1 | first vertex y |
x2 | second vertex x |
y2 | second vertex y |
x3 | third vertex x |
y3 | third vertex y |
color | fill color 0xRRGGBB |
returns void
gpu_draw_triangle(340, 330, 430, 330, 385, 255, rgb(210, 210, 70));
gpu_draw_roundrect(x, y, w, h, roundness, col)
filled rounded rectangle (raylib DrawRectangleRounded)
Corner smoothness is a fixed internal default of 8 segments/corner (not a parameter -- no consumer needs it; a one-line signature extension if one ever does). Native-only (no register-VM path).
x | left edge, pixels |
y | top edge, pixels |
w | width, pixels |
h | height, pixels |
roundness | corner radius as raylib's 0..1 fraction of the SHORT side, inherited VERBATIM (0 = square corners, 1 = fully rounded / capsule). raylib clamps out-of-range values to [0,1] itself. |
col | 0xRRGGBB colour, drawn opaque. |
returns void
gpu_draw_roundrect(x, y, 200, 80, 0.3, 0x2244aa)
gpu_draw_roundrect_alpha(x,y,w,h, roundness, col, alpha)
filled rounded rectangle with an explicit alpha
Twin of gpu_draw_roundrect (family idiom, cf. gpu_draw_rect / gpu_draw_rect_alpha). 8 segments/corner. Native-only.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
col | 0xRRGGBB colour |
alpha | 0..255 opacity (0 = transparent, 255 = opaque) |
returns void
gpu_draw_roundrect_alpha(x, y, 200, 80, 0.3, 0x2244aa, 128)
gpu_draw_roundrect_lines(x,y,w,h, roundness, thick, col)
rounded-rectangle OUTLINE (raylib DrawRectangleRoundedLinesEx)
8 segments/corner. Native-only.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
thick | outline thickness, pixels |
col | 0xRRGGBB colour, drawn opaque |
returns void
gpu_draw_roundrect_lines(x, y, 200, 80, 0.3, 2.0, 0x8fb2ff)
gpu_draw_roundrect_lines_alpha(x,y,w,h, roundness, thick, col, alpha)
rounded-rectangle outline with an explicit alpha
Twin of gpu_draw_roundrect_lines. The Gigantica shield HUD box (rounded outline + intensity) is the named consumer. 8 segments/corner. Native-only.
x,y,w,h | rectangle, pixels |
roundness | 0..1 fraction of the short side (raylib semantics, verbatim) |
thick | outline thickness, pixels |
col | 0xRRGGBB colour |
alpha | 0..255 opacity |
returns void
gpu_draw_roundrect_lines_alpha(x, y, 200, 80, 0.3, 2.0, 0x8fb2ff, 180)
gpu_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.
x | centre x |
y | centre y |
radius | outer radius |
progress_pct | opening progress 0-100 |
color | ring color 0xRRGGBB |
rings | number 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.
x | rect top-left x |
y | rect top-left y |
width | rect width |
height | rect height |
intensity | brightness 0-100 |
color | field color 0xRRGGBB |
rings | number 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.
img | texture handle for the flame core (0 = none) |
x | emitter x |
y | emitter y |
angle_deg | thrust direction in degrees (float) |
size | flame sprite size in pixels |
speed | max particle travel distance |
tint_color | flame sprite tint 0xRRGGBB |
cone_deg | particle spread half-angle in degrees |
particle_color | particle color 0xRRGGBB |
density | particle 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).
x | rect top-left x |
y | rect top-left y |
width | rect width |
height | rect height |
color_a | first sparkle color 0xRRGGBB |
color_b | second sparkle color 0xRRGGBB |
density | number of dots (<=0 -> 32) |
lifetime_ms | currently unused |
returns void
gpu_fx_glitter(50, 50, 200, 120, rgb(255,255,180), rgb(255,200,80), 40, 500);
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.
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.
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.
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).
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.
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.
s | source string |
n | how many bytes to take from the start |
returns the leading n bytes, the whole string if n exceeds its length, or "" if n <= 0
code.s = left(line, 3);
right(s, n)
the last n bytes of a string
The mirror of left(), with the same clamping: over-long n gives the whole string, n <= 0 gives "".
s | source string |
n | how many bytes to take from the end |
returns the trailing n bytes, the whole string if n exceeds its length, or "" if n <= 0
ext.s = right(path, 4);
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".
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
returns the binary digits; "0" for zero
println(bin(mask));
space(n)
a string of n 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.
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.
hay | string to search |
needle | substring to count |
returns the number of non-overlapping occurrences; 0 if the needle is empty or longer than the haystack
n.i = countstring(csv, ",");
replacestring(s, old, nw)
replace every occurrence of a substring
Replaces ALL non-overlapping matches in one pass, left to right, and never re-scans what it wrote -- so replacing "a" with "aa" terminates rather than running away. An empty `old` is a no-op returning a copy, not an insertion at every position.
s | source string |
old | substring to find; empty means "change nothing" |
nw | text to put in its place; empty deletes (that is removestring) |
returns a new string with every match replaced
out.s = replacestring(path, "\\", "/");
removestring(s, sub)
delete every occurrence of a substring
Exactly replacestring(s, sub, "") -- same non-overlapping, all-occurrences rule.
s | source string |
sub | substring to delete; empty means "change nothing" |
returns a new string with every occurrence removed
clean.s = removestring(raw, "\r");
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.
hay | string to search |
needle | substring to look for |
start | 1-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.
s | source string |
ins | text to insert |
pos | 1-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.
s | string to place |
width | field width in bytes |
returns a string of exactly `width` bytes, or "" if width <= 0
println(lset(name, 20) + str(score));
rset(s, width)
right-justify a string in a fixed-width field
Pads on the LEFT with spaces to reach `width`. Over-long input is truncated to the width keeping the LEFT-hand bytes -- the same truncation as lset(), which is worth knowing because right-justified numbers lose their least significant digits, not their most.
s | string to place |
width | field width in bytes |
returns a string of exactly `width` bytes, or "" if width <= 0
println(rset(str(n), 8));
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.
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().
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.
hay | string to search |
needle | substring 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.
hay | string to test |
prefix | prefix to look for |
returns 1 if `hay` begins with `prefix` (or the prefix is empty), else 0
if (startswith(path, "http://")) { ... }endswith(hay, suffix)
does a string end with this suffix?
Case-SENSITIVE, byte-exact. An empty suffix answers 1. The usual way to test a file extension -- lowercase the name first if the check should be case-blind.
hay | string to test |
suffix | suffix to look for |
returns 1 if `hay` ends with `suffix` (or the suffix is empty), else 0
if (endswith(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.
a | first string |
b | second string |
returns -1 if a sorts before b, 0 if they match case-insensitively, +1 if after
if (strcmpi(cmd, "QUIT") == 0) { ... }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.
s | string to split |
n | 1-indexed field number |
sep | separator; 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.
s | string to split |
sep | separator to split on |
out | an 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.
s | source string |
pos | 1-indexed byte position to start at; <= 0 yields "" |
n | how 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.
s | string to re-case |
mode | 0 upper, 1 lower, 2 title; omit for title-case |
returns a new re-cased string
title.s = capitalize(raw);
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".
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.
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.
path | file to append to; an empty path returns 0 |
fmt | format string, printf-style |
returns the number of bytes written -- a genuine count, unlike fwrite. 0 on any failure (bad format, unopenable path).
n.i = fprintf("log.txt", "run %d took %f s\n", id, secs);ftoa(v)
render a float as a string (C's name for str on a float)
The same renderer str reaches for a float argument, so the decimal count follows `#pragma decimals`. strf(v, d) is the form that takes the decimals per call.
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.
s | string to read |
idx | 0-indexed byte position |
returns the byte as 0..255, or 0 if idx is out of range
c.i = getc(line, 0);
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.
s | the string to search |
sub | the 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.
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`.
v | the 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.
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.
s | string to read the first byte of |
returns the byte as 0..255; 0 for an empty string
code.i = ord(key);
parsehex(s)
read a hexadecimal string as an integer
A plain base-16 parse: no "0x" prefix is required (one is accepted), case does not matter, and parsing stops at the first non-hex character with no error channel -- unparseable text gives 0, 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.
s | hex digits, with or without a leading "0x" |
returns the value, or 0 if the text does not start with a hex digit
col.i = parsehex("ff8800");prts(s)
write a STRING to stdout with no newline
The raw piece-by-piece output form: no newline, no formatting, no conversion. prti, prtf and prtc are the int, float and character siblings.
returns void
sprintf(fmt)
format text. ONE name, TWO shapes, and ARGUMENT 1 chooses
`sprintf(fmt, args...)` returns the formatted STRING. This is CX's form and the one nearly every program wants: nothing to size, nothing to overflow. `fmt` may be a literal or a `string` variable; conversions are libc's (the compiler rewrites `%d` to `%lld` upstream) and honour `#pragma decimals`. `sprintf(buf, fmt, args...)` where `buf` is a `char` buffer is C's form, verbatim: it writes into the buffer, NUL-terminates, and returns the BYTE COUNT. It exists so that C pasted into CX means what it meant in C -- the `Examples/C/` programs are Rosetta-Code source, unedited. Both backends answer identically; the register VM clamps the write to the declared size rather than running off the array, since C's own answer there is undefined. Argument 1's DECLARED TYPE picks the shape, at compile time. Anything that is neither a `char` buffer nor a string -- an int, a float, a container -- is CX-E1065 at the .cx line, because there is no third reading to guess at.
fmt | the format string (CX form), OR the destination buffer (C form) |
returns the formatted string (CX form), or the byte count written (C form)
label.s = sprintf("%s: %d%%", name, pct);
char buf[64]; int n = sprintf(buf, "%d/%d", num, den);str(n)
render a number as a string
The compiler picks the int or the float renderer from the argument's type, so str(3) gives "3" and str(3.0) gives "3.000000" or whatever `#pragma decimals` is set to. Use strf when you want to fix the decimal count at the call site instead.
n | the int or float to render |
returns the number as text
line.s = "score: " + str(points);
strchr(s, ch)
0-indexed position of a character code in a string, C-style
Takes the character as a NUMERIC CODE, as C does, not as a one-character string. Returns -1 when the character is absent, so the test is `>= 0`; instr is the 1-indexed BASIC twin that takes a string needle.
s | the string to search |
ch | the character code to look for |
returns the 0-indexed position, or -1 if the character does not occur
at.i = strchr(line, 61);
strcmp(a, b)
compare two strings in byte order, C-style
Returns a SIGN, not a boolean: negative if `a` sorts before `b`, 0 if they are equal, positive if after. Comparison is by raw byte value, so it is case-sensitive and not locale-aware. CX's `==` on strings is the readable way to test equality; strcmp is for ordering.
a | the first string |
b | the second string |
returns negative, 0, or positive as `a` sorts before, equal to, or after `b`
if strcmp(name, pivot) < 0 { ... }strf(v, d)
a float as text, with the number of decimals you choose
`strf(v)` renders at the file's `#pragma decimals` setting (default 3), which is what `str()` and `ftoa()` do. `strf(v, d)` overrides it FOR THAT CALL -- the pragma is file-global and last-wins, so this is the only way to write a price at 2 decimals and an angle at 6 in the same program. `d` is C's `%.*f` precision and behaves as C does, including the corner: a NEGATIVE `d` means "precision omitted", which in C is six decimals. There is no upper limit -- ask for 40 and you get 40, correctly, however long the result runs.
v | the float to render |
d | decimals to show; omit to use `#pragma decimals` |
returns the rendered string
label.s = strf(price, 2) + " at " + strf(angle, 6) + " rad";
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).
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 "".
s | the string to break up |
sep | the separator to split on |
out | the 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.
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.
h | string to search |
n | substring 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.
s | source string |
pos | 0-indexed byte position to start at |
n | how 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.
returns the leading base-10 integer, or 0 if there is none
n.i = val(row);
math 39 builtins
sin(x)
sine of an angle in radians
returns the sine, in -1.0 .. 1.0
y.f = amplitude * sin(t);
cos(x)
cosine of an angle in radians
returns the cosine, in -1.0 .. 1.0
x.f = radius * cos(angle);
tan(x)
tangent of an angle in radians
returns the tangent; unbounded, and huge near odd multiples of pi/2
asin(x)
arc sine: the angle whose sine is `x`
x | a value in -1.0 .. 1.0; outside that range the result is NaN |
returns the angle in radians, in -pi/2 .. pi/2
acos(x)
arc cosine: the angle whose cosine is `x`
x | a value in -1.0 .. 1.0; outside that range the result is NaN |
returns the angle in radians, in 0 .. pi
atan(x)
arc tangent: the angle whose tangent is `x`
Takes one argument, so it cannot tell quadrant II from IV -- use `atan2` when you have both components of a direction.
returns the angle in radians, in -pi/2 .. pi/2
atan2(y, x)
the angle of the vector (x, y), using both signs to pick a quadrant
NOTE THE ARGUMENT ORDER: `y` FIRST, as in C. It is the usual way to turn a delta into a heading, and the reason it beats `atan(y/x)` is that it is defined when `x` is 0 and it knows which half of the circle you are in.
y | the vertical component |
x | the horizontal component |
returns the angle in radians, in -pi .. pi
heading.f = atan2(ty - py, tx - px);
sinh(x)
hyperbolic sine
returns the hyperbolic sine; overflows to infinity for large `x`
cosh(x)
hyperbolic cosine
returns the hyperbolic cosine, always >= 1.0
tanh(x)
hyperbolic tangent
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.
x | a positive value; 0 gives -infinity and a negative gives NaN |
returns the natural log
log10(x)
base-10 logarithm
x | a positive value; 0 gives -infinity and a negative gives NaN |
returns the base-10 log
digits.i = 1 + floor(log10(n));
exp(x)
e raised to the power `x`, the inverse of `log`
returns e**x; overflows to infinity for large `x`
sqrt(x)
square root
x | a non-negative value; a negative gives NaN |
returns the square root
pow(b, e)
raise `b` to the power `e`
b | the base |
e | the exponent; need not be a whole number |
returns b**e
area.f = pow(side, 2.0);
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.
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.
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.
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.
returns x without its sign
fmin(a, b)
the smaller of two FLOATS
a | first value |
b | second value |
returns whichever is smaller
fmax(a, b)
the larger of two FLOATS
a | first value |
b | second value |
returns whichever is larger
sign(x)
which side of zero a value is on, as an INT
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.
a | the dividend |
b | the divisor; 0.0 gives 0.0 |
returns the remainder, with the sign of `a`
wrapped.f = mod(angle, 360.0);
clamp(x, lo, hi)
pull a value inside `lo`..`hi`, returning a FLOAT
Float is the default because that is what the register VM answers; an inverted range (lo > hi) is not checked and yields `hi`.
x | the value to constrain |
lo | lower bound, returned when x is below it |
hi | upper bound, returned when x is above it |
returns x, lo or hi, as a float
volume.f = clamp(volume + step, 0.0, 1.0);
lerp(a, b, t)
linear interpolation between `a` and `b`
Computed as a + (b - a) * t, so it is NOT clamped: t below 0 or above 1 extrapolates past the endpoints, which is often what you want.
a | the value at t = 0 |
b | the value at t = 1 |
t | the fraction of the way from a to b |
returns the interpolated value
x.f = lerp(startX, endX, elapsed / duration);
between(x, lo, hi)
is `x` inside `lo`..`hi`, endpoints INCLUDED
A macro, so it works for ints and floats alike. It evaluates `x` TWICE -- do not pass a call with side effects.
x | the value to test |
lo | lower bound, counted as inside |
hi | upper bound, counted as inside |
returns 1 if lo <= x <= hi, else 0
if between(mx, panelX, panelX + panelW) { ... }distance(x1, y1, x2, y2)
straight-line distance between two 2-D points
x1 | first point's x |
y1 | first point's y |
x2 | second point's x |
y2 | second point's y |
returns the Euclidean distance, as a float
if distance(px, py, ex, ey) < range { attack(); }remap(x, aLo, aHi, bLo, bHi)
rescale `x` from one range onto another, linearly
Not clamped: an `x` outside the input range lands outside the output range in proportion. A ZERO-WIDTH input range answers `bLo` rather than dividing by zero.
x | the value to rescale |
aLo | input range low |
aHi | input range high |
bLo | output range low, and the answer when aLo equals aHi |
bHi | output range high |
returns x expressed in the output range
px.f = remap(value, 0.0, 100.0, left, right);
degrees(r)
convert radians to degrees
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.
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.
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.
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.
x | the dividend |
y | the 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.
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.
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`.
a | first value |
b | second value |
returns whichever is larger
hp.i = max(0, hp - damage);
min(a, b)
the smaller of two INTS
The float form is `fmin`.
a | first value |
b | second value |
returns whichever is smaller
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.
returns x with its fraction removed, rounded toward zero
n = trunc(-2.7); // -2.0
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.
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.
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.
handle | the grown-array handle |
value | the 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.
handle | the grown-array handle |
returns void
array d.i[4] = {9}; // the compiler emits the padarrayflatidx(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.
handle | the 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.
handle | the grown-array handle |
flat | row-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.
ndims | number 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).
handle | the grown-array handle |
ndims | number 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.
handle | the grown-array handle |
key | the 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.
handle | the grown-array handle |
flat | row-major flat index |
value | the 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.
handle | the grown-array handle |
desc | non-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.
handle | the 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.
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.
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.
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.
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.
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.
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.
returns the total -- int if every element was an int, otherwise float; 0 when empty
average(c)
the mean of a container (the other name for avg)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Identical to avg, and always float.
c | the array, list, map or JSON array to reduce |
returns the mean as a float; 0 when empty
avg(c)
the mean of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. ALWAYS FLOAT, unlike sum/minof/maxof, because a mean rarely is one. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the mean as a float; 0 when empty
fill(c, v)
set every element of a container to one value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Writes in place, through the same element-ownership path a normal assignment uses, so replacing strings does not leak.
c | the container to overwrite |
v | the value to write into every slot |
returns void
fill(grid, 0);
find(c, key)
the index of the first element equal to a value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
LINEAR and first-match, so it works on unsorted data -- arraysearch is the binary search that needs the container sorted first. Ints compare exactly, floats within the default tolerance, strings by content.
c | the container to search |
key | the value to look for |
returns the 0-indexed position of the first match, or -1 if there is none
at.i = find(names, "ada");
listadd(list, value)
append a value to the end of a list
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
value | the value to append |
returns void
listAdd(xs, 42);
listclear(list)
remove every element, leaving an empty list
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
returns void
listClear(xs);
listdelete(list, index)
remove the element at a position, closing the gap
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
index | 0-based position to remove |
returns void
listDelete(xs, 0);
listfirst(lst)
move a list cursor to the first element
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
CX containers carry ONE internal cursor, so these walk a list without an index variable and without a second handle -- and because there is only one cursor per container, two interleaved walks of the same list tread on each other. Returns 0 for an empty list, which is what ends the loop before it starts.
returns 1 if the list has an element to stand on, 0 if it is empty
if listFirst(items) { repeat { use(listGet(items)); } while listNext(items); }listget(lst, i)
read a list element -- at the cursor, or at an index
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
TWO FORMS, told apart by the argument count: listGet(lst) reads where the cursor stands, listGet(lst, i) reads element `i`. listGetAt is the always-indexed spelling for when you want no ambiguity.
lst | the list to read |
i | 0-indexed element; omit to read at the cursor |
returns the value of the element
listgetat(lst, i)
read a list element by index, always (never the cursor)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The unambiguous half of listGet: always the indexed form, so a one-argument mistake cannot become a silent cursor read.
lst | the list to read |
i | 0-indexed element to read |
returns the value of the element
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.
returns the 0-indexed cursor position, or -1 if the cursor is not on an element
listinsert(list, index, value)
insert a value at a position, shifting the rest along
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Operates on the shared list runtime both backends use, so behaviour is identical native and VM. A list parameter is byref by default, so a call inside a function mutates the caller's list.
list | the list |
index | 0-based position to insert at |
value | the value to insert |
returns void
listInsert(xs, 0, 42);
listlast(lst)
move a list cursor to the LAST element
The starting point for a backwards walk with listPrev, as listFirst is for listNext.
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.
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.
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.
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.
lst | the list to position |
i | 0-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.
lst | the list to write into |
i | 0-indexed element; omit to write at the cursor |
v | the 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.
lst | the list to write into |
i | 0-indexed element to write |
v | the 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.
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.
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.
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.
map | the map |
hash_kind | which bucket hash to use |
multi | non-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.
m | the map to test |
k | the key to look for |
returns 1 if the key is present, 0 if not
mapcreate()
create a map and return it as a plain int handle
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved.
returns an int handle to a new integer-valued map
m = mapCreate();
mapdelete(m, k)
delete one entry from a map, by key
A key that is not present is not an error -- the map is simply unchanged. This is the ONE name for the operation: `mapRemove` was retired at v3.237.0 (one verb per op).
m | the map to remove from |
k | the key to remove |
returns void
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.
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.
handle | the int handle from mapCreate |
key | the string key |
returns the value, or 0 if the key is absent or the handle is zero -- so a stored 0 and a missing key read alike
v = mapGet(m, "hp");
maphas(m, k)
does a map hold this key? (the other name for mapHasKey)
Tests for the KEY, not the value.
m | the map to test |
k | the 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.
handle | the int handle from mapCreate |
key | the string key |
returns non-zero if the key is present; 0 otherwise (including a zero handle)
if (mapHasKey(m, "hp")) { ... }mapkey(m)
the KEY of the map entry the cursor is standing on
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Only meaningful between a mapNext that returned 1 and the one that returns 0.
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.
returns 1 if the cursor landed on an entry, 0 once past the end
mapReset(m); while mapNext(m) { print mapKey(m) + "=" + mapValue(m); }mapput(handle, key, value)
store a value under a key in a handle-held map
Part of the EXPLICIT-HANDLE map pattern: `m = mapCreate()` keeps a map in a plain int variable, and these adapt that int back to a real map. For a declared `map m.i;` the compiler dispatches straight to the typed runtime and these are never involved.
handle | the int handle from mapCreate |
key | the string key |
value | the integer value |
returns void; a zero handle is ignored
mapPut(m, "hp", 10);
mapreset(m)
put a map cursor back before the first entry
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Maps carry the same single cursor as lists, over their entries in HASH order -- which is UNSPECIFIED by contract and is NOT insertion order. Do not write code that depends on it; put a sortndx over the map when you need a defined order. This rewinds the walk; it does not remove anything (use mapClear to empty the map).
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.
handle | the 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.
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.
c | the array, list, map or JSON array to reduce |
returns the largest element; 0 when empty
top.i = maxof(scores);
minof(c)
the smallest element of a container
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
UNIVERSAL: one name over arrays, lists, maps and JSON arrays, because all of them reduce through the same core on both backends. Over a MAP it reduces the VALUES. Elements that are not numbers are coerced leniently (a numeric string parses, a bool counts 0 or 1), the same rule the rule engine uses. INT-PRESERVING: an all-int sequence answers an INT; the first float element promotes the whole reduction to float. An empty container answers 0.
c | the array, list, map or JSON array to reduce |
returns the smallest element; 0 when empty
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.
array | the grown array being used as an ordered map |
key | the 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.
array | the grown array being used as an ordered map |
key | the string key |
value | the 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.
list | the list being used as an ordered map |
key | the 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.
list | the list being used as an ordered map |
key | the string key |
value | the 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.
c | the list or array to reverse |
returns void
search(c, key)
binary-search a SORTED list or array for a value
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
NEEDS THE CONTAINER SORTED FIRST -- it is a binary search, so on unsorted data it will miss values that are there. `find` is the linear first-match that works on any order; use that unless the container is already sorted and large.
c | the SORTED list or array to search |
key | the value to look for |
returns the 0-indexed position, or -1 if the value is not found
sort(ids); at.i = search(ids, wanted);
sort(c, desc)
sort a list or array in place, ascending by default
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
IN PLACE: it rearranges the container rather than returning a sorted copy, so the original order is gone. An optional second argument of 1 sorts descending. Elements compare by their natural order -- numeric for numbers, byte order for strings. sortCmp is the form that takes your own comparator.
c | the list or array to sort |
desc | 1 to sort descending; omit or 0 for ascending |
returns void
sort(scores, 1);
sortarray(array)
sort an array in place (compiler-emitted)
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
EMITTED BY THE COMPILER, not written by hand: this is the lowering CX generates for an array sort. Prefer the CX spelling `sort(a)`.
returns void
sort(a);
sortcmp(c, cmp)
sort a list or array in place using a CX function as the comparator
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
The comparator is one of your own functions, and it follows C ordering: return negative if the first argument sorts first, 0 if they tie, positive if the second sorts first. In place, like sort.
c | the list or array to sort |
cmp | the comparator function to order by |
returns void
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.
c | the list of structs to sort |
field | the struct field to order by |
desc | 1 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.
c | the array, list, map or JSON array to reduce |
returns the total -- int if every element was an int, otherwise float; 0 when empty
total.i = sum(scores);
varslot(v)
the address of a fixed-size local, as an integer slot value
An ESCAPE HATCH, not everyday CX. Its live consumer is the native-only three-argument form of jsonLoadArray, which needs an array's address to fill it in place. Prefer passing the array directly -- an array handed to a function is byref already (rule 31).
v | the local whose address is wanted |
returns the variable's address as an integer
jsonLoadArray(data, path, varslot(arr));
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.
etype | element type code — 0 int, 1 float, 2 string, 3 struct |
elem_size | per-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.
q | the queue (declared `queue q.i;`) |
capacity | ring size. ANY RUNTIME EXPRESSION -- this is the whole point over an array dimension, which must be a literal or #define: the bound can come from json. Must be > 0 (loud otherwise). |
policy | 0 = REJECT (push on full is a loud error -- a dropped item is a lost fact), 1 = ROLL (push on full overwrites the OLDEST, giving a "last N" sliding window; count pins at capacity). |
returns void
queueInit(impacts, lookup(cfg, "maxImpact"), 0)
queueclear(q)
drop every pending element, keeping capacity and policy
This is the DATA op; queueInit is the SHAPE op. One name each. THE per-bout op -- it is what replaces the game's `impN = 0`.
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.
returns nothing
function f.v() { queue tmp.i; ... } // queuefree emitted at the closing bracequeuecount(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.
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.
returns capacity
if (q->count == q->cap) { ... } // fullqueuevalid(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.
h | queue handle (any integer -- an out-of-range or freed one is fine) |
returns 1 when `h` names a live queue, 0 otherwise
if (q->valid) { print(q->count); }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.
q | the queue (declared `.i`) |
v | the 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.
q | the queue (declared `.f`) |
v | the 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.
q | the queue (declared `.s`) |
v | the 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`.
q | the 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`.
q | the 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`.
q | the 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`.
q | the 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`.
q | the 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`.
q | the queue (declared `.s`) |
returns the newest string, which is removed from the queue
while recent->count > 0 { println(queuePop(recent)); } // newest firstqueuepushst(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.
q | the queue (declared over a struct type) |
src | the 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`.
q | the queue (declared over a struct type) |
dst | the 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`.
q | the queue (declared over a struct type) |
dst | the struct variable the newest element is copied into |
returns nothing — the result arrives in `dst`
struct Sample out; queuePop(window, out); // most recent reading
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.
src | the 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.
blob | an 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).
mfh | a 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.
doc | a 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.
handle | any 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.
handle | any json handle (a doc or a doc["k"] view) |
returns void
jsonincref(doc);
jsonvalue(doc)
unwrap a parsed document to its root value node
DOC-only: always returns the doc wrapper's first child. Use it before the raw jsonMember/jsonElement/jsonSize accessors (which do not deref a doc).
doc | a doc handle from 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).
node | an 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.
node | json array/object (or a document handle wrapping one) |
returns the first member's handle, 0 when empty or not a container
h = jsonFirst(items); while (h != 0) { print(jsonAsStr(jsonGet(items))); h = jsonNext(items); }jsonnext(node)
advance a walk started by jsonFirst
Takes the SAME container handle jsonFirst was given -- not the handle it returned. Returns the next member's handle, or 0 at the end.
node | the container being walked (a document handle is deref'd) |
returns the next member's handle, 0 when the walk is finished
h = jsonNext(items);
jsonget(node)
read the member the walk is currently on
Takes the container, and returns the member handle the cursor sits on -- the accessor a `foreach` body uses to reach the current element. 0 before the walk starts or after it ends.
node | the container being walked (a document handle is deref'd) |
returns the current member's handle, 0 when the cursor is not on one
foreach items { total = total + jsonAsInt(jsonGet(items)); }jsonkey(node)
the KEY of the member this container's cursor is on
jsonGet's twin: same argument (the CONTAINER being walked, not the member), same both-backend binding, so one `foreach` body can read a name and a value without changing what it passes. foreach doc { println(jsonKey(doc) + " = " + str(jsonGet(doc))); } WITHOUT IT A JSON OBJECT COULD NOT BE ENUMERATED IN CX AT ALL (D27, filed 2026-08-19 by the page-renderer arc, fixed v3.307.0). The walk could reach every value and never learn a single name; `mapKey(doc)` -- which the foreach refusal CX-E0044 pointed at -- refused natively and answered a silent 0 on the register VM, so the language's own advice named the one thing that does not work.
node | the container being walked (a document handle is deref'd) |
returns the member's key, or "" for an ARRAY element (which has no name) and for a cursor that is not on anything
k = jsonKey(doc);
jsonmember(obj, key)
look up a key on a JSON object
Linear scan of the object's members by key. Raw form -- does NOT deref a DOC wrapper (use jsonValue first or the doc-aware jsonMemberV / j["key"]).
obj | the object value node to search |
key | the member key string to find |
returns the member value node handle; 0 if the key is absent
nameV = jsonmember(root, "name");
jsonelement(arr, idx)
fetch an array element by index
Walks the child list to the given 0-based index. Raw form -- does NOT deref a DOC wrapper.
arr | the array value node |
idx | the 0-based element index |
returns the element node handle; 0 if the index is out of range
assertFloatEqual(10.0, jsonnumber(jsonelement(arrRoot, 0)));
jsonstring(node)
read the text of a JSON string node
Raw accessor: returns the node's stored string handle with no coercion or DOC-deref (a number/bool/container reads empty -- use jsonAsStr to coerce).
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).
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.
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.
node | any 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).
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.
node | json 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.
node | json 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.
node | an object value node or a parsed-doc wrapper |
key | the 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.
obj | json object (a document handle is deref'd) |
key | the member name to navigate to or create |
want | CX_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.
arr | json array (a document handle is deref'd) |
idx | zero-based element index; the array grows to reach it |
want | CX_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.
node | an array value node or a parsed-doc wrapper |
idx | the 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.
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.
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.
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.
node | any json value node |
rhs | the 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.
node | any json value node |
rhs | the 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.
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.
node | json array/object node, or a doc handle wrapping one |
opch | the 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.
node | json array/object node, or a doc handle wrapping one |
key | the value to look for |
aspred | 0 -> 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.
node | json array/object node, or a doc handle wrapping one |
key | the text to look for |
aspred | 0 -> 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
arr | the array node to append to |
v | the float value to append |
returns void
foreach scores { jsonArrNum(arr, listGet(scores)); }jsonarrint(arr, v)
append an int64-exact number element to a JSON array
Stores the value exactly (int-born node), avoiding the double rounding that corrupts integers past 2^53.
arr | the array node to append to |
v | the int value to append |
returns void
jsonArrInt(arr, 9007199254740993);
jsonarrstr(arr, v)
append a string element to a JSON array
arr | the array node to append to |
v | the string value to append |
returns void
jsonArrStr(slots, mods[slotMod[si]].id);
jsonarrbool(arr, v)
append a boolean element to a JSON array
arr | the array node to append to |
v | the value; stored as 1 if nonzero else 0 |
returns void
jsonArrBool(arr, 1);
jsonarrnull(arr)
append a null element to a JSON array
arr | the array node to append to |
returns void
jsonArrNull(arr);
jsonarradd(arr, node)
append a node (object/array) as the next array element
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.
arr | the array node to append to |
node | the 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.
obj | the object node to add to |
key | the member key for the nested node |
node | the object/array node to nest (must be unparented) |
returns void
jsonAddNode(doc, "slots", slots);
jsonaddnum(obj, key, v)
append a float member to a JSON object
Pure append (part of the build API) -- allows ordered duplicates while constructing; use jsonSet* for update-or-append semantics.
obj | the object node to add to |
key | the member key |
v | the float value |
returns void
jsonaddnum(built, "count", 42.0);
jsonaddint(obj, key, v)
append an int64-exact member to a JSON object
Int-born node stored exactly (no double rounding past 2^53). Emitters route here when the RHS is statically int.
obj | the object node to add to |
key | the member key |
v | the int value |
returns void
jsonAddInt(obj, "id", 9007199254740993);
jsonaddstr(obj, key, v)
append a string member to a JSON object
Pure append (build API); use jsonSetStr for update-or-append.
obj | the object node to add to |
key | the member key |
v | the string value |
returns void
jsonAddStr(mE, "axis", "ENERGY");
jsonaddbool(obj, key, v)
append a boolean member to a JSON object
obj | the object node to add to |
key | the member key |
v | the value; stored as 1 if nonzero else 0 |
returns void
jsonaddbool(built, "active", 1);
jsonaddnull(obj, key)
append a null member to a JSON object
obj | the object node to add to |
key | the member key |
returns void
jsonAddNull(obj, "note");
jsonsetnum(obj, key, v)
set an object member to a float, updating in place or appending
mapPut / `obj["k"]=v` semantics: updates the existing key or appends if absent -- never a shadowing duplicate. Codegen target of a float subscript write; also user-callable.
obj | the object node |
key | the member key |
v | the float value to store |
returns void
jsonSetNum(obj, "speed", 3.5);
jsonsetint(obj, key, v)
set an object member to an int64-exact number, update-or-append
Int64-exact lane of jsonSetNum (mapPut semantics).
obj | the object node |
key | the member key |
v | the int value to store |
returns void
jsonsetint(gCgiFonts, sFont, nH);
jsonsetstr(obj, key, v)
set an object member to a string, update-or-append
String lane of the mapPut/subscript-write setters.
obj | the object node |
key | the member key |
v | the string value to store |
returns void
jsonsetstr(gCgiVars, sVar, sVal);
jsonsetnode(obj, key, src)
copy a JSON value node into obj[key] by the source's type
For `dst["k"] = src[...]`: a subscript read yields a node handle, so this stores the VALUE (scalar-by-value), not the handle. A nested object/array source is not supported (would alias) and is reported rather than corrupting.
obj | the destination object node |
key | the destination member key |
src | the source json value node to copy from |
returns void
jsonSetNode(dst, "hp", src);
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.
arr | json array (a document handle is deref'd) |
idx | zero-based index; negative is refused loudly with no write |
v | the 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.
arr | json array (a document handle is deref'd) |
idx | zero-based index; negative is refused loudly with no write |
v | the integer value to store |
jsonSetElemInt(ids, 0, 9007199254740993);
jsonsetelemstr(arr, idx, v)
store a string into arr[idx], growing the array to reach it
arr | json array (a document handle is deref'd) |
idx | zero-based index; negative is refused loudly with no write |
v | the 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.
arr | json array (a document handle is deref'd) |
idx | zero-based index; negative is refused loudly with no write |
src | the 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`.
obj | the object node |
key | the member key |
op | a one-character op string: "+" "-" "*" "/" "%" |
rhs | the float right-hand operand |
returns the new float value (assignment reads as its RHS); on a bad op returns the unchanged member value with no write
float v = jsonRmwNum(e, "hp", "+", 5.0);
jsonrmwint(obj, key, op, rhs)
fused read-modify-write of an object member in one key walk (int64-exact lane)
Int-exact when the member is int-born (or missing) and op is + - * % ; '/' always falls to the float lane (div/mod unification). Missing key reads 0 and creates.
obj | the object node |
key | the member key |
op | a one-character op string: "+" "-" "*" "/" "%" |
rhs | the int right-hand operand |
returns the new int value; on a bad op returns the unchanged value with no write
int v = jsonRmwInt(e, "hp", "-", 3);
jsonrmwstr(obj, key, op, rhs)
fused read-modify-write string concat of an object member in one key walk
Computes obj[key] = obj[key] + rhs (the read coerces to text). '+' is the only string RMW op; any other reports and writes nothing. Missing key concats from "" and creates.
obj | the object node |
key | the member key |
op | a one-character op string -- only "+" (concatenate) |
rhs | the string to concatenate |
returns the new concatenated string; on a bad op returns the unchanged member text with no write
string s = jsonRmwStr(e, "log", "+", "!");
jsonexport(node)
serialize a JSON value to a compact string
Machine output (no whitespace); int-born numbers emit exact digits. Derefs a DOC wrapper.
node | any 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.
node | any 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.
path | the file path to read |
returns the parsed doc handle; -1 on a parse error (an empty/missing file parses as failure)
json doc = jsonLoad("data/eventsrules.json");jsonsave(path, j)
serialize a JSON value (compact) and write it to a file
jsonExport + fwrite in one call.
path | the destination file path |
j | the json value/doc to serialize |
returns the fwrite status int (nonzero/bytes on success, 0 on write failure)
jsonSave("data/lastloadout.json", doc);jsonsavepretty(path, j)
serialize a JSON value (pretty) and write it to a file
jsonExportPretty + fwrite in one call.
path | the destination file path |
j | the json value/doc to serialize |
returns the fwrite status int (nonzero/bytes on success, 0 on write failure)
jsonSavePretty("out.json", doc);jsonflush(view)
write a file-bound JSON view's document to disk now if it is dirty
Manual flush point for a jsonBind LAZY-WRITE view (the exit hook is the backstop). No-op on an unbound handle or a clean doc.
view | a file-bound json view handle from jsonBind |
returns void
jsonFlush(gBindings);
jsondirty(view)
test whether a file-bound JSON view has unsaved changes
view | a file-bound json view handle from jsonBind |
returns 1 if the bound doc has pending changes; 0 if clean or the handle is unbound/invalid
if (jsonDirty(view)) { jsonFlush(view); }jsonbound(view)
test whether a JSON handle is a file-bound view
returns 1 if this json is a jsonBind view; 0 otherwise (unbound/invalid)
if (jsonBound(j)) { jsonFlush(j); }jsonunbind(view)
release a file-bound JSON view
Decrements the shared doc's refcount; on the last view it flushes any pending changes then frees the whole doc. A still-shared doc just detaches this view's subtree.
view | a file-bound json view handle from jsonBind |
returns void
jsonUnbind(view);
jsonbind(path, policy, subpath)
bind a JSON file to a policy-tagged view (shared doc, disjoint views)
One shared refcounted document per path with disjoint policy-tagged sub-views. policy 0=READONLY (a write is an FP4 error), 1=LAZY-WRITE (writes mark dirty, flushed at checkpoint/exit). The optional subpath (default "" = whole file) selects an existing top-level section. Overlapping views are rejected.
path | the JSON file path to bind |
policy | 0 = READONLY, 1 = LAZY-WRITE |
subpath | optional top-level member to view; omitted/empty = the whole file |
returns a json view node handle; 0 on error (missing RO file, overlapping/duplicate view, subpath not found, parse failure)
gRules = jsonBind("data/eventsrules.json", JSON_READONLY, "rules");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.
node | the array or object node |
key | the key, or an empty string for an array append |
value | the string value |
returns the result of the underlying add
jsonAdd(arr, "", "sword");
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.
map | the flat map from parseFullJson |
prefix | the dotted key prefix |
out | a list (both backends), or varslot(array) on native only |
returns the number of elements loaded; 0 on failure
jsonLoadArray(m, "items", xs);
parsefulljson(path, map)
read a JSON file and flatten it into a string map with dotted-path keys
The whole document becomes one flat map: `player.name` = "Alice", `items.0` = "sword", and a `items._count` entry giving each array's length. Useful when you want lookups by path rather than a walked tree.
path | the JSON file to read |
map | the string map to fill |
returns non-zero on success; 0 on failure
parseFullJson("save.json", m);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.
src | the 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.
mfh | a 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.
doc | the 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.
doc | the 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.
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
node | the 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.
node | the 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
node | the 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
node | the 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.
node | the element node handle |
key | the 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.
parent | the parent element, or the document handle to add a root to |
name | the 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).
node | the element node handle |
text | the 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).
node | the element node handle |
key | the attribute name |
val | the 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.
node | the 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
parent | the node to add under |
name | the 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
node | the element |
text | the 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
node | the element |
value | the 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.
returns the tag name
s.s = xmlTag(row);
xmlvalue(node)
read a node's text content
returns the node's text
s.s = xmlValue(row);
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".
path | path to test; an empty path returns 0 |
returns 1 if any filesystem object exists at path, else 0
if (fileexists("save.json")) { s.s = fread("save.json"); }fdelete(path)
delete a file. CX's portable delete
This is the one to reach for instead of shelling out: `system("del x")` is cmd.exe-only and `system("rm x")` is POSIX-only, so either one silently does nothing on the other platforms. Wraps C's remove(), so on POSIX it will also unlink an empty directory; removedir() is the explicit spelling for that.
path | file to delete; an empty path returns 0 |
returns 1 if the file was deleted, 0 if it was not there or could not be removed
fdelete("scratch.tmp");direxists(path)
is this path a directory?
LINKS ARE FOLLOWED, on every OS. This answers about the file the path NAMES, not about a symbolic link or Windows junction pointing at it -- matching POSIX stat(), which is what CX has always done on macOS and Linux. Windows answered about the LINK until v3.204.1, because MSVCRT stat() reports the reparse point; see cx_path_stat in cx_builtins_string.c. A link whose target is missing or unreadable therefore reports ABSENT, not "a zero-byte thing that exists".
path | path to test; empty returns 0 |
returns 1 if path exists AND is a directory, else 0 (a plain file gives 0)
if (!direxists("out")) { makedir("out"); }filesize(path)
a file's size in bytes
LINKS ARE FOLLOWED, on every OS. This answers about the file the path NAMES, not about a symbolic link or Windows junction pointing at it -- matching POSIX stat(), which is what CX has always done on macOS and Linux. Windows answered about the LINK until v3.204.1, because MSVCRT stat() reports the reparse point; see cx_path_stat in cx_builtins_string.c. A link whose target is missing or unreadable therefore reports ABSENT, not "a zero-byte thing that exists".
returns the byte size, or -1 if the path is empty, does not exist, or is a directory. -1 rather than 0, so an empty file (0) stays distinguishable from a missing one.
n.i = filesize("data.bin");makedir(path)
create one directory
Creates a SINGLE level: it is mkdir, not `mkdir -p`, so creating "a/b/c" needs a call per level. Created 0755 on POSIX.
path | directory to create; empty returns 0 |
returns 1 on success, 0 on failure -- INCLUDING when the directory already exists. Guard with direxists() rather than treating 0 as fatal.
if (!direxists("out")) { makedir("out"); }removedir(path)
remove an EMPTY directory
Refuses a non-empty directory; there is no recursive delete builtin, so clearing a tree means dirlist() + fdelete() per entry, then this.
path | directory to remove; empty returns 0 |
returns 1 on success, 0 on failure (missing, not a directory, or not empty)
removedir("scratch");renamefile(oldp, newp)
rename a file, or move it within one filesystem
A thin rename(): it does NOT cross devices, and on Windows it fails when the destination already exists. movefile() is the one that handles both.
oldp | existing path; empty returns 0 |
newp | new path; empty returns 0 |
returns 1 on success, 0 on failure
renamefile("draft.txt", "final.txt");copyfile(srcp, dstp)
copy a file's bytes to a new path
Binary-safe, streamed in 16KB blocks, so file size is not bounded by memory. The destination is TRUNCATED if it exists. Copies content only -- permission bits and timestamps are not carried over.
srcp | file to read; empty returns 0 |
dstp | file to write; empty returns 0 |
returns 1 on success, 0 on failure (unreadable source, unwritable destination, or a short write partway through -- in which case the destination is left partially written, not removed)
copyfile("save.json", "save.bak");movefile(srcp, dstp)
move a file, across devices if need be
Tries rename() first, which is atomic and instant when source and destination share a filesystem. If that fails it falls back to copyfile() + delete the source, which is how a move onto another volume succeeds. The fallback is not atomic: an interrupted cross-device move can leave both copies.
srcp | file to move; empty returns 0 |
dstp | destination path; empty returns 0 |
returns 1 on success, 0 if both the rename and the copy failed
movefile("out.log", "archive/out.log");dirlist(path, pattern, names)
list a directory's entries into a string list
Appends to `names` (it does not clear it first), so listing two directories into one list accumulates. "." and ".." are always skipped. The pattern is a tiny CASE-INSENSITIVE glob: `*` matches any run including empty, `?` matches exactly one character; there are no character classes. An empty pattern means `*`. Entry NAMES are returned, not paths -- join them to `path` yourself. Files and subdirectories are both listed, with no marker distinguishing them; call direxists() on the joined path to tell.
path | directory to scan; an empty path means the current directory |
pattern | glob to match entry names against; empty means everything |
names | string list the matches are APPENDED to |
returns the number of entries appended; 0 if the directory cannot be opened
list files.s
n.i = dirlist("data", "*.json", files);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.
path | the file to append to |
content | the 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.
returns the file's contents; an EMPTY STRING if the path is empty, the file cannot be opened, or the file is zero bytes. A missing file and an empty file are indistinguishable in the return value -- use fileexists() when the difference matters.
s.s = fread("config.json");fwrite(path, content, append)
write a whole file, replacing whatever was there
The write is STAGED: content goes to "<path>.tmp" and is renamed over the target only once it is fully written and closed, so a disk-full or short write leaves the original file intact rather than truncated. A read-only target is refused (probed with fopen "r+b", which neither truncates nor creates, so the answer honours ACLs and read-only mounts). On POSIX the original file's permission bits are re-applied after the rename, because the staged file would otherwise arrive with umask's bits -- a 0600 file came back 0644 before that.
path | file to write; an empty path is a no-op returning 0 |
content | bytes to write; may be empty, which truncates the file to zero |
append | 0 replaces the file, 1 appends to it (the CX `fappend` spelling) |
returns 1 on success, 0 on failure. NOT a byte count.
ok.i = fwrite("out.txt", "hello\n");print(v)
write a value to stdout followed by a NEWLINE
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
Takes any printable value and renders it the way CX renders it, so it needs no format string. printf is the format-string form and prt* the newline-free one.
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.
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.
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.
ch | the character code to write |
returns the code written, as C's putchar does
putc(65);
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.
port | TCP port to bind (1-65535; below 1024 needs privilege) |
returns a server handle > 0, or < 0 on failure -- netError() then has a NAMED reason ("the port is already in use"), never a bare errno
srv.i = netListen(7000);
netlistenany(port)
listen for TCP connections on a port, on EVERY interface
The explicit counterpart to netListen: this one is reachable from other machines. A distinct name rather than a flag argument is the point -- exposing a port beyond this machine should be visible at a glance and greppable across a codebase. On Windows the first such bind in a program's life may raise a one-time firewall prompt; that is machine configuration, not a program error.
port | TCP port to bind (1-65535; below 1024 needs privilege) |
returns a server handle > 0, or < 0 on failure (netError() has the detail)
srv.i = netListenAny(7000);
netaccept(server)
take the next incoming connection on a server handle
BLOCKS until a client arrives. To wait with a deadline -- or not to wait at all -- ask netPoll first: a zero timeout never blocks, and netPoll(srv, -1) blocks exactly as this does. The returned connection is independent of the server: closing it leaves the server listening, and closing the server does not close connections already accepted from it.
server | a handle from netListen or netListenAny |
returns a connection handle > 0, or < 0 on failure (netError() has the detail)
c.i = netAccept(srv);
netconnect(host, port)
open a TCP connection to a host and port
The host may be a name ("localhost", "build01.local") or a literal address ("127.0.0.1"); resolution covers IPv4 and IPv6. The attempt is BOUNDED by netTimeout(ms) -- the same knob the HTTP client uses -- so a peer that is switched off costs the timeout rather than the operating system's own retry policy, which can run to tens of seconds. Set netTimeout(1000) before sweeping a peer list.
host | hostname or literal IP address |
port | TCP port to connect to |
returns a connection handle > 0, or < 0 on failure -- netError() names the condition ("connection refused", "host not found", "timed out")
c.i = netConnect("127.0.0.1", 7000);netpoll(handle, timeout_ms)
ask whether a handle is ready, waiting at most timeout_ms
The tier's one waiting primitive, deliberately the C mechanism named plainly (select on one handle). "Ready" means the next operation will not block: on a SERVER handle a connection is waiting, so netAccept returns at once; on a CONNECTION handle bytes are waiting (or the peer has closed), so netRead returns at once. timeout_ms = 0 polls and returns immediately -- the form that drops into a frame loop. timeout_ms < 0 waits forever, which makes netPoll(srv, -1) followed by netAccept exactly a blocking-accept server. A program holding several connections polls each with 0 and sleeps once, rather than paying the timeout per handle.
handle | a server or connection handle |
timeout_ms | 0 = poll, < 0 = wait forever, > 0 = wait up to this long |
returns 1 ready, 0 timed out, < 0 on failure
ready.i = netPoll(srv, 100);
netread(handle)
read the bytes waiting on a connection
BLOCKS until at least one byte arrives or the peer closes (netPoll first to avoid that). Returns up to one buffer's worth: TCP is a byte STREAM, not a message queue, so one netWrite by the peer may arrive as two netReads and two may arrive as one -- a protocol that needs message boundaries must put them in the bytes (a length prefix, or a newline). BINARY-SAFE: CX strings carry their length, so the result may contain embedded NUL bytes and survives netWrite -> netRead unchanged. Printing such a value stops at the first NUL -- that is print's contract, not a truncation of the data.
handle | a connection handle |
returns the bytes read; "" if the peer closed cleanly OR on error -- netError() distinguishes them (empty after a clean close)
msg.s = netRead(c);
netwrite(handle, data)
write bytes to a connection
Writes ALL of the data or fails: partial writes are retried internally, so this never reports success having sent half a message. Binary-safe -- the length comes from the CX string, so embedded NUL bytes are sent like any other byte.
handle | a connection handle |
data | the bytes to send |
returns the number of bytes written (always the full length on success), or < 0 on failure -- a peer that vanished mid-write is reported as "connection reset", never as a short write
n.i = netWrite(c, "hello");
netclose(handle)
close a connection, or stop a server listening
Closing a connection sends the peer an orderly end-of-stream, which its next netRead sees as "". Closing a server handle stops new connections arriving and leaves already-accepted connections untouched. The handle is invalid afterwards: using it again is a program defect, not a network condition, and is refused loudly (CX-E5038) rather than read as an empty message.
handle | a server or connection handle |
returns 0 on success, < 0 on failure (netError() has the detail)
netClose(c);
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.
returns the response body, or an empty string on failure
body.s = httpGet("https://example.com/api/items");httppost(url, body)
POST a body to a URL and return the response body
Sends `body` verbatim, with no Content-Type set and no form encoding: build the payload (JSON, form text) yourself.
url | the URL to post to |
body | the request body, sent as-is |
returns the response body, or an empty string on failure
reply.s = httpPost(url, 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.
ms | the new transfer timeout in milliseconds; 0 disables it |
returns the timeout that was in force before this call
prev.i = netTimeout(5000); body.s = httpGet(u); netTimeout(prev);
ftpget(url)
download a file over ftp or ftps and return its contents
The same transport as `httpGet`, chosen by the URL scheme, so the timeout and error reporting are identical.
url | an ftp:// or ftps:// URL naming a file |
returns the file's contents, or an empty string on failure
data.s = ftpGet("ftp://host/pub/readme.txt");sftpget(url)
download a file over SSH (sftp) and return its contents
url | an sftp:// URL naming a file |
returns the file's contents, or an empty string on failure
ftplist(url)
list a remote directory
The URL must name a DIRECTORY and end with a slash; without the trailing slash the server is being asked for a file instead.
url | an ftp:// URL ending in "/" |
returns the listing as text, or an empty string on failure
entries.s = ftpList("ftp://host/pub/");ftpput(url, localpath)
upload a local file over ftp or ftps
url | the destination ftp:// or ftps:// URL, including the remote filename |
localpath | the local file to read and send |
returns 0 on success, a negative number on failure
if ftpPut("ftp://host/in/out.csv", "R:/tmp/out.csv") < 0 { print netError(); }sftpput(url, localpath)
upload a local file over SSH (sftp)
url | the destination sftp:// URL, including the remote filename |
localpath | the local file to read and send |
returns 0 on success, a negative number on failure
emailsend(smtpurl, from, to, subject, body)
send one email through an SMTP server
`body` is the message text only; no headers are synthesised beyond the addresses and subject, and there are no attachments.
smtpurl | the server URL, e.g. "smtp://mail.example.com:587" |
from | the sender address |
to | the recipient address |
subject | the subject line |
body | the message text |
returns 0 on success, a negative number on failure
emailSend("smtp://mail:25", "cx@host", "me@host", "build", log);sshexec(host, user, pass, cmd)
run a command on a remote host over SSH and return its stdout
PORT 22 IS FIXED and so is the 15-second timeout; there is no port argument. Only stdout comes back -- stderr and the command's exit status do not, so a command that fails looks like one that printed nothing. `sshExecKey` is the key-based form and is the better habit.
host | the host name or address |
user | the remote user name |
pass | that user's password |
cmd | the command line to run remotely |
returns the command's stdout, or an empty string on failure
out.s = sshExec("build01", "ci", pw, "uname -a");sshexeckey(host, user, keyfile, cmd)
the same remote exec, authenticated with a PRIVATE KEY FILE
Same fixed port 22, same 15-second timeout, same stdout-only result.
host | the host name or address |
user | the remote user name |
keyfile | path to the OpenSSH or PEM private key |
cmd | the command line to run remotely |
returns the command's stdout, or an empty string on failure
out.s = sshExecKey("build01", "ci", "/home/ci/.ssh/id_ed25519", "uname -a");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.
nIdx | the site index the emitter assigned (dense, from 0). |
sName | the watched variable, spelled as the author wrote it. |
sSite | the function the instrumented writes live in ("main" for module-level statements) -- the site half of the key. |
nKind | CX_WATCH_INT / CX_WATCH_FLOAT / CX_WATCH_STRING. |
nTrigger | 0 = onchange; N > 0 = report at most once every N ms. |
nRelay | 0 = 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. |
nFn | the 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. |
nIsNative | 1 = 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
nIdx | the site index. |
nVal | the value AFTER the write -- so a condition written over the variable reads the new value, which is what a debugger means by a watch. |
nCond | the 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
nIdx | the site index. |
fVal | the value after the write. |
nCond | the evaluated condition (1 when none was written). |
watchHitF(1, angle, angle > 3.0);
watchhits(nIdx, sVal, nCond)
a STRING write reached an instrumented site
nIdx | the site index. |
sVal | the value after the write. |
nCond | the 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.
cond | the condition that must hold |
msg | text identifying the assertion in the output |
returns void
assert(count > 0, "parser produced tokens");
assertequal(expected, actual, msg)
fail the run unless two values are equal, reporting both
Callable, but its argument list is not derivable from its binding: it is bound only by the register VM's name table, where the C signature is the generic (vm, N) wrapper form. Named here so an absence is never mistaken for non-existence.
TYPE-DISPATCHED, and on BOTH backends the same way: a string pair compares by content, and a pair where EITHER side is a float compares as floats. That second rule is the one that matters -- comparing an int literal against a float that happens to be 100.5 must not truncate and pass.
expected | the value the code should produce |
actual | the value it did produce |
msg | text identifying the assertion in the output |
returns void
assertEqual(3, len(parts), "three fields");
assertequalstr(expected, actual, msg)
fail the run unless two strings have the same contents (the other name for assertStringEqual)
Identical to assertStringEqual; both reach the same string comparison.
expected | the text the code should produce |
actual | the text it did produce |
msg | text identifying the assertion in the output |
returns void
assertfloatequal(expected, actual, msg)
fail the run unless two floats are equal within tolerance
Compares with a tolerance rather than exactly, which is what makes it usable on computed floats where the last bits will not match.
expected | the value the code should produce |
actual | the value it did produce |
msg | text identifying the assertion in the output |
returns void
assertnotequal(a, b, msg)
fail the run unless two INTEGERS differ
INT ONLY, deliberately: there are no float or string siblings, because a native-only float form would be a divergence between the backends rather than a feature.
a | the first value |
b | the second value |
msg | text identifying the assertion in the output |
returns void
assertstringequal(expected, actual, msg)
fail the run unless two strings have the same contents
Compares by content, not by handle. assertEqualStr is the same function under another name.
expected | the text the code should produce |
actual | the text it did produce |
msg | text identifying the assertion in the output |
returns void
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.
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.
title | the 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.
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.
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.
x | panel left |
y | panel top |
w | panel width |
h | panel 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.
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.
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.
title | window title (optional) |
w | window width (optional) |
h | window height (optional) |
returns void
debugOpen("trace");debugprint(text)
write text straight into the captured buffer
The Orfeus-era spelling of console -- no trailing newline.
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.
x | panel left |
y | panel top |
w | panel width |
h | panel 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.
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.
title | window title (optional) |
w | window width (optional) |
h | window 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.
ch | the character code to write |
returns void
prtf(v)
write a FLOAT to stdout with no newline
The float sibling of prts; decimals follow `#pragma decimals`.
returns void
prti(n)
write an INTEGER to stdout with no newline
The int sibling of prts.
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