stateless

Crates.iostateless
lib.rsstateless
version0.1.0
created_at2025-10-31 21:05:52.76815+00
updated_at2025-10-31 21:05:52.76815+00
descriptionA lightweight, zero-cost state machine library that separates structure from behavior. Guards and actions live in wrapper code, not the DSL.
homepagehttps://github.com/matthewjberger/stateless
repositoryhttps://github.com/matthewjberger/stateless
max_upload_size
id1910764
size44,443
Matthew J. Berger (matthewjberger)

documentation

README

stateless

A lightweight, zero-cost state machine library for Rust that separates structure from behavior.

Crates.io Documentation License: MIT

Philosophy

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.

Why Use This?

  • Zero coupling: State machine knows nothing about your types
  • Idiomatic Rust: Use Result, methods, and proper error handling
  • Zero cost: Compiles to efficient sequential checks
  • Type safe: Leverages Rust's type system fully
  • No dependencies: no_std compatible
  • Clear code: Business logic lives in one place, not scattered

Installation

[dependencies]
stateless = "0.1.0"

Quick Start

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;
        }
    }
}

Features

Guards and Actions

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:

  • Full control over when to apply transitions
  • Multiple guards with early returns
  • Actions only happen if all guards pass
  • Zero coupling between state machine structure and business logic
  • Clean, idiomatic Rust

State Patterns

Multiple states can share transitions:

statemachine! {
    transitions: {
        *Ready | Waiting + Start = Active,
        Active + Stop = Ready,
    }
}

Event Patterns

Multiple events can trigger the same transition:

statemachine! {
    transitions: {
        *Active + Pause | Stop = Idle,
    }
}

Wildcard Transitions

Transition from any state:

statemachine! {
    transitions: {
        *Idle + Start = Running,
        _ + Reset = Idle,  // From any state
    }
}

Internal Transitions

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.

Custom Derives

statemachine! {
    derive_states: [Debug, Clone, PartialEq, Eq, Hash],
    derive_events: [Debug, Clone, PartialEq],
    transitions: {
        *Idle + Start = Running,
    }
}

Multiple State Machines

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()

DSL Syntax

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 = _,
    }
}

Generated Code

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
    }
}

Error Handling

The process_event method returns Option<State>:

  • Some(new_state): Transition is valid
  • None: No valid transition for current state + event

You 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;
    }
}

Compile Time Validation

The macro validates your state machine at compile time.

Duplicate Transitions

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

Performance

  • Zero cost: Compiles to sequential if checks with early returns
  • No allocations: All operations are stack based
  • Optimal codegen: Uses matches!() macro for efficient pattern matching
  • No runtime overhead: All validation happens at compile time

FAQ

Q: 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.

Examples

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 transitions
  • hierarchical.rs: Hierarchical state machines using composition (player movement + weapon states)

Run examples with:

cargo run -r --example demo
cargo run -r --example hierarchical

License

This project is licensed under the MIT License. See the MIT.md file for details.

Commit count: 0

cargo fmt