WallRiderLangWRL CORE 0.1.2 · FROZEN
TUTORIAL

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.

The architectural rule. WRL surface never lowers directly to the runtime. The only sanctioned path is source or canvas → canonical semantic graph → Semantic IR → runtime. Every surface converges on the same seam, which is why a drawing and a document can be the same program.

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 seeLawIt means
[ ] square bracketsshape says what a thing isa durable identity — something that persists across periods
( ) parenthesesshape says what a thing isstatic configuration — how this identity is built, fixed when it is declared
{ } bracesshape says what a thing iswiring — which ports the identity exposes
--sig-->line texture says how it movesa solid route: synchronous, in-period, cannot silently drop
role:namethe colon introducesthis 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.

Parentheses hold configuration, not running state. (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:

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.

RoleInOutIs
Pulsersig_outa clock; the only source of signal
Relaysig_insig_outa pass-through, so signal can travel
Doorsig_ina sink; signal arrives and stops
Spinnersig_insocketrotation: takes signal, drives a pose
Orbposethe 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 writeKindConnects
--sig-->SignalWirea sig_out to a sig_in
--socket-->SocketControla 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.

One controller per port. A node admits at most one incoming SignalWire, and at most one incoming SocketControl. Two controllers reaching the same input is 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.

FieldMeansConstraint
wtotal lane width in bitsw > 0
nhow many of those bits are fractional0 ≤ n ≤ w
rotorfour integer quaternion laneseach lane in [0, 2^w)
configurablewhether a claim may re-rotor it at runtimea 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:

NameLanes at fractional width nExact?
identity2^n . 0 . 0 . 0yes, at any n
reverse_x0 . 2^n . 0 . 0yes, at any n
reverse_y0 . 0 . 2^n . 0yes, at any n
reverse_z0 . 0 . 0 . 2^nyes, at any n
quarter_turn_zq . 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.

nq = round(2^n / √2)rotor
41111.0.0.11
8181181.0.0.181
164634146341.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:

sem-67e954cfe3115166b49388366df3f062a46572ba2baf53380f1520f4050b60ae

That is a real SemanticArtifactID. It is produced by a five-step pipeline you can follow exactly:

desugarparsevalidate canonicalizeserializesha-256
  1. Desugar. A source-to-source pre-pass rewrites every 2 to mode=periodic, period=2, phase=0 and quarter_turn_z to 181.0.0.181. After this step there is no sugar left.
  2. Parse. The strict world mouth builds a graph — objects and edges, nothing else.
  3. 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.
  4. Canonicalize. Objects are sorted identity-first by (object_id, role); edges by (kind, src, dst). Declaration order is now gone.
  5. 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.

ChangeIdBecause
Reordering declarationssamecanonicalization sorts identity-first
Putting edges before nodessamethe graph is a set, not a script
Extra whitespace or blank linessamelayout never reaches the graph
Adding or editing ; commentssamecomments are lexical
Adding or removing {ports}sameports derive from the role
Writing sugar instead of its explicit twinsamesugar is gone before a graph exists
Moving a box on a canvassameposition is presentation, not semantics
Switching the backend counter encodingsamethat 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.

ChangeId
Renaming an objectmoves — identity is part of the graph
Adding, removing or rerouting an edgemoves
Changing a rotor, a clock, a widthmoves
Adding or removing the configurable flagmoves
Changing n under a policy-governed rotormoves — the projection changes
Declaring a Mailbox anywhere in the worldmoves — the whole semantic surface shifts to v1.1

Try it. Change quarter_turn_z to identity:

sem-…a different 64 hex digits…

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.

The rule behind the errors. An operation enforces only its own precondition; the seal judges structural legality. A surface expansion that produces an illegal graph is not blocked at expansion time — it reaches the seal and is rejected there with the ordinary typed code. That keeps each layer small and keeps every structural rule in exactly one place.

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.

What binds them. A world id and a scenario digest together produce a 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

SugaredExplicit
(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=8rotor=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.

Expansion has bounds, enforced before allocation. A pre-pass that turns one line into N is a denial-of-service surface, so replication is capped at 1024 members, fan-out at 1024, and total emitted lines at 65536 — and the count is checked before any list is built. These are surface-tooling limits on what you may spell, not limits on what a graph may contain. A world with five thousand spinners is perfectly legal; it just cannot be written with one *.

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.