Skip to content
← zdeceptron docs

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.

FileWhat it teaches
hello.zdThe 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.zdstarting 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.zdThe 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:

guestbook.zdcompiled, cannot run here

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 visits

Placement: 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.

FileWhat it teaches
tally.zddurable 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.zdThe 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.zdA 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

FileWhat it teaches
blog.zdThe 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.zdA 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.zdThe 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.zdThe two components blog.zd composes its pages out of — a component in one file, the view that uses it in another.
model.zdWhat 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.

blog.zdcompiled and running

// 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.body

Composition and the view

FileWhat it teaches
components.zdUser-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.zdComponents that render, as opposed to components that only describe.
page.zdA 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.zdWhat a handler receives. on click used to emit a zero-argument arrow: a handler could say that something happened and never what.
terminal-help.zdA 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

FileWhat it teaches
leaderboard.zdThe 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.zdThe 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:

todo.zdcompiled and running

// 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 todos

Algorithms — 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.

FileWhat it teaches
graph-traversal.zdDepth-first and breadth-first over a declared graph. The first example here whose answer is not obvious from reading it.
shortest-path.zdDijkstra 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.zdA backtracking search over a state space, with the state space on the page.
knapsack.zdWhat to take when the bag is too small for everything: eight items, 256 subsets.
edit-distance.zdLevenshtein distance, the table it needs, and the two-dimensional structure the language does not have.
sorting.zdTwo sorts written in the language, beside the one the language provides, all three over the same twenty numbers.
poker.zdFive-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.zdAn 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:

poker.zdcompiled and running

// 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.score

Then 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.

edit-distance.zdcompiled and running

// 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 2

The edges

FileWhat it teaches
gauge.zdA 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.

gauge.zdcompiled and running

// 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 level

One that this page will not run

webgl.zdcompiled, cannot run here

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.