<engine>
Declares a state machine — the canonical structural form for "UI as a fully-handled state machine."
Syntax
<engine for=TypeName initial=.Variant>
<Variant rule=.OtherVariant />
...
</engine>
for=
names the discriminating enum type. Every variant of that type
must have a corresponding state-child or the compiler fires
E-ENGINE-STATE-CHILD-MISSING.
initial=
picks the starting variant. rule=
on a state-child constrains the legal transitions out of that
variant. Compile-time enforcement where statically resolvable;
runtime otherwise.
Worked example
A three-state traffic light. One enum type, one engine, three state-children, one button to cycle.
type Light:enum = { Red, Yellow, Green }
<engine for=Light initial=.Red>
<Red rule=.Green>
Stop. Wait for green.
</Red>
<Green rule=.Yellow>
Go.
</Green>
<Yellow rule=.Red>
Slow.
</Yellow>
</engine>
<button onclick=@light.advance(.Green)>Cycle</button>
scrml compile to verify. For a
running engine you can inspect, the
showcase
mounts a live state-machine app beside its source and its
compiled transition diagram.
Semantics
-
Singleton per declaration site. One
<engine>tag → one running machine for that scope. Cross-file mounting is via<EngineName/>; the singleton remains one instance per the outer-times-inner invariant per SPEC §51.0.K Machine Cohesion footnote. -
Auto-declared variable. The engine type
name auto-derives the handle, lowercase-first
(
Light→light, per SPEC §51.0.C). Read the current variant via@light. Write via@light.advance(.Variant). Override the auto-name viavar=if it would collide. -
rule=is a contract. Compile-time when the target is statically resolvable; runtime otherwise. Transitions not listed fire E-ENGINE-INVALID-TRANSITION. -
initial=is required at top-level engines. Omission fires W-ENGINE-INITIAL-MISSING. Engines without an initial state are usually accidents.
Errors this feature can fire
- E-ENGINE-VAR-DUPLICATE
- E-ENGINE-INVALID-TRANSITION
- W-ENGINE-INITIAL-MISSING
- W-ENGINE-SELF-WRITE-DETECTED
- E-COMPONENT-ENGINE-SCOPE
- E-HISTORY-NO-INNER-ENGINE
- E-INTERNAL-RULE-NOT-COMPOSITE
Edge cases
-
Self-writes are runtime no-ops.
@light.advance(.Red)from inside.Redis silently discarded (v0.3 Option-d idempotent self-write semantics). NOT arule=violation. The compiler emits W-ENGINE-SELF-WRITE-DETECTED to surface the no-op so it doesn't slip past code review. -
<engine>inside a component body fires E-COMPONENT-ENGINE-SCOPE. Components are multi-instance; engines are singletons. They don't compose. Use a derived engine (derived=expr) or pass the current variant in as a prop and let the component render off the read. -
Temporal triggers ride on the engine surface.
<onTimeout after=… to=…/>as a child of a state-child is the engine temporal surface. NOT a separate timer system. Per SPEC §51.0.M. -
Hierarchy is composite state-children.
Nested
<engine>declarations inside composite state-children preserve singleton semantics (outer-times-inner = one running inner instance). Thehistory=trueattribute on a composite state-child enables shallow history restoration via the.Variant.historytarget form. Per SPEC §51.0.N.
Related features
-
<match for=Type>
— Tier 1 sibling. Pattern matching as a block;
same exhaustiveness check, no active rules.
bun scrml promote --matchlifts to<engine>. -
<onTransition>
— effect on transition. Attributes:
from=,to=,once,if=. - <onTimeout> — temporal trigger as a state-child child element.
- derived=expr — derived engines. Compute the active variant from another engine's state.
- <auth role=…> — per-role visibility. Wraps engine state-children that should only render for specific roles.
Availability
| Surface | Since | Notes |
|---|---|---|
| <engine> shape | S57 D2.8 (2026-05-04) | Spec ratified; parser S70 (A1c Wave 1 C12-C15 in flight) |
| history= / internal:rule= / nested engines | S67 (2026-05-08) | Spec landed S68 1de05ef; parser+typer S70 |
| <onTimeout> | S70 A5-2 | Spec § 51.0.M; runtime rides § 51.12 |
| Idempotent self-write | v0.3 Option-d (2026-05-12) | W-ENGINE-SELF-WRITE-DETECTED surfaces the no-op at compile time |
Specification
This page summarizes SPEC §51.0 (~21293-24415). The normative text lives at compiler/SPEC.md in the scrml repository. When this page disagrees with SPEC.md, SPEC.md is authoritative — surface the contradiction and we'll fix the docs.
Quick-lookup anchors: SPEC-INDEX.md entries — engine declaration, engines as singleton, auto-declared engine variable, engine mount position, engine rule= contract, idempotent self-write semantics.