Zornux docs
Get started Spec

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.

zornux
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
Cooperative, not preemptive

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.

zornux
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
'message' is reserved

Name your variables note, msg, or itemmessage 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:

zornux
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:

zornux
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:

zornux
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)
ConstructMeaning
start task XRun X in the background; returns a handle.
wait for handleFinish the function; as an expression, yields an async function's result.
cancel handleCancel a function that hasn't run yet.
send x to q / receive from qChannel send / receive (add up to N seconds to time out).
after N seconds … endRun a block once, later.
every N seconds, M times … endRun a block on an interval, M times.
The same clock powers jobs

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.

zornux
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
What a branch may touch

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.