A hierarchical state machine framework for Rust, built around a runtime-constructed state tree.
The tree describes topology. Behaviors describe reactions. The runner is a pure function. None of the three owns the others.
A compile-time state machine is a program. A runtime state tree is data.
Declarative state machine libraries — whether type-based like statig or DSL-based like banish — fix topology at compile time. When topology is data, it can be loaded from a file, assembled from user input, or shared across a thousand instances without copying. The cost is that errors surface at runtime rather than compile time, and performance is strictly worse than a compile-time solution. If a declarative library solves your problem, use that instead.
Runner holds only a reference to the tree. The caller owns the current state and behaviors.
One tree and one runner can drive any number of independent instances simultaneously — each with its own State and its
own Behaviors. No coordination required.
Dispatch is event-driven and follows Run-To-Completion semantics. Each event is fully processed before the next can be dispatched. The entire exit and enter sequence completes before dispatch returns.
dispatch is not re-entrant. Calling dispatch from within on_event, on_exit, or on_enter is undefined behavior. This is a usage contract, not a type-level guarantee.
Behavior<E> returns an EventReply: do nothing, bubble to parent, or transition to a target state. It does not
execute the transition. The runner does.
A behavior can be tested without a runner and written without knowing the tree's shape.
© 2026 tarnishablec — Licensed under the Mozilla Public License 2.0