- Notifications
You must be signed in to change notification settings - Fork1
Compile-time state machine DSL for Rust, inspired by the Ruby state_machines gem.
License
Apache-2.0, MIT licenses found
Licenses found
state-machines/state-machines-rs
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A learning-focused Rust port of Ruby's state_machines gem
This is a Rust port of the popularstate_machines Ruby gem, created as alearning platform for Rubyists transitioning to Rust.
While learning Rust, I chose to port something familiar and widely used—so I could compare implementations side-by-side and understand Rust's patterns through a lens I already knew. This library is intentionallyover-commented, not because the code is disorganized, but because it's designed to be ateaching tool. The goal is elegant, idiomatic Rust code that Rubyists can learn from without the usual compile-pray-repeat cycle.
- Learning Ground First: Extensive inline comments explain Rust concepts, ownership, trait bounds, and macro magic
- Ruby Parallels: Familiar DSL syntax and callbacks make the transition smoother
- Production Ready: Despite the educational focus, this is a fully functional state machine library with:
- Typestate pattern for compile-time state safety
- Zero-cost abstractions using PhantomData
- Guards and unless conditions
- Before/after event callbacks
- Sync and async support
no_stdcompatibility (for embedded systems)- Payload support for event data
- Move semantics preventing invalid state transitions
You're welcome to open PRs to fix fundamentally wrong Rust concepts—but pleasedon't remove comments just because "we know it". This codebase serves beginners. If something can be explained better, improve the comment. If a pattern is unidiomatic, fix itand document why.
Typestate Pattern – Compile-time state safety using Rust's type system with zero runtime overhead
Guards & Unless – Conditional transitions at event and transition levels
Callbacks –before/after hooks at event level
Around Callbacks – Wrap transitions with Before/AfterSuccess stages for transaction-like semantics
Async Support – First-classasync/await for guards and callbacks
Event Payloads – Pass data through transitions with type-safe payloads
No-std Compatible – Works on embedded targets (ESP32, bare metal)
Type-safe – Invalid transitions become compile errors, not runtime errors
Hierarchical States – Superstates with polymorphic transitions via SubstateOf trait
Dynamic Dispatch – Runtime event dispatch for event-driven systems (opt-in via feature flag or explicit config)
Add to yourCargo.toml:
[dependencies]state-machines ="0.1"
use state_machines::state_machine;// Define your state machinestate_machine!{ name:TrafficLight, initial:Red, states:[Red,Yellow,Green], events{ next{ transition:{ from:Red, to:Green} transition:{ from:Green, to:Yellow} transition:{ from:Yellow, to:Red}}}}fnmain(){// Typestate pattern: each transition returns a new typed machinelet light =TrafficLight::new(());// Type is TrafficLight<Red>let light = light.next().unwrap();// Type is TrafficLight<Green>let light = light.next().unwrap();// Type is TrafficLight<Yellow>}
use state_machines::{state_machine, core::GuardError};use std::sync::atomic::{AtomicBool,Ordering};staticDOOR_OBSTRUCTED:AtomicBool =AtomicBool::new(false);state_machine!{ name:Door, initial:Closed, states:[Closed,Open], events{ open{ guards:[path_clear], before:[check_safety], after:[log_opened], transition:{ from:Closed, to:Open}} close{ transition:{ from:Open, to:Closed}}}}impl<C,S>Door<C,S>{fnpath_clear(&self,_ctx:&C) ->bool{ !DOOR_OBSTRUCTED.load(Ordering::Relaxed)}fncheck_safety(&self){println!("Checking if path is clear...");}fnlog_opened(&self){println!("Door opened at {:?}", std::time::SystemTime::now());}}fnmain(){// Successful transitionlet door =Door::new(());let door = door.open().unwrap();let door = door.close().unwrap();// Failed guard checkDOOR_OBSTRUCTED.store(true,Ordering::Relaxed);let err = door.open().expect_err("should fail when obstructed");let(_door, guard_err) = err;assert_eq!(guard_err.guard,"path_clear");// Inspect the error kinduse state_machines::core::TransitionErrorKind;match guard_err.kind{TransitionErrorKind::GuardFailed{ guard} =>{println!("Guard '{}' failed", guard);} _ =>unreachable!(),}}
For embedded systems or applications where the context type is known at compile time, you can specify aconcrete context type in the macro. This allows guards and callbacks to directly access context fields without generic trait bounds.
Generic Context (Default):
state_machine!{ name:Door,// No context specified - machine is generic over C}impl<C,S>Door<C,S>{fnguard(&self,_ctx:&C) ->bool{// C is generic - can't access its fieldsfalse}}
Concrete Context (Embedded-Friendly):
use state_machines::state_machine;#[derive(Debug,Default)]structHardwareSensors{temperature_c:i16,pressure_kpa:u32,}state_machine!{ name:Door, context:HardwareSensors,// ← Concrete context type initial:Closed, states:[Closed,Open], events{ open{ guards:[safe_conditions], transition:{ from:Closed, to:Open}} close{ transition:{ from:Open, to:Closed}}}}impl<S>Door<S>{fnsafe_conditions(&self,ctx:&HardwareSensors) ->bool{// Direct field access! ctx.temperature_c >= -40 && ctx.temperature_c <=85 && ctx.pressure_kpa >=95 && ctx.pressure_kpa <=105}}fnmain(){let sensors =HardwareSensors{temperature_c:22,pressure_kpa:101,};let door =Door::new(sensors);let door = door.open().unwrap();let _door = door.close().unwrap();}
Key Differences:
| Aspect | Generic Context | Concrete Context |
|---|---|---|
| Struct signature | Machine<C, S> | Machine<S> |
| Impl blocks | impl<C, S> | impl<S> |
| Guard signature | fn(&self, &C) | fn(&self, &HardwareType) |
| Field access | Not possible | Direct access |
| Flexibility | Works with any context | Fixed to one type |
| Use case | Libraries, flexibility | Embedded, hardware |
When to Use:
- Embedded systems – Hardware types known at compile time
- no_std environments – Direct hardware register access
- Fixed architectures – Single deployment target
- Performance critical – Compiler can optimize better
When to Avoid:
- Libraries – Users need context flexibility
- Multiple deployments – Different hardware configs
- Generic code – Need to work with various types
Seeexamples/guards_and_validation for a complete example using concrete context for spacecraft telemetry.
The typestate pattern works seamlessly with async Rust:
use state_machines::state_machine;state_machine!{ name:HttpRequest, initial:Idle,async:true, states:[Idle,Pending,Success,Failed], events{ send{ guards:[has_network], transition:{ from:Idle, to:Pending}} succeed{ transition:{ from:Pending, to:Success}} fail{ transition:{ from:Pending, to:Failed}}}}impl<C,S>HttpRequest<C,S>{asyncfnhas_network(&self,_ctx:&C) ->bool{// Async guard checks network availability tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;true}}#[tokio::main]asyncfnmain(){// Type: HttpRequest<Idle>let request =HttpRequest::new(());// Type: HttpRequest<Pending>let request = request.send().await.unwrap();// Type: HttpRequest<Success>let request = request.succeed().await.unwrap();}
use state_machines::state_machine;#[derive(Clone,Debug)]structLoginCredentials{username:String,password:String,}state_machine!{ name:AuthSession, initial:LoggedOut, states:[LoggedOut,LoggedIn,Locked], events{ login{ payload:LoginCredentials, guards:[valid_credentials], transition:{ from:LoggedOut, to:LoggedIn}} logout{ transition:{ from:LoggedIn, to:LoggedOut}}}}impl<C,S>AuthSession<C,S>{fnvalid_credentials(&self,_ctx:&C,creds:&LoginCredentials) ->bool{// Guard receives context and payload reference creds.username =="admin" && creds.password =="secret"}}fnmain(){let session =AuthSession::new(());// Type is AuthSession<(), LoggedOut>let good_creds =LoginCredentials{username:"admin".to_string(),password:"secret".to_string(),};let session = session.login(good_creds).unwrap();// Type is AuthSession<LoggedIn>}
Group related states into superstates for polymorphic transitions and cleaner state organization:
use state_machines::state_machine;#[derive(Default,Debug,Clone)]structPrepData{checklist_complete:bool,}#[derive(Default,Debug,Clone)]structLaunchData{engines_ignited:bool,}state_machine!{ name:LaunchSequence, initial:Standby, states:[Standby, superstateFlight{ stateLaunchPrep(PrepData), stateLaunching(LaunchData),},InOrbit,], events{ enter_flight{ transition:{ from:Standby, to:Flight}} ignite{ transition:{ from:Standby, to:LaunchPrep}} cycle_engines{ transition:{ from:LaunchPrep, to:Launching}} ascend{ transition:{ from:Flight, to:InOrbit}} abort{ transition:{ from:Flight, to:Standby}}}}fnmain(){// Start in Standbylet sequence =LaunchSequence::new(());// Transition to Flight superstate resolves to initial child (LaunchPrep)let sequence = sequence.enter_flight().unwrap();// Access state-specific data (guaranteed non-None)let prep_data = sequence.launch_prep_data();println!("Checklist complete: {}", prep_data.checklist_complete);// Move to Launching within Flight superstatelet sequence = sequence.cycle_engines().unwrap();// abort() is defined on Flight, but works from ANY substatelet sequence = sequence.abort().unwrap();// Type: LaunchSequence<C, Standby>// Go directly to LaunchPrep (bypassing superstate entry)let sequence = sequence.ignite().unwrap();// Type: LaunchSequence<C, LaunchPrep>// abort() STILL works - polymorphic transition!let _sequence = sequence.abort().unwrap();}
Key Features:
- Polymorphic Transitions: Define transitions
from: Flightthat work from ANY substate (LaunchPrep, Launching) - Automatic Resolution:
to: Flighttransitions resolve to the superstate's initial child state - State Data Storage: Each state with data gets guaranteed accessors like
launch_prep_data()andlaunching_data() - SubstateOf Trait: Generated trait implementations enable compile-time polymorphism
- Storage Lifecycle: State data is automatically initialized on entry, cleared on exit
Under the Hood:
The macro generates:
// Marker trait for polymorphismimplSubstateOf<Flight>forLaunchPrep{}implSubstateOf<Flight>forLaunching{}// Polymorphic transition implementationimpl<C,S:SubstateOf<Flight>>LaunchSequence<C,S>{pubfnabort(self) ->Result<LaunchSequence<C,Standby>, ...>{// Works from ANY state where S implements SubstateOf<Flight>}}// State-specific data accessors (no Option wrapper!)impl<C>LaunchSequence<C,LaunchPrep>{pubfnlaunch_prep_data(&self) ->&PrepData{ ...}pubfnlaunch_prep_data_mut(&mutself) ->&mutPrepData{ ...}}
Ruby Comparison:
Ruby'sstate_machines doesn't have formal superstate support in this way. The closest equivalent would be using state predicates:
# Ruby approachdefin_flight?[:launch_prep,:launching].include?(state)end# Rust: Compile-time polymorphism via trait boundsimpl<C,S:SubstateOf<Flight>>LaunchSequence<C,S>{pubfnabort(self) -> ...{}}
Rust's typestate pattern makes this compile-time safe with zero runtime overhead.
Around callbacks wrap transitions withtransaction-like semantics, providing Before and AfterSuccess hooks that bracket the entire transition execution:
use state_machines::{state_machine, core::{AroundStage,AroundOutcome}};use std::sync::atomic::{AtomicUsize,Ordering};staticCALL_COUNT:AtomicUsize =AtomicUsize::new(0);state_machine!{ name:Transaction, initial:Idle, states:[Idle,Processing,Complete], events{ begin{ around:[transaction_wrapper], transition:{ from:Idle, to:Processing}} succeed{ transition:{ from:Processing, to:Complete}}}}impl<C,S>Transaction<C,S>{fntransaction_wrapper(&self,stage:AroundStage) ->AroundOutcome<Idle>{match stage{AroundStage::Before =>{println!("Starting transaction...");CALL_COUNT.fetch_add(1,Ordering::SeqCst);AroundOutcome::Proceed}AroundStage::AfterSuccess =>{println!("Transaction committed!");CALL_COUNT.fetch_add(10,Ordering::SeqCst);AroundOutcome::Proceed}}}}fnmain(){let transaction =Transaction::new(());let transaction = transaction.begin().unwrap();// CALL_COUNT is now 11 (Before: +1, AfterSuccess: +10)assert_eq!(CALL_COUNT.load(Ordering::SeqCst),11);}
Execution Order:
- Around Before – Runs first, can abort the entire transition
- Guards – Event/transition guards evaluated
- Before callbacks – Event-level before hooks
- State transition – Actual state change occurs
- After callbacks – Event-level after hooks
- Around AfterSuccess – Runs last, guaranteed to execute after successful transition
Aborting Transitions:
Around callbacks at the Before stage can abort transitions by returningAroundOutcome::Abort:
use state_machines::{ state_machine, core::{AroundStage,AroundOutcome,TransitionError},};state_machine!{ name:Guarded, initial:Start, states:[Start,End], events{ advance{ around:[abort_guard], transition:{ from:Start, to:End}}}}impl<C,S>Guarded<C,S>{fnabort_guard(&self,stage:AroundStage) ->AroundOutcome<Start>{match stage{AroundStage::Before =>{// Abort at Before stageAroundOutcome::Abort(TransitionError::guard_failed(Start,"advance","abort_guard",))}AroundStage::AfterSuccess =>{// Won't be called when Before abortsAroundOutcome::Proceed}}}}fnmain(){let machine =Guarded::new(());let result = machine.advance();assert!(result.is_err());let(_machine, err) = result.unwrap_err();assert_eq!(err.guard,"abort_guard");}
Distinguishing Error Types:
Around callbacks preserve the fullTransitionErrorKind, allowing you to distinguish between guard failures and action failures:
use state_machines::{ state_machine, core::{AroundStage,AroundOutcome,TransitionError,TransitionErrorKind},};state_machine!{ name:Workflow, initial:Pending, states:[Pending,Validated,Complete], events{ validate{ around:[validation_wrapper], transition:{ from:Pending, to:Validated}}}}impl<C,S>Workflow<C,S>{fnvalidation_wrapper(&self,stage:AroundStage) ->AroundOutcome<Pending>{match stage{AroundStage::Before =>{// Abort with ActionFailed (not GuardFailed)AroundOutcome::Abort(TransitionError{from:Pending,event:"validate",kind:TransitionErrorKind::ActionFailed{action:"validation_wrapper",},})}AroundStage::AfterSuccess =>AroundOutcome::Proceed,}}}fnmain(){let workflow =Workflow::new(());let result = workflow.validate();ifletErr((_workflow, err)) = result{// Inspect the error kind to distinguish failure typesmatch err.kind{TransitionErrorKind::GuardFailed{ guard} =>{println!("Guard '{}' prevented transition", guard);}TransitionErrorKind::ActionFailed{ action} =>{println!("Action '{}' aborted transition", action);}TransitionErrorKind::InvalidTransition =>{println!("Invalid state transition");}}}}
Use Cases:
- Database transactions – Begin/commit semantics
- Resource locking – Acquire before, release after
- Logging/tracing – Instrument transitions
- Performance monitoring – Measure transition duration
- Validation – Pre/post-condition checks
- Cleanup – Ensure resources are released after transition
Multiple Around Callbacks:
You can specify multiple around callbacks that all execute in order:
state_machine!{ name:Multi, initial:X, states:[X,Y], events{ go{ around:[logging_wrapper, metrics_wrapper, transaction_wrapper], transition:{ from:X, to:Y}}}}
All Before stages run in order, then the transition, then all AfterSuccess stages.
Performance:
Around callbacks achievezero-cost abstraction when optimized:
| Configuration | Overhead | Notes |
|---|---|---|
| Single around callback | ~411 ps | Same as simple transition |
| Multiple around callbacks (3) | ~411 ps | Compiler optimizes away empty wrappers |
| Around + guards + callbacks | ~412 ps | All features combined, negligible overhead |
Seestate-machines/benches/typestate_transitions.rs for detailed benchmarks.
While the typestate pattern provides excellent compile-time safety, sometimes you needruntime flexibility when events come from external sources (user input, network messages, event queues). Dynamic dispatch mode solves this by generating a runtime wrapper alongside your typestate machine.
Use Typestate When:
- ✅ Control flow is known at compile time
- ✅ Want maximum type safety
- ✅ Performance critical (zero overhead)
- ✅ Building DSLs or configuration pipelines
Use Dynamic When:
- ✅ Events from external sources (UI, network, queues)
- ✅ Runtime event routing/dispatch
- ✅ Need to store machines in collections
- ✅ Building event-driven systems or GUIs
Use Both When:
- ✅ Type-safe setup phase, then dynamic runtime
- ✅ Want compile-time safety where possible
Dynamic dispatch isopt-in to keep binaries small by default. Enable it via:
Option 1: Explicit in macro (always generates dynamic code)
state_machine!{ name:TrafficLight, dynamic:true,// ← Enable dynamic dispatch initial:Red, states:[Red,Yellow,Green], events{/* ... */}}
Option 2: Cargo feature flag (conditional compilation)
[dependencies]state-machines = {version ="0.2",features = ["dynamic"] }
With the feature flag enabled, ALL state machines get dynamic dispatch without explicitdynamic: true.
use state_machines::state_machine;state_machine!{ name:TrafficLight, dynamic:true, initial:Red, states:[Red,Yellow,Green], events{ next{ transition:{ from:Red, to:Green} transition:{ from:Green, to:Yellow} transition:{ from:Yellow, to:Red}}}}fnmain(){// Create dynamic machineletmut light =DynamicTrafficLight::new(());// Runtime event dispatch light.handle(TrafficLightEvent::Next).unwrap();assert_eq!(light.current_state(),"Green"); light.handle(TrafficLightEvent::Next).unwrap();assert_eq!(light.current_state(),"Yellow"); light.handle(TrafficLightEvent::Next).unwrap();assert_eq!(light.current_state(),"Red");}
Whendynamic: true is set, the macro generates:
- Event Enum – Runtime representation of events
pubenumTrafficLightEvent{Next,// With payloads:// SetSpeed(u32),}
- Dynamic Machine – Runtime dispatch wrapper
pubstructDynamicTrafficLight<C>{// Internal state wrapper}impl<C:Default>DynamicTrafficLight<C>{pubfnnew(ctx:C) ->Self{/* ... */}pubfnhandle(&mutself,event:TrafficLightEvent) ->Result<(),DynamicError>{/* ... */}pubfncurrent_state(&self) ->&'staticstr{/* ... */}}
- Conversion Methods – Switch between modes
impl<C>TrafficLight<C,Red>{pubfninto_dynamic(self) ->DynamicTrafficLight<C>{/* ... */}}impl<C>DynamicTrafficLight<C>{pubfninto_red(self) ->Result<TrafficLight<C,Red>,Self>{/* ... */}pubfninto_yellow(self) ->Result<TrafficLight<C,Yellow>,Self>{/* ... */}pubfninto_green(self) ->Result<TrafficLight<C,Green>,Self>{/* ... */}}
Convert from typestate to dynamic when you need runtime flexibility:
// Start with typestate for setuplet light =TrafficLight::new(());// Type: TrafficLight<(), Red>// Perform type-safe transitionslet light = light.next().unwrap();// Type: TrafficLight<(), Green>// Convert to dynamic for event loopletmut dynamic_light = light.into_dynamic();// Now handle runtime eventsloop{let event =receive_event();// From network, user input, etcmatch dynamic_light.handle(event){Ok(()) =>println!("Transitioned to {}", dynamic_light.current_state()),Err(e) =>eprintln!("Transition failed: {:?}", e),}}
Convert back to typestate when you know the current state:
letmut dynamic =DynamicTrafficLight::new(());dynamic.handle(TrafficLightEvent::Next).unwrap();// Extract typed machine if in Green stateifletOk(typed) = dynamic.into_green(){// Type: TrafficLight<(), Green>// Now have compile-time guarantees againlet _ = typed.next();}
A common pattern is using dynamic mode with external event sources:
use state_machines::{state_machine,DynamicError};state_machine!{ name:Connection, dynamic:true, initial:Disconnected, states:[Disconnected,Connecting,Connected,Failed], events{ connect{ transition:{ from:Disconnected, to:Connecting}} established{ transition:{ from:Connecting, to:Connected}} timeout{ transition:{ from:Connecting, to:Failed}} disconnect{ transition:{ from:[Connecting,Connected], to:Disconnected}}}}fnhandle_network_events(conn:&mutDynamicConnection<()>){// Receive events from network layerlet events =vec![ConnectionEvent::Connect,ConnectionEvent::Established,ConnectionEvent::Disconnect,];for eventin events{match conn.handle(event){Ok(()) =>{println!("State: {}", conn.current_state());}Err(DynamicError::InvalidTransition{ from, event}) =>{eprintln!("Can't {} from {}", event, from);}Err(DynamicError::GuardFailed{ guard, event}) =>{eprintln!("Guard {} failed for {}", guard, event);}Err(DynamicError::ActionFailed{ action, event}) =>{eprintln!("Action {} failed for {}", action, event);}}}}fnmain(){letmut conn =DynamicConnection::new(());handle_network_events(&mut conn);}
Dynamic mode providesDynamicError with three variants:
pubenumDynamicError{InvalidTransition{from:&'staticstr,event:&'staticstr},GuardFailed{guard:&'staticstr,event:&'staticstr},ActionFailed{action:&'staticstr,event:&'staticstr},}
Unlike typestate mode (which returns the old machine on error), dynamic mode keeps the machine in a valid state:
letmut machine =DynamicTrafficLight::new(());// Invalid transitionlet result = machine.handle(TrafficLightEvent::Next);// Red → Green (valid)assert!(result.is_ok());// Machine is now in Green state, regardless of success/failureassert_eq!(machine.current_state(),"Green");
| Mode | Overhead | Safety | Use Case |
|---|---|---|---|
| Typestate | Zero (PhantomData) | Compile-time | Known sequences |
| Dynamic | Enum match (~few ns) | Runtime | Event-driven |
Dynamic mode adds minimal runtime overhead (enum discriminant check + match). For most applications, this is negligible compared to the actual business logic.
This library providesboth modes:
- Typestate by default – Zero-cost abstractions, compile-time safety
- Dynamic opt-in – Runtime flexibility when needed
- Seamless conversion – Switch modes as requirements change
You're never forced to choose one over the other. Start with typestate for safety, convert to dynamic for flexibility, and back again when you need guarantees.
If you're coming from Ruby, here's how the concepts map:
classVehiclestate_machine:state,initial::parkeddoevent:ignitedotransitionparked::idlingendbefore_transitionparked::idling,do::check_fuelenddefcheck_fuelputs"Checking fuel..."endend# Usagevehicle=Vehicle.newvehicle.ignite# Mutates vehicle in place
use state_machines::state_machine;state_machine!{ name:Vehicle, initial:Parked, states:[Parked,Idling], events{ ignite{ before:[check_fuel], transition:{ from:Parked, to:Idling}}}}impl<C,S>Vehicle<C,S>{fncheck_fuel(&self){println!("Checking fuel...");}}fnmain(){// Type: Vehicle<Parked>let vehicle =Vehicle::new(());// Type: Vehicle<Idling>let vehicle = vehicle.ignite().unwrap();}
Key Differences:
- Typestate pattern: Each state is encoded in the type system (
Vehicle<Parked>vsVehicle<Idling>) - Move semantics: Transitions consume the old state and return a new one
- Compile-time validation: Can't call
ignite()twice - second call won't compile! - Zero overhead: PhantomData optimizes away completely
- Explicit errors: Guards return
Result<Machine<NewState>, (Machine<OldState>, GuardError)> - No mutation: Callbacks take
&self, not&mut self(machine is consumed by transition)
Works on embedded targets like ESP32:
#![no_std]use state_machines::state_machine;state_machine!{ name:LedController, initial:Off, states:[Off,On,Blinking], events{ toggle{ transition:{ from:Off, to:On}} blink{ transition:{ from:On, to:Blinking}}}}fnembedded_main(){// Type: LedController<Off>let led =LedController::new(());// Type: LedController<On>let led = led.toggle().unwrap();// Type: LedController<Blinking>let led = led.blink().unwrap();// Wire up to GPIO pins...}#fnmain(){}// For doctest
- Disable default features:
state-machines = { version = "0.1", default-features = false } - The library uses no allocator - purely stack-based with zero-sized state markers
- CI runs
cargo build --no-default-featuresto prevent std regressions - See
examples/no_std_flight/for a complete embedded example
This library achievestrue zero-cost abstractions for typestate mode:
| Feature | Overhead | Notes |
|---|---|---|
| Typestate mode | ||
| Guards | ~0 ps | Compiled to inline comparisons |
| Callbacks | ~0 ps | Compiled to inline function calls |
| Around callbacks | ~0 ps | Compiled to inline function calls |
| Hierarchical transitions | ~3-4 ns | Minimal cost for storage lifecycle |
| State data access | ~1 ns | Direct field access |
| Dynamic mode | ||
| Event dispatch | ~few ns | Enum match + method call |
| State introspection | ~0 ps | Direct field access |
Guards, callbacks, and around callbacks in typestate mode addliterally zero runtime overhead - the compiler optimizes them completely. Dynamic mode adds minimal overhead (enum matching), typically under 10ns per transition.
Run benchmarks yourself:
cargo bench --bench typestate_transitions
Contributions are welcome! This is a learning project, so:
- Keep comments – Explainwhy, not justwhat
- Show Rust idioms – If something is unidiomatic, fix itand document the correct pattern
- Test thoroughly – All tests must pass (
cargo test --workspace) - Compare to Ruby – If you're changing behavior, note how it differs from the Ruby gem
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE orhttp://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT orhttp://opensource.org/licenses/MIT)
at your option.
About
Compile-time state machine DSL for Rust, inspired by the Ruby state_machines gem.
Topics
Resources
License
Apache-2.0, MIT licenses found
Licenses found
Uh oh!
There was an error while loading.Please reload this page.