Purpose: This document specifies the
lit-bitlibrary, including its core concepts, macro grammar, public API, and intended behavior. It serves as the source of truth for Phase 0 and beyond. The library is licensed under MIT OR Apache-2.0. Last Updated: 2025-07-27
- Introduction 1.1. Goals 1.2. Non-Goals 1.3. Core Concepts
- Macro Grammar (
statechart!) - Public API
3.1.
StateMachineTrait 3.2. State & Event Enums 3.3. Context Data - Behavior & Semantics 4.1. State Transitions 4.2. Entry/Exit Actions 4.3. Guards 4.4. Hierarchy (Nested States) 4.5. Parallel States 4.6. Delayed Transitions / Timers 4.7. Invoked Services / Child Statecharts 4.8. History States (TBD)
- Error Handling 5.1. Compile-Time Errors 5.2. Runtime Errors/Panics
- Feature Flags
6.1.
std6.2.async6.3.diagram - Actor Model (Phase 4 Target)
- Future Considerations (Post v0.1)
- Design Insights & Mitigation Strategies (2025-05 Research Audit)
Brief overview of the library, its purpose, and the problems it aims to solve. Inspired by XState but tailored for Rust's strengths (type safety, performance), focusing on #![no_std] compatibility by default and providing an optional actor model wrapper for concurrency.
- Ergonomic, declarative statechart definition via a procedural macro.
- Type-safe states, events, and transitions.
#![no_std]compatibility by default for embedded systems.- Minimal binary footprint.
- High performance for event processing.
- Support for Harel statecharts (hierarchy, parallel regions, history (TBD)).
- Optional actor model integration (
Mailbox,Actortrait). - Clear compile-time error messages for invalid chart definitions.
- Automated diagram generation (DOT / Mermaid) behind
diagramfeature (Phase 8).
- Full XState compatibility (some features may be Rust-idiomatic or deferred).
- Visual editor or GUI tooling (focus on library core).
- Automatic interpretation of SCXML.
- Distributed statecharts.
- Statechart: A specification of system behavior.
- State: A condition in which a system can be.
- Atomic State: A state with no substates.
- Compound State: A state with substates (child states).
- Parallel State: A compound state whose child states are active concurrently.
- Final State: A state that indicates the completion of its parent state's behavior. (TBD post-v0.1)
- Event: An occurrence that can trigger a state transition.
- Transition: A change from one state to another, triggered by an event.
- Action: An executable piece of code performed upon state entry, exit, or during a transition. Can be a reference to a method on the context (e.g.,
.my_action_method). - Guard (Condition): A boolean predicate that must be true for a transition to occur. Can be a reference to a method on the context (e.g.,
.my_guard_method). - Context: Data storage associated with the statechart instance.
- Delayed Transition (Timer): A transition that occurs after a specified duration if the state remains active.
- Invoked Service / Child Statechart: A statechart can invoke or spawn other services or child statecharts, managing their lifecycle and communication.
This section defines the EBNF grammar for the statechart! macro. This grammar specifies the syntax for defining state machines, including states, events, transitions, actions, guards, and other features.
statechart ::= 'statechart!' '{'
// Header fields defining the overall machine
'name:' IDENT ','
'initial:' state_ref ','
'context:' TYPE ','
// State definitions (can be nested)
state_definition+
'}'
state_definition ::= 'state' state_ref state_attributes? '{'
state_body_item*
'}'
state_ref ::= IDENT // Reference to a state name (e.g., 'Idle', 'Processing')
state_attributes ::= '[' attribute (',' attribute)* ']'
attribute ::= 'parallel' // State contains parallel regions.
// | 'history' ('shallow' | 'deep')? // Future attribute
// | 'final' // Future attribute
state_body_item ::= state_definition // Nested state
| 'initial:' state_ref ';' // Required for compound/parallel states
| 'entry' '=>' action_ref ';' // Action on entering this state
| 'exit' '=>' action_ref ';' // Action on exiting this state
| 'on' event_ref transition_guard? '=>' transition_target transition_action? ';' // Event transition
| 'after' DURATION '=>' transition_target transition_action? ';' // Delayed transition
| 'invoke' invocation_details ';' // Invoke child machine/service
event_ref ::= IDENT // Reference to an event name (e.g., 'Submit', 'Cancel')
// A transition_guard requires the 'guard' keyword to disambiguate from a potential action.
transition_guard ::= '[' 'guard' condition_ref ']'
condition_ref ::= '.' IDENT // Method on context returning bool
transition_target::= state_ref // Target state for the transition
// | 'none' // Explicit internal transition (stay in state) - Deferred post-v0.1
transition_action::= '[' 'action' action_ref ']'
action_ref ::= '.' IDENT // Method on context
DURATION ::= NUMBER ('ms' | 's' | 'm' | 'h') // e.g., 5s, 500ms
invocation_details ::= /* Syntax for invoking children, see Phase 7 on roadmap */
// Example sketch: 'child' child_name ':' child_statechart_ref ('{' mapping* '}')?
IDENT ::= /* A valid Rust identifier */
TYPE ::= /* A valid Rust type path */
NUMBER ::= /* A Rust integer literal */
// Note: Actions and guards reference methods defined on the `Context` struct.
// The method signature is inferred (e.g., guards take `&Context` and return `bool`, actions take `&mut Context`).Define the core trait that all generated statecharts will implement. This trait provides the fundamental methods for interacting with a state machine instance.
pub trait StateMachine {
/// The type representing the states of this state machine, typically an enum.
/// Must be comparable, cloneable, and debuggable.
type State: Copy + Clone + PartialEq + core::fmt::Debug;
/// The type representing the events that can be sent to this state machine, typically an enum.
/// Must be comparable, cloneable, and debuggable.
type Event: Copy + Clone + PartialEq + core::fmt::Debug;
/// The type for the context data associated with this state machine.
type Context;
/// Sends an event to the state machine, potentially causing state transitions and actions.
///
/// # Arguments
/// * `event`: The event to process (passed by reference).
///
/// # Returns
/// * `SendResult::Transitioned` if the event resulted in one or more transitions (including self-transitions).
/// * `SendResult::NoMatch` if the event was ignored (e.g., no matching transition for the current state, or a guard condition prevented the transition).
/// * `SendResult::Error` if an error occurred during processing.
/// _Note: This method is typically used directly in bare-metal or synchronous contexts. When using the Actor Model, events are usually sent via the mailbox (`try_send`/`send().await`)._
fn send(&mut self, event: &Self::Event) -> SendResult;
/// Returns the current active state of the state machine.
fn state(&self) -> Self::State;
/// Returns an immutable reference to the state machine's context data.
fn context(&self) -> &Self::Context;
/// Returns a mutable reference to the state machine's context data.
/// This allows actions or other external logic to modify the context.
fn context_mut(&mut self) -> &mut Self::Context;
// Note: For ergonomic state checking, the `statechart!` macro will typically generate
// a `matches(&self, state: Self::State) -> bool` method on the concrete state machine struct.
// Example: `if my_machine.matches(MyState::Active) { /* ... */ }`
// This helper is not part of the core `StateMachine` trait itself to keep the trait minimal,
/// but is a convention for generated code.
///
/// # Thread Safety
/// The generated state machine instance is typically `Send` but **not** `Sync`,
/// as internal state transitions require mutable access. Access via the Actor Model
/// ensures safe concurrent access.
}The macro will generate enums for states and events based on the definition.
Example:
// From a definition like: states: { Green, Yellow, Red }, events: { Timer, PowerOutage }
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum TrafficLightState {
Green,
Yellow,
Red,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum TrafficLightEvent {
Timer,
PowerOutage,
}To enable robust pattern matching on events (including variants with payloads) in the statechart! macro, event enums must be annotated with the #[statechart_event] attribute:
use lit_bit_macro::statechart_event;
#[derive(Debug, Clone, PartialEq)]
#[statechart_event] // Required for pattern matching support
pub enum MediaPlayerEvent {
Play,
Stop,
LoadTrack { path: String<64> }, // Variants with payloads are fully supported
VolumeUp,
VolumeDown,
}
// Event enums support both Copy and non-Copy types
#[derive(Debug, Clone, PartialEq)]
#[statechart_event]
pub enum DataEvent {
Simple, // Copy variant
WithString(String), // Non-Copy owned data
WithVec(Vec<u8>), // Non-Copy owned data
WithCustomStruct { data: MyData }, // Non-Copy struct with owned fields
}Event Type Compatibility:
The library supports comprehensive event type compatibility:
- Copy types: Simple enums, primitive data types, small structs that implement
Copy - Non-Copy types: Enums with owned data like
String,Vec<T>, custom structs with owned fields - Mixed variants: Event enums can contain both Copy and non-Copy variants in the same enum
Why is this attribute required?
Rust's macro system operates on tokens and cannot introspect type information from external modules or crates. The #[statechart_event] attribute enables the macro to:
- Generate metadata about enum variants and their payload types at compile time
- Support pattern matching in transition definitions (e.g.,
on LoadTrack { path: _ } => ...) - Enable exhaustive matching for compile-time verification of handled events
- Work with external enums defined in other modules or crates
- Handle both Copy and non-Copy event types uniformly through reference-based matching
This approach aligns with established Rust patterns used by popular crates like:
- Strum: Requires
#[derive(EnumIter)]for variant iteration - Serde: Requires
#[derive(Serialize, Deserialize)]for serialization - enum_dispatch: Requires attributes for trait delegation
The single attribute annotation provides maximum functionality with minimal user burden, enabling features that would be impossible without it on stable Rust.
External Event Enums:
If your event enum is defined in an external crate that you cannot modify, you have several options:
- Request upstream support: Ask the crate maintainer to add
#[statechart_event] - Create a wrapper enum: Define a local enum that wraps the external one
- Use newtype pattern: Wrap the external enum in a newtype struct
See the examples directory for demonstrations of these patterns.
How context data is defined in the macro and accessed/modified.
The Context stores the quantitative or persistent data associated with the state machine instance. Actions and guards operate on this data.
- Definition: The type of the context data structure is specified in the
statechart!macro header using thecontext: TYPE,field, whereTYPEis a path to a Rust struct or tuple defined elsewhere.- Example:
statechart! { name: TrafficLight, initial: Green, context: TrafficLightContext, ... }
- Example:
- Data Structure: The specified
TYPEmust be a concrete Rust struct or tuple. It typically contains fields relevant to the state machine's operation (e.g., counters, flags, user data, configuration).// Example context struct defined outside the macro pub struct TrafficLightContext { pub cycles: u32, pub emergency_mode: bool, }
- Initialization: An instance of the context struct must be provided when creating the state machine instance. The macro-generated constructor will typically take this initial context as an argument.
- Access:
- Guards: Guard methods defined on the context struct receive immutable access (
&Context) via theselfparameter. - Actions: Entry, exit, and transition action methods defined on the context struct receive mutable access (
&mut Context) via theselfparameter, allowing them to modify the data. (Performance Note: In embedded contexts, consider marking guard/action methods with#[inline]or#[inline(always)]if appropriate).
- Guards: Guard methods defined on the context struct receive immutable access (
- Ownership: The state machine instance owns the
Contextdata. External access is provided via thecontext()andcontext_mut()methods defined in theStateMachinetrait. - Serialization (Optional): If the
diagramor other future features requiring serialization are enabled, theContexttype may need to derive or implementserde::Serializeandserde::Deserialize. This is not required for core functionality.
Detailed explanation of how the statechart operates.
Event dispatch order, transition selection, internal vs. external transitions.
When an event is sent to the state machine via send(event), the following process occurs to determine and execute transitions:
- Event Matching: The machine checks if the current active state configuration has any transitions defined for the received
event. This check includes transitions defined on the current state(s) and any parent states (up to the root). - Guard Evaluation: If a matching transition has a
guardcondition ([.guard .my_guard_method]), the referenced method on theContextis called (with&Contextaccess). If the guard returnsfalse, the transition is blocked, and the machine continues searching for other potential transitions for the same event (e.g., on parent states). If the guard returnstrue, the transition is selected. If there's no guard, it's implicitlytrue. - Transition Selection:
- Priority: Transitions defined on deeper (child) states take priority over transitions defined on ancestor (parent) states for the same event.
- First Match: If multiple transitions are defined on the same state for the same event (e.g., with different guards), the first one defined in the macro whose guard evaluates to
trueis chosen. - No Match: If no matching, unblocked transition is found in the current state or its ancestors, the event is considered unhandled, and the machine remains in its current state configuration.
send()returnsSendResult::NoMatch.
- Transition Execution: If a transition is selected,
send()will returnSendResult::Transitioned, and the following steps execute in order:- Exit Actions: Execute exit actions (
exit => .action) for all states being exited, starting from the deepest child state and moving upwards towards the least common ancestor (LCA) state of the source and target states. - Transition Actions: Execute the action associated with the transition itself (
=> TARGET_STATE [action .action]), if defined. This action receives&mut Context. - Enter Actions: Execute entry actions (
entry => .action) for all states being entered, starting from the state just below the LCA and moving downwards to the target state. If the target state is compound, itsinitial:state is entered recursively, triggering its entry actions as well.
- Exit Actions: Execute exit actions (
- Internal vs. External Transitions:
- External (Default): A transition from state
Ato stateB(whereA != B) causesAto be exited andBto be entered, triggering relevant exit/entry actions. A self-transition (on EVENT => A) also causes exit and entry actions forA. - Internal (Implicit): If a transition is handled by a parent state without specifying a target state change for the child, only the parent's transition action (if any) runs. The child state remains active, and its exit/entry actions are not executed. (Explicit syntax for internal transitions, like
target: none, might be added later if needed for clarity). - Parallel State Override Example: If a parallel state
Phas regionsR1andR2, and an eventEarrives: IfPdefineson E => TargetState;andR1(currently active) also defineson E => R1_Target;, the transition defined onPtakes precedence.R1andR2(and their children) will be exited,Pwill be exited, andTargetStatewill be entered. The transition inR1is ignored because the event was handled by the parentP.
- External (Default): A transition from state
Order of execution, parameters, error handling.
Entry and exit actions allow the state machine to perform side effects when states are entered or exited. They are defined using the entry => .action_name and exit => .action_name syntax within a state definition.
- Purpose:
- Entry Actions: Typically used for setup tasks specific to the state being entered (e.g., starting timers, initializing state-local data, sending commands).
- Exit Actions: Typically used for cleanup tasks specific to the state being exited (e.g., stopping timers, clearing state-local data, finalizing operations).
- Execution Order: As detailed in 4.1 State Transitions, during a transition:
- Exit actions of the source state (and its children, if compound) are executed first, from deepest child upwards.
- Entry actions of the target state (and its children, if compound and entering its initial state) are executed last, from the highest parent being entered downwards to the final target state(s).
- Context Access: Both entry and exit action methods defined on the
Contextstruct receive mutable access (&mut Context) allowing them to modify the machine's context data. - Idempotency: Actions should ideally be designed to be idempotent, especially if error recovery or complex scenarios might lead to re-entry or repeated exits, although the core execution model guarantees execution only once per standard transition.
- Error Handling: Actions are expected to complete successfully. If an action needs to signal a failure, it should typically do so by modifying the context or potentially enqueueing a failure event for the state machine to process in a subsequent step. Direct panicking within actions is discouraged, especially in
no_stdenvironments or release builds. (#![forbid(panic)]might be enforced in some profiles).
Evaluation timing, access to context/event data.
Guards (or conditions) determine whether a potential transition, triggered by an event, should actually be taken. They are defined using the [.guard .condition_name] syntax attached to an on EVENT transition.
- Purpose: To add conditional logic to transitions based on the current state of the
Contextor potentially properties of the triggeringEvent(if the guard method signature accepts it, TBD). Guards allow multiple transitions for the same event from the same state, each leading to a different target state or action based on specific conditions. - Evaluation Timing: Guards are evaluated after an event matches a transition defined on a state (or its ancestors) but before any exit actions, transition actions, or entry actions are executed. See 4.1 State Transitions Step 2.
- Context Access: Guard methods defined on the
Contextstruct receive immutable access (&Context) because they should be side-effect free; their sole purpose is to returntrueorfalse. They must not modify the context. - Return Value: A guard method must return
bool. If it returnstrue, the transition is allowed to proceed (assuming no higher-priority transition was also triggered). If it returnsfalse, the transition is blocked, and the event processing might continue searching for other valid transitions (e.g., on parent states). - Absence of Guard: If a transition definition does not include a
[.guard ...], it is considered to have a guard that always returnstrue. - Multiple Guards: If multiple transitions are defined on the same state for the same event but with different guards, they are evaluated in the order they appear in the macro definition. The first one whose guard returns
trueis selected.
Event bubbling, initial states of compound states, parent/child relationships.
Statecharts can organize states hierarchically, creating parent-child relationships. A state containing other state definitions is called a compound state.
- Definition: A compound state is defined by nesting
state ... {}definitions within anotherstate ... {}block. - Initial State: Every compound state must declare an initial substate using the
initial: SUBSTATE_NAME;syntax within its definition block. When the state machine transitions into a compound state, it automatically enters this declared initial substate (triggering the initial substate's entry actions, if any). - Event Bubbling: When an event occurs, if the currently active child state does not define a transition for that event (or its guards block it), the event "bubbles up" to its parent compound state. The parent state is then checked for transitions matching the event. This bubbling continues up the hierarchy until a state handles the event or the root of the statechart is reached.
- Transition Priority: As mentioned in 4.1 State Transitions, transitions defined on child states have higher priority than transitions defined on parent states for the same event. The event is first checked against the innermost active state, and only bubbles up if unhandled.
- Entering/Exiting Compound States:
- Entering: When transitioning into a compound state
Ptargeting its initial substateC, entry actions execute fromPdownwards toC(and further down ifCis also compound). See 4.1 Transition Execution. - Exiting: When transitioning out of a substate
Cwithin a compound statePto a state outside ofP, exit actions execute fromCupwards toP. See 4.1 Transition Execution. - Transitions within Compound State: If a transition occurs between two substates
C1andC2both directly within the same compound parentP, onlyC1's exit actions andC2's entry actions (and the transition action) are executed.P's exit/entry actions are not executed because the machine remains withinP.
- Entering: When transitioning into a compound state
Region activation/deactivation, event processing in parallel regions. Defined using the [parallel] attribute on a state.
Parallel states allow a state machine to be in multiple orthogonal (independent) child states simultaneously. This is useful for modeling components that operate concurrently.
- Definition: A state is declared as parallel by adding the
[parallel]attribute to its definition:state MyParallelState [parallel] { ... }. - Regions: A parallel state must contain two or more direct child state definitions (effectively, regions). These regions are active concurrently. If a state is marked
[parallel], it cannot be an atomic state; it must define these regions. Unlike compound states which have only one active child state at a time, a parallel state has all of its direct child regions active concurrently. Each region is itself a standard state (atomic or compound) with its own initial state (if compound), transitions, etc.- Example:
state Parent [parallel] { initial: // Not applicable for parallel state itself state RegionA { initial: A1; state A1 {} state A2 {} } state RegionB { initial: B1; state B1 {} state B2 {} } }
- Example:
- Entering a Parallel State: When a transition targets a parallel state
P:- The entry action of
P(if any) is executed. - Then, all direct child regions (
RegionA,RegionB, etc.) are entered simultaneously. This means theinitial:state for each region is entered, triggering their respective entry actions according to hierarchy (e.g.,RegionAentry, thenA1entry;RegionBentry, thenB1entry). The exact order of execution between orthogonal regions' entry actions is generally not guaranteed and should not be relied upon.
- The entry action of
- Exiting a Parallel State: When a transition leads out of the parallel state
P:- Exit actions for the active states within all regions are executed first (from deepest child upwards within each region).
- Then, the exit action of
Pitself (if any) is executed. The exact order of execution between orthogonal regions' exit actions is generally not guaranteed.
- Event Processing: When the state machine is in a parallel state
Pand receives an event:- The event is dispatched to all active child regions concurrently.
- Each region attempts to handle the event based on its current state and transitions (including bubbling within that region).
- It's possible for multiple regions to react to the same event independently. All resulting transitions and actions within those regions will occur as part of processing the single incoming event.
- If the parallel state
Pitself defines a transition for the event, that transition takes priority over transitions defined within the regions (unless the event is handled entirely within a region without bubbling up toP). A transition defined onPwill cause all regions to be exited.
- Completion (Implicit): A parallel state implicitly reaches a "completed" status only when all of its orthogonal regions have independently reached a final state (Final states are TBD for v0.1, but this is the standard semantic). Transitions out of the parallel state can be conditioned on this completion, or triggered explicitly by events defined on the parallel state itself.
Transitions triggered by the passage of time. Defined using the after DURATION => TARGET_STATE [action .optional_action]; syntax within a state body. When a state with an after transition is entered, an internal timer is started. If the state is exited before the timer fires, the timer is cancelled. If the timer fires, the specified transition occurs.
Delayed transitions allow a state machine to automatically transition to another state after a specified duration has elapsed, provided it remains in the source state for that duration. This is defined using the after DURATION => TARGET_STATE [action .optional_action]; syntax within a state body.
- Definition: A delayed transition is specified within a state definition using the
afterkeyword, followed by a duration (e.g.,500ms,2s), the target state, and an optional transition action.- Example:
state Waiting { after 5s => TimedOut [action .handle_timeout]; ... }
- Example:
- Timer Activation: When the state machine enters a state that defines one or more
aftertransitions, internal timers corresponding to each defined delay are started. - Timer Cancellation: If the state machine transitions out of the state before a delayed transition's timer fires, that specific timer is automatically cancelled. This ensures the delayed transition only occurs if the machine remains in the source state for the full duration.
- Timer Firing: If a timer associated with an
aftertransition fires (i.e., the specified duration elapses while still in the source state), the state machine executes the corresponding transition:- The source state is exited (triggering exit actions).
- The transition action (if specified in the
afterdefinition) is executed. - The target state is entered (triggering entry actions).
- Multiple Delays: A state can define multiple
aftertransitions with different durations and targets. Each will start its timer upon state entry. The first timer to fire will trigger its transition, cancelling any other pending timers defined within the same source state. - Interaction with Events: Delayed transitions behave like internal events generated by the timer mechanism. If an external event triggers a transition out of the state before the timer fires, the external event takes precedence, and the timer is cancelled. If the timer fires, its transition is processed like any other event-triggered transition.
- Implementation: The underlying timer mechanism may depend on the enabled features.
- With
stdandasyncfeatures, this might integrate withtokio::time. - In
no_stdenvironments, a simpler tick-based approach or integration with platform-specific timers might be required. This typically involves the user providing timer services (perhaps via aTickProvidertrait) or periodically calling atick()method on the state machine or associated timer management struct. The precision and maximum duration may vary based on the implementation.
- With
A state can invoke other services or child statecharts. This is defined using the invoke child SERVICE_NAME -> statechart!(...); syntax (actual invocation mechanism TBD). The parent statechart can send events to and receive events from the invoked child. The lifecycle of the child (start, stop) is typically tied to the parent state's entry and exit.
(Phase 7 Target)
Statecharts can invoke other long-running services or spawn child statecharts, managing their lifecycle and potentially communicating with them. This feature allows for composing complex systems from smaller, reusable state machine components.
- Definition: Invocation is declared within a state using the
invoke ...;syntax. The exact syntax for specifying the invoked service/child and communication mapping is To Be Defined in Phase 7. A potential sketch isinvoke child ChildMachineName: ChildMachineType { /* optional event mapping */ };. - Lifecycle:
- Activation: When the parent state machine enters a state containing an
invokedeclaration, the specified child service or statechart instance is started/spawned. - Termination: When the parent state machine exits the state containing the
invokedeclaration, the invoked child service or statechart instance is automatically stopped/terminated.
- Activation: When the parent state machine enters a state containing an
- Communication (TBD): Mechanisms will be defined to allow:
- The parent machine to send events to the invoked child.
- The invoked child to send events back to the parent machine (potentially causing transitions in the parent).
- Sharing or mapping context data between parent and child.
- Use Cases: Useful for managing background tasks, interacting with external systems (represented as statecharts), or breaking down very large statecharts into more manageable, composable units.
- Implementation Details: The exact implementation will depend heavily on the
asyncandstdfeatures, likely involving task spawning and message passing channels when available.no_stdsupport might be limited or require specific external integration points. - Error Handling (Release Builds /
stdfeature):- When the
stdfeature is enabled, operations that can potentially fail at runtime (e.g., interacting with invoked services, timer management if using fallible system calls) should ideally returnResult<T, E>where appropriate. The exact error types are TBD. The coresendmethod itself is designed to returnbool(indicating if a transition occurred) and notResult, as failure to transition due to guards or lack of matching events is considered normal operation, not an error. - Failures within invoked children might result in specific events being sent back to the parent machine.
- When the
no_stdEnvironments: Inno_stdenvironments without thestdfeature, the emphasis is heavily on compile-time validation. Runtime operations are designed to be infallible where possible. If unavoidable runtime failures can occur (e.g., timer allocation failure in a hypotheticalno_stdtimer service), the behavior might involve specific error states, context flags, or defined fallback transitions rather than returningResult. Panics in releaseno_stdbuilds must be avoided entirely; the generated code should strive to be compatible with#![forbid(panic)]in release mode.
Shallow vs. deep history, default transitions.
(TBD for v0.1)
History states allow a state machine to remember and automatically re-enter the last active substate(s) of a compound or parallel state when it is transitioned back into.
- Concept: When transitioning out of a compound state that has a history mechanism, the machine recorded which substate(s) were active. If a later transition targets the compound state's history state marker, instead of entering the compound state's
initial:substate, it directly enters the previously recorded substate(s). - Types:
- Shallow History: Remembers and restores only the direct active child state of the compound state. If that child was itself compound, its own initial state is entered upon restoration.
- Deep History: Remembers and restores the full active state configuration within the compound state, down to the innermost nested atomic states.
- Syntax: Specific syntax (e.g., a
historyattribute or pseudo-state likestate H*) is To Be Defined. - Use Cases: Useful for implementing features like interruption and resumption, where returning to a parent state should resume the specific work-in-progress that was interrupted (e.g., restoring the specific tab or sub-menu a user was in).
- v0.1 Status: History states are not targeted for v0.1. This section serves as a placeholder for future specification if the feature is prioritized later.
List of errors the macro should detect (e.g., unknown state, duplicate transition, unreachable region). Reference statechart.mdc.
The statechart! macro should perform extensive validation of the statechart definition at compile time, providing clear error messages to guide the user. Errors detected at compile time prevent the generation of incorrect or unsound state machine code.
Key compile-time errors include (but are not limited to):
- Syntax Errors:
- Malformed macro input that does not conform to the EBNF grammar (e.g., missing commas, incorrect keywords, unbalanced braces).
- Header Field Errors:
- Missing mandatory header fields (
name,initial,context). - Duplicate header fields.
- Invalid type for
context(e.g., not a valid Rust type path). initial:state not defined in the statechart.
- Missing mandatory header fields (
- State Definition Errors:
- Duplicate state names (at the same hierarchical level).
initial:substate in a compound state not defined within that compound state.- Missing
initial:substate declaration in a compound state. - Missing
initial:substate declaration in any direct child region of a[parallel]state (thoughinitialfor the parallel state itself is not applicable). - Invalid state attributes (e.g.,
[foo]). Unknown attribute on a state.
- Transition Errors:
- Transition target state not defined in the statechart.
- Event name in
on EVENTnot defined (if a global event enum is inferred or required, TBD). Currently, event names are identifiers. - Duplicate identical transitions (same event, same source, same target, same guard if present) on the same state.
- Action method referenced in
[action .my_action]not found on theContextstruct, or has an incompatible signature. - Guard method referenced in
[.guard .my_guard]not found on theContextstruct, or has an incompatible signature (e.g., does not returnbool, takes&mut Context).
- Hierarchy and Parallelism Errors:
- Invalid
[parallel]nesting (e.g., a direct child region of a[parallel]state cannot itself be[parallel]without an intermediate compound state; see rule instatechart.mdc). - A parallel state must have at least two child regions.
- Invalid
- Timer Errors:
- Invalid
DURATIONformat inafter DURATION .... - Target state for an
aftertransition not defined.
- Invalid
- Unreachable States/Regions (Potentially):
- The macro may attempt to detect states or regions that can never be entered due to the transition logic. This can be complex and might be a best-effort feature or deferred.
- Resolver Errors:
- Failure to resolve Rust type paths or identifiers correctly.
The error messages should, where possible, point to the specific location in the macro input that caused the error.
When (if ever) the runtime component might panic. Prefer Result types in std builds.
The generated state machine code aims to be robust and panic-free in release builds, especially for no_std targets.
- Panics (Debug Builds Only):
- In debug builds (
debug_assertionsenabled), the runtime may panic under exceptional circumstances that indicate a fundamental logic error or violation of internal invariants (e.g., attempting to enter an invalid state representation, encountering corrupted internal data). These panics serve as early detection for bugs during development.
- In debug builds (
- Panics (Discouraged in User Code): Panicking within user-provided action or guard methods is strongly discouraged as it can leave the state machine in an inconsistent state. Actions needing to signal failure should modify context or emit events instead.
- Error Handling (Release Builds /
stdfeature):- When the
stdfeature is enabled, operations that can potentially fail at runtime (e.g., interacting with invoked services, timer management if using fallible system calls) should ideally returnResult<T, E>where appropriate. The exact error types are TBD. The coresendmethod itself is designed to returnbool(indicating if a transition occurred) and notResult, as failure to transition due to guards or lack of matching events is considered normal operation, not an error. - Failures within invoked children might result in specific events being sent back to the parent machine.
- When the
no_stdEnvironments: Inno_stdenvironments without thestdfeature, the emphasis is heavily on compile-time validation. Runtime operations are designed to be infallible where possible. If unavoidable runtime failures can occur (e.g., timer allocation failure in a hypotheticalno_stdtimer service), the behavior might involve specific error states, context flags, or defined fallback transitions rather than returningResult. Panics in releaseno_stdbuilds must be avoided entirely; the generated code should strive to be compatible with#![forbid(panic)]in release mode.
As defined in statechart.mdc and ROADMAP.md. A potential future trace feature might be added for detailed instrumentation hooks, possibly integrating with the tracing crate.
Enables Tokio mailbox, file I/O for diagrams, etc.
The std feature enables functionality that depends on the Rust standard library (std), including features requiring memory allocation (beyond potential stack usage) and integration with operating system services.
- Purpose: To allow the statechart library to be used in environments where
stdis available (e.g., typical desktop/server applications) and leveragestd-specific features. The library remains#![no_std]compatible by default when this feature is not enabled. - Enabled Functionality:
- Integration with
std::error::Errortrait (potentially viathiserror). - Support for standard collections if needed internally (though
heaplessmight still be preferred where applicable for performance/predictability). - Use of
std::fmtby default for generated enums (instead ofcore::fmt). - Potential use of standard library primitives for timers or concurrency if the
asyncfeature is also enabled (e.g.,tokiointegration relies onstd). - File I/O capabilities, primarily used by the
diagramfeature for generating output files. - Potentially richer debugging and logging integrations.
- Integration with
- Dependencies: Enabling
stdpulls in optional dependencies likeanyhow,thiserror, potentially parts oftokio(ifasyncis also enabled), andfutures/std. SeeCargo.toml.
Pulls alloc, futures, async-trait. Allows async fn in actions/guards.
The async feature enables integration with Rust's asynchronous programming ecosystem, allowing actions, guards, and potentially invoked services to perform non-blocking operations.
- Purpose: To support use cases where state machine actions need to interact with external I/O or perform long-running computations without blocking the execution thread, particularly when used with the Actor Model (Section 7) in an async runtime like Tokio.
- Enabled Functionality:
- Allows defining action and guard methods on the
Contextstruct asasync fn. The state machine runtime (especially the Actor Model) will correctlyawaitthese functions. - Enables the Actor Model's
Mailbox::send(event).awaitmethod for asynchronous event submission with back-pressure. - Facilitates integration with async timer mechanisms (e.g.,
tokio::time) when thestdfeature is also enabled. - Enables invoking child services/statecharts that operate asynchronously (details TBD in Phase 7).
- Allows defining action and guard methods on the
- Dependencies: Enabling
asyncpulls in optional dependencies likefutures(configured forno_stdcompatibility where possible) andasync-trait. It implicitly requires an allocator (alloccrate), even inno_stdenvironments, due to the nature ofasync-traitandFuturepinning/state storage. Ifstdis also enabled,tokiomight be pulled in as well, depending on other features. SeeCargo.toml. - Core Semantics: Even with
asyncactions/guards, the state machine's core transition logic remains synchronous and sequential for a given event. The Actor Model ensures that anasyncaction associated with a transition completes before the next event is processed from the mailbox, preserving determinism.
Emits TRANSITIONS table, formatters for DOT/Mermaid. Off in firmware builds.
The diagram feature enables the generation of visual representations of the statechart definition, aiding in documentation and understanding.
- Purpose: To provide tools for visualizing the structure and transitions of the state machine defined in the
statechart!macro. This is primarily intended for documentation generation and debugging, not for runtime use in resource-constrained environments. - Enabled Functionality:
- Exposes internal data structures or metadata representing the statechart's topology (states, transitions, hierarchy, etc.). This might involve generating a constant data structure (e.g.,
TRANSITIONS) within the macro output, conditionally compiled based on this feature. - Provides functions or methods (e.g.,
MyStateMachine::to_dot()or similar) to format this structural information into common diagram description languages. - Supported Formats (Target): Graphviz DOT language (
.dot) and Mermaid flowchart syntax (.mmd). - These formatters allow rendering the statechart using external tools (like Graphviz) or directly in Markdown environments that support Mermaid (like GitHub).
- Exposes internal data structures or metadata representing the statechart's topology (states, transitions, hierarchy, etc.). This might involve generating a constant data structure (e.g.,
- Dependencies: Enabling
diagrampulls in optional dependencies likeserde(for serializing the internal representation if needed by the formatters). If file output helpers are provided, it might also implicitly require thestdfeature for file I/O. no_stdImpact: This feature is generally not intended for use inno_stdfirmware builds due to its purpose (offline generation) and potential dependencies (serde,alloc, possiblystd). The generated metadata structure itself might beno_stdcompatible, but the formatting functions likely requireallocorstd. Build configurations for firmware should typically disable this feature.
(Phase 4 Target)
To facilitate integration into concurrent applications, especially when using the std or async features, lit-bit provides an actor model layer. This wraps the core state machine logic, providing a message-passing interface and supervision tree.
pub trait Actor {
type Message: Send + 'static;
async fn on_event(&mut self, msg: Self::Message);
// Supervision hooks (OTP-inspired)
async fn on_start(&mut self) -> Result<(), ActorError> { Ok(()) }
async fn on_stop(self) -> Result<(), ActorError> { Ok(()) }
fn on_panic(&self, info: &PanicInfo) -> RestartStrategy { RestartStrategy::OneForOne }
}
#[derive(Debug, Clone)]
pub enum RestartStrategy {
OneForOne,
OneForAll,
RestForOne,
}- Direct StateMachine Integration:
impl Actor for MyStateMachine { type Message = MyEvent; async fn on_event(&mut self, event: MyEvent) { self.send(&event); } }
- Supervision: Hierarchical parent/child actors, restart strategies, and panic isolation (Akka/OTP/XState-inspired).
- Address: Type-safe handle for sending events/messages to an actor.
- Embedded:
heapless::spsc::Queue(fail-fast, zero-alloc) - Std/async:
tokio::sync::mpsc(await-based back-pressure)
- Embedded:
- API:
// Embedded addr.try_send(MyEvent::Start).unwrap(); // Async addr.send(MyEvent::Start).await.unwrap();
- Spawning:
spawn_actor_embassy!for Embassy/embeddedspawn_actor_tokio!for async/Tokio
- Single-threaded guarantee: Each event is processed to completion before the next is dequeued.
- Back-pressure:
- Embedded: fail-fast if mailbox full (
try_sendreturnsErr) - Std/async: await until space is available (
send().await)
- Embedded: fail-fast if mailbox full (
- No global alloc in no_std: All embedded mailboxes are fixed-size, zero-alloc.
- Conditional mailbox selection: Compile-time selection for platform, no trait abstraction needed.
- Direct event type integration: Use the state machine's event type as the actor message for zero-cost forwarding.
- Supervision and restart: Parent/child relationships, restart on panic, and clean shutdown patterns.
- Performance targets: ≤512B RAM overhead per actor, ≥1M events/sec (Tokio), <200ns/message (desktop).
- See
prompts/phases/04-minimal-actor-layer/04_checklist.mdandprompts/decomposition/04_minimal_actor_layer_tasks.mdfor implementation details and research rationale.
Ideas for v0.2 and beyond (e.g., statechart inspection API, advanced testing utilities, SCXML import/export if demand exists).
- History States (Shallow & Deep)
- Parallel JOIN Transitions (Completion of all nested states in parallel regions)
- Statechart Inspection/Serialization API
- Event Payloads (Allowing events to carry data)
- More sophisticated Timer options (e.g., cron-like scheduling)
- SCXML Import/Export
The following distilled lessons are drawn from an audit of Rust-native state-machine crates (e.g. statig, rust-fsm, async_fsm) and from prior art such as Boost.SML (C++) and XState (JavaScript). They inform lit-bit's public API, CI policy, and roadmap.
- Context Lifetimes & Ownership
- Use flexible lifetime parameters or GATs so context borrowing does not over-constrain the API (addresses
statig#19). - Prefer owned event payloads to sidestep complex lifetime chains; allow borrowing as an optimisation, not a requirement.
- Use flexible lifetime parameters or GATs so context borrowing does not over-constrain the API (addresses
- Compile-Time & Binary-Size Budget
- Macro expansion must scale linearly with the number of states/events. CI runs a
bench_1000_statescrate and fails if compilation exceeds 30 s or binary size regresses >10 %. - Expensive tooling (diagram export, tracing) is feature-gated so typical firmware builds stay lean.
- Macro expansion must scale linearly with the number of states/events. CI runs a
- Hierarchy Semantics
- Every compound state must declare an
initial:sub-state; the macro emits a compile-time error otherwise. - Parent→child and child→parent transitions execute entry/exit actions exactly once; tests assert correct LCA behaviour.
- Every compound state must declare an
- Async & Timer Determinism
- The Actor layer serialises event handling; a transition (incl. awaited actions) must finish before the next event dequeues.
- Timers are cancelled automatically on state exit via an internal
TimerHandleabstraction.
- Diagram Generation Accuracy
- The
diagramfeature generates DOT/Mermaid directly from the transition table during compilation, eliminating stale docs. - A CI check parses the emitted graph to ensure every defined state/transition appears exactly once.
- The
- Soundness &
unsafePolicy- Core crates carry
#![deny(unsafe_code)]. Any unavoidableunsafeis isolated behind a feature flag (unsafe_opt) and documented. - Fuzz and MIRI jobs run nightly to detect UB or double-drop scenarios across random event sequences.
- Core crates carry
- Custom Lints & Clippy Pedantic
- Development builds enable
clippy::pedantic; additional lints flag duplicate state attributes, large enum variants, or reference-holding state structs.
- Development builds enable
These mitigations feed directly into the updated roadmap KPIs and CI steps (see
ROADMAP.md).