Skip to main content

Crate sml

Crate sml 

Source
Expand description

§sml.rs

A no_std state-machine library whose primary sml! procedural macro mirrors the sml.cpp transition-table DSL.

§sml! transition-table DSL

The primary macro deliberately mirrors an sml.cpp transition table:

use sml::sml;

pub struct E1;
pub struct E2 { pub value: u32 }

sml! {
    Example {
        *"idle"_s + event<E1> / start = "running"_s,
         "done"_s <= "running"_s + event<E2> [valid] / (capture, audit),
         "done"_s / finish = X,
    }
}

The machine name replaces the C++ transition-table type. It prefixes generated items: ExampleStateMachine, ExampleStateMachineContext, ExampleStates, ExampleEvents, and ExampleError.

§Mechanical C++ to Rust mapping

sml.cpp table expressionsml.rs macro expression
*"idle"_s + event<E> = "run"_sidentical
"run"_s <= *"idle"_s + event<E>identical
"idle"_s + event<E> [guard] / action = Xidentical
/ (first, second)identical for named Rust callbacks
"idle"_s + event<E> / actionidentical internal transition
"idle"_s + event<E> = "idle"_sidentical external self-transition
*"initial"_s / action = "ready"_sidentical anonymous transition
"name"_eidentical named-event trigger
state + on_entry<_> / actionidentical
state + on_exit<_> / actionidentical
state + sml::on_entry<_> / actionaccepted qualified spelling
... = sml::Xaccepted qualified terminal spelling
state + unexpected_event<E> / actionidentical
state + unexpected_event<_> / actionidentical wildcard form
origin-aware completionstate + completion<E> / action = target
/ deferidentical bounded deferred-event action
/ process(E {})identical processed-event action

Rust callback bodies are implemented on the generated context trait rather than written as C++ lambdas inside the table. External event<E> values are ordinary Rust types and can be passed directly to process_event.

§Grammar

sml! {
    MachineName {
        transition (, transition)* [,]
    }
}

transition := [*] state + trigger [guard] [/ action] [= state]
            | state <= [*] state + trigger [guard] [/ action]
            | [*] state [/ action] = state

state   := "state name"_s | RustIdentifier | X
trigger := event<RustType>
         | "named event"_e
         | on_entry<_> | on_exit<_>
         | unexpected_event<RustType> | unexpected_event<_>
         | completion<RustType> | completion<_>
         | exception<RustError> | exception<_>
guard   := [name] | [!name] | [(a && !b) || c]
action  := name | (first, second, ...)
         | (..., eval [guard] / action, ...)

A leading * selects the initial state. Omitting the target makes a true internal transition: no exit or entry callbacks run. An explicit target equal to the source is an external self-transition, so exit and entry callbacks do run. X is the terminal state.

Multiple leading * states define orthogonal regions exactly as in sml.cpp:

use sml::sml;

pub struct E1;
pub struct E2;
pub struct E3;

sml! {
    Regions {
        *"idle"_s  + event<E1> = "s1"_s,
         "s1"_s   + event<E2> = X,
        *"idle2"_s + event<E2> = "s2"_s,
         "s2"_s   + event<E3> = X,
    }
}

Each event is borrowed and broadcast to every active region, so E2 can move both regions during one call. Orthogonal machines expose states(), state(region), is(&[...]), and is_region(region, &state), and are terminated only when every region is X. Specific and wildcard unexpected handlers are resolved independently per region. Anonymous rows stabilize every region during initialize() and again after each handled broadcast. Prefixing any orthogonal guard or action with async generates an async broadcast and stabilization path and awaits callbacks in every region.

State strings are converted to PascalCase generated variants; for example, "fin wait 1"_s becomes States::FinWait1. Named events are converted the same way. Generated event enum variants remain useful for named events, while typed event<E> transitions generate From<E> and support direct dispatch.

In a flat table, state<T> creates a States::T(T) payload variant. Initial typed states and actionless typed targets use T::default(). When a transition action constructs the target, its final action returns T; this is the ownership-safe Rust counterpart to sml.cpp injecting a mutable destination state object. Use new_with_state_data(context, value) to override an inferred initial value.

§Context callbacks

Implement the generated MachineStateMachineContext trait. Guards borrow the event and return Result<bool, Error>; actions borrow external events and return Result<(), Error>. If a transition constructs a data-bearing output state, the final action returns that state’s data. Earlier actions in a sequence return ().

impl ExampleStateMachineContext for Context {
    fn valid(&self, event: &E2) -> Result<bool, ()> {
        Ok(event.value != 0)
    }

    fn capture(&mut self, event: &E2) -> Result<(), ()> {
        self.value = event.value;
        Ok(())
    }

    fn audit(&mut self, _event: &E2) -> Result<(), ()> {
        Ok(())
    }
}

Call initialize() once after construction to run initial entry behavior and anonymous-transition stabilization. Normal event processing automatically stabilizes subsequent anonymous and completion transitions.

state() borrows the generated state enum, while is(&States::Idle) performs the payload-insensitive state identity check corresponding to sm.is("idle"_s) in C++.

Prefix a callback with async in a guard or action position to generate an async machine, for example [async ready] / async send. Rust futures provide the coroutine behavior directly.

Action sequences accept eval [guard] / action in any position. The nested action runs only when its guard expression passes, while surrounding actions retain their original order. Both the eval guard and action may be async.

For flat machines, state + exception<_> / recover = target handles a guard or action that returned Err. A typed exception<MyError> additionally infers MyError as the machine callback-error type and injects &MyError into the handler action. The original GuardFailed/ActionFailed is intercepted and the exception transition becomes the event result. Sync and async callbacks are supported. A machine currently uses one concrete Rust error type; use an enum when several error variants need typed routing.

The reserved / defer action stores the current event in a generated, allocation-free queue and retries it after the next state change. The reserved / process(Event {}) action dispatches the supplied event after the current transition has installed its target state. Queue actions work in sync/async flat, orthogonal, and composite tables through allocation-free iterative dispatch. Deferring an owned structural payload requires that payload to implement Clone.

§Composite machines

Place the child and parent tables adjacently in one sml! invocation and use the same state<Sub> spelling as C++:

use sml::sml;
pub struct Enter;
pub struct ChildEvent;
pub struct Leave;

sml! {
    Sub {
        *"idle"_s + event<ChildEvent> = X,
    }

    Parent {
        *"idle"_s + event<Enter> = state<Sub>,
         state<Sub> + event<Leave> = X,
    }
}

The generated parent owns both state values and one unified context. While state<Sub> is active, events route to the child first and bubble to the parent only when the child has no matching transition. Leaving and re-entering the child resets it to its initial state by default. Mark the child’s initial state as *"idle"_s(H) to retain shallow history across re-entry, exactly as in sml.cpp. child_state(), is_child(...), and child_is_active() expose typed composite queries. Parent and child lifecycle rows and unexpected handlers participate in the same child-first ordering. Sync or async guards/actions are supported. A parent anonymous transition from state<Sub> becomes eligible only after the child reaches X, matching composite completion semantics.

The public macro surface is sml!.

Modules§

utility
Allocation-free utilities for runtime dispatch and groups of state machines.

Macros§

sml
Defines a state machine using sml.cpp-shaped transition-table syntax.

Traits§

Machine
Common synchronous interface implemented by generated state machines that do not require a temporary context.
Terminated
Reports whether a state machine has reached its terminal state.