Finite State Machines
FSMs are first-class language constructs in Cure. They define state machines declaratively, compile to OTP gen_statem BEAM modules, and are verified at compile time for reachability and deadlock freedom.
Defining FSMs
Use fsm followed by a name. Each line in the body defines a transition: SourceState --event--> TargetState.
fsm TrafficLight
Red --timer--> Green
Green --timer--> Yellow
Yellow --timer--> Red
* --emergency--> Red
This defines a traffic light with three states (Red, Green, Yellow) and two events (timer, emergency).
Wildcard transitions
The * wildcard matches any source state. It creates a transition from every state to the target when that event is received:
* --emergency--> Red
This means: from Red, Green, or Yellow, receiving emergency transitions to Red. Useful for reset and panic transitions.
Initial state
The first non-wildcard source state in the definition becomes the initial state. In the traffic light example above, Red is the initial state because it appears first as a source.
fsm DoorLock
Locked --unlock--> Unlocked
Unlocked --lock--> Locked
Unlocked --open--> Open
Open --close--> Unlocked
# Initial state: Locked (first non-wildcard source)
Compilation to gen_statem
FSMs compile to standard OTP gen_statem BEAM modules. When you compile:
cure compile traffic_light.cure
The compiler generates a BEAM module named after the FSM with a Cure.FSM. prefix. For fsm TrafficLight, the module is :"Cure.FSM.TrafficLight". Each transition becomes a handle_event clause in the gen_statem callback module.
Generated API
Every compiled FSM module exports:
-
start_link/0– start the FSM process with the initial state -
start_link/1– start with options (e.g., registered name) -
send_event/2– send an event to an FSM process (asynchronous cast) -
get_state/1– get the current state (synchronous call)
Usage from Elixir:
{:ok, pid} = :"Cure.FSM.TrafficLight".start_link()
:"Cure.FSM.TrafficLight".send_event(pid, :timer)
{:ok, {:green, %{}}} = :"Cure.FSM.TrafficLight".get_state(pid)
Compile-time verification
The FSM compiler (Cure.FSM.Verifier) automatically checks three properties:
Reachability
Every state must be reachable from the initial state via BFS traversal of the transition graph. If a state is defined as a target but cannot be reached, the compiler emits a warning.
Deadlock freedom
Every non-terminal state must have at least one outgoing transition. A state with no outgoing transitions that was not declared terminal is flagged as a potential deadlock.
Terminal state validation
Declared terminal states must exist in the transition graph. The verifier checks that terminal states are actually reachable and that they correctly have no outgoing transitions.
Example that triggers a reachability warning:
fsm Broken
A --go--> B
C --go--> D
# Warning: states C, D are unreachable from initial state A
Guards on transitions
Transitions can have when guards that restrict when they fire. The guard expression is compiled to Erlang guard sequences on the handle_event clause:
fsm Counter
Counting --tick when count > 0--> Counting
Counting --tick when count == 0--> Done
The guard has access to the FSM’s state data. In this example, count is a field in the state data map. The transition from Counting to Counting only fires when count > 0; when count reaches 0, the FSM transitions to Done.
Actions on transitions
Transitions can include do blocks that mutate state data during the transition:
fsm Counter
Counting --tick when count > 0 do count = count - 1--> Counting
Counting --tick when count == 0--> Done
The do expression compiles to code in the handle_event clause body. The action has access to the current state data and returns modified data. In this example, each tick event decrements count by 1.
Full counter FSM example
A complete FSM with guards, actions, and multiple states:
fsm Counter
Counting --tick when count > 0 do count = count - 1--> Counting
Counting --tick when count == 0--> Done
* --reset--> Counting
This FSM:
-
Starts in
Countingstate -
On each
tick, decrementscountif it is positive -
Transitions to
Donewhencountreaches 0 -
Can be reset from any state back to
Countingvia theresetevent
Compiled gen_statem behavior: the start_link/1 function accepts initial data (e.g., %{count: 5}), and each transition clause pattern-matches on the event atom and applies guards/actions accordingly.
Runtime API via Std.Fsm
From Cure code, use the Std.Fsm stdlib module to interact with FSMs:
mod MyApp
fn run_light() -> Atom =
let pid = Std.Fsm.spawn(:"Cure.FSM.TrafficLight")
Std.Fsm.send(pid, :timer)
let state = Std.Fsm.state(pid)
Std.Fsm.stop(pid)
state
Available functions in Std.Fsm:
-
spawn(module)– start an FSM process, returns the pid -
send(pid, event)– send an event to the FSM -
state(pid)– get the current state -
history(pid)– get the event history -
lookup(name)– look up a named FSM in the registry -
stop(pid)– stop the FSM process
From Elixir, the equivalent is Cure.FSM.Runtime:
# Spawn with a registered name
{:ok, pid} = Cure.FSM.Runtime.spawn_fsm(:"Cure.FSM.TrafficLight", name: "light1")
# Send events
Cure.FSM.Runtime.send_event(pid, :timer)
# Get state
{:ok, {:green, %{}}} = Cure.FSM.Runtime.get_state(pid)
# Batch events
Cure.FSM.Runtime.send_batch(pid, [:timer, :timer, :timer])
# Event history
Cure.FSM.Runtime.event_history(pid)
# Registry lookup by name
{:ok, pid} = Cure.FSM.Runtime.lookup("light1")
# Stop
Cure.FSM.Runtime.stop_fsm(pid)
Health check API
The FSM Runtime provides a health check endpoint for monitoring:
{:ok, health} = Cure.FSM.Runtime.health_check(pid)
# Returns:
# %{
# alive: true,
# state: :green,
# event_count: 42,
# uptime_ms: 15000,
# last_event: :timer
# }
This is useful for supervision, dashboards, and operational monitoring of long-running FSM processes.
Type safety analysis
The compiler performs static analysis on FSM definitions to catch common errors:
Duplicate transitions
Two transitions with the same source state and event (without guards to disambiguate) produce a warning:
fsm Bad
A --go--> B
A --go--> C
# Warning: duplicate transition from A on event 'go'
With guards, the same source/event pair is allowed because the guards disambiguate:
fsm Ok
A --go when x > 0--> B
A --go when x <= 0--> C
# No warning: guards make the transitions distinct
Wildcard shadows
If a wildcard transition and an explicit transition handle the same event, the explicit transition takes precedence. The compiler warns when a wildcard completely shadows an unreachable explicit transition:
fsm Shadowed
A --go--> B
* --go--> C
# The wildcard creates transitions from all states on 'go',
# but A --go--> B takes precedence for state A.
# Warning if B --go--> C is never reachable due to shadowing.
Self-loops
A transition from a state to itself is valid but flagged as informational when combined with no action (it has no observable effect):
fsm Loop
A --noop--> A
# Info: self-loop on state A with event 'noop' (no action)
With an action, self-loops are meaningful and produce no diagnostic:
fsm Counter
Counting --tick do count = count + 1--> Counting
# No warning: self-loop has an action
Complete example
A door lock FSM with guards, actions, and multiple events:
fsm DoorLock
Locked --enter_code when code == secret do attempts = 0--> Unlocked
Locked --enter_code when code != secret do attempts = attempts + 1--> Locked
Locked --enter_code when attempts >= 3--> Blocked
Unlocked --lock--> Locked
Unlocked --open--> Open
Open --close--> Unlocked
Blocked --admin_reset--> Locked
* --emergency_open--> Open
This FSM:
-
Starts
Locked - Accepts a code; if correct, unlocks and resets attempts; if wrong, increments attempts
- Blocks after 3 failed attempts
-
Can be admin-reset from
Blocked - Has an emergency override from any state
- Compile-time verified: all states reachable, no deadlocks, terminal states valid