A flexible and reliable statechart library for Rust, implementing hierarchical state machines with support for parallel regions, history states, guards, and actions.
- 🎯 Hierarchical States: Organize states in a tree structure with parent-child relationships
- 🔀 Parallel Regions: Run multiple orthogonal state machines concurrently
- 📜 History States: Remember and restore previous state configurations (shallow and deep)
- 🛡️ Guards: Conditional transitions based on context
- ⚡ Actions: Execute code on state entry, exit, and during transitions
- 🏗️ Builder API: Fluent, type-safe API for constructing state machines at runtime
- 🎨 Derive Macros: Declarative state machine definitions using Rust attributes
- 🔍 Validation: Comprehensive validation of state machine configurations
- 📊 Visualization: Export state machines to DOT/Graphviz format
- 🧪 Well-Tested: Extensive unit and integration tests
- 📚 Well-Documented: Comprehensive API documentation and guides
Add this to your Cargo.toml:
[dependencies]
statechart = "0.1"use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum State {
Locked,
Unlocked,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Event {
Coin,
Push,
}
#[derive(Debug)]
struct Context {
coins: u32,
passages: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build the state machine
let definition = StateMachineBuilder::<State, Event, Context>::new()
.initial_state(State::Locked)
.add_simple_state(State::Locked)
.add_simple_state(State::Unlocked)
.add_transition(
State::Locked,
State::Unlocked,
Event::Coin,
None,
vec![Box::new(|ctx| {
ctx.coins += 1;
println!("Coin accepted. Total: {}", ctx.coins);
Ok(())
})],
)
.add_transition(
State::Unlocked,
State::Locked,
Event::Push,
None,
vec![Box::new(|ctx| {
ctx.passages += 1;
println!("Person passed. Total: {}", ctx.passages);
Ok(())
})],
)
.build()?;
// Create and run the state machine
let context = Context { coins: 0, passages: 0 };
let mut machine = StateMachine::new(definition, context)?;
// Process events
machine.process_event(Event::new(Event::Coin))?;
assert_eq!(machine.current_state(), &State::Unlocked);
machine.process_event(Event::new(Event::Push))?;
assert_eq!(machine.current_state(), &State::Locked);
Ok(())
}use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum PlayerState {
Stopped,
Playing,
PlayingNormal,
PlayingFastForward,
Paused,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum PlayerEvent {
Play,
Pause,
Stop,
FastForward,
Normal,
}
// Create composite "Playing" state
let playing_state = StateBuilder::<PlayerState, PlayerEvent, ()>::new(PlayerState::Playing)
.composite()
.initial(PlayerState::PlayingNormal)
.build();
let definition = StateMachineBuilder::<PlayerState, PlayerEvent, ()>::new()
.initial_state(PlayerState::Stopped)
.add_simple_state(PlayerState::Stopped)
.add_state(playing_state)
.add_simple_state(PlayerState::PlayingNormal)
.add_simple_state(PlayerState::PlayingFastForward)
.add_simple_state(PlayerState::Paused)
// Top-level transitions
.add_transition(PlayerState::Stopped, PlayerState::Playing, PlayerEvent::Play, None, vec![])
.add_transition(PlayerState::Playing, PlayerState::Stopped, PlayerEvent::Stop, None, vec![])
.add_transition(PlayerState::Playing, PlayerState::Paused, PlayerEvent::Pause, None, vec![])
.add_transition(PlayerState::Paused, PlayerState::Playing, PlayerEvent::Play, None, vec![])
// Transitions within Playing state
.add_transition(PlayerState::PlayingNormal, PlayerState::PlayingFastForward, PlayerEvent::FastForward, None, vec![])
.add_transition(PlayerState::PlayingFastForward, PlayerState::PlayingNormal, PlayerEvent::Normal, None, vec![])
.build()?;For more examples and detailed tutorials, see the User Guide.
States represent the different conditions or modes your system can be in. States can be:
- Simple: Leaf states with no substates
- Composite: States containing substates (hierarchical)
- Parallel: States with multiple orthogonal regions
- Initial: Entry point pseudo-state
- Final: Terminal state
- History: Pseudo-states that remember previous configurations
Transitions define how the system moves from one state to another in response to events. Each transition can have:
- Source State: Where the transition starts
- Target State: Where the transition ends
- Event: What triggers the transition
- Guard: Optional condition that must be true
- Actions: Code executed during the transition
Events are signals that trigger transitions. They can carry optional payload data.
Guards are boolean conditions that determine whether a transition can occur. They have access to the state machine's context.
Actions are side effects executed at specific points:
- Entry Actions: Run when entering a state
- Exit Actions: Run when leaving a state
- Transition Actions: Run during a transition
The context is shared state accessible to all guards and actions. It represents the data your state machine operates on.
Run multiple state machines concurrently within a single parent state. See the User Guide for detailed examples.
Remember and restore previous state configurations. See the User Guide for detailed examples.
Handle events without changing state or triggering entry/exit actions using transition kinds.
Export your state machine to DOT format for visualization:
use statechart::visualization::to_dot;
let dot = to_dot(&definition);
std::fs::write("machine.dot", dot)?;
// Then: dot -Tpng machine.dot -o machine.png- User Guide - Comprehensive tutorials and examples
- API Reference - Complete API documentation
- Architecture Overview - Design and architecture details
- API Specification - Formal API specification
- Implementation Plan - Development roadmap
See the examples/ directory for complete working examples:
traffic_light.rs- Simple traffic light with basic transitionsdoor_controller.rs- Door with guards and safety featuresmedia_player.rs- Media player using derive macros
Run an example:
cargo run --example traffic_light
cargo run --example door_controller
cargo run --example media_player --features macrosSee the examples README for detailed descriptions and learning path.
Run the test suite:
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_basic_transition
# Run benchmarks
cargo benchThe library is designed for performance:
- O(1) state and transition lookups using hash maps
- Minimal allocations during transitions
- Lazy evaluation of guards
- Optional arena allocation for state storage
See benches/ for detailed benchmarks.
statechart/
├── src/
│ ├── lib.rs # Public API exports
│ ├── model/ # Core data structures
│ ├── builder/ # Builder pattern API
│ ├── runtime/ # State machine execution
│ ├── engine/ # Event processing
│ ├── hierarchy/ # Hierarchical state support
│ ├── parallel/ # Parallel region support
│ ├── history/ # History state support
│ ├── visualization/ # DOT export
│ └── error.rs # Error types
├── statechart-macros/ # Procedural macros
├── examples/ # Example applications
├── tests/ # Integration tests
├── benches/ # Performance benchmarks
└── docs/ # Documentation
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project is licensed under Apache License, Version 2.0.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.
This library is inspired by:
- Statecharts.dev - Comprehensive statechart documentation
- XState - JavaScript state machine library
- Harel's Statecharts - Original statechart paper
- Core state machine implementation
- Hierarchical states
- Parallel regions
- History states
- Builder API
- Derive macros
- Async support
- WebAssembly support
- Visual editor integration
- State machine composition
- Hot reloading