Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async on_transition() #29

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [

"examples/no_macro/basic",
"examples/no_macro/blinky",
"examples/no_macro/blinky_async",
"examples/no_macro/bench",
"examples/no_macro/history",
"examples/no_macro/calculator",
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,16 @@ Short answer: nothing. `#[state_machine]` simply parses the underlying `impl` bl

I would say they serve a different purpose. The [typestate pattern](http://cliffle.com/blog/rust-typestate/) is very useful for designing an API as it is able to enforce the validity of operations at compile time by making each state a unique type. But `statig` is designed to model a dynamic system where events originate externally and the order of operations is determined at run time. More concretely, this means that the state machine is going to sit in a loop where events are read from a queue and submitted to the state machine using the `handle()` method. If we want to do the same with a state machine that uses the typestate pattern we'd have to use an enum to wrap all our different states and match events to operations on these states. This means extra boilerplate code for little advantage as the order of operations is unknown so it can't be checked at compile time. On the other hand `statig` gives you the ability to create a hierarchy of states which I find to be invaluable as state machines grow in complexity.

## Testing

Install the following dependencies:

```sh
sudo apt install cmake libfontconfig1-dev
cargo test --workspace
```


---

## Credits
Expand Down
18 changes: 18 additions & 0 deletions examples/macro/async_blinky/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#![allow(unused)]

use futures::executor;
use futures::future::poll_fn;
use statig::prelude::*;
use std::fmt::Debug;
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
use std::thread::spawn;

#[derive(Debug, Default)]
Expand All @@ -29,6 +32,8 @@ pub enum Event {
superstate(derive(Debug)),
// Set the `on_transition` callback.
on_transition = "Self::on_transition",
// Set the `on_transition_async` callback.
on_transition_async = "Self::on_transition_async",
// Set the `on_dispatch` callback.
on_dispatch = "Self::on_dispatch"
)]
Expand Down Expand Up @@ -83,6 +88,19 @@ impl Blinky {
println!("transitioned from `{source:?}` to `{target:?}`");
}

async fn transitioning(&mut self, from: &State, to: &State) {
println!("transitioning from {:?} to {:?}", from, to);
}

fn on_transition_async<'a>(
&'a mut self,
source: &'a State,
target: &'a State,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
println!("transitioned async from `{source:?}` to `{target:?}`");
Box::pin(self.transitioning(source, target))
}

fn on_dispatch(&mut self, state: StateOrSuperstate<Self>, event: &Event) {
println!("dispatching `{event:?}` to `{state:?}`");
}
Expand Down
8 changes: 8 additions & 0 deletions examples/no_macro/blinky_async/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "no_macro_blink_async"
version = "0.1.0"
edition = "2021"

[dependencies]
statig = { path = "../../../statig", features = ["async"] }
tokio = { version = "*", features = ["full"] }
150 changes: 150 additions & 0 deletions examples/no_macro/blinky_async/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#![allow(unused)]

use statig::awaitable::{self, *};
use std::{
future::{poll_fn, Future},
io::Write,
pin::Pin,
task::Poll,
};

#[derive(Default)]
pub struct Blinky {
field: String,
}

// The event that will be handled by the state machine.
pub enum Event {
TimerElapsed,
ButtonPressed,
}

// The enum representing all states of the state machine. These are
// the states you can actually transition to.
#[derive(Debug)]
pub enum State {
LedOn,
LedOff,
NotBlinking,
}

// The enum representing the superstates of the system. You can not transition
// to a superstate, but instead they define shared behavior of underlying states or
// superstates.
pub enum Superstate {
Blinking,
}

// The `statig` trait needs to be implemented on the type that will
// imlement the state machine.
impl IntoStateMachine for Blinky {
/// The enum that represents the state.
type State = State;

type Superstate<'sub> = Superstate;

/// The event type that will be submitted to the state machine.
type Event<'evt> = Event;

type Context<'ctx> = ();

/// The initial state of the state machine.
const INITIAL: State = State::LedOn;

const ON_TRANSITION_ASYNC: for<'fut> fn(
&'fut mut Self,
&'fut Self::State,
&'fut Self::State,
)
-> Pin<Box<dyn Future<Output = ()> + Send + 'fut>> = |blinky, from, to| {
println!("transitioned from {:?} to {:?}", from, to);
Box::pin(blinky.transition_and_print_internal_state(from, to))
};
}

