View Source Cure FSM API Design
Last Updated: November 22, 2025
This document describes the design of the Finite State Machine (FSM) API in Cure.
Overview
FSMs in Cure are first-class language constructs that compile to BEAM gen_statem processes. They provide:
- Declarative state machine definitions using arrow-based transition syntax
- Record-based payload types for carrying data through transitions
- Simple event-driven transitions between states
- Process-based isolation with BEAM message-passing semantics
- Named FSM instances for easy reference via atoms
FSM Definition Syntax
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
endKey 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
fsmdeclaration
Core Types
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)API Functions
start_fsm(module: Atom): Pid
Spawns a new FSM process using the FSM definition from the given module.
- Initial state: First state in the transition list
- Initial payload: Values specified in the
fsmdeclaration - Returns: Process ID (Pid) of the FSM instance
Example:
# 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): Any
Sends an event asynchronously to an FSM instance (fire-and-forget).
- target: Either a Pid or a registered FsmName (atom)
- event: A Pair containing
(event_name, event_data)where event_data is a list - Returns: Cast result (typically
ok)
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): Any
Registers 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): Any
Queries the current state of an FSM instance.
- Returns: Current state atom (e.g.,
: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): Any
Spawns an FSM instance with a specific type and initial data.
- fsm_type: The FSM type atom (matches the payload record name)
- initial_data: Initial payload data (usually a record instance)
- Returns: Process ID (Pid) of the spawned FSM
Example:
let initial_data = TrafficPayload{cycles_completed: 0, timer_events: 0, emergency_stops: 0}
let fsm_pid = fsm_spawn(:TrafficPayload, initial_data)Additional API Functions
fsm_stop(pid: Any): Any
Gracefully stops an FSM instance.
Example:
fsm_stop(fsm_pid)
fsm_info(pid: Any): Any
Get detailed information about an FSM instance including state, data, and event history.
Example:
let info = fsm_info(fsm_pid)
fsm_is_alive(pid: Any): Any
Check 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)Implementation Notes
Process-Based FSM Runtime
- Each FSM instance runs as a separate BEAM process
- Events are sent as messages to the FSM process
- The process maintains the current state and payload
- Transition handlers are called within the FSM process context
FSM Process Message Protocol
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
Compiler Responsibilities
The Cure compiler should:
- Parse
fsmblocks and extract transition table - Generate FSM definition structure with initial state/payload
- Create transition map linking
{state, event}to handler functions - Generate process spawning code that initializes FSM state
- Implement message loop for handling events and state queries
Complete Example: Traffic Light
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
endThis 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/2andfsm_cast/2 - Querying state with
fsm_state/1 - Self-transitions (emergency event from Red stays in Red)
Future Enhancements
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