Zornux docs
Get started Spec

Language

Standard Library

Zornux ships a set of built-in functions for everyday work — text, lists, math, dates, files, and JSON. They're always available (no imports) and strictly typed.

How built-ins work

  • Call them by name, like any function: uppercase("hi").
  • Strict types, no coercion. Passing the wrong kind of value is an error, not a silent conversion — convert first with text(...).
  • Values aren't mutated. List helpers like add_item return a new list rather than changing the original.
  • Friendly errors. Misuse raises a clear diagnostic in the ZX1000–1099 range (wrong argument count, wrong type, index out of range, …).
text() is the universal converter

Because Number + Text never coerces, text(value) turns any value into Text so you can join it: show "Score: " + text(42). It converts the kind but keeps the trust level — untrusted input stays untrusted (see Trust-aware text).

Asking what a value is

type_of gives back a descriptor — a Zornux value kind, never a host-language type — which you can compare like any other value:

zornux
show type_of("hi")                     # <type Text>
show type_of(p)                        # <class Person>
show type_of(p) is equal to Person     # true

show type_of("a") is equal to type_of("b")   # true  — same kind
show type_of(1) is equal to type_of("x")     # false

Text

FunctionResult
text(value)Convert any value to Text.
length(text)Number of characters (also works on lists).
is_empty(text)Truth — whether it has no characters (also works on lists).
uppercase(text) / lowercase(text)Change case.
trim(text)Remove leading and trailing whitespace.
trim_start(text) / trim_end(text)Remove whitespace from only the leading / trailing side (same whitespace set as trim).
contains(text, part)Truth — whether part appears in text.
starts_with(text, part) / ends_with(text, part)Truth — prefix / suffix test.
replace(text, find, with)Replace every occurrence of find.
split(text, separator)Split into a List of Text.
zornux
create name = "  Ada Lovelace  "
show uppercase(trim(name))          # ADA LOVELACE
show contains(name, "Ada")          # true

create parts = split("a,b,c", ",")  # ["a", "b", "c"]
show length(parts)                  # 3

Regular expressions

Six built-ins bring safe pattern matching. A pattern is compiled once from trusted text with regex(...), then reused; every operation runs under a finite match timeout, so a pathological pattern is a clean diagnostic rather than a hang. Untrusted text may be the subject of a match, but never the pattern — closing off ReDoS.

FunctionResult
regex(pattern)Compile trusted Text into a reusable Regex value. Case-sensitive, Unicode-aware (\w \d \s). An invalid or untrusted pattern is refused.
regex_matches(pattern, text)Truth — whether the pattern matches anywhere. Anchor with ^…$ for a whole-string check. text may be untrusted.
regex_find(pattern, text)The first matched substring, or nothing. A match of untrusted text stays untrusted.
regex_find_all(pattern, text)A List of every non-overlapping match, in order (empty when none).
regex_replace(pattern, text, replacement)A new text with every match replaced by the literal replacement ($1 is inserted verbatim, not expanded).
regex_split(pattern, text)A List of the pieces between matches.
zornux
create digits = regex("[0-9]+")               # compiled once, reused
show regex_matches(digits, "order 12")        # true
show regex_find_all(digits, "a12b3")          # [12, 3]
show regex_replace(digits, "a12b3", "#")      # a#b#

# The subject may be untrusted; the pattern must be trusted:
create raw = read_text("input.txt")           # UntrustedText
show regex_matches(regex("^[a-z]+$"), raw)     # ok — untrusted subject
The pattern must be trusted

regex(...) only accepts trusted Text as the pattern — an untrusted pattern is refused, because a caller-supplied regex is a denial-of-service risk. The subject you search may be untrusted. A bad pattern raises ZX1050; one that backtracks past its timeout raises ZX1051 — never a hang.

Lists

List positions are zero-based — the first item is at index 0.

FunctionResult
add_item(list, value)A new list with value appended.
remove_at(list, index)A new list with the item at index removed.
item_at(list, index)The item at index.
join(list, separator)Join a list of Text into one Text.
sort_by(list, selector)A new list ordered by the key a selector gives back for each item — the structured-data counterpart of sort. Stable (equal keys keep input order); the source list is unchanged.
length(list) / is_empty(list)Count of items / whether there are none.
zornux
create fruits = ["apple", "banana"]
create more = add_item(fruits, "cherry")   # ["apple", "banana", "cherry"]

show item_at(more, 0)                       # apple
show join(more, ", ")                       # apple, banana, cherry

create people = [{"name": "Bo", "age": 30}, {"name": "Al", "age": 25}]
create by_age = sort_by(people, function with p give back p["age"] end)
show by_age[0]["name"]                      # Al  (youngest first, stable)
They return new lists

add_item and remove_at don't change the original list — capture the result: create more = add_item(fruits, "cherry"). An out-of-range index raises a clear diagnostic.

Maps

A map holds Text keys mapped to any value. Literals use { … }; read and write entries with [ … ]. Maps are mutable and keep insertion order.

FunctionResult
map_keys(map)A List of the keys, in insertion order.
map_values(map)A List of the values, in insertion order.
map_has(map, key)Truth — whether the key is present.
map_remove(map, key)Removes the key in place; returns the map.
zornux
create user = {"name": "Alice", "age": 30}
show user["name"]                # Alice
user["age"] = 31                 # update
user["email"] = "[email protected]"        # add

for each key in map_keys(user)
    show key + ": " + text(user[key])