// Implement the `statig::State` trait for the state enum.
impl awaitable::State<Blinky> for State {
fn call_handler<'fut>(
&'fut mut self,
blinky: &'fut mut Blinky,
event: &'fut Event,
_: &'fut mut (),
) -> Pin<Box<(dyn Future<Output = statig::Response<State>> + Send + 'fut)>> {
match self {
State::LedOn => Box::pin(Blinky::timer_elapsed_turn_off(event)),
State::LedOff => Box::pin(Blinky::timer_elapsed_turn_on(event)),
State::NotBlinking => Box::pin(Blinky::not_blinking_button_pressed(event)),
}
}

fn superstate<'fut>(&mut self) -> Option<Superstate> {
match self {
State::LedOn => Some(Superstate::Blinking),
State::LedOff => Some(Superstate::Blinking),
State::NotBlinking => None,
}
}
}

// Implement the `statig::Superstate` trait for the superstate enum.
impl awaitable::Superstate<Blinky> for Superstate {
fn call_handler<'fut>(
&'fut mut self,
blinky: &'fut mut Blinky,
event: &'fut Event,
_: &'fut mut (),
) -> Pin<Box<(dyn Future<Output = Response<State>> + Send + 'fut)>> {
Box::pin(match self {
Superstate::Blinking => Blinky::blinking_button_pressed(event),
})
}
}

impl Blinky {
async fn transition_and_print_internal_state(&mut self, from: &State, to: &State) {
println!(
"transitioned (current test value is: {}) from {:?} to {:?}",
self.field, from, to
);
}
async fn timer_elapsed_turn_off(event: &Event) -> Response<State> {
match event {
Event::TimerElapsed => Transition(State::LedOff),
_ => Super,
}
}

async fn timer_elapsed_turn_on(event: &Event) -> Response<State> {
match event {
Event::TimerElapsed => Transition(State::LedOn),
_ => Super,
}
}

async fn blinking_button_pressed(event: &Event) -> Response<State> {
match event {
Event::ButtonPressed => Transition(State::NotBlinking),
_ => Super,
}
}

async fn not_blinking_button_pressed(event: &Event) -> Response<State> {
match event {
Event::ButtonPressed => Transition(State::LedOn),
_ => Super,
}
}
}

