How the language works
There is one idea. Everything else follows from it.
Placement is a property of state, not of code.
You declare where each value lives. The compiler works out the rest: which code goes in the browser, which becomes a server endpoint, what crosses the wire, and what the browser is allowed to see.
The four placements
Change the one word and watch everything else change with it.
state count is client Whole starting 0- Lives
- In the browser tab, for as long as the page is open.
- Browser sees
- The value — it is the browser’s own.
- Crosses the wire
- Nothing. There is no boundary to cross.
- Compiler emits
client.js
A reload starts it over at 0. Navigation is a new program instance, so client state does not survive it.
// one word changed. Everything else followed from it.
Functions have no placement
This is the part that surprises people, so it is worth stating flatly:
function politeGreeting with who
if who is ""
give "Hello, stranger."
give "Hello, " + who + "."
politeGreeting is not a server function or a client function. It is a
function. If a server signal calls it, it runs on the server; if a client
signal calls it, it runs in the browser — byte for byte the same code.
There is no async, no "use server", and no colour to propagate up the call
chain. A function used on both sides is emitted into both bundles.
Signals, and no dependency arrays
Every piece of state is a signal. starting declares one you set directly,
from declares one the compiler recomputes:
state count is client Whole starting 0
state doubled is client Whole from count * 2
doubled re-derives when count changes. You do not list that dependency
anywhere — the compiler knows it from the signal graph, because it read the
expression.
The boundary shows up in the type
When a value crosses a boundary, its type says so. That is Remote of T:
state greeting is server Text from politeGreeting with name
view
when greeting
Loading show Spinner
Failed with error show ErrorBar message is error.message
Ready with text show Text text
You cannot forget to handle the failure, because there is nothing to read until you have named all three arms. The network is not hidden from you; it is made into a type.
static is the interesting exception. It is evaluated at build time and
inlined, so nothing crosses at run time, so it is plain T — no boundary, no
Remote.
Secrets are enforced, not documented
secret state apiKey is server Text from environment "STRIPE_KEY"
A secret value reaching the view is a compile error. This is checked by
grepping the built bundle for the value, rather than asserted in a comment —
if apiKey appeared in client.js, the compiler's own test suite would fail.
Check yourself
Reading is not remembering. Four questions, each a rule you will meet in the first hour:
state total is server Whole from sumOf with items is cartWhere does `sumOf` run?
What this buys, concretely
A guestbook — client input, a server-side greeting, a durable visit counter —
is one file with no fetch, no API route, no schema, no migration and no deploy
config. zdc build on it emits the client bundle and one file per derived
server endpoint, and apiKey appears in none of the client output.
Where to go next
- The examples — twenty-six working programs, ordered by what they teach, including the ones that exist to record what the language cannot do yet.
- The standard library — what the prelude gives you, and why adding a name to it costs something.