The whole language, in the order it was designed.
The tutorial teaches you to write a world. This guide explains what every part of the language means and why it is shaped that way — including the parts that are designed and frozen as families but not yet writable. Where a construct is not yet writable, it says so in the margin rather than pretending.
What WRL is
WallRiderLang is a spatial actor language. A program is not a sequence of statements; it is a graph of durable identities connected by textured routes and separated by boundaries. The name is literal. Riders are identities in motion along routes. Walls are the boundaries that gate, commit and seal what crosses them.
The reason for that shape is a single design goal: a run must be checkable by re-execution. Not merely logged. Executing a WRL world produces a film, an ordered schema-versioned record of what occurred, and that film is byte-identical regardless of which machine produced it or how the work was scheduled. Everything strange-looking about the language falls out of that requirement.
Read that pipeline left to right and most of this guide's structure becomes predictable. Part I is the surface and its meaning families. Part II is the canonical graph and the machine that advances it. Part III is the subset you can type today.
1 · The four reading laws
WRL is meant to be read before it is parsed. Four laws, frozen, govern every piece of notation in the language.
| Law | What it means |
|---|---|
| Shape says what a thing is | Bracket shape identifies the kind of object. You know what [x] is before you read x. |
| Line texture says how it moves | Route style identifies the operational guarantee — deterministic, asynchronous, verified, or fault. |
| The colon introduces; the slash bounds | Both families cap at three strokes, ordered by permanence: transient · committed · sealed. |
| Marks for architecture, words for computation | Process notation owns the punctuation. Expression notation defaults to words, so arithmetic never competes with topology for symbols. |
The third law is the one that pays off later. :,
::, ::: and
/, //,
/// are not four unrelated sigils; they are two
ladders of increasing permanence, and stroke count is the ordering.
2 · The ten construct kinds
Every WRL construct is exactly one of ten kinds. The set is closed — this is frozen, and a future revision may refine members but may not add an eleventh kind.
| Kind | Role | Status |
|---|---|---|
| Function | calculates — pure local computation | experimental |
| Actor | persists — durable identity + owned state | core |
| Route | communicates — a directed, textured transition relation or channel | core |
| Wall | authorizes — gate / commit / seal on propagation | core |
| Period | orders — the logical-time step under which events settle | core |
| Fragment | carries code — a quoted graph as movable data | proposed |
| Stencil | constructs graphs — a parameterized fragment producer | proposed |
| Derive | transforms typed graphs | proposed |
| Film | proves what occurred — an append-only replayable log | core |
| Hash | identifies what was built — a content address | core |
The creed, which is how the ten are meant to be memorised:
Six of the ten — Actor, Route, Wall, Period, Film, Hash — are grounded by a running implementation today. The other four are frozen as kinds; their detailed semantics are still draft.
--sig-->: it is part of the semantic
graph, it enters the sem- id, and it settles
within the period. A route is the broader family — a directed,
textured transition relation between identities. The solid texture
-- happens to be realised as a structural edge,
which is exactly why it is the only one you can write today. The async texture
~~ cannot be a structural edge: an async
message does not settle within the period, so it needs a canonical logical
route declaration distinct from the edge declaration. Reading "route" as
"edge" makes that requirement look like an oversight rather than the
consequence it is.3 · Containers: [x], (x), {x}
Three container shapes, frozen, closed.
| Shape | Kind | Rule |
|---|---|---|
[x] | durable identity | an actor, region, mailbox, or named artifact — something that survives across periods |
(x) | state | a current cell, mutable by replacement, single-owner |
{x} | wiring | permitted topology, capabilities, ports — static structure, not current value |
The distinction between ( ) and
{ } is not stylistic. ( )
is what the object is right now; { } is what
the object is permitted to be connected to, ever. Configuration lives in
parens because it is state that was chosen at construction; ports live in braces
because they are structure. In the world surface you write today, both appear on
a declaration line and the parser treats them under exactly these rules.
4 · Identity and addressing
| Symbol | Meaning-role | Status |
|---|---|---|
# | canonical identity / content hash | core |
@ | address / placement / stamp | core |
? | pattern variable introduced by matching | core |
* | replication-by-position | implemented |
* | wildcard match | reserved |
* is the one symbol whose two meanings are at very
different maturities, and the spec is explicit about not letting that slide.
Replication-by-position ([relay:r*3]) is implemented
as bounded, identity-equivalent surface sugar — see §18.
Wildcard matching is reserved: no lowering for it exists.
5 · Routes and the four textures
The reduction relation is defined over exactly four irreducible route textures. All surface sugar canonicalizes down to one of them plus attributes. Reading a WRL diagram, the texture of the line tells you the operational guarantee without reading a single word.
| Texture | Guarantee | Surface status |
|---|---|---|
--x--> solid | deterministic local transition; settles within the period | surface-grounded |
~~x~~> async | asynchronous message; appended to a mailbox; observable next period | partial |
==x==> verified | evidence-backed / committed transition under a named policy | partial |
!!x!!> fault | crash / cancel / reject / interrupt; engages supervision | partial |
The set of four and their guarantee-roles are frozen. Label, payload and guard grammar, the sugar table, and branch/lane rules are not.
-- is surface-grounded
today. Freezing a texture's meaning-role is not the same as grounding
its construct. You can declare, emit and round-trip a solid route right
now. You cannot write ~~, ==
or !! in a world document — the machinery beneath
them exists in varying degrees, but the surface forms do not. See
§22 for what each one still needs.Why async cannot simply be another edge
This is the single most instructive constraint in the language. An async route
does not settle within the period. A solid route participates in the
within-period reaction cascade and reaches a fixpoint before the period commits;
an async message is observable next period by construction. Those are
different objects in the canonical graph, and so promoting
~~ requires a canonical logical route declaration
that is distinct from the structural edge declaration — not a new edge kind.
Adding it as an edge kind would silently claim async messages join the fixpoint,
which is false.
The mailbox is the worked example of the distinction the language is careful
about. A mailbox declaration exists in the canonical IR, the runtime honours it,
and a battery covers it. It is IR/runtime-grounded. There is no
~~ surface form to write and the formatter cannot
emit one, so it is not surface-grounded, and it is therefore not a
sixth writable role.
| Term | Means |
|---|---|
| IR/runtime-grounded | a canonical IR declaration exists, the runtime executes it, and a battery covers it |
| surface-grounded | additionally: WRL source can declare it, the formatter can emit it, and it round-trips through the canonical bytes |
A construct is eligible for promotion into a frozen Core family only when it is surface-grounded. IR grounding alone is necessary and not sufficient.
6 · Walls: gate, commit, seal
Walls are the boundaries a rider crosses, and the slash ladder orders them by permanence.
| Boundary | Reduction effect | Status |
|---|---|---|
/gate | require a capability; emit an effect-request node — no ambient I/O | reserved |
//commit | canonicalize the fragment so far; assign a content id; freeze it | grounded |
///seal | commit, then register the artifact under a content hash | partial |
The frozen part is the three-tier ordering and one rule with teeth: effects cross named walls only. There is no ambient authority anywhere in the language — a computation that wants to touch the outside world must emit an effect-request that crosses a declared boundary, which is what makes the film complete rather than merely detailed.
7 · Periods and superdense time
- Logical time is superdense: every event carries a pair
(period t, microstep m). - A period is the settle-and-commit unit. Events within the same period are ordered by microstep.
- Wall-clock time enters only through an explicit boundary, and is recorded as a signed fact.
Period, epoch, microstep
Three words for time appear across this site and they are not synonyms.
Because once at 3 is spelled with an
epoch while the cycle is described in periods, it is worth
saying plainly which is which.
| Word | What it counts | Where you meet it |
|---|---|---|
| period | one full turn of the six-phase cycle — the settle-and-commit unit | the execution model: §8, the phase table |
| epoch | the index of a period within a particular run | run inputs and clocks: once at 3, [epoch:3], periods 7 |
| microstep | ordering within one period, during the REACT cascade | superdense pairs (period t, microstep m) |
So a period is a kind of step and an epoch is a number naming one. "Run this world for 7 periods" and "run it for epochs 0…6" describe the same run.
[epoch:1] is the
earliest epoch at which you may inject a claim. The gap is epoch 0 itself: it
is reserved for the world settling from its declared initial configuration, so
that "what this world does on its own" and "what this world does when something
is done to it" are never mixed into the same frame.8 · The five memory kinds
| Shape | Memory kind | Lifetime rule |
|---|---|---|
(state) | volatile cell | mutable by replacement; single-owner |
{facts} | monotonic knowledge | grow-only; merges by lattice union; never retracts |
[archive] | durable store | persistent addressable actor; survives restart |
:: fragment // | code memory | quoted graph as portable data |
#hash | sealed identity | immutable, content-addressed |
{facts}, merged by monotone union. Shared identity is
[archive]. There is no third option. That single-owner
discipline is what makes execution deterministic without a runtime
conflict resolver — the resolver is not fast, it is absent.9 · The period cycle
Every period advances the whole world through exactly six phases, in this order. This is frozen.
| Phase | What happens |
|---|---|
| OBSERVE | canonicalize incoming claims and insert the distinct ones |
| ACCEPT | create missing receipts according to the pinned acceptance policy |
| MAP | turn newly-accepted successful operations into controls |
| COMMIT | apply control writes and fault resets to owned cells |
| REACT | run the deterministic within-period token cascade to fixpoint; latch any current overflow |
| FILM | record exactly the entries needed to replay this period |
Notice the ordering of COMMIT before REACT. Configuration writes land first, then the cascade runs against the world those writes produced. A spinner reconfigured this period is reconfigured for this period's reaction, not the next one. That is a decision, not an accident, and it is why the phase list is ordered rather than merely enumerated.
10 · Two invariants, and what conflicts remain
Two invariants are enforced statically, before anything runs:
- Single-owner cells. No shared mutable cell exists anywhere.
- Disjoint deterministic guards. At most one deterministic rule is enabled per actor-state. Genuine choice must be written as an explicit scored branch, so a nondeterministic decision is always visible in the source.
Conflicts are narrowed, not dissolved
It is tempting to claim that single ownership makes conflict impossible. It does not, and the spec says so explicitly. Ownership removes generic write-write races; append and fact-union structures merge deterministically. But a real distributed world still faces conflicts that ownership cannot touch, and each of those is resolved by an explicit, policy-pinned law:
| Question | Pinned law |
|---|---|
| Recognition — did we see this claim before? | distinct-CandidateKey count |
| Acceptance — which candidate wins? | MIN CandidateKey / first receipt |
| Application — in what order do accepted events apply? | canonical event order |
| Fault — reset vs. new overflow in the same period | COMMIT clears, then REACT ORs |
| Capacity — partial failure | atomic latch; there is no partial |
11 · What a run leaves behind
"Film" is not one undifferentiated object. Three artifacts are frozen distinct:
| Artifact | Role |
|---|---|
| WorldFrame | authoritative observable state, sufficient for future behaviour — the physical snapshot |
| EventLedger | accepted events, receipts, effects, outcomes — the claim-aware layer |
| BuildFilm | compiler and build provenance proposed |
A replay package is frozen in this shape:
ReplayBundle {
initial_artifact,
initial_state,
event_ledger,
frames,
policy_ids
}
The byte layouts are a backend concern and are not frozen. The three-way distinction and the bundle shape are.
12 · Canonicalization and content identity
Meaning is normalized before it is addressed. Three rules follow:
- Comments, spacing, alignment, decorative route length and presentation colour never affect the semantic hash.
- Identity is content-addressed: the same normalized graph yields the same hash on every host.
- Builds and expansion are hermetic and host-blind: same source, same canonical bytes.
Concretely, the compiler's first job is to produce a canonical graph:
declarations sorted identity-first, edges sorted, configuration normalized to
its typed form, empty port groups omitted rather than emitted as empty. That
canonical graph is serialized to JSON with sorted keys and no incidental
whitespace, and hashed with SHA-256. The result is the
SemanticArtifactID.
For the starter world shown at the top of this guide, that value is:
The playground computes exactly that value in your browser, from a faithful port of the same canonicalizer — which is why you can watch an edit move the id, or fail to.
Two worlds are used throughout this site, and it is worth keeping them apart. The starter world above — one pulser, one relay, one spinner, one orb — is the teaching example, and the world the tutorial builds. The pinned conformance fixture is a larger six-object world carrying a second pulser and a door; it is what the toolchain's test batteries assert against, so its id is frozen across every implementation:
Both are loadable from the playground's example buttons, and both are
asserted by test/conformance.mjs on every change.
13 · The identity ladder
Six content-addressed prefixes, each naming a different question. They are deliberately distinct: conflating any two of them would let a change to one thing silently invalidate another.
| Prefix | Names | Moves when… |
|---|---|---|
sem- | SemanticArtifactID — the world's meaning | the canonical graph changes |
bknd- | BackendArtifactID — meaning + lowering profile | the semantic id moves, or the lowering profile does |
scen- | ScenarioDigest — the run inputs | periods or claims change |
replay- | ReplayBundleID — a specific run of a specific world | either of the two above moves |
bundle- | ForgeBundleID — a self-contained exported project | the exported bytes change |
env- | an evaluation environment | its splits change |
sem- to two different bknd-
values and still produce byte-identical films — which is the whole point of
having a semantic layer at all.14 · The world document
A world document is one profile line, then declarations, then edges. Comments
start with ; and run to end of line.
; every world names the profile it conforms to
profile forge.world.core.v1
; declarations: [role:id](config){ports}
[pulser:p0](every 2){sig_out}
[relay:r0]{sig_in, sig_out}
[spinner:sp](w=16, n=8, rotor=quarter_turn_z, configurable){sig_in, socket}
[orb:ob]{pose}
; edges: [src] --kind--> [dst]
[p0] --sig--> [r0]
[r0] --sig--> [sp]
[sp] --socket--> [ob]
That is the entire grammar of the current core surface. There is one profile, five roles, two edge kinds, one configuration group per role, and one port group per declaration. The restraint is deliberate: every construct in that list is surface-grounded, meaning it round-trips through the canonical bytes and its identity behaviour is covered by a battery.
# is not a comment. In the
core surface the comment marker is ; only.
# belongs to the identity family and is preserved.
Writing # note where you meant a comment is a parse
error, not a silently-ignored line.15 · The five roles
| Role | Does | Ports (out / in) | Config |
|---|---|---|---|
pulser | emits a signal on a clock | sig_out / — | a clock |
relay | forwards a signal | sig_out / sig_in | none |
door | consumes a signal | — / sig_in | none |
spinner | holds a rotor; drives a socket | socket / sig_in | w, n, rotor, configurable |
orb | carries a pose | — / pose | none |
A role's ports are derived from the registry, not from what you type.
The {…} group on a declaration is checked against the
frozen signature for that role: name the wrong ports, or name a subset, and you
get WRL_PORT_SIGNATURE. The braces are an assertion
about structure, not a definition of it.
In the canonical artifact, empty port groups are omitted entirely. A
door carries {"in":["sig_in"]} with no
"out" key at all — not an empty array. This is the
kind of detail that a re-implementation gets wrong and a content hash catches
immediately.
Clocks
| You write | Canonical form | Meaning |
|---|---|---|
every 2 | ["periodic", 2, 0] | fires every 2 periods, no phase offset |
every 3, phase 1 | ["periodic", 3, 1] | fires every 3 periods, offset by 1 |
once at 1 | ["once", 1] | fires exactly once, at period 1 |
16 · Edges, ports and wiring
Two edge kinds exist. Each one is a fixed pair of ports, and the pair is what makes the edge legal or illegal.
| You write | Canonical kind | Source port | Destination port |
|---|---|---|---|
--sig--> | SignalWire | sig_out | sig_in |
--socket--> | SocketControl | socket | pose |
Validation is therefore purely mechanical. An edge is legal when the source
role owns the source port and the destination role owns the destination port.
Wire a pulser into a pulser and the destination has no
sig_in, so you get
WRL_ILLEGAL_PORT_PAIR. Wire a spinner's socket into a
relay and the relay has no pose, same error.
WRL_CONTROLLER_CONFLICT. This
is the single-owner invariant from §8 showing up as a
topology rule: an input port is an owned cell, and owned cells have exactly one
writer. Fan-out is fine and common; fan-in is a structural
error.Where the error is caught
A useful mental model, because it explains error messages that otherwise look
misplaced: an operation enforces only its own precondition; the seal judges
structural legality. Removing an object does not cascade to its edges,
because removal's precondition is only that the object exists. The dangling edge
it leaves behind is caught when the world is sealed, as
WRL_UNKNOWN_ENDPOINT. An honest delete is therefore
two operations — unwire, then remove — and the design prefers making you say that
to silently doing more than you asked.
17 · Numbers
A spinner's rotor is four integer quaternion lanes in a fixed-point Q-format that the spinner itself declares:
| Field | Means |
|---|---|
w | total bit width of a lane |
n | number of fractional bits; the unit value is 1 << n |
rotor | four integer lanes, written a.b.c.d, or a named rotor |
configurable | whether the rotor may be rewritten at run time |
So at n=8 the unit is 256 and the identity rotor is
256.0.0.0. A quarter turn about z needs
u/√2 in two lanes, which is not an integer — so the
language does not compute it in floating point and round. It computes the exactly
rounded integer with integer arithmetic only:
; round(u / sqrt2) with integers only
u = 1 << n
two_uu = 2 * u * u
q0 = isqrt(two_uu) / 2
result = q0 + 1 if two_uu > 4*q0*q0 + 4*q0 + 1 else q0
| n | unit | quarter-turn lane |
|---|---|---|
| 4 | 16 | 11 |
| 8 | 256 | 181 |
| 16 | 65536 | 46341 |
Those values are reproducible on any host with integer arithmetic, which is exactly the requirement. Named rotors that involve rounding carry a provenance policy id recording how they were rounded — provenance only; it never enters the artifact bytes.
18 · Sugar
Sugar is a source-to-source pre-pass that runs in front of the untouched canonical parser. It is not a second compiler, and it has no path to the seal of its own.
| Sugared | Expands to | Kind |
|---|---|---|
every 2 | mode=periodic, period=2, phase=0 | value |
once at 1 | mode=once, epoch=1 | value |
rotor=quarter_turn_z | rotor=181.0.0.181 at n=8 | value |
[relay:r*3] | r0, r1, r2 | structural |
[p0] --sig--> {r0, r1} | two separate edges | structural |
sem- id. Sugar introduces no identity
of its own and no privileged path to the seal.sp*3 → sp*4 is a
different program and moves the identity, exactly as the explicit spelling
would. The looser phrasing is false and the spec is careful to say so.Two properties are required of any sugar, and both are gated by battery:
- Bounded expansion. A one-to-many form must reject an absurd count with
a typed diagnostic before allocating the expansion.
sp*1000000000is a diagnostic, not an out-of-memory event. Replication and fan-out are bounded at 1024, total expansion at 65536 lines, and a count may not exceed 9 digits. - Original-source mapping. An expansion must preserve the authored spans, so diagnostics, completion, semantic diff and editor operations all refer to what you wrote rather than to generated text.
Generated names are judged by the ordinary seal. A collision between a
generated name and an explicit one is a normal
WRL_DUPLICATE_ID, not a special sugar rule.
Status: implemented, battery green, identity-equivalent — not closure-proven, not frozen.
19 · Worlds versus runs
A world and a run of that world are different documents. This is normative.
| Document | Carries | Enters sem-? |
|---|---|---|
| World document | profile, objects, edges, static config, wiring | yes |
| ScenarioV1 | periods, [epoch:N] claims, run inputs | no |
Three consequences are frozen:
- Run inputs are not world content. Changing how long you run a world,
or what claims you feed it, does not move the
SemanticArtifactID. One world, many scenarios. - A world formatter emits a world document — and must exclude scenario
syntax both semantically (reparsing yields no run inputs) and
lexically (the emitted text does not contain scenario syntax at all). The
lexical half is load-bearing: a formatter emitting a literal
periods 0would satisfy the semantic half while still writing scenario syntax into a world document. - Round-trip laws split in two. The world round-trip preserves the
complete canonical world projection; the identity round-trip preserves the
canonical bytes and the
sem-id. Neither subsumes the other.
(profile, nodes, edges). A fixed tuple silently stops
covering any field a later slice adds.The two parser mouths
| Entry point | Accepts | Status |
|---|---|---|
parse_wrl_core(text) | world source only; rejects run inputs with a typed WRL_WORLD_SOURCE_HAS_SCENARIO | normative |
parse_wrl_legacy_document(text) | a pre-boundary combined document | explicit compatibility path |
Parameterizing the parser is sanctioned only as a migration bridge, and only through an unmistakable API — never a boolean flag on one function. The legacy path's destination is to split a combined document into a world plus a scenario, not to keep parsing it forever.
20 · Diagnostics
Every rejection is a typed code, never a raw exception and never a free-text string. A tool downstream can branch on the code; a human reads the detail. The full catalogue lives in the reference; the ones you will meet first:
| Code | Means |
|---|---|
WRL_DUPLICATE_ID | a second object claims an id already declared |
WRL_UNKNOWN_ENDPOINT | an edge names an object that was never declared |
WRL_ILLEGAL_PORT_PAIR | an edge asks for a port the role does not have |
WRL_CONTROLLER_CONFLICT | two controllers reach the same input port |
WRL_PORT_SIGNATURE | a {ports} group does not match the frozen signature |
WRL_WORLD_SOURCE_HAS_SCENARIO | run inputs appeared in world source |
WRL_SUGAR_MALFORMED | a sugared spelling could not be expanded |
WRL_UNSUPPORTED_FEATURE | the construct is outside frozen Semantic IR v1 |
Two disciplines make these worth relying on. First, a rejected draft stays editable — an invalid candidate never commits, but it also never destroys what you were editing. Second, a failure to parse leaves the previous sealed world runnable. You cannot break a working world by typing badly into an editor.
21 · Conformance families
These observable properties are frozen as families of guarantee. Their exact test matrices remain draft.
| Family | Guarantee |
|---|---|
| Formatting invariance | presentation never changes the hash |
| Replay exactness | a live run and its replay share all observables |
| Build invariance | same inputs → identical film and hashes across hosts |
| Merge laws | fact union is commutative, associative, idempotent |
| Boundary safety | undeclared effects cannot cross walls |
| Scheduler invariance | worker ordering never changes the committed film |
| World round-trip | a format→parse cycle preserves the complete canonical world projection |
| Identity round-trip | a format→parse cycle preserves the canonical bytes and the sem- id |
| Document separation | a world document excludes run inputs both semantically and lexically |
The determinism property, stated precisely:
22 · The road to Core 0.2
The promotion order below is steered, not merely preferred. Each step supplies something the next one needs, so reordering them produces constructs that cannot be grounded honestly.
| # | Step | Gate it must pass |
|---|---|---|
| 1 | L-0 closure | round-trip and document-boundary laws restated and green — complete |
| 2 | ~~ async route | needs a canonical logical route declaration distinct from the structural edge declaration |
| 3 | == verified route | see below — it is not almost free |
| 4 | # references and & composition | — |
| 5 | //commit and ///seal | — |
| 6 | !! fault route | may only follow a minimal supervision floor actually existing |
| 7 | /gate capability gate | last |
== is not "almost free"
It is tempting to read the verified route as nearly grounded, because the
acceptance core already accepts claims. That confuses two different things: the
declaration == would introduce is
authorization structure, while the frozen texture
== denotes an evidence-backed transition.
== is grounded only when
acceptance enforces all four of: the claimant, the target, the
operation family, and a named policy. Anything less is a route that
looks verified and is not.The layering that comes with it is approved: the world document declares
a verified channel and its schema; the scenario carries the claim
instances; receipts prove acceptance through that channel. One explicit
constraint attaches — a principal-shaped role such as
[worker:w1] must not be introduced as part of
that work. How principals exist in the role system is a separate question
requiring its own sanction, and smuggling one in would freeze a role-system
decision nobody ruled on.