#[tokio::main]
async fn main() {
let mut state_machine = Blinky {
field: "test field value".to_string(),
}
.state_machine();

state_machine.handle(&Event::TimerElapsed).await;
state_machine.handle(&Event::ButtonPressed).await;
state_machine.handle(&Event::TimerElapsed).await;
state_machine.handle(&Event::ButtonPressed).await;
}
13 changes: 13 additions & 0 deletions macro/src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct StateMachine {
pub visibility: Visibility,
/// Optional `on_transition` callback.
pub on_transition: Option<Path>,
/// Optional `on_transition_async` callback.
pub on_transition_async: Option<Path>,
/// Optional `on_dispatch` callback.
pub on_dispatch: Option<Path>,
}
Expand Down Expand Up @@ -180,6 +182,7 @@ pub fn analyze_state_machine(attribute_args: &AttributeArgs, item_impl: &ItemImp
let mut superstate_derives = Vec::new();

let mut on_transition = None;
let mut on_transition_async = None;
let mut on_dispatch = None;

let mut visibility = parse_quote!(pub);
Expand Down Expand Up @@ -224,6 +227,14 @@ pub fn analyze_state_machine(attribute_args: &AttributeArgs, item_impl: &ItemImp
_ => abort!(name_value, "must be a string literal"),
}
}
NestedMeta::Meta(Meta::NameValue(name_value))
if name_value.path.is_ident("on_transition_async") =>
{
on_transition_async = match &name_value.lit {
Lit::Str(input_pat) => Some(input_pat.parse().unwrap()),
_ => abort!(name_value, "must be a string literal"),
}
}
NestedMeta::Meta(Meta::NameValue(name_value))
if name_value.path.is_ident("on_dispatch") =>
{
Expand Down Expand Up @@ -341,6 +352,7 @@ pub fn analyze_state_machine(attribute_args: &AttributeArgs, item_impl: &ItemImp
superstate_derives,
on_dispatch,
on_transition,
on_transition_async,
event_ident,
context_ident,
visibility,
Expand Down Expand Up @@ -660,6 +672,7 @@ fn valid_state_analyze() {
superstate_ident,
superstate_derives,
on_transition,
on_transition_async: None,
on_dispatch,
event_ident,
context_ident,
Expand Down
10 changes: 9 additions & 1 deletion macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pub fn codegen(ir: Ir) -> TokenStream {
#superstate_impl
)
}

fn codegen_state_machine_impl(ir: &Ir) -> ItemImpl {
let shared_storage_type = &ir.state_machine.shared_storage_type;
let (impl_generics, _, where_clause) =
Expand Down Expand Up @@ -66,6 +65,13 @@ fn codegen_state_machine_impl(ir: &Ir) -> ItemImpl {
),
};

let on_transition_async = match &ir.state_machine.on_transition_async {
None => quote!(),
Some(on_transition_async) => quote!(
const ON_TRANSITION_ASYNC: for <'a> fn(&'a mut Self, &'a Self::State, &'a Self::State) -> core::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> = #on_transition_async;
),
};

let on_dispatch = match &ir.state_machine.on_dispatch {
None => quote!(),
Some(on_dispatch) => quote!(
Expand All @@ -84,6 +90,8 @@ fn codegen_state_machine_impl(ir: &Ir) -> ItemImpl {

#on_transition

#on_transition_async

#on_dispatch
}
)
Expand Down
7 changes: 7 additions & 0 deletions macro/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ pub struct StateMachine {
pub superstate_generics: Generics,
/// The path of the `on_transition` callback.
pub on_transition: Option<Path>,
/// The path of the `on_transition_async` callback.
pub on_transition_async: Option<Path>,
/// The path of the `on_dispatch` callback.
pub on_dispatch: Option<Path>,
/// The visibility for the derived types,
Expand Down Expand Up @@ -137,6 +139,8 @@ pub fn lower(model: &Model) -> Ir {
let state_ident = model.state_machine.state_ident.clone();
let superstate_ident = model.state_machine.superstate_ident.clone();
let on_transition = model.state_machine.on_transition.clone();

let on_transition_async = model.state_machine.on_transition_async.clone();
let on_dispatch = model.state_machine.on_dispatch.clone();
let event_ident = model.state_machine.event_ident.clone();
let context_ident = model.state_machine.context_ident.clone();
Expand Down Expand Up @@ -421,6 +425,7 @@ pub fn lower(model: &Model) -> Ir {
superstate_derives,
superstate_generics,
on_transition,
on_transition_async,
on_dispatch,
visibility,
event_ident,
Expand Down Expand Up @@ -708,6 +713,7 @@ fn create_analyze_state_machine() -> analyze::StateMachine {
superstate_ident: parse_quote!(Superstate),
superstate_derives: vec![parse_quote!(Copy), parse_quote!(Clone)],
on_transition: None,
on_transition_async: None,
on_dispatch: None,
visibility: parse_quote!(pub),
event_ident: parse_quote!(input),
Expand All @@ -733,6 +739,7 @@ fn create_lower_state_machine() -> StateMachine {
superstate_derives: vec![parse_quote!(Copy), parse_quote!(Clone)],
superstate_generics,
on_transition: None,
on_transition_async: None,
on_dispatch: None,
visibility: parse_quote!(pub),
event_ident: parse_quote!(input),
Expand Down
Loading