end

show to_json(user)               # {"name":"Alice","age":31,"email":"[email protected]"}
Unknown keys read as nothing

Reading a missing key returns nothing rather than failing — use map_has(map, key) to test membership. Keys must be Text, and a duplicate key in a literal is a diagnostic. Maps are distinct from parsed JSON objects and OOP items.

Math

FunctionResult
round(number)Nearest whole number (ties round away from zero).
floor(number) / ceiling(number)Round down / up to a whole number.
truncate(number)Drop the fractional part, toward zero.
is_whole(number) / is_decimal(number)Whether the number has no / some fractional part.
absolute(number)Magnitude, without sign.
min(a, b) / max(a, b)The smaller / larger of two numbers.
random_between(low, high)A random whole number in the inclusive range low … high.
zornux
show round(3.7)             # 4
show floor(3.7)            # 3
show max(10, 25)           # 25
show random_between(1, 6)  # a dice roll, 1 to 6

Date & time

FunctionResult
now()The current date and time.
today()The current date (no time).
format_date(date, pattern)Format a date/time as Text using a pattern.
zornux
show format_date(today(), "yyyy-MM-dd")    # e.g. 2026-06-28
show format_date(now(), "HH:mm:ss")        # e.g. 09:05:03
Patterns

Use patterns like yyyy (year), MM (month), dd (day), HH:mm:ss (time). An unknown pattern raises a diagnostic suggesting a valid one.

Files

FunctionResult
read_text(path)File contents as UntrustedText (it came from outside).
write_text(path, content)Write content to a file.
file_exists(path)Truth — whether the file is there.
zornux
if file_exists("notes.txt")
    create raw = read_text("notes.txt")    # UntrustedText
    show sanitize(raw)                      # clean it before trusting
end

write_text("greeting.txt", "Hello!")
Sandboxed by design

File built-ins only touch a single folder the host allows, and path traversal (../) outside it is rejected. If the host enables no folder, file access is disabled entirely. Because read_text returns UntrustedText, you must sanitize or validate it before combining it with trusted text.

JSON

FunctionResult
parse_json(text)Parse JSON text into a value you can read fields from (a JSON object becomes a map).
to_json(value)Serialize a value to JSON Text.
to_canonical_json(value)Deterministic canonical JSON — object keys sorted by code point at every level, 2-space indent, one trailing newline. The same logical value always produces the same bytes, for hashing, signing, and reproducible manifests.
zornux
create data = parse_json("{ \"name\": \"Ada\", \"age\": 36 }")
show data.name                  # Ada

show to_json(["a", "b", "c"])   # ["a","b","c"]

# Stable bytes for a signature or content hash:
show sha256(to_canonical_json(data))

Environment & streams

For command-line tools, Zornux reads environment variables and the standard streams. Interactive input from the operator is trusted Text; the write built-ins accept any value (like text(...)).

FunctionResult
environment(name)A variable's value as Text, or nothing if unset.
environment_or(name, fallback)The value, or the fallback text when unset.
read_line() / read_line(prompt)One line from input (optionally after a prompt); nothing at end of input.
write(value) / write_line(value)Write to standard output, without / with a newline.
write_error(value) / write_error_line(value)Write to standard error, without / with a newline.
zornux
function main with args
    create mode = environment_or("APP_MODE", "dev")
    create name = read_line("Your name: ")
    write_line("Hello " + name + " (" + mode + ")")
    write_error_line("done")
    give back 0
end
Redirect results and errors apart

show and write_line go to standard output; write_error goes to standard error — so zornux run app.zx > out.txt 2> errors.log keeps them separate. zornux test feeds empty input, so read_line() never blocks a test.

Time & timers

Read the clock and pause without touching threads. wait is the friendly timer; sleep is for finer precision. Timing runs through an injectable clock and delay, so tests stay instant and deterministic.

FunctionResult
wait(seconds)Pause for a number of seconds (fractions allowed).
sleep(milliseconds)Pause for a number of milliseconds.
current_time()The current time of day (HH:mm:ss).
current_date()Today's date (yyyy-MM-dd).
current_datetime()The current date and time.
elapsed_time(start)Seconds elapsed since a date/time captured earlier.
zornux
create start = current_datetime()
wait(1)
show "Elapsed: " + text(elapsed_time(start)) + " seconds"
Built for concurrency

These timers share the same injectable clock as Zornux's structured concurrency — background tasks, wait for, and message channels — so concurrent programs stay just as testable.

Trust-aware text

Five built-ins manage the boundary between outside input and trusted Text. They're the foundation of Zornux's security model — read_text and network input arrive as UntrustedText, and these decide when (and whether) it becomes trusted.

FunctionResult
untrusted(text)Mark a value as UntrustedText.
sanitize(text)Strip dangerous characters and return trusted Text.
sanitize_lines(text)Like sanitize, but keeps newlines and tabs so multi-line input keeps its structure — for file bodies, request bodies, and other multi-line untrusted text.
validate(text)Truth — whether the value is already clean.
trust(text)Advanced override: trust a value without cleaning it.
text() does not launder

text(...) converts kinds (a Number to Text), but it preserves taint — untrusted input passed through text() stays UntrustedText. To cross the trust boundary you must go through sanitize, sanitize_lines, or trust; the compiler points you there rather than letting text() smuggle unchecked data into a trusted sink.

Full details, including the ZX1205 rule, are on the Security page.

Next: Zornux's take on objects — Classes.