| Crates.io | stateless |
| lib.rs | stateless |
| version | 0.1.0 |
| created_at | 2025-10-31 21:05:52.76815+00 |
| updated_at | 2025-10-31 21:05:52.76815+00 |
| description | A lightweight, zero-cost state machine library that separates structure from behavior. Guards and actions live in wrapper code, not the DSL. |
| homepage | https://github.com/matthewjberger/stateless |
| repository | https://github.com/matthewjberger/stateless |
| max_upload_size | |
| id | 1910764 |
| size | 44,443 |
A lightweight, zero-cost state machine library for Rust that separates structure from behavior.
This library separates state machine structure from behavior. The DSL defines valid state transitions, while your wrapper code handles guards, actions, and business logic in idiomatic Rust.
Result, methods, and proper error handlingno_std compatible[dependencies]
stateless = "0.1.0"
use stateless::statemachine;
statemachine! {
transitions: {
*Idle + Start = Running,
Running + Pause | Stop = Idle,
Idle | Running + Connect = Connected,
Connected + Disconnect = Idle,
Connected + Tick = _,
_ + Reset = Idle,
}
}
struct Machine {
state: State,
battery: u32,
ticks: u32,
}
impl Machine {
fn new() -> Self {
Self {
state: State::default(),
battery: 100,
ticks: 0,
}
}
fn start(&mut self) {
let Some(new_state) = self.state.process_event(Event::Start) else {
return;
};
if self.battery < 20 {
return;
}
self.battery -= 10;
self.state = new_state;
}
fn tick(&mut self) {
let Some(new_state) = self.state.process_event(Event::Tick) else {
return;
};
self.ticks += 1;
self.state = new_state;
}
fn reset(&mut self) {
if let Some(new_state) = self.state.process_event(Event::Reset) {
self.battery = 100;
self.ticks = 0;
self.state = new_state;
}
}
}
Guards and actions live in your wrapper code, not the DSL.
state.process_event(event) returns Option<State> - the new state if the transition is valid. You check guards, perform actions, then apply the state:
fn connect(&mut self, id: u32) {
// Check if transition is valid for current state
let Some(new_state) = self.state.process_event(Event::Connect) else {
return;
};
// Guard: check preconditions
if id > self.max_connections {
return;
}
// Guard: check resources
if self.battery < 5 {
return;
}
// Actions: side effects
self.connection_id = id;
self.battery -= 5;
// Apply transition
self.state = new_state;
}
This approach gives you:
Multiple states can share transitions:
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:
statemachine! {
transitions: {
*Idle + Start = Running,
_ + Reset = Idle, // From any state
}
}
Stay in the same state while performing side effects:
statemachine! {
transitions: {
Moving + Tick = _, // Stays in Moving
}
}
impl Robot {
fn tick(&mut self) {
let Some(new_state) = self.state.process_event(Event::Tick) else {
return;
};
self.movement_ticks += 1; // Side effect without changing state
self.state = new_state;
}
}
Internal transitions are useful for periodic updates, counters, or logging while remaining in the current state.
statemachine! {
derive_states: [Debug, Clone, PartialEq, Eq, Hash],
derive_events: [Debug, Clone, PartialEq],
transitions: {
*Idle + Start = Running,
}
}
Use namespacing for multiple state machines:
statemachine! {
name: Player,
transitions: {
*Idle + Move = Walking,
}
}
statemachine! {
name: Enemy,
transitions: {
*Patrol + Spot = Chasing,
}
}
// Generates: PlayerState, PlayerEvent with PlayerState::process_event()
// Generates: EnemyState, EnemyEvent with EnemyState::process_event()
statemachine! {
// Optional: namespace for multiple state machines
name: MyMachine,
// Optional: custom derives for State enum
derive_states: [Debug, Clone, PartialEq],
// Optional: custom derives for Event enum
derive_events: [Debug, Clone, PartialEq],
// Required: transition definitions
transitions: {
// Basic transition (initial state marked with *)
*Idle + Start = Running,
// State patterns (multiple source states)
Ready | Waiting + Start = Active,
// Event patterns (multiple trigger events)
Active + Stop | Pause = Idle,
// Wildcard (from any state)
_ + Reset = Idle,
// Internal transition (stay in same state)
Active + Tick = _,
}
}
The macro generates:
// State enum
pub enum State {
Idle,
Running,
}
impl Default for State {
fn default() -> Self {
State::Idle // First state marked with *
}
}
// Event enum
pub enum Event {
Start,
Stop,
}
// Transition method on State
impl State {
pub fn process_event(&self, event: Event) -> Option<State> {
// Returns Some(new_state) if transition is valid
// Returns None if no valid transition
}
}
The process_event method returns Option<State>:
Some(new_state): Transition is validNone: No valid transition for current state + eventYou control when to apply the transition:
impl Machine {
fn try_transition(&mut self, event: Event) {
let Some(new_state) = self.state.process_event(event) else {
println!("Invalid transition");
return;
};
// Guards and actions here
self.state = new_state;
}
}
The macro validates your state machine at compile time.
statemachine! {
transitions: {
*A + Event = B,
A + Event = C, // ERROR: duplicate transition
}
}
Error message:
error: duplicate transition: state 'A' + event 'Event' 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
if checks with early returnsmatches!() macro for efficient pattern matchingQ: How do I write guards and actions?
A: Call state.process_event(event) to get the new state if valid. Check your guards, perform actions, then apply the state. This gives you full control, zero coupling, and clean code. See the Guards and Actions section.
Q: Can I use this in no_std environments?
A: Yes! The library uses #![no_std] and only requires alloc for compilation (not at runtime).
Q: How do I handle conditional transitions?
A: state.process_event(event) returns Option<State>. Get the new state, check your guards with early returns, perform actions, then assign self.state = new_state. All guards must pass before the state changes.
See the examples directory for complete working examples:
demo.rs: Comprehensive robot control demonstrating all DSL features including guards, actions, state patterns, internal transitions, and wildcard transitionshierarchical.rs: Hierarchical state machines using composition (player movement + weapon states)Run examples with:
cargo run -r --example demo
cargo run -r --example hierarchical
This project is licensed under the MIT License. See the MIT.md file for details.