Spine in 15 Minutes
The previous sixteen articles explain why Spine works the way it does. This article shows what you write and what happens when you compile it. No category theory. No proof theory. Just syntax, compiler behavior, and output.
What Spine Is
Spine is a language for describing systems — what things exist, how they can change, and what must always be true. The compiler reads those descriptions and produces two things: native code (via Zig) and verification artifacts (Z3, TLA+, mCRL2). You write one source file. The compiler decides which parts need runtime code and which parts need proofs.
1. Data and Functions
Spine has algebraic data types and pattern matching. If you know Haskell or ML, this will look familiar.
data Maybe a : Type where
Nothing : Maybe a
Just : a -> Maybe a
data List a : Type where
Nil : List a
Cons : a -> List a -> List a
record Person where
field name : String
field age : Nat
field email : String
Functions use equation-style definitions with pattern matching:
factorial : Nat -> Nat
factorial 0 = 1
factorial n = n * factorial (n - 1)
add : Int -> Int -> Int
add x y = x + y
compose : (b -> c) -> (a -> b) -> a -> c
compose f g x = f (g x)
This compiles to Zig. A record becomes a Zig struct. A data becomes a tagged union. Pattern matching becomes branching. Here is an actual compiler output fragment — the lexer's token type, from lexer.model:
-- Source (lexer.model):
enum TokenType =
INT | FLOAT | STRING | CHAR | BOOL
| DEF | LET | IN | IF | THEN | ELSE
| MATCH | CASE | OF | DATA | ENUM
| RECORD | TYPE | MODULE | WHERE
| IMPORT | EXPORT | EXTERNAL
| CLASS | INSTANCE | INDUCTIVE | FORALL
| PLUS | MINUS | STAR | SLASH ...
record Token where
kind : TokenType
value : String
line : Int
column : Int
// Generated (lexer.zig):
pub const TokenType = enum {
INT, FLOAT, STRING, CHAR, BOOL,
DEF, LET, IN, IF, THEN, ELSE,
MATCH, CASE, OF, DATA, ENUM,
RECORD, TYPE, MODULE, WHERE,
IMPORT, EXPORT, EXTERNAL,
CLASS, INSTANCE, INDUCTIVE, FORALL,
PLUS, MINUS, STAR, SLASH, ...
};
pub const Token = struct {
kind: TokenType,
value: []const u8,
line: i64,
column: i64,
length: i64,
};
The compiler that produced this Zig is itself written in Spine (145 modules). It compiles its own source through its own pipeline — that is what self-hosting means here.
2. Effects
Spine has algebraic effects. An effect declares operations; a handler says what those operations do.
effect State s where
get : State s s
put : s -> State s Unit
effect Console where
readLine : Console String
printLine : String -> Console Unit
effect Error e where
throw : e -> Error e a
Handlers are separate from effect declarations:
handler constReader where
get _ k -> k 42
put _ k -> k ()
Effects can carry correctness properties. These are not comments — the compiler extracts them for verification:
constraint GetPutLaw {
description: "get >>= put ≡ return ()"
predicate: "forall handler in StateHandler, s in State :
(perform get >>= \x -> perform (put x)) ≡ return Unit"
}
constraint PutPutLaw {
description: "put x >> put y ≡ put y"
predicate: "forall handler in StateHandler, x in s, y in s :
(perform (put x) >> perform (put y)) ≡ perform (put y)"
}
3. Type Classes
Type classes work as you would expect from Haskell, with one addition: Spine's type system is dependently typed, so class constraints can express richer properties.
class Eq a where
eq : a -> a -> Bool
neq : a -> a -> Bool
neq x y := not (eq x y)
class Functor f where
fmap : (a -> b) -> f a -> f b
class Functor m => Monad m where
unit : a -> m a
bind : m a -> (a -> m b) -> m b
instance Eq Color where
eq Red Red = true
eq Green Green = true
eq Blue Blue = true
eq _ _ = false
4. Behavior: State Machines
This is where Spine diverges from general-purpose functional languages. A behavior block declares a state machine — states, transitions, guards, and message sends.
behavior ActorLifecycle where
state Spawned [initial]
state Ready
state Running
state Waiting
state Stopped [final]
state Crashed [final]
on Enqueue : Spawned → Ready
on Schedule : Ready → Running
on ProcessComplete : Running → Ready
guard (has_messages)
on ProcessComplete : Running → Waiting
guard (no_messages)
on Enqueue : Waiting → Ready
on Shutdown : Running → Stopped
on Shutdown : Ready → Stopped
on Shutdown : Waiting → Stopped
on Exception : Running → Crashed
This is the lifecycle of every actor in Spine's own runtime — the system models itself using its own syntax. Transitions can send messages to other state machines:
behavior Supervisor where
state Booting [initial]
state SpawningActors
state Running
state ShuttingDown
state Stopped [final]
on Start : Booting → SpawningActors
on AllSpawned : SpawningActors → Running
on ShutdownSignal : Running → ShuttingDown
send Registry.Shutdown
send Storage.Shutdown
send HTTP.Shutdown
send PG.Shutdown
on AllStopped : ShuttingDown → Stopped
The compiler translates behaviors into mCRL2 process algebra for model checking. Here is the actual generated output from a wafer processing example:
-- Source (groote_demo.model):
behavior WaferCycleSpec where
state Idle [initial]
state Loading
state Aligning
state Exposing
state Measuring
state Unloading
on load : Idle → Loading
on align : Loading → Aligning
on expose : Aligning → Exposing
on measure : Exposing → Measuring
on unload : Measuring → Unloading
on done : Unloading → Idle
% Generated (groote_demo.mcrl2):
sort State_WaferCycleSpec = struct
Idle | Loading | Aligning
| WCS_Exposing | WCS_Measuring | WCS_Unloading;
act align, begin_measurement, check_quality,
coarse_align, cycle_done, done, eject_wafer,
end_expose, expose, fine_align, load,
load_wafer, measure, unload, verify_align;
proc WaferCycleSpec(s: State_WaferCycleSpec) =
(s == Idle) -> load . WaferCycleSpec(Loading) +
(s == Loading) -> align . WaferCycleSpec(Aligning) +
(s == Aligning) -> expose . WaferCycleSpec(WCS_Exposing) +
(s == WCS_Exposing) -> measure . WaferCycleSpec(WCS_Measuring) +
(s == WCS_Measuring) -> unload . WaferCycleSpec(WCS_Unloading) +
(s == WCS_Unloading) -> done . WaferCycleSpec(Idle);
The mCRL2 output is not a pretty-printed version of the source. It is a process algebra specification that the mCRL2 toolset can model-check for deadlock freedom, liveness, and bisimulation equivalence.
5. Catching Bugs: Simulation Refinement
Suppose an engineer adds a shortcut — skip_alignment — that bypasses alignment and goes straight to exposure. The spec says alignment always precedes exposure. How does the compiler catch this?
The implementation declares the shortcut:
behavior WaferCycleImpl where
state WaitingForWafer [initial]
state PreAlignment
state FineAlignment
state AlignmentVerified
state PreExposure
state Exposing
...
on load_wafer : WaitingForWafer → PreAlignment
on coarse_align : PreAlignment → FineAlignment
send Aligner.StartAlignment
on fine_align : FineAlignment → AlignmentVerified
on skip_alignment : PreAlignment → PreExposure -- the shortcut
on start_expose : PreExposure → Exposing
send ExposureUnit.BeginExposure
...
You declare a refinement mapping between the implementation and the spec:
simulation WaferRefinement : WaferCycleImpl refines WaferCycleSpec where
map WaitingForWafer = Idle
map PreAlignment = Loading
map FineAlignment = Aligning
map AlignmentVerified = Aligning
map PreExposure = Aligning
map Exposing = Exposing
...
on load_wafer : matches load
on coarse_align : matches align
on fine_align : internal
on skip_alignment : internal
on start_expose : matches expose
...
The skip_alignment transition maps PreAlignment → PreExposure, which in spec-space maps Loading → Aligning. An internal transition (one with no spec counterpart) requires source and target to map to the same spec state. Loading ≠ Aligning. The simulation checker flags this as a refinement violation.
This check happens statically, through the generated mCRL2 model. The mCRL2 toolset verifies whether the implementation is a weak bisimulation of the spec. The shortcut breaks that relation.
6. Spatial Structure
Spine can model physical containment and topology using region blocks. This is relevant for systems where where something is affects what it can do.
region WaferStage where
sort equipment_sort
region Aligner where
sort equipment_sort
region ExposureUnit where
sort equipment_sort
region LithoCell where
contains WaferStage, Aligner, ExposureUnit, MeasurementUnit
links control : ControlBus, wafer_in : WaferPort, wafer_out : WaferPort
sort machine_sort
region CleanRoom where
contains LithoCell
links transport : TransportBus
sort facility_sort
Spatial rewrites describe how the topology can change:
react add_litho_cell where
redex CleanRoom
reactum CleanRoom, LithoCell
If a rewrite references a region that does not exist — say, CoolingUnit without a prior declaration — the compiler catches it. The spatial model compiles to BigraphER for topological verification.
7. Temporal Properties and Z3 Laws
Temporal claims and arithmetic laws live in the same source file as the rest of the system.
temporal StageStability where
always stage_locked_during_exposure
over WaferStage, ExposureUnit
temporal WaferCompletion where
eventually wafer_processed
over LithoCell
temporal LoadToMeasure where
wafer_loaded leads_to wafer_measured
over WaferStage, MeasurementUnit
Temporal properties are compiled to TLA+ for model checking. Arithmetic laws go to Z3:
-- Z3 proves this:
law wafer_throughput_positive :
∀ (n : Int). n > 0 → n * 1 > 0
-- Z3 refutes this:
law bad_throughput_claim :
∀ (x y : Int). x + y > x
The compiler dispatches each claim to the appropriate solver. You do not write Z3 assertions or TLA+ specs by hand. You write Spine. The compiler decides what goes where.
8. The Controlled Natural Language Surface
Everything above uses Spine's functional syntax — the fragment closest to Haskell or ML. Spine also has a controlled natural language (CNL) surface for domain modeling:
Person has exactly 1 LastName.
Engine is part of Car.
it is obligatory that each Asset has at most 1 owner.
it is forbidden that an Agent transfers an Asset it does not own.
TransferRequested leads to TransferApproved.
within jurisdiction NL, it is obligatory that asset transfers
above 10000 EUR require notarial approval [BW Art. 3:89].
This is not a separate language. The CNL surface is a fragment — a set of grammar rules that expand the parser's vocabulary. Internally, the sentence Person has exactly 1 LastName lowers to the same typed representation as a record field constraint. The deontic modalities (obligatory, forbidden, permitted) lower to temporal and constraint annotations that the compiler dispatches to the appropriate verifier.
The CNL exists because the people who know the domain rules — lawyers, engineers, compliance officers — do not write Haskell. The functional syntax and the CNL surface compile through the same pipeline to the same IR.
How the Pieces Fit Together
A single Spine source file can contain data declarations, functions, effects, behaviors, spatial structure, temporal properties, Z3 laws, and CNL facts. The compiler reads the file and does the following:
- Parse all fragments. The parser handles functional syntax, behavior blocks, region blocks, temporal declarations, and CNL sentences.
- Lower everything to a shared intermediate representation — a graph of primitive operations.
- Partition the graph. Constraint subgraphs go to Z3. Temporal and behavioral subgraphs go to TLA+ and mCRL2. Spatial subgraphs go to BigraphER. The computational subgraph goes to Zig.
- Emit artifacts for each backend. The Zig output compiles to a native binary. The verification artifacts are checked by their respective tools.
The compiler does not do all of this because it is clever. It does it because the IR makes partitioning a structural operation — different subgraphs, same graph.
What Exists Today
The compiler is self-hosting: 145 modules (lexer, parser, type checker, code generator, verification dispatch) written in Spine, compiled by the Spine compiler, producing Zig. The pipeline works for the functional fragment, behavior fragment, and constraint dispatch. The spatial and probabilistic backends are at an earlier stage.
Spine is not packaged for public use yet. This article and the article series document the design. The Discord is the best place to ask questions or see updates.