A zero-cost state machine macro that separates structure from behavior.
cargo add stateless
Most state machine libraries couple behavior to the state machine itself. Guards, actions, and context structs all get tangled into the DSL. This makes the state machine hard to test, hard to refactor, and impossible to compose.
stateless takes the opposite approach: the macro is a pure transition table. It generates two enums and a lookup function. Guards, side effects, and error handling live in your own code, using normal Rust patterns. The state machine doesn't know your types exist, and your types don't depend on any framework trait.
use stateless::statemachine;
statemachine! {
transitions: {
*Idle + Start = Running,
Running + Stop = Idle,
_ + Reset = Idle,
}
}
let mut state = State::default(); // Idle (marked with *)
assert_eq!(state, State::Idle);
if let Some(new_state) = state.process_event(Event::Start) {
state = new_state;
}
assert_eq!(state, State::Running);process_event returns Some(new_state) if the transition is valid, None if not. This lets you insert guards and actions between checking validity and applying the transition.
Given this DSL:
statemachine! {
transitions: {
*Idle + Start = Running,
Running + Stop = Idle,
}
}The macro generates:
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum State {
Idle,
Running,
}
impl Default for State {
fn default() -> Self {
State::Idle
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Event {
Start,
Stop,
}
impl State {
pub fn process_event(&self, event: Event) -> Option<State> {
if matches!(*self, State::Idle) && matches!(event, Event::Start) {
return Some(State::Running);
}
if matches!(*self, State::Running) && matches!(event, Event::Stop) {
return Some(State::Idle);
}
None
}
}Guards and actions live in your wrapper code, not the DSL. Call process_event to check validity, verify your guards, perform side effects, then apply the state:
fn connect(&mut self, id: u32) {
let Some(new_state) = self.state.process_event(Event::Connect) else {
return;
};
if id > self.max_connections {
return;
}
if self.battery < 5 {
return;
}
self.connection_id = id;
self.battery -= 5;
self.state = new_state;
}Mark the initial state with *. This state is used for the generated Default implementation:
statemachine! {
transitions: {
*Idle + Start = Running, // Idle is the initial state
Running + Stop = Idle,
}
}
let state = State::default(); // State::IdleMultiple source states can share a transition:
statemachine! {
transitions: {
*Ready | Waiting + Start = Active,
Active + Stop = Ready,
}
}Multiple events can trigger the same transition:
statemachine! {
transitions: {
*Active + Pause | Stop = Idle,
}
}Transition from any state. Specific transitions always take priority over wildcards, regardless of declaration order:
statemachine! {
transitions: {
*Idle + Start = Running,
_ + Reset = Idle,
}
}Stay in the current state while performing side effects:
statemachine! {
transitions: {
*Moving + Tick = _,
Moving + Arrive = Idle,
}
}
impl Robot {
fn tick(&mut self) {
let Some(new_state) = self.state.process_event(Event::Tick) else {
return;
};
self.movement_ticks += 1;
self.state = new_state;
}
}Default derives are Debug, Copy, Clone, PartialEq, Eq. Override with derive_states and derive_events:
statemachine! {
derive_states: [Debug, Clone, PartialEq, Eq, Hash],
derive_events: [Debug, Clone, PartialEq],
transitions: {
*Idle + Start = Running,
}
}Use name for namespacing when you need multiple state machines in the same scope:
statemachine! {
name: Player,
transitions: {
*Idle + Move = Walking,
}
}
statemachine! {
name: Enemy,
transitions: {
*Patrol + Spot = Chasing,
}
}
// Generates: PlayerState, PlayerEvent, EnemyState, EnemyEventThe macro validates your state machine at compile time:
- Duplicate transitions: same state + event pair defined twice
- Multiple initial states: more than one state marked with
* - Empty transitions: no transitions defined
- Duplicate wildcards: same event used in multiple wildcard transitions
statemachine! {
transitions: {
*A + Go = B,
A + Go = C, // ERROR: duplicate transition
}
}error: duplicate transition: state 'A' + event 'Go' is already defined
help: each combination of source state and event can only appear once
note: if you need conditional behavior, use different events or handle logic in your wrapper
statemachine! {
name: MyMachine, // Optional: generates MyMachineState, MyMachineEvent
derive_states: [Debug, Clone, PartialEq], // Optional: custom derives for State enum
derive_events: [Debug, Clone, PartialEq], // Optional: custom derives for Event enum
transitions: {
*Idle + Start = Running, // Initial state marked with *
Ready | Waiting + Start = Active, // State patterns (multiple source states)
Active + Stop | Pause = Idle, // Event patterns (multiple trigger events)
_ + Reset = Idle, // Wildcard (from any state, lowest priority)
Active + Tick = _, // Internal transition (stay in same state)
}
}Q: Can I use this in no_std environments?
A: Yes. The generated code uses only core types (Option, Default) and requires no allocator at runtime.
See the examples directory for complete working examples:
demo.rs: Robot control demonstrating guards, actions, state patterns, internal transitions, and wildcardshierarchical.rs: Hierarchical state machines using composition (player movement + weapon states)
cargo run -r --example demo
cargo run -r --example hierarchicalThis project is licensed under the MIT License. See the MIT.md file for details.