Last Updated: November 22, 2025
This document describes the design of the Finite State Machine (FSM) API in Cure.
FSMs in Cure are first-class language constructs that compile to BEAM gen_statem processes. They provide:
Cure FSMs use a simple, declarative syntax:
module MyModule do
# Define a payload record to track FSM data
record MyPayload do
field1: Type1
field2: Type2
end
# Define FSM with initial payload values
fsm MyPayload{field1: initial_value1, field2: initial_value2} do
State1 --> |event1| State2
State1 --> |event2| State3
State2 --> |event3| State1
end
end
Key Points:
- The first state in the transition list is the initial state
- States are capitalized atoms (:State1, :State2, etc.)
- Events are lowercase atoms enclosed in |event| syntax
- Each FSM must have an associated record type for the payload
- The record fields are initialized in the fsm declaration
From the Std.Fsm module:
type FsmName = Atom # Registered FSM name (e.g., :traffic_light)
type FsmError = Atom # Error types (:invalid_state, :not_found, etc.)
type EventName = Atom # Event identifier (e.g., :timer, :coin)
type StateName = Atom # State identifier (e.g., :Red, :Green)
start_fsm(module: Atom): PidSpawns a new FSM process using the FSM definition from the given module.
fsm declarationExample:
# This would start the FSM defined in the TrafficLightFSM module
let fsm_pid = start_fsm(:TrafficLightFSM)
Note: This is a legacy function. The recommended approach is fsm_spawn/2.
fsm_cast(target: Any, event: Any): AnySends an event asynchronously to an FSM instance (fire-and-forget).
(event_name, event_data) where event_data is a listok)Example:
import Std.Pair [pair/2]
# Send event with empty data
let empty_list = []
let event = pair(:coin, empty_list)
fsm_cast(fsm_pid, event)
# Or to a named FSM
let event2 = pair(:push, [])
fsm_cast(:my_turnstile, event2)
fsm_advertise(pid: Any, name: Atom): AnyRegisters a name for an FSM process, allowing it to be referenced by name instead of Pid.
Example:
let adv_result = fsm_advertise(fsm_pid, :main_turnstile)
# Now you can use :main_turnstile instead of fsm_pid
fsm_state(target: Any): AnyQueries the current state of an FSM instance.
:Red, :Locked)Example:
let current_state = fsm_state(:traffic_light)
# Returns atom like :Red, :Green, or :Yellow
fsm_spawn(fsm_type: Atom, initial_data: Any): AnySpawns an FSM instance with a specific type and initial data.
Example:
let initial_data = TrafficPayload{cycles_completed: 0, timer_events: 0, emergency_stops: 0}
let fsm_pid = fsm_spawn(:TrafficPayload, initial_data)
fsm_stop(pid: Any): AnyGracefully stops an FSM instance.
Example:
fsm_stop(fsm_pid)
fsm_info(pid: Any): AnyGet detailed information about an FSM instance including state, data, and event history.
Example:
let info = fsm_info(fsm_pid)
fsm_is_alive(pid: Any): AnyCheck if an FSM process is still alive.
Returns: Boolean-like value indicating if the FSM is running
Example:
let alive = fsm_is_alive(fsm_pid)
The FSM process should handle:
- {:event, Event}: Trigger a state transition
- {:get_state, Pid}: Query current state (reply to sender)
- {:register, FsmName}: Register a name for this FSM
The Cure compiler should:
1. Parse fsm blocks and extract transition table
2. Generate FSM definition structure with initial state/payload
3. Create transition map linking {state, event} to handler functions
4. Generate process spawning code that initializes FSM state
5. Implement message loop for handling events and state queries
Here's a complete working example from examples/06_fsm_traffic_light.cure:
module TrafficLightFSM do
export [main/0]
import Std.Fsm [fsm_spawn/2, fsm_cast/2, fsm_advertise/2, fsm_state/1]
import Std.Io [println/1]
import Std.Pair [pair/2]
# Payload record - tracks traffic light statistics
record TrafficPayload do
cycles_completed: Int
timer_events: Int
emergency_stops: Int
end
# Define the TrafficLight FSM
# Initial state is Red (first state in transitions)
fsm TrafficPayload{cycles_completed: 0, timer_events: 0, emergency_stops: 0} do
Red --> |timer| Green
Red --> |emergency| Red
Green --> |timer| Yellow
Green --> |emergency| Red
Yellow --> |timer| Red
Yellow --> |emergency| Red
end
# Main demonstration
def main(): Int =
# Initialize FSM with starting data
let initial_data = TrafficPayload{cycles_completed: 0, timer_events: 0, emergency_stops: 0}
let fsm_pid = fsm_spawn(:TrafficPayload, initial_data)
# Give the FSM a friendly name
let adv_result = fsm_advertise(fsm_pid, :traffic_light)
# Check initial state (should be Red)
let state0 = fsm_state(:traffic_light)
# Send timer event: Red -> Green
let event1 = pair(:timer, [])
let cast1 = fsm_cast(:traffic_light, event1)
let state1 = fsm_state(:traffic_light) # Now Green
# Send timer event: Green -> Yellow
let event2 = pair(:timer, [])
let cast2 = fsm_cast(:traffic_light, event2)
let state2 = fsm_state(:traffic_light) # Now Yellow
# Send emergency event: Yellow -> Red
let event3 = pair(:emergency, [])
let cast3 = fsm_cast(:traffic_light, event3)
let state3 = fsm_state(:traffic_light) # Back to Red
0
end
This example demonstrates:
- Defining a payload record with fields
- Creating an FSM with initial payload values
- Spawning the FSM with fsm_spawn/2
- Naming the FSM with fsm_advertise/2
- Sending events using pair/2 and fsm_cast/2
- Querying state with fsm_state/1
- Self-transitions (emergency event from Red stays in Red)
Potential additions to the FSM API:
- fsm_call/2: Synchronous event sending with reply
- Timeout support: State --> |timeout(1000)| NextState
- State entry/exit actions with explicit handler functions
- Guards on transitions: State --> |event| when condition State2
- FSM supervision trees integration
- FSM serialization/deserialization for persistence
- Pattern matching on event data in transitions