Language
Concurrency
Zornux runs concurrent work on a single deterministic timeline — no threads, no locks, no shared-memory bugs. Tasks cooperate through messages, and timers fire on a virtual clock, so a concurrent program prints the same thing every time and tests never flake.
Background tasks
Start a function in the background with start task. It returns a
handle you can wait for, cancel,
or inspect (is_completed / has_failed). A started
function always finishes — or is cancelled — before the program ends.
function download
wait(2)
show "Download complete"
end
function main
create worker = start task download
show "Doing other work while it downloads..."
wait for worker
show "Worker done? " + text(worker.is_completed)
give back 0
end
A started function runs when you wait for it (or, if you never do, when its parent finishes). Among tasks there is no real parallelism and no race conditions — execution is fully reproducible.
Message channels
A channel is a first-in, first-out queue tasks
communicate through. send value to channel puts a message in;
receive from channel takes one out. channel() is
unbounded; channel(n) buffers up to n messages.
create jobs = channel()
function producer
send "job 1" to jobs
send "job 2" to jobs
send "job 3" to jobs
end
start task producer
repeat 3 times
create note = receive from jobs
show "Handling " + note
end
Name your variables note, msg, or item — message itself is a reserved word (it's part of give back status N with message).
Timers: after & every
Run a block later, or on a repeating interval, without blocking:
after 4 seconds
send "deploy" to jobs
end
every 1 seconds, 3 times
show "beat"
end
Scheduled blocks fire in due-time order on the virtual clock whenever the
program is willing to wait — at the end of the run, or during a bounded
wait. Add a timeout to a blocking receive with
up to N seconds so it never waits forever:
show "picked up: " + receive from jobs up to 5 seconds
Awaitable async tasks
An async function returns a handle instead of running inline;
wait for is an expression that awaits it and
yields its give back value. This is how services call
repositories and other components:
async function fetch_total with cart
create total = 0
for each price in cart
total = total + price
end
give back total
end
create handle = fetch_total([12, 30, 7])
show "Total: " + text(wait for handle)
| Construct | Meaning |
|---|---|
start task X | Run X in the background; returns a handle. |
wait for handle | Finish the function; as an expression, yields an async function's result. |
cancel handle | Cancel a function that hasn't run yet. |
send x to q / receive from q | Channel send / receive (add up to N seconds to time out). |
after N seconds … end | Run a block once, later. |
every N seconds, M times … end | Run a block on an interval, M times. |
Background jobs reuse this timeline for delayed and scheduled work, and the timers here share the injectable clock behind wait and current_datetime, so every concurrent program stays deterministic.
Real parallelism — parallel / compute
For CPU-bound fan-out there's one construct that uses real OS
threads: a parallel block runs each compute
branch at the same time and yields their results as a list, always in source
order. It's made safe — and deterministic in result — by strict
share-nothing isolation: each branch runs in a fresh world,
immutable values cross as-is, mutable ones are deep-copied, and a branch may not
touch shared or external state (no database, files, network, or show).
On platforms without threads it falls back to an identical sequential run, so the
result never changes.
function score with values
create total = 0
for each n in values
total = total + n
end
give back total
end
create totals = parallel
compute score([1, 2, 3])
compute score([4, 5, 6])
end
show totals # [6, 15] — always in source order
Each branch may call the program's functions and pure declarations and read the values it captures, but it can't reach shared or external state — no database, files, network, or show — and a non-transferable value (a class item, connection, or secret) is refused at the boundary. That's what makes the result deterministic.
Next: building APIs — Controllers.