statig

CI

Hierarchical state machines for designing event-driven systems.

Features


statig in action

```rust

[derive(Default)]

pub struct Blinky { led: bool, }

pub struct Event;

impl StateMachine for Blinky { type State = State;

type Superstate<'a> = Superstate;

type Event = Event;

type Context = Self;

const INIT_STATE: State = State::off();

}

[state_machine]

impl Blinky { #[state] fn on(&mut self, event: &Event) -> Response { self.led = false; Transition(State::off()) }

#[state]
fn off(&mut self, event: &Event) -> Response<State> {
    self.led = true;
    Transition(State::on())
}

}

fn main() { let mut statemachine = Blinky::default().statemachine().init();

state_machine.handle(&Event);

} ```

(See the macro/basic example for the full code with comments. Or see no_macro/basic for a version without using macro's).


Concepts

States

States are defined by writing methods inside the impl block and adding the #[state] attribute to them. By default the event argument will map to the event handled by the state machine.

```rust

[state]

fn on(event: &Event) -> Response { Transition(State::off()) } ```

Every state must return a Response. A Response can be one of three things:

Superstates

Superstates allow you to create a hierarchy of states. States can defer an event to their superstate by returning the Super response.

```rust

[state(superstate = "playing")]

fn on(event: &Event) -> Response { match event { Event::TimerElapsed => Transition(State::off()), Event::ButtonPressed => Super } }

[superstate]

fn playing(event: &Event) -> Response { match event { Event::ButtonPressed => Transition(State::paused()), _ => Handled } } ```

Superstates can themselves also have superstates.

Actions

Actions run when entering or leaving states during a transition.

```rust

[state(entryaction = "enteron", exitaction = "exiton")]

fn on(event: &Event) -> Response { Transition(State::off()) }

[action]

fn enter_on() { println!("Entered on"); }

[action]

fn exit_on() { println!("Exited on"); } ```

Context

If the type on which your state machine is implemented has any fields, you can access them inside all states, superstates or actions.

```rust

[state]

fn on(&mut self, event: &Event) -> Response { self.led = false; Transition(State::off()) } ```

Or alternatively, set led inside the entry action.

```rust

[action]

fn enter_off(&mut self) { self.led = false; } ```

State-local storage

Sometimes you have data that only exists in a certain state. Instead of adding this data to the context and potentially having to unwrap an Option<T>, you can add it as an input to your state handler.

```rust

[state]

fn on(counter: &mut u32, event: &Event) -> Response { match event { Event::TimerElapsed => { *counter -= 1; if *counter == 0 { Transition(State::off()) } else { Handled } } Event::ButtonPressed => Transition(State::on(10)) } } ```

counter is only available in the on state but can also be accessed in its superstates and actions.

FAQ

What is this #[state_machine] proc-macro doing to my code? 🤨

Short answer: nothing. #[state_machine] simply parses the underlying impl block and derives some code based on its content and adds it to your source file. Your code will still be there, unchanged. In fact #[state_machine] could have been a derive macro, but at the moment Rust only allows derive macros to be used on enums and structs. If you'd like to see what the generated code looks like take a look at the test with and without macros.

How is this different from the typestate pattern?

How do I design event-driven systems?

Does this support {insert uml feature}?

Credits

The idea for this library came from reading the book Practical UML Statecharts in C/C++. I highly recommend it if you want to learn how to use state machines to design complex systems.