WRL in 20 minutes.
You will build one small world from an empty file, seal it, and then spend most of the time on the interesting part: watching exactly when its identity moves and when it does not. Keep the playground open in another tab — every snippet here is meant to be pasted into it.
Why the language has this shape
Before the syntax, one paragraph of motivation, because WRL will look strange otherwise.
WRL exists to author worlds whose runs can be proven. Not logged — proven. A run produces a film: an ordered, schema-versioned record of what happened, and that film is byte-identical no matter which machine produced it or how the work was scheduled. For that to be possible, a program cannot be a bag of instructions with ambient access to time, memory and the network. It has to be a graph: a fixed set of durable identities, a fixed set of connections between them, and a fixed cycle that advances them all together.
So WRL's surface is designed to be a readable spelling of a graph, and its compiler's first job is to turn your text into the canonical form of that graph. Everything else — the identity, the determinism, the ability to swap backends without changing meaning — falls out of that one decision.
0 · Read before you write
Here is a complete WRL world. You are not expected to understand it yet — just notice that you can already tell which parts are things, which are settings, and which are connections.
profile forge.world.core.v1
[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}
[p0] --sig--> [r0]
[r0] --sig--> [sp]
[sp] --socket--> [ob]
That readability is not an accident. Three of WRL's four reading laws are already doing work here:
| You see | Law | It means |
|---|---|---|
| [ ] square brackets | shape says what a thing is | a durable identity — something that persists across periods |
| ( ) parentheses | shape says what a thing is | static configuration — how this identity is built, fixed when it is declared |
| { } braces | shape says what a thing is | wiring — which ports the identity exposes |
| --sig--> | line texture says how it moves | a solid route: synchronous, in-period, cannot silently drop |
| role:name | the colon introduces | this is a pulser, and it is called p0 |
The fourth law — marks for architecture, words for computation — is why those brackets and arrows are punctuation rather than keywords. Structure is drawn; work is named.
(w=16, n=8, rotor=quarter_turn_z) is read once, when
the world is built: it says how that spinner is made — its lane geometry,
and the rotation it starts holding. What the spinner is holding on period 40 is
runtime state, and runtime state is nowhere in the document. It lives in
cells the world writes as it runs, and those cells are seeded from this
declaration and then left alone by it. That separation is what makes the
document hashable at all: the same source always describes the same world,
because it describes the machine and not the machine's history.1 · Declare the profile
Start with an empty file and one line.
profile forge.world.core.v1
A profile declares which frozen registry of roles, edges, policies and schemas this world is written against. It is the first thing validated and the first thing hashed. There is exactly one profile today, and naming it is not optional — a world that does not say what language it is written in cannot be given an identity.
Paste that into the playground on its own. It seals. An empty world is still a world, and it still has an id.
2 · A thing that ticks
Add a source of signal.
profile forge.world.core.v1
[pulser:p0](every 2){sig_out}
Read it left to right. [pulser:p0] — a durable identity
named p0, playing the Pulser role.
(every 2) — its configuration: a clock that fires every
second period. {sig_out} — its wiring: it exposes one output port
called sig_out.
Three things about that brace group are worth knowing now, because they will save you time later:
- It is checked, not decorative. A Pulser's frozen port signature is
exactly
{sig_out}. Writing{sig_in}, or{}, or{sig_out, socket}is a typed rejection (WRL_PORT_SIGNATURE). Visible source is never silently ignored. - It is optional. Omit the braces entirely and the ports come from the role registry. Include them and they must be right. The language would rather you write nothing than write something false.
- It carries no meaning of its own. Ports are derived from the role, so the braces are a projection you can choose to show. Adding or removing them does not move the id.
The five surface roles
Core 0.1.2 freezes five roles you can write in text. Each has a fixed port signature, and that signature is what makes edge validation structural rather than special-cased.
| Role | In | Out | Is |
|---|---|---|---|
| Pulser | — | sig_out | a clock; the only source of signal |
| Relay | sig_in | sig_out | a pass-through, so signal can travel |
| Door | sig_in | — | a sink; signal arrives and stops |
| Spinner | sig_in | socket | rotation: takes signal, drives a pose |
| Orb | pose | — | the thing that gets moved |
There is a sixth role, Mailbox, in the IR. It has no surface spelling in Core 0.1.2 — you cannot type it — so we will leave it until the guide.
3 · Wire it to something
A pulser on its own does nothing observable. Give it somewhere to go.
profile forge.world.core.v1
[pulser:p0](every 2){sig_out}
[door:d0]{sig_in}
[p0] --sig--> [d0]
An edge line names two identities and the tag of the connection between them. Core 0.1.2 has exactly two edge kinds:
| You write | Kind | Connects |
|---|---|---|
--sig--> | SignalWire | a sig_out to a sig_in |
--socket--> | SocketControl | a socket to a pose |
Edge validation is entirely port-driven. There is no table of "which role may connect to which role" — the compiler asks whether the source has the required out-port and the destination has the required in-port, and that is the whole rule. It is worth pausing on why that is a good design: because the rule is structural, a role that has no ports at all (like Mailbox) is automatically un-wireable, without anyone writing a special case to forbid it.
WRL_CONTROLLER_CONFLICT. This is the constraint that
makes a world's behaviour a function of its structure rather than of arrival
order — and it is the one beginners hit first.4 · Rotation and pose
Now the interesting pair. A Spinner converts arriving signal into rotation; an Orb holds the pose that results.
profile forge.world.core.v1
[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}
[p0] --sig--> [r0]
[r0] --sig--> [sp]
[sp] --socket--> [ob]
The spinner's configuration has four fields, and they are the first place where WRL's insistence on exactness becomes visible.
| Field | Means | Constraint |
|---|---|---|
| w | total lane width in bits | w > 0 |
| n | how many of those bits are fractional | 0 ≤ n ≤ w |
| rotor | four integer quaternion lanes | each lane in [0, 2^w) |
| configurable | whether a claim may re-rotor it at runtime | a bare flag |
configurable is where the
configuration/state distinction earns its keep. The declared rotor is the
initial value of a runtime cell; configurable
says whether the running world is permitted to overwrite that cell when a claim
asks it to. So on period 40 the spinner may well be holding a rotation nobody
wrote down. The document still says
rotor=quarter_turn_z, and the world's id has not
moved a bit — because the id describes what was declared, and the
rotation is what happened. Those are different questions, and WRL keeps
them in different places: declarations in the world document, happenings in the
film.There are no floating-point numbers anywhere in WRL. A rotor is four
integers in a fixed-point format the spinner itself declares:
w=16, n=8 means sixteen bits total, of which eight are
fractional, so one unit is 1 << 8 = 256. This is
not austerity for its own sake — it is the reason two machines can agree on a
film byte for byte.
Named rotors, and why one of them is special
rotor=quarter_turn_z is sugar. It resolves to four
explicit integers before the compiler ever sees a graph. Four of the five names
are exact at any geometry, because their quaternion components are 0 or 1:
| Name | Lanes at fractional width n | Exact? |
|---|---|---|
identity | 2^n . 0 . 0 . 0 | yes, at any n |
reverse_x | 0 . 2^n . 0 . 0 | yes, at any n |
reverse_y | 0 . 0 . 2^n . 0 | yes, at any n |
reverse_z | 0 . 0 . 0 . 2^n | yes, at any n |
quarter_turn_z | q . 0 . 0 . q, q = round(2^n / √2) | no — policy-governed |
A quarter turn about z has components of √2⁄2, which is irrational and
therefore not representable. WRL does not paper over this. Instead the projection
is pinned by a named policy,
forge_named_rotor_rne_sym_v1: round each of the two
equal lanes independently to nearest, with no residual redistribution and no
renormalization, computed in exact integer arithmetic — never a float.
| n | q = round(2^n / √2) | rotor |
|---|---|---|
| 4 | 11 | 11.0.0.11 |
| 8 | 181 | 181.0.0.181 |
| 16 | 46341 | 46341.0.0.46341 |
The consequence is worth stating plainly: the same name at a different
n is a different rotor and therefore a different
world. That is not a leak in the abstraction — it is the abstraction being
honest. An approximation's value depends on how much room you gave it, and WRL
makes you see that in the id.
5 · Seal it
Paste the world from step 4 into the playground. You get:
That is a real SemanticArtifactID. It is produced by a five-step pipeline you can follow exactly:
- Desugar. A source-to-source pre-pass rewrites
every 2tomode=periodic, period=2, phase=0andquarter_turn_zto181.0.0.181. After this step there is no sugar left. - Parse. The strict world mouth builds a graph — objects and edges, nothing else.
- Validate. Closed roles, unique well-formed ids, closed edge kinds, declared endpoints, legal port pairs, one controller per port, every numeric field in range. Each failure has a stable machine-readable code.
- Canonicalize. Objects are sorted identity-first by
(object_id, role); edges by(kind, src, dst). Declaration order is now gone. - Serialize and hash. Sorted-key JSON with compact separators, then
SHA-256, then the
sem-prefix.
Every step is a pure function. There is no clock, no randomness, no environment lookup, and no build timestamp anywhere in that chain — which is why the id is the same everywhere.
6 · What does not move the identity
This is the half of the story that makes WRL comfortable to work in. Try each of these in the playground and watch the id stay put.
| Change | Id | Because |
|---|---|---|
| Reordering declarations | same | canonicalization sorts identity-first |
| Putting edges before nodes | same | the graph is a set, not a script |
| Extra whitespace or blank lines | same | layout never reaches the graph |
Adding or editing ; comments | same | comments are lexical |
Adding or removing {ports} | same | ports derive from the role |
| Writing sugar instead of its explicit twin | same | sugar is gone before a graph exists |
| Moving a box on a canvas | same | position is presentation, not semantics |
| Switching the backend counter encoding | same | that moves the BackendArtifactID instead |
The last two are the ones that matter in practice. Because presentation and backend both live outside the semantic artifact, you can redesign a diagram or retarget a compiler without a single downstream consumer noticing — and you can say so with a hash rather than a promise.
7 · What does move it
Exactly one thing: a change in meaning.
| Change | Id |
|---|---|
| Renaming an object | moves — identity is part of the graph |
| Adding, removing or rerouting an edge | moves |
| Changing a rotor, a clock, a width | moves |
Adding or removing the configurable flag | moves |
Changing n under a policy-governed rotor | moves — the projection changes |
| Declaring a Mailbox anywhere in the world | moves — the whole semantic surface shifts to v1.1 |
Try it. Change quarter_turn_z to
identity:
Change it back and the original id returns, byte for byte. Identity in WRL is a function, not a counter — there is no "version" that only goes up, and no state to get out of sync.
8 · Break it on purpose
WRL's error behaviour is a feature, so it is worth meeting deliberately rather than by accident. Every rejection carries a stable machine code, and no rejection is ever a warning that proceeds anyway.
Two controllers on one port
[pulser:p0](every 2){sig_out}
[pulser:p1](every 3){sig_out}
[relay:r0]{sig_in, sig_out}
[p0] --sig--> [r0]
[p1] --sig--> [r0] ; WRL_CONTROLLER_CONFLICT
A port that does not exist
[door:d0]{sig_in}
[orb:ob]{pose}
[d0] --sig--> [ob] ; WRL_ILLEGAL_PORT_PAIR — a Door has no sig_out
A texture the runtime does not implement
[p0] ~~sig~~> [d0] ; WRL_UNSUPPORTED_FEATURE
That last one deserves comment, because it is the clearest example of how this language handles its own incompleteness. The async texture is frozen in the notation — the spec reserves the spelling and says what it will mean. It is not implemented in the runtime. So rather than lowering it to something approximate and hoping, the compiler refuses and tells you which frozen tier the construct sits in. There is no speculative lowering anywhere in WRL.
9 · The document boundary
Try adding a line like this to a world:
periods 7
[epoch:3] SetRotor sp 181.0.0.181
You get WRL_WORLD_SOURCE_HAS_SCENARIO. This looks
like pedantry and is actually the most load-bearing rule in the language.
A world and its run inputs are different documents. The world is the
structure: what exists and how it is connected. The run inputs — how many periods
to run, and which claims arrive at which epoch — are a separate
ScenarioV1, with its own identity (scen-).
Keeping them apart is what lets you say something precise and useful: I ran the same world against a hundred different scenarios. If run inputs could leak into the world document, that sentence would be unverifiable, because every scenario would silently produce a different world id. The boundary is enforced lexically, and it is a typed rejection rather than a silent acceptance, specifically so that nobody can drift across it by accident.
replay- ReplayBundleID. Editing the
world moves the bundle id and leaves the scenario digest untouched — the two
halves stay independently addressable, which is exactly what you want when
comparing runs.10 · Sugar, and the law it obeys
WRL's sugar is deliberately small, and it obeys a rule stricter than most languages bother with.
Value sugar
| Sugared | Explicit |
|---|---|
(every 2) | (mode=periodic, period=2, phase=0) |
(every 4, phase 1) | (mode=periodic, period=4, phase=1) |
(once at 5) | (mode=once, epoch=5) |
rotor=identity at n=8 | rotor=256.0.0.0 |
Structural sugar
* mints a group by position, zero-based:
[spinner:sp*3](w=8, n=4, rotor=identity){sig_in, socket}
; becomes sp0, sp1, sp2 — three explicit declarations
{...} on an edge line fans out to each member,
and * on both endpoints pairs positionally with equal
counts required:
[p0] --sig--> {[a], [b]} ; two edges
[p*3] --sig--> [d*3] ; p0→d0, p1→d1, p2→d2
The law
There is no sugar-specific identity. Desugaring is not exempt from identity — it is upstream of it. Because the sugar is gone before a graph exists, no separate id can be minted for the sugared spelling, and none can be added later without breaking the pre-pass.
Note what that does not claim. It is not the stronger and false statement that
a sugar edit cannot change an id. Changing sp*3 to
sp*4 changes the program and moves the id, exactly as
writing the fourth spinner out by hand would.
*.Where to go next
You now know the whole of WRL Core 0.1.2's text surface: one profile, five roles, two edge kinds, one config group per role, two sugar tiers, and the document boundary. That is genuinely all of the frozen core.
The complete guide
The other nine construct kinds, the memory kinds, the execution model, and which parts of the design are still ahead of the runtime.
KEEP OPENReference tables
Ports, config keys, every error code, the sugar forms, the identity ladder, the stability tiers.
AUTHORITYThe frozen spec
Core 0.1.2 itself. When the guide and the spec disagree about what is settled, the spec wins.