The examples, and what each one is for
The programs in examples/ all
pass zdc check and produce a bundle from zdc build. There is deliberately
no count here: the directory grows, and a number written into prose is the
first thing about a page like this to become false.
STATUS.md also has a table of these files. It is a different document on
purpose: it records what the compiler manages, file by file, as evidence.
This one is ordered by what you want to learn, which is not the same
order and rarely the same sentence.
Every example is a working program. Run any of them:
zdc build examples/guestbook.zd -o dist
Or read them here, because several are running on this page. zdc build
compiled them from the files in that directory while this site was being
built, and each one is mounted from the client.js it emitted. Nothing below
is a screenshot, a recording, or a reimplementation in JavaScript.
That is worth one more sentence, because it is the reason for the arrangement rather than a remark about it. If one of these examples stops compiling, this site stops building. A page that quotes a language decays quietly: the compiler moves, the page does not, and a reader finds out by pasting a snippet that no longer works. These pages cannot decay that way without going down first.
Start here
Three files, in this order, and you will have seen the whole idea.
| File | What it teaches |
|---|---|
hello.zd | The smallest program there is: one piece of client state, one view. No build config, no bundler entry point, no framework import — the file is the program. |
counter.zd | starting declares state you set directly; from declares state the compiler recomputes. There is no dependency array: doubled re-derives because it reads count, and the compiler knows that from the signal graph. |
guestbook.zd | The whole point of the language in one file. Three placements, no fetch, no API route, no schema, no migration, no deploy config. You declare where state lives and the compiler derives the network. |
If you read only one, read guestbook.zd. It is also the file the README
uses to demonstrate that apiKey reaches no client output — a claim checked
by grepping the built bundle rather than asserted.
It is also the one example on this page that cannot run on it, and the
reason is the thing it teaches. A static site is files on a CDN; guestbook.zd
declares state that lives somewhere else, so the compiler derived a store and
three endpoints for it, and this page has neither. It is compiled on every
build anyway, and the file names below are read out of the manifest zdc
wrote rather than typed into this page:
This program has state that does not live in the browser. The compiler turned it into endpoints and a store, and a static site has neither. It is compiled on every build so that what follows stays true; it is not mounted.
- apiKey
- server
- visits
- durable
- name
- client
- greeting
- server
endpoints the compiler derived
- greeting.js
- visits.incr.js
- visits.js
durable signals, which want a store
- visits
Nothing in the source names a route, a handler, a schema or a fetch. The three files above are what the compiler produced from the words server and durable, and they are the reason a CDN is not enough.
the file that was compiled
# guestbook.zd — the whole point of ZDeceptron in one file.
#
# Three placements. No fetch, no API route, no schema, no migration,
# no deploy config. You declare where state lives; the compiler derives
# the network.
# Lives in a serverless invocation. Never reaches the browser — and that
# is enforced by the type system, not by convention. Writing `Text apiKey`
# anywhere in the view below is a compile error.
secret state apiKey is server Text from environment "GREETING_API_KEY"
# Lives in a persistent store. Survives reload, shared across visitors.
# The compiler emits the storage and the live sync; there is no schema
# to write and no migration to run.
state visits is durable Whole starting 0
# Lives in browser memory. Lost on reload, and that is correct for a
# text box.
state name is client Text starting ""
# Computed in a serverless invocation, because it reads a server signal.
# The compiler re-runs it when `name` changes and never more often,
# because the signal graph knows the dependency.
state greeting is server Text from politeGreeting with name, apiKey
function politeGreeting with who, key
if who is ""
give "Hello, stranger."
give "Hello, " + who + "."
view
Column
Heading "Guestbook"
Input name, hint is "your name"
# `greeting` lives on the server, so reading it from the client
# gives Remote of Text, not Text. The network is visible in the
# type exactly where the network is. You cannot forget the
# loading state, because you cannot read through the variant
# without eliminating it.
# `greeting`'s endpoint reads `apiKey`, so §14G.1.3(d) makes this
# payload's `message` secret: the host was holding the key when it
# failed, and an error text routinely carries the request it was
# making. Writing `error.message` here is E-IFC-05.
#
# `error.code` is the other field, and it is public: the browser's
# own runtime writes it from the transport outcome — no answer, its
# own deadline, or a status line — never from a byte the server
# sent. So a developer learns *how* the call failed and never *what
# the host said about it*, which is enough to tell "you are
# offline" from "the service said no", and not enough to carry a
# credential.
#
# Its type is `Code`, a built-in `choice`, so the three outcomes
# are eliminated with `when` exactly as `Remote`'s three arms are.
# All three must be written. That is what a misspelling costs
# here: `Timout` names no arm and does not compile, where a
# comparison against the text `"Timout"` used to be quietly false
# for ever.
when greeting
Loading show Spinner
Failed with error
when error.code
Unreachable show ErrorBar message is "the greeting service did not answer: Unreachable"
Timeout show ErrorBar message is "the greeting service did not answer: Timeout"
Rejected show ErrorBar message is "the greeting service did not answer: Rejected"
Ready with text show Text text
# `visits` is durable, so it is Remote here too.
Row
Text "visits so far:"
when visits
Loading show Spinner
Failed with error show ErrorBar message is error.message
Ready with total show Text total
# Mutating durable state from a click compiles to a generated,
# end-to-end typechecked RPC. Open two browser windows and both
# counters move.
Button "sign the guestbook"
on click
add 1 to visitsPlacement: where state lives
The language's one sentence is that placement is a property of state. These are the files where that does the work.
| File | What it teaches |
|---|---|
tally.zd | durable state that is not a counter. guestbook.zd's visits is a Whole, and a store that only ever holds numbers is not much of a store; this is the same three-line story with a Map of Text to Whole, which is the shape most real durable state has. |
writing.zd | The static placement, running. Content is computed at build time and inlined, and the program emits rss.xml into the bundle as a file rather than an endpoint. |
voting-board.zd | A live voting board. Every construct in the language appears here — useful as a reference sheet once the basics are familiar, and dense as a first read. |
Content, routing, and modules
| File | What it teaches |
|---|---|
blog.zd | The static placement reading real files off disk through the build capabilities, then rendering the markdown inside the compiler. Verified to build with an empty PATH, so no toolchain is consulted. |
site.zd | A multi-page site: five URLs out of one program, one emitted document per URL. A route is a choice plus a bijection onto URLs, so when page dispatches exactly as any when does. |
content.zd | The posts site.zd publishes. A module is a unit of naming rather than of deployment, so the static list a route parameter ranges over can live in its own file. |
layout.zd | The two components blog.zd composes its pages out of — a component in one file, the view that uses it in another. |
model.zd | What a module is. Nothing marks a declaration exportable: every top-level declaration is importable, and the importing file decides what it wants by naming it after for. |
blog.zd is the one to see running, because what it demonstrates happened
before the page existed. The posts below were read off disk and the markdown
turned into elements by the compiler, on the machine that built this site;
by the time anything reached a browser it was already a bundle with the
content inlined. static state has no Remote type for exactly this reason —
nothing crosses at run time, so there is no boundary to be Remote about.
// this one runs in your browser, so it needs JavaScript
Composed out of components declared in layout.zd, a different file. A module is a unit of naming rather than of deployment, so a component and the view that uses it need not live together.
the file that was compiled
# blog.zd — a static blog, modelled on the milestone-7 portfolio target.
#
# What it demonstrates: the `static` placement (§14C.3b) reading real
# files off disk through the `build` capabilities (§4.4), `record`
# declarations (§14B.1), user components across a module boundary (§14D),
# the one text operator §14F.1 adds to the closed infix set, and the
# `Markup` type reaching the page through `Prose` (§16.3.5).
#
# Note what this program does NOT need: no `server` placement, no
# `durable` placement, no identity, no database, no API. A blog is a
# `client` + `static` application, and that is why it is reachable.
#
# **This file was aspirational and is now measured.** It used to open with
# `state posts is static List of Post from readMarkdown "content/blog"`
# and a `foreign renderMarkdown` reaching for `marked`. Neither was a form
# the language has: a call with a bare argument has no production in §4.4,
# and a build-time `foreign` has no host to import from, because the
# compiler is the host (§17.4.8). The spec respelled that line on
# 2026-08-03 and this is the respelling, running.
#
# The last gap has closed too. `body` used to be `Text`, and `Text` sets
# `nodeValue`, so a post's `<h1>` arrived on the page as four visible
# characters. There is now a type the renderer parses and exactly one
# expression that produces one, so the body is rendered rather than shown.
use "./layout" for PageShell, PostCard
record Post
slug is Text
title is Text
# `Markup`, not `Text`. The field's type is what makes the body
# renderable and the title not: `build markdown` is the only expression
# that produces a `Markup`, `Prose` is the only element that renders
# one, and neither converts to the other in either direction.
body is Markup
published is Whole
draft is Truth
# Read at compile time and inlined into the bundle. Because no boundary is
# crossed at runtime, this is `List of Post` — not `Remote of List of
# Post`. Rule 1 (§5.2) is satisfied rather than excepted.
#
# `build list` sorts, so the three posts inline in the same order on every
# machine, and every path it yields is resolved against the project
# directory before it is opened. A build reads the project it is building
# and nothing else.
state posts is static List of Post from readPosts with directory is "content/blog"
function readPosts with directory
from build list directory
map each path to postFrom with path
function postFrom with path
give Post with slug is path, title is (titleOf with path), body is build markdown (build read path), published is (publishedOf with path), draft is (draftOf with path)
# The body comes off disk; the title, the date and the draft flag do not,
# and the reason is §14F rather than §4.4. Front matter is a second
# grammar inside the file, and parsing one needs text operations the
# language does not have — there is no `split`, no `first line`, and `at`
# over `Text` is not defined. So the three are written out here, in the
# shape `content.zd` already uses for `titleOf` and `bodyOf`. That is a
# missing library, not a missing capability.
function titleOf with path
if path is "content/blog/a-blog-is-two-placements.md"
give "A blog is two placements"
otherwise
if path is "content/blog/reading-a-file-is-not-importing-one.md"
give "Reading a file is not importing one"
otherwise
give "A draft is still a file"
function publishedOf with path
if path is "content/blog/a-blog-is-two-placements.md"
give 2024
otherwise
if path is "content/blog/reading-a-file-is-not-importing-one.md"
give 2026
otherwise
give 2025
function draftOf with path
give path is "content/blog/a-draft-is-still-a-file.md"
# Derived at build time from build-time data: still static, still free.
# The draft is filtered out *here*, on the build host, so its rendered
# HTML never reaches the bundle at all — nothing is hidden from the reader
# by the browser, because nothing was sent to the browser to hide.
state visible is static List of Post from onlyPublished with posts
function onlyPublished with all
from all
keep each post where not post.draft
sort each post by post.published
# Client state: the reader's filter box. Lost on reload, which is correct.
state query is client Text starting ""
view
PageShell "Writing"
Input query, hint is "filter posts"
each post in visible
if post.title contains query or query is ""
PostCard post.slug, title is post.title
# The post, rendered. `Prose` is the only element whose
# argument is parsed as HTML rather than written into a
# text node, and it takes `Markup` and nothing else —
# `Prose post.title` is a compile error, and so is
# `Text post.body`.
#
# It has no children, and cannot: interleaving parsed
# nodes with templated ones would make the sibling
# offsets every binding is scheduled against depend on
# how many nodes a *file* parsed into, which is not
# known at compile time. That costs this card nothing.
# `PostCard` nests the title and the slug in its own
# `Column` and takes the body as `children`, so the
# three are siblings inside the card rather than the
# body being wrapped around them — the document is the
# last of the three, and the only one with a parse
# behind it.
Prose post.bodyComposition and the view
| File | What it teaches |
|---|---|
components.zd | User-defined components and modules. Written before the implementation existed, to test the design — building the compiler against it found four things wrong, all listed at the bottom of the file and corrected there. |
disclosure.zd | Components that render, as opposed to components that only describe. |
page.zd | A page a content site would actually serve. Every element in it was unreachable before the element vocabulary opened up — the language could emit div, span, h2, button and input, so a portfolio could not render a paragraph, a list, an image, or a link. |
events.zd | What a handler receives. on click used to emit a zero-argument arrow: a handler could say that something happened and never what. |
terminal-help.zd | A multi-line text literal, and what it replaces. Ported from a real help command in the portfolio this language is measured against. |
Collections and the standard library
| File | What it teaches |
|---|---|
leaderboard.zd | The pipeline, conditionals, and nested types. Un-writable until the standard library landed. table at player.name yields Option of Whole, because indexing is bounds-checked — unlike TypeScript's unchecked lookup. |
todo.zd | The canonical UI benchmark. It is the acceptance test for type declarations, the membership verbs, and the collection and record literals: every one of them appears in it. |
Add an item, tick it, filter it away:
// this one runs in your browser, so it needs JavaScript
The filter is not a query and the counts are not maintained. Both are derived state over the one list, recomputed when it changes because the compiler read the expressions that mention it.
the file that was compiled
# todo.zd — the canonical UI benchmark, in ZDeceptron.
#
# This file is the acceptance test for the type declarations of §14B.1 as
# amended by §14G.1.2, the membership verbs of §14B.2, and the collection
# and record literals of §14B.4. Every one of them appears below, and the
# file compiles, renders, and updates.
#
# Two deliberate departures from the version that used to live here, both
# of them honest about what the compiler can do rather than what the
# language will one day say:
#
# 1. `todos` is `client`, not `durable`. Crossing a placement boundary
# needs the placement closure, the RPC client and the durable store,
# and none of the three exists (§16.5, M6). A `durable` list here
# would typecheck and then refuse to build, which is a worse
# benchmark than a smaller one that runs.
# 2. Toggling rebuilds the list rather than writing through a path.
# `set todos at todo.id . done to ...` parses and is unreadable, and
# §14B.3 has not settled what replaces it — so the toggle is written
# as the pipeline it really is.
#
# Note what is still absent: no store setup, no reducer, no API route, no
# useEffect, no dependency array, and no key prop. The list re-renders the
# rows that changed because the signal graph knows which those are.
record Todo
id is Whole
title is Text
done is Truth
choice Filter
Everything
Unfinished
Finished
# A populated collection literal. Each element is parenthesised because a
# record literal's fields are comma-separated too (§14G.1.1).
state todos is client List of Todo starting [(Todo with id is 1, title is "write the parser", done is yes), (Todo with id is 2, title is "write the checker", done is no)]
state draft is client Text starting ""
state nextId is client Whole starting 3
state filter is client Filter starting Everything
# Derived state. Recomputed when `todos` or `filter` changes, and never
# otherwise — the signal graph knows the dependencies, so there is nothing
# to declare and nothing to keep in sync.
state visible is client List of Todo from filterTodos with todos, filter
function filterTodos with all, mode
from all
keep each todo where shows with todo, mode
# A `when` over a user-declared choice. Every variant must be written, in
# every context (§14G.1.6), so a fourth `Filter` variant would break this
# function at compile time rather than at run time.
function shows with todo, mode
when mode
Everything show yes
Unfinished show not todo.done
Finished show todo.done
function toggled with all, id
from all
map each todo to flipped with todo, id
function flipped with todo, id
if todo.id is id
give Todo with id is todo.id, title is todo.title, done is not todo.done
otherwise
give todo
view
Column
Heading "Todos"
Row
Input draft, hint is "what needs doing"
Button "add"
on click
append (Todo with id is nextId, title is draft, done is no) to todos
add 1 to nextId
set draft to ""
Row
Button "everything"
on click
set filter to Everything
Button "unfinished"
on click
set filter to Unfinished
Button "finished"
on click
set filter to Finished
when filter
Everything show Text "showing everything"
Unfinished show Text "showing what is left"
Finished show Text "showing what is done"
each todo in visible
Row
# A done item is struck through. Written as two elements
# under an `if` rather than as one element with a styled
# value, because a style argument's words are translated
# into CSS when the class is folded: `struck` becomes
# `line-through` here and nowhere at run time. The signal
# graph swaps the two branches when `todo.done` changes,
# so this costs one anchor pair and no effect per row that
# does not change.
if todo.done
Text todo.title, decoration is "struck", color is "grey"
otherwise
Text todo.title
Button "toggle"
on click
set todos to toggled with todos, todo.id
Button "delete"
on click
remove todo from todosAlgorithms — programs whose answers are not obvious
These compute rather than demonstrate. Each has a working interface, and each
was written partly as evidence about what the language cannot yet do. The
first six are the set STATUS.md counts as the algorithm examples;
dungeon.zd is grouped here because it computes too, but it began as a port
of existing TypeScript rather than as evidence.
| File | What it teaches |
|---|---|
graph-traversal.zd | Depth-first and breadth-first over a declared graph. The first example here whose answer is not obvious from reading it. |
shortest-path.zd | Dijkstra over a weighted graph — and the priority queue that is not there. The cheapest route is not the shortest one, which is why it is a separate file rather than a flag on the last one. |
queens.zd | A backtracking search over a state space, with the state space on the page. |
knapsack.zd | What to take when the bag is too small for everything: eight items, 256 subsets. |
edit-distance.zd | Levenshtein distance, the table it needs, and the two-dimensional structure the language does not have. |
sorting.zd | Two sorts written in the language, beside the one the language provides, all three over the same twenty numbers. |
poker.zd | Five-card draw, and a total order over a structured value in a language that refuses comparators. Every hand is folded into one Whole so the whole comparison can be >. |
dungeon.zd | An accumulator that survives across iterations, ported from real TypeScript. |
Two of them, running. Deal a few hands — one signal moves when you click, and the shuffle, both hands, their categories, their scores and the verdict all recompute from it:
// this one runs in your browser, so it needs JavaScript
`sort each … by` takes a key, not a two-argument comparator, so a hand cannot be compared to a hand directly. scoreOf folds each one into a single number instead, and the comparison that decides the game is one greater-than.
the file that was compiled
# poker.zd - five-card draw, two players, and the hand ranking that decides
# it.
#
# Click "deal" and both hands are re-dealt from a fresh shuffle, evaluated,
# and compared. Nothing below is a callback: `deals` is the only signal a
# click moves, and the shuffle, the two hands, their categories, their
# scores and the verdict all recompute from it.
#
# WHAT THIS EXAMPLE IS FOR. Poker hand ranking is the smallest interesting
# problem that needs a *total order over a structured value*, and this
# language cannot express one directly. §17.2.5 refuses comparators by
# design: `sort each … by` takes a key, not a two-argument function. So a
# hand cannot be compared to another hand by a `compare` written here.
#
# The answer is the one `sorting.zd` names in its first paragraph — "a sort
# by two keys at once is not spellable at all without folding both into a
# single number" — taken seriously and used on purpose. Every hand is
# folded into one `Whole`, and the whole of the comparison is `>`. That
# fold is `scoreOf`, and it is the part of this file worth reading.
#
# WHERE THE RANDOMNESS COMES FROM, AND WHY IT IS NOT `clock`. A shuffle
# needs entropy and this language has exactly one impure primitive, which
# is the clock, and it may only be read in a signal initialiser (see
# `prelude/time.zd`). That is enough to seed a game but it is the wrong
# shape for this one: a deal has to be *reproducible*, because a page that
# renders a different hand on every recompute is a page whose answer you
# cannot check against anything.
#
# So the seed is the click count. `deals` is a `Whole` that starts at 0 and
# goes up by one per click, the shuffle is a pure function of it, and deal
# number 7 is the same five cards on every machine and every reload. The
# entropy is the user's finger, which is the only honest source a pure
# dataflow program has.
#
# ⚠️ DO NOT WRITE YOUR OWN MIXER, AND THIS FILE IS THE EVIDENCE. The first
# draft shuffled by sorting on a hand-rolled three-round hash of the card
# index. It produced a permutation, it produced a different one per seed,
# and it was wrong in a way no amount of staring found: `i * 257` writes
# the same byte into both halves of a sixteen-bit word, so the byte swap
# that was supposed to be doing the mixing was a no-op on exactly the
# input it was given. Every rank ended up with two of its four cards
# adjacent, and the deal produced a **full house every sixth hand**.
#
# The prelude already has mulberry32 — `randomBits`, `nextSeed`,
# `randomBelow` in `prelude/number.zd`, written in ZDeceptron over the
# bitwise primitives. It is used below, and the hand frequencies it gives
# over forty thousand hands match the true ones to within a tenth of a
# percentage point: 50.50% high card against 50.12%, 0.39% straight
# against 0.39%, 0.15% full house against 0.14%.
#
# `wrappingProduct` rather than `*` for the seed, for the reason
# `prelude/number.zd` gives beside it: `a * b` on two numbers near 2^32 is
# exact in f64 only up to 2^53 and silently drops the low bits above it,
# and the low bits are the whole point of a mixing function. `Whole`
# overflow is unguarded on the client (#5), so that loss is a wrong
# answer and not a diagnostic.
# A card is a number from 0 to 51, and the two things worth knowing about
# it are recovered by division. Rank 0 is a two and rank 12 is an ace;
# aces are high everywhere except the wheel, which `straightHigh` handles.
record Card
rank is Whole
suit is Whole
# One evaluated hand: what it holds, what it is called, and the single
# number the comparison actually uses.
record Hand
cards is List of Card
category is Whole
score is Whole
# The only signal a click moves. Everything else on the page is derived
# from it, which is the whole demonstration.
state deals is client Whole starting 0
# --- the shuffle ----------------------------------------------------------
# `mod` and `quotient` answer with an `Option`, because a divisor can be
# zero and "undefined" is not a number. Every divisor here is a literal, so
# the `None` is unreachable — but it still has to be spelled, and these two
# are where that is done once instead of at fourteen call sites.
function wholeMod with value, divisor
give valueOr with maybe is (mod with value is value, divisor is divisor), fallback is 0
function wholeDiv with value, divisor
give valueOr with maybe is (quotient with value is value, divisor is divisor), fallback is 0
# The key a card is sorted by: mulberry32 over the seed and the card.
#
# The seed is spread across the whole 32-bit window before the card index
# is added, so consecutive deals do not start from adjacent states and
# then produce adjacent orderings. `wrappingProduct` is `Math.imul` and
# wraps instead of overflowing, which is what makes that spread exact.
function noiseAt with seed, index
give randomBits of (toUnsigned32 of ((wrappingProduct with left is seed, right is 2654435761) + index))
# The shuffle, as one pipeline clause.
#
# Not Fisher–Yates. That needs a swap, a swap needs `setAt`, and `setAt`
# copies the list — so shuffling 52 cards would be 52 copies of a
# 52-element list to do what a sort does in one pass. Sorting by a random
# key is a permutation whenever the keys are distinct, and with a 32-bit
# key over 52 cards a collision has probability about 3 in ten million;
# if two ever did collide the sort is stable (`sorting.zd` records that
# guarantee), so the result would still be a permutation, just with those
# two cards in deck order.
function shuffled with cards, seed
from cards
sort each card by (noiseAt with seed is seed, index is card)
state deck is client List of Whole from range with start is 0, stop is 52
state mixed is client List of Whole from shuffled with cards is deck, seed is deals
function cardsFrom of numbers
from numbers
map each n to (Card with rank is (wholeMod with value is n, divisor is 13), suit is (wholeDiv with value is n, divisor is 13))
# Two hands off the top. Dealing five-then-five rather than alternating,
# because with a shuffled deck the two are the same distribution and this
# one is one `listDrop` instead of two pipelines.
state northCards is client List of Card from cardsFrom of (listTake with items is mixed, count is 5)
state southCards is client List of Card from cardsFrom of (listTake with items is (listDrop with items is mixed, count is 5), count is 5)
# --- what a hand is made of -----------------------------------------------
function withRank with cards, rank
from cards
keep each card where card.rank is rank
function countRank with cards, rank
give length of (withRank with cards is cards, rank is rank)
function suitMatches with cards, suit
from cards
map each card to (card.suit is suit)
function isFlush of cards
when first of cards
None
give no
Some with lead
give allOf of (suitMatches with cards is cards, suit is lead.suit)
function ranksOf of cards
from cards
map each card to card.rank
# The straight, and the one hand in poker that reads an ace as a one.
#
# Five distinct ranks spanning exactly four apart is a straight, and its
# high card is the top of the span. The wheel — ace, two, three, four,
# five — is the exception every implementation has to write down: its
# ranks are 12, 0, 1, 2, 3, which span twelve, so the general test misses
# it and it is checked for by name. Its high card is the five, which is
# rank 3, and that is what makes it the lowest straight rather than the
# highest.
function straightHigh of cards
with ranks is (ranksOf of cards)
with low is (valueOr with maybe is (minOf of ranks), fallback is 0)
with high is (valueOr with maybe is (maxOf of ranks), fallback is 0)
if (length of (distinctRanks of cards)) is not 5
give None
if (high - low) is 4
give Some with value is high
if (isWheel of ranks)
give Some with value is 3
give None
function isWheel of ranks
give allOf of [ranks contains 12, ranks contains 0, ranks contains 1, ranks contains 2, ranks contains 3]
function distinctRanks of cards
from (indices of 13)
keep each rank where (countRank with cards is cards, rank is rank) > 0
# --- the fold that makes hands comparable ---------------------------------
#
# THE POINT OF THE FILE. Nine categories and, inside each, a tie-break over
# up to five ranks. With no comparator the whole of that has to become one
# number, and the number has to be *positional* — every hand contributing
# exactly the same number of digits, or a shorter hand's digits line up
# against a longer hand's and the comparison is nonsense.
#
# So: base sixteen, five digits of tie-break, and the category above them.
# A rank is 0..12 and fits in a digit; five digits is 16^5 = 1048576, which
# is the category's multiplier; nine categories reach 8 * 1048576, so the
# whole score is under 2^24 and nowhere near where a `Whole` stops being
# exact.
#
# The five digits are the hand's ranks written out **by group, largest
# group first, and within equal groups by rank** — which is exactly the
# order poker breaks ties in. A full house is three of one rank then two of
# another, so its digits are `RRRSS`; two pair is `RRSST`. Because every
# hand contributes five digits, `>` on the result is the rules of poker.
# `count * 16 + rank`, one per rank that appears. Sorting these descending
# orders by count first and rank second, in one key — the two-key sort
# `sorting.zd` says is only reachable by folding both into a number.
function groupKeys of cards
from (distinctRanks of cards)
map each rank to ((countRank with cards is cards, rank is rank) * 16) + rank
function groupsDescending of cards
give reverse of (ascendingKeys of cards)
function ascendingKeys of cards
from (groupKeys of cards)
sort each key by key
# Each group's rank, written once per card in it, so five digits always.
#
# A `fold each` (#33): one element per step, the answer a binder rather
# than a parameter of a second function.
function tieDigits of keys
from keys
fold each key into taken starting empty to (repeatRank with rank is (wholeMod with value is key, divisor is 16), count is (wholeDiv with value is key, divisor is 16), taken is taken)
function repeatRank with rank, count, taken
if count < 1
give taken
give repeatRank with rank is rank, count is count - 1, taken is (append rank to taken)
# The five digits read as one base-16 number, most significant first.
function foldDigits of digits
from digits
fold each digit into total starting 0 to (total * 16) + digit
# 8 straight flush, 7 four of a kind, 6 full house, 5 flush, 4 straight,
# 3 three of a kind, 2 two pair, 1 one pair, 0 high card.
function categoryOf of cards
with flush is (isFlush of cards)
with straight is (isSome of (straightHigh of cards))
with counts is (countsDescending of cards)
with top is (valueOr with maybe is (first of counts), fallback is 0)
with second is (valueOr with maybe is (listAt with value is counts, index is 1), fallback is 0)
if flush and straight
give 8
if top is 4
give 7
if (top is 3) and (second is 2)
give 6
if flush
give 5
if straight
give 4
if top is 3
give 3
if (top is 2) and (second is 2)
give 2
if top is 2
give 1
give 0
function countsDescending of cards
from (groupsDescending of cards)
map each key to (wholeDiv with value is key, divisor is 16)
# A straight is compared by its high card alone — two straights never
# differ in anything else — so it takes the short path. Everything else
# takes the five digits. The two cannot be confused because the category
# sits above both and a straight's category is never a non-straight's.
function scoreOf of cards
with category is (categoryOf of cards)
if (category is 4) or (category is 8)
give (category * 1048576) + (valueOr with maybe is (straightHigh of cards), fallback is 0)
give (category * 1048576) + (foldDigits of (tieDigits of (groupsDescending of cards)))
function handFrom of cards
give Hand with cards is cards, category is (categoryOf of cards), score is (scoreOf of cards)
state north is client Hand from handFrom of northCards
state south is client Hand from handFrom of southCards
# --- naming things --------------------------------------------------------
function nameAt with names, index
give valueOr with maybe is (listAt with value is names, index is index), fallback is "?"
state rankNames is client List of Text starting ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
state suitNames is client List of Text starting ["♠", "♥", "♦", "♣"]
state categoryNames is client List of Text starting ["high card", "one pair", "two pair", "three of a kind", "straight", "flush", "full house", "four of a kind", "straight flush"]
function cardText with card, ranks, suits
give (nameAt with names is ranks, index is card.rank) + (nameAt with names is suits, index is card.suit)
function handTexts with cards, ranks, suits
from cards
map each card to (cardText with card is card, ranks is ranks, suits is suits)
function handText with cards, ranks, suits
give join with parts is (handTexts with cards is cards, ranks is ranks, suits is suits), using is " "
state northText is client Text from handText with cards is northCards, ranks is rankNames, suits is suitNames
state southText is client Text from handText with cards is southCards, ranks is rankNames, suits is suitNames
state northName is client Text from nameAt with names is categoryNames, index is north.category
state southName is client Text from nameAt with names is categoryNames, index is south.category
state northWins is client Truth from north.score > south.score
state southWins is client Truth from south.score > north.score
# --- the page -------------------------------------------------------------
view
Column
Heading "Five-card draw"
Text "Two hands off one shuffle. The deal number is the seed, so deal 7 is the same cards everywhere.", color is "grey"
Column padding is 8
Row
Text "north: "
Text northText, weight is "bold"
Row
Text " "
Text northName, color is "grey"
Row
Text "south: "
Text southText, weight is "bold"
Row
Text " "
Text southName, color is "grey"
Column padding is 8
if northWins
Text "north takes it", color is "green", weight is "bold"
otherwise
if southWins
Text "south takes it", color is "green", weight is "bold"
otherwise
Text "split pot — the same hand, rank for rank", color is "grey", weight is "bold"
Column padding is 8
Row
Button "deal"
on click
add 1 to deals
Button "back to the first deal"
on click
set deals to 0
Row
Text "deal number: "
Text deals
Column padding is 8
Heading "the score each hand was compared by"
Text "One `Whole` per hand: the category above five base-sixteen digits of tie-break. Comparing hands is `>` on these, which is the only comparison this language gives you.", color is "grey"
Row
Text "north: "
Text north.score
Row
Text "south: "
Text south.scoreThen type in both boxes and watch the whole table redraw. The inputs are capped at twelve characters, and the file says why: a dynamic-programming table has to be filled in the order the recurrence visits it, and there is no way to write into an inner list, so the table is flat.
// this one runs in your browser, so it needs JavaScript
kitten to sitting is 3 — substitute k for s, substitute e for i, add a g. Nothing here is memoised by hand; the table is state derived from the two words.
the file that was compiled
# edit-distance.zd - Levenshtein distance, the table it needs, and the
# two-dimensional structure the language does not have.
#
# Type in both boxes and the whole table is recomputed and redrawn. The
# default pair is the classic one: kitten to sitting is 3, by substituting
# k for s, substituting e for i, and adding a g.
#
# THE GAP THIS FILE IS EVIDENCE FOR. Dynamic programming is a table, and
# a table is two-dimensional. This language has `List of List of Whole` in
# its type system, and no way to write into either dimension in less than
# linear time: `setAt` copies the list it writes into, a `Map` insert
# copies the table, and `insertAt` plus `removeAt` costs two copies. So
# the table here is **one flat list** and every access is
# `(row * width) + col` computed by hand.
#
# That is not a disaster. It is 1970s Fortran, and it works. What it costs
# is:
#
# * `cellAt` takes four arguments where a two-dimensional read takes
# two, and a wrong `width` at any call site is a silent wrong answer
# rather than a type error. There were two of those while this file
# was being written and both were found by the answer being wrong.
# * The table is grown a row at a time with `flatten of [table, row]`,
# which copies the whole table once per row. Building an n by m table
# is therefore O(n squared times m) rather than O(n times m). For the
# twelve-character cap below that is 20736 element copies at worst,
# which is nothing; for a diff over two files it would be the program.
# * The row being built cannot be read out of the table, because it is
# not in the table yet. So `rowFrom` carries it as a second list and
# reads the left neighbour from that and the two upper neighbours from
# the table. Two lookup paths for one conceptual table.
#
# Both halves of that have moved since. `setAt` is in the prelude now
# (#195), and a `Map` can be built (#115, #117) — but neither rescues the
# table. `setAt` still copies the list it writes into, and a `Map` insert
# copies the table, so the O(n squared times m) above is unchanged by
# either. What the table wants is a persistent structure with an O(1)
# write, and that is the thing still missing rather than the two names
# this comment used to be waiting on. It is #233. The two-dimensional
# read is filed separately by this file.
#
# WHAT WENT RIGHT. The recurrence itself reads exactly as it is written in
# a textbook, because `min` is in the prelude and takes two numbers, and
# three-way minimum is `min` of a `min`. And the whole thing is driven by
# two `Input`s: the signal graph recomputes the table, the answer, the
# edit script and the grid from one keystroke, with nothing declared.
# The two words. `Input` binds a `client Text` signal, which is the only
# kind it binds, so these cannot be `durable` and nothing here needs them
# to be.
state fromWord is client Text starting "kitten"
state toWord is client Text starting "sitting"
# Twelve characters each. The table is quadratic in both and it is drawn
# on the page, so a long paste would render a wall rather than a grid. The
# cap is a `slice`, which is the text prelude's own.
function capped of word
give slice with value is word, start is 0, stop is 12
state left is client Text from capped of fromWord
state right is client Text from capped of toWord
state rows is client Whole from (length of left) + 1
state width is client Whole from (length of right) + 1
# --- reading the flat table ----------------------------------------------
# The cell at `row`, `col` of a table `width` cells wide.
#
# The fallback is never taken for an index the recurrence asks for, and it
# is written rather than propagated because a `None` here would mean the
# index arithmetic is wrong, which is a bug and not a value.
function cellAt with table, width, row, col
give valueOr with maybe is (listAt with value is table, index is (row * width) + col), fallback is 0
function letterAt with word, index
give valueOr with maybe is (textAt with value is word, index is index), fallback is ""
function followedBy with before, after
give flatten of [before, after]
function leastOfThree with a, b, c
give min with first is a, second is (min with first is b, second is c)
# --- building the table ---------------------------------------------------
# One row of the table, left to right.
#
# `made` is the row so far. The cell to the left is read out of it and the
# two cells above are read out of `table`, because the row being built has
# not been added to the table yet. That split is the price of having no
# way to write into a table in place.
function rowFrom with table, left, right, width, row, col, made
if col >= width
give made
if col is 0
give rowFrom with table is table, left is left, right is right, width is width, row is row, col is 1, made is (append row to made)
with up is (cellAt with table is table, width is width, row is row - 1, col is col) + 1
with back is (valueOr with maybe is (listAt with value is made, index is col - 1), fallback is 0) + 1
with swap is (cellAt with table is table, width is width, row is row - 1, col is col - 1) + (costOfSwap with left is left, right is right, row is row, col is col)
give rowFrom with table is table, left is left, right is right, width is width, row is row, col is col + 1, made is (append (leastOfThree with a is up, b is back, c is swap) to made)
# Zero when the two letters already match, which is the "keep" move, and
# one when they do not, which is the substitution.
function costOfSwap with left, right, row, col
if (letterAt with word is left, index is row - 1) is (letterAt with word is right, index is col - 1)
give 0
give 1
# Row zero is 0, 1, 2, ..., which is what it costs to add every letter of
# `right` to nothing. `range` builds it, and every row after it is built
# by the recurrence.
function tableFor with left, right
give tableRows with left is left, right is right, width is (length of right) + 1, row is 1, rows is (length of left) + 1, table is (range with start is 0, stop is (length of right) + 1)
function tableRows with left, right, width, row, rows, table
if row >= rows
give table
give tableRows with left is left, right is right, width is width, row is row + 1, rows is rows, table is (followedBy with before is table, after is (rowFrom with table is table, left is left, right is right, width is width, row is row, col is 0, made is empty))
# --- reading the answer back out ------------------------------------------
# The edits themselves, walked backwards from the bottom right corner and
# turned round at the end. Which move was taken is not recorded anywhere,
# so it is worked out again from the numbers: the neighbour whose cost
# plus the move's own cost equals this cell is the one the recurrence
# came from.
function scriptFrom with table, left, right, width, row, col, taken
if (row is 0) and (col is 0)
give reverse of taken
if row is 0
give scriptFrom with table is table, left is left, right is right, width is width, row is row, col is col - 1, taken is (append ("add " + (letterAt with word is right, index is col - 1)) to taken)
if col is 0
give scriptFrom with table is table, left is left, right is right, width is width, row is row - 1, col is col, taken is (append ("drop " + (letterAt with word is left, index is row - 1)) to taken)
with here is (cellAt with table is table, width is width, row is row, col is col)
with diagonal is (cellAt with table is table, width is width, row is row - 1, col is col - 1)
if here is diagonal + (costOfSwap with left is left, right is right, row is row, col is col)
give scriptFrom with table is table, left is left, right is right, width is width, row is row - 1, col is col - 1, taken is (append (moveAcross with left is left, right is right, row is row, col is col) to taken)
if here is (cellAt with table is table, width is width, row is row - 1, col is col) + 1
give scriptFrom with table is table, left is left, right is right, width is width, row is row - 1, col is col, taken is (append ("drop " + (letterAt with word is left, index is row - 1)) to taken)
give scriptFrom with table is table, left is left, right is right, width is width, row is row, col is col - 1, taken is (append ("add " + (letterAt with word is right, index is col - 1)) to taken)
function moveAcross with left, right, row, col
if (letterAt with word is left, index is row - 1) is (letterAt with word is right, index is col - 1)
give "keep " + (letterAt with word is left, index is row - 1)
give "swap " + (letterAt with word is left, index is row - 1) + " for " + (letterAt with word is right, index is col - 1)
# Only the edits, with the letters that were kept left out, because a
# distance of three should read as three lines and not as ten.
function changesOnly of moves
from moves
keep each move where not (move contains "keep")
# --- what the page shows -------------------------------------------------
state table is client List of Whole from tableFor with left is left, right is right
state distance is client Whole from cellAt with table is table, width is width, row is rows - 1, col is width - 1
state script is client List of Text from scriptFrom with table is table, left is left, right is right, width is width, row is rows - 1, col is width - 1, taken is empty
state changes is client List of Text from changesOnly of script
state rowNumbers is client List of Whole from range with start is 0, stop is rows
state colNumbers is client List of Whole from range with start is 0, stop is width
view
Column
Heading "How far apart are two words?"
Text "Levenshtein distance: the fewest single-letter additions, removals and substitutions that turn one word into the other. Type in either box."
Row
Input fromWord, hint is "from"
Input toWord, hint is "to"
Row
Text "distance: "
Text distance, weight is "bold", size is "large"
Column padding is 8
Heading "the edits"
each change in changes
Text change
Text "Kept letters are left out. The full walk, including the letters that did not change, is the script the table was read back into.", color is "grey"
Column padding is 8
Heading "the table"
Text "Row i, column j is the distance between the first i letters of one word and the first j letters of the other. The answer is the bottom right corner."
each row in rowNumbers
Row gap is 4
if row is 0
Text " ", color is "grey"
otherwise
Text (letterAt with word is left, index is row - 1), color is "grey", weight is "bold"
each col in colNumbers
Text (cellAt with table is table, width is width, row is row, col is col), padding is 2The edges
| File | What it teaches |
|---|---|
gauge.zd | A foreign that owns a DOM node. Every other example renders through elements the compiler knows; this one hands a <div> to JavaScript and lets it draw, which is the only way a canvas, a chart, a map or a WebGL scene can exist in a ZDeceptron program. |
Move the slider and the needle animates; hide the gauge and the module's
destroy runs, cancelling its animation frame. mount is never called twice,
so the canvas and its 2D context survive every write — which is the point of
the handle having an update hook rather than the runtime re-running the
constructor.
// this one runs in your browser, so it needs JavaScript
The only example here whose drawing code is JavaScript. gauge.js is copied into the bundle by the compiler, and this page loaded it from there — the same file, not a port.
the file that was compiled
# gauge.zd — a foreign that owns a DOM node (spec §14E.1, §14E.3).
#
# Every other example renders through elements the compiler knows. This
# one hands a `<div>` to JavaScript and lets it draw, which is the only
# way a canvas, a chart, a map or a WebGL scene can exist in a ZDeceptron
# program: the compiler has no vocabulary for any of them and should not
# grow one.
#
# Three things it exists to demonstrate:
#
# 1. **Creation.** `gauge` is written where a built-in element is
# written. It becomes a `<div>` in the static markup, handed to
# `mount` from `./gauge.js` along with an object carrying one
# property per `takes` argument.
# 2. **Reactive update, not re-invocation.** `level` and `label` are
# read inside the props, so a write reaches the module's `update`
# hook. `mount` is never called a second time — the canvas and its
# 2D context survive every write. Re-creating them on each keystroke
# is the failure this form exists to prevent, and it is why the
# handle has an explicit `update` rather than the runtime simply
# re-running the constructor.
# 3. **Teardown.** Hiding the gauge disposes the `if` branch, and the
# branch's disposer list is what calls the module's `destroy`. The
# module cancels its animation frame there. Nothing in this file
# says so — it falls out of the branch already rendering inside an
# owned region.
#
# What the declaration grants, stated plainly: `./gauge.js` runs in the
# browser with the full DOM. It could rewrite the page, read
# `document.cookie`, or make requests. The compiler prevents exactly two
# things — a `secret` crossing in, and any value crossing back out.
# Everything else is granted by the five lines below, and those five
# lines are the audit surface.
foreign gauge is client
from "./gauge.js" as "mount"
takes level is Whole, label is Text
gives view
state level is client Whole starting 40
state shown is client Truth starting yes
view title is "Gauge"
Column
Heading "A canvas a foreign owns"
if shown
gauge level is level, label is "load"
# The same number, through the element the language has. A `Meter`
# is not a second drawing of the canvas above: it is what a reader
# who cannot see the canvas gets, and what a program that does not
# want a foreign at all would write instead. `low`, `high` and
# `best` are the part a `Row` with a width cannot express.
Meter level, least is 0, most is 100, low is 20, high is 80, best is 60, label is "load"
Row
Button "less"
on click
subtract 10 from level
Button "more"
on click
add 10 to level
Button "hide or show"
on click
set shown to not shown
Text levelOne that this page will not run
This program loads from a third-party origin at run time, so running it here would mean serving somebody else’s script from this page. It is compiled on every build; it is not mounted.
third-party origins
- https://esm.sh
tree-webgl/webgl.zd drives three.js from ZDeceptron with no JavaScript file of its own. It compiles on every build of this site; it is not mounted, because doing so would mean this page fetching and executing a third party's module. The manifest records the origin, which is how the build step knows to leave it out rather than somebody remembering to.
What the examples are evidence against
Several of these files exist to record a limit rather than a feature, and they say so in their own comments. This is the fastest way to learn where the language currently stops:
shortest-path.zd— there is no heap in the prelude and no way to write one better than a scan, so three linear passes replace a binary heap's one logarithmic one. It says plainly that this is why the graph has seven towns.edit-distance.zd— a dynamic-programming table has to be built in the order the recurrence visits it, and flat, because there was no way to write into an inner list. It caps its inputs at twelve characters for that reason.components.zd— kept as written-before-the-implementation, with the four things the compiler found wrong listed at the bottom.
For the current boundary in one place, see Where it stops in the
README.
How this page runs them
Every example above was compiled by zdc build during this site's build,
from the file in the compiler's own repository. Two things follow.
Which ones can run here is not a matter of taste. Each bundle carries a
manifest.json naming the endpoints the compiler derived, the durable signals
it found, and the third-party origins the program loads from. A program with
none of those is self-contained, and a static site can serve it; anything else
is compiled and then left unmounted, with the manifest's own list of what it
needs printed in place of the program. So guestbook.zd, tally.zd,
leaderboard.zd, voting-board.zd and components.zd cannot run here —
they have durable or server state, which is a store and a function host —
and tree-webgl/webgl.zd cannot, because it fetches three.js at run time.
site.zd emits one document per URL rather than a single bundle, so it is a
site rather than a component and does not fit in a box on a page either.
If one of them stops compiling, this page does not deploy. That is the whole reason for the arrangement. The alternative — pasting the output of a compiler into a Markdown file — produces a page that is correct on the day it is written and silently wrong afterwards.