mdeloof / statig Goto Github PK
View Code? Open in Web Editor NEWHierarchical state machines for designing event-driven systems
Home Page: https://crates.io/crates/statig
License: MIT License
Hierarchical state machines for designing event-driven systems
Home Page: https://crates.io/crates/statig
License: MIT License
Can anyone give a small example of accessing a GPIO or some other peripheral from a state? I got the example blinky
working on an ESP32-C3, but I can't get an LED to blink.
Hi! Thanks for the library.
I have some experience with Hierarchial State Machines in C++ world. I've used HSM library: https://github.com/erikzenker/hsm
Could you please compare statig
to the HSM? E.g. which feature from HSM are already supported in statig. I hope HSM would be for you at least a good roadmap :)
Thanks in advance!
Hi
I am attempting to run the example (https://github.com/mdeloof/statig/tree/main/examples/macro/basic) as an independent project and receiving this error . Am I missing anything?
Compiling machine v0.1.0 (/Users/chetanconikee/pgithub/machine)
error[E0599]: no method named `uninitialized_state_machine` found for struct `Blinky` in the current scope
--> src/main.rs:40:43
|
7 | pub struct Blinky {
| ----------------- method `uninitialized_state_machine` not found for this struct
...
40 | let state_machine = Blinky::default().uninitialized_state_machine();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `Blinky`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `machine` due to previous error
failed to resolve: could not find `awaitable` in `statig
Running into this error when trying to run async_blinky
. Known issue?
Hi!
I'm liking this library and would like to switch to it but I need to pass multiple references in for context. Creating a type that composes several references like so
struct Context<'a, 'b>{
foo: &'a mut LargeStruct1,
bar: &'b mut LargeStruct2
}
won't work since I cant create function-level generic lifetimes. Is there a way around this?
Thanks!
I'm trying to add an FnMut to a state machine via:
pub struct Thing<F>
where
F: FnMut(Signal)
{
emit: F
}
#[state_machine(initial = "State::init()")]
impl<F> Thing<F>
where
F: FnMut(Signal),
{
in order to allow a state machine to emit a signal as it handles transitions. Unfortunately, adding generics like that to the state machine appears to cause a proc macro panic that just says error: custom attribute panicked
with message: expected function call expression
When I run RUSTFLAGS="-Z proc-macro-backtrace" cargo +nightly check
, I get the following lines in the backtrace:
16: 0x7fe276683569 - core::panicking::panic_display::hbb06a95bf5a75324
17: 0x7fe27663a60c - syn::parse_quote::parse::h36c31769ce163720
18: 0x7fe27667b8bb - statig_macro::lower::lower_state::h72f60dda88bc84b7
19: 0x7fe27664e470 - statig_macro::lower::lower::{{closure}}::hafaf3d3de1282b22
20: 0x7fe27666c0ab - core::iter::adapters::map::map_fold::{{closure}}::h2a21cabf95abed4d
21: 0x7fe27665e579 - core::iter::traits::iterator::Iterator::fold::ha24dab897807eb32
22: 0x7fe27666abc2 - <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold::h97d4d2d660189e34
23: 0x7fe27666b79f - core::iter::traits::iterator::Iterator::for_each::h78d7f095ca550b39
24: 0x7fe276636e9a - <hashbrown::map::HashMap<K,V,S,A> as core::iter::traits::collect::Extend<(K,V)>>::extend::h407fb9dd206eee45
25: 0x7fe27665d7d5 - <std::collections::hash::map::HashMap<K,V,S> as core::iter::traits::collect::FromIterator<(K,V)>>::from_iter::h0325534979972350
26: 0x7fe27666b47c - core::iter::traits::iterator::Iterator::collect::hb88b7d1cd8683440
27: 0x7fe2766784a6 - statig_macro::lower::lower::he8b7b3aa35a21378
28: 0x7fe27663eaef - statig_macro::state_machine::{{closure}}::h25adf5f267d5cf6b
29: 0x7fe27666e706 - <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once::haed139dfc15334ae
30: 0x7fe27665ec9d - std::panicking::try::do_call::haa01aefa55582a9c
31: 0x7fe27665f0bb - __rust_try
32: 0x7fe27665e7de - std::panicking::try::h239dda7bdbd68854
33: 0x7fe276681fed - std::panic::catch_unwind::h2aef9b4fc512d321
34: 0x7fe276674b70 - proc_macro_error::entry_point::h249af567a823643d
35: 0x7fe27666898b - statig_macro::state_machine::hfdbd09263af6b545
36: 0x7fe276656824 - core::ops::function::Fn::call::h98bd3f42fef67228
Unfortunately for some reason I can't seem to get line numbers for the lines that occur within statig_macro, however I tried to narrow down the line causing the issue, and it appears to be this line in lower.rs
, in the lower_state
function:
let handler_call =
parse_quote!(#shared_storage_type::#state_handler_name(#(#handler_inputs),*));
Unfortunately I'm not very good with proc macros, so I'm not sure how to further debug this, but hopefully this is helpful?
Alternatively, perhaps I just screwed something up. In any case, let me know if there's anything else I can do to help diagnose this.
Thanks!
I'm having trouble making multiple simple async state machines run in tokio::spawn
tasks since it requires the Send
trait and I don't think I can resolve this inside my crate. I recently saw a commit on cleaning up some trait bounds so I'm unsure if the async HSM have statig::Response<rescources::State>
not Send
on purpose and I should deal with this on my side.
Am I using a wrong approach to try and create multiple state machines and manage them in some larger state machine? Or should I just make one larger with lots of superstates
. I find that having multiple would be nice to independently test how simpler state machines react to their events.
If needed I can supply a minimal reproducible main.rs
, for now here is the cargo build error I get:
note: future is not `Send` as this value is used across an await
--> /home/ivan/.cargo/git/checkouts/statig-9f81b8df0f2647e3/19da112/statig/src/inner.rs:69:13
|
67 | / self.state
68 | | .enter(&mut self.shared_storage, context, enter_levels)
| |___________________________________________________________________- has type `std::pin::Pin<Box<dyn Future<Output = ()>>>` which is not `Send`
69 | .await;
| ^^^^^^- the value is later dropped here
| |
| await occurs here, with the value maybe used later
note: required by a bound in `tokio::spawn`
|
163 | T: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
error: future cannot be sent between threads safely
--> src/rescources/prepare.rs:29:17
|
29 | async move { material_magazine.unload().await },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
|
= help: the trait `Send` is not implemented for `dyn Future<Output = statig::Response<rescources::State>>`
note: required by a bound in `tokio::spawn`
--> /home/ivan/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.26.0/src/task/spawn.rs:163:21
|
163 | T: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
warning: `millimess-1003` (bin "millimess-1003") generated 6 warnings
error: could not compile `millimess-1003` due to 2 previous errors; 6 warnings emitted
Traits like Clone & serde's (De)serialize come to mind mostly. Something like the proc-macro generating the final state machine's struct directly could work? It'd be nice to have same ability to derive traits similar to deriving them on State/SuperState.
Hey all, I've been trying to integrate this library into Bevy for helping managing entity states. However I've encountered some issues with the borrow checker and passing in mutable references via Context to the state machine. I believe the root of the issue is that in IntoStateMachine
type Ctx<'ctx>
only has a single lifetime, which is not expressive enough to pass around mutable references to structs that contain mutable borrows themselves.
I've tried to reduce the problem to some minimal examples
mod example_broken {
struct Foo;
struct Event<'a> {
value: &'a mut Foo,
}
struct Context<'a, 'b> {
event: &'a mut Event<'b>,
}
type Ctx<'a> = Context<'a, 'a>;
fn takes_context(context: &mut Ctx<'_>) {
unimplemented!()
}
fn example(mut event: Event) {
// Error: Event does not live long enough
takes_context(&mut Context { event: &mut event })
}
}
This example I think shows the root of the problem, the type alias type Ctx<'a> = Context<'a, 'a>
forces the lifetime of the borrow 'a
in event: &'a mut Event<'b>
to be the same as the data borrowed by Event 'b
. In this case, Event
contains data borrowed from the ECS system, whereas 'a
only lives for the lifetime of example
. Hence the problem, it wants event
in example to life as long as the data borrowed from the ECS system.
If we allow type Ctx<'a>
to have two lifetime parameters type Ctx<'a, 'b>
this is enough to allow the borrow checker to track the lifetimes independently.
mod example_working {
struct Foo;
struct Event<'a> {
value: &'a mut Foo,
}
struct Context<'a, 'b> {
event: &'a mut Event<'b>,
}
type Ctx<'a, 'b> = Context<'a, 'b>;
fn takes_context(context: &mut Ctx<'_, '_>) {
unimplemented!()
}
fn example(mut event: Event) {
takes_context(&mut Context { event: &mut event })
}
}
Another solution I explored was adding lifetime constraints to the function itself, however this makes the function longer a valid system for bevy.
mod example_lifetime_constraints {
struct Foo;
struct Event<'a> {
value: &'a mut Foo,
}
struct Context<'a, 'b> {
event: &'a mut Event<'b>,
}
type Ctx<'a> = Context<'a, 'a>;
fn takes_context(context: &mut Ctx<'_>) {
unimplemented!()
}
fn example<'event, 'value>(event: &'event mut Event<'value>)
where
'event: 'value,
{
takes_context(&mut Context { event: event })
}
}
Another approach is to use Rc<RefCell<Event>>
which lets us eliminate the compile checking of the problematic borrow of event so that Context
only has a single lifetime and is thus compatible with the type Ctx<'ctx>
definition.
mod example_ref_cell {
use std::{cell::RefCell, rc::Rc};
struct Foo;
struct Event<'a> {
value: &'a mut Foo,
}
impl<'a> Event<'a> {
fn mutate(&mut self) {
unimplemented!()
}
}
struct Context<'a> {
event: Rc<RefCell<Event<'a>>>,
}
type Ctx<'a> = Context<'a>;
fn takes_context(context: &mut Ctx<'_>) {
context.event.borrow_mut().mutate()
}
fn example(mut event: Event) {
let rc = Rc::new(RefCell::new(event));
for i in 1..3 {
takes_context(&mut Context { event: rc.clone() })
}
}
}
Curiously, if you pass the &mut event
directly i.e. Context
is just a type alias, it works?
mod example_bare {
struct Foo;
struct Event<'a> {
value: &'a mut Foo,
}
impl<'a> Event<'a> {
fn mutate(&mut self) {
unimplemented!()
}
}
type Context<'a> = Event<'a>;
type Ctx<'a> = Context<'a>;
fn takes_context(context: &mut Ctx<'_>) {
context.mutate()
}
fn example(mut event: Event) {
for i in 1..3 {
takes_context(&mut event);
}
}
}```
Hey all, I noticed the Bevy feature flag when installing statig
which allows StateMachine
to implement Component
however I can't seem to get this to work correctly. I get the error
the trait bound `statig::prelude::StateMachine<monster::Behaviour>: bevy::prelude::Component` is not satisfied
the following other types implement trait `bevy::prelude::Component`:
animation_link::AnimationLink
...
Code snippets
#[derive(Bundle)]
pub struct MonsterBundle {
pub scene: SceneBundle,
pub speed: Speed,
pub stats: Stats,
pub animations: NamedAnimations,
pub behaviour: StateMachine<Behaviour>,
}
#[derive(Default)]
pub struct Behaviour;
pub enum Event {
TimerElapsed,
Start,
Stop,
Dance,
}
#[state_machine(initial = "State::idle()")]
impl Behaviour {
#[state(entry_action = "enter_idle")]
fn idle(event: &Event) -> Response<State> {
...
Wrapping it in a newtype seems to work correctly
#[derive(Component)]
pub struct BehaviourState(pub StateMachine<Behaviour>);
Hi,
I really love the idea and your implementation and was wondering if it's currently possible to define states for a single state machine across multiple files, by having multiple 'impl' blocks? I tried different approaches but couldn't find a working solution. Or is that something that would be feasible and that you would maybe consider implementing?
Thanks!
Valentin
This is tangential to #7, but not completely related. I don't know if I'd consider it a bug, but it's a bit of a pain.
I've been trying to get a state machine working where I have a generic type in the state.
My initial attempt looks something like this:
use std::marker::PhantomData;
use statig::prelude::*;
pub struct Event;
pub enum State<T> {
Init,
Other(PhantomData<T>),
}
impl<T> statig::State<Machine> for State<T> {
fn call_handler(
&mut self,
machine: &mut Machine,
event: &Event,
) -> Response<Self> {
todo!()
}
}
#[derive(Default)]
pub struct Machine;
impl<T> statig::StateMachine for Machine {
type Event<'a> = Event;
type State = State<T>;
type Superstate<'a> = ();
const INITIAL: Self::State = State::Init;
}
fn main() {
let machine = Machine::default().state_machine().init();
}
However I get the error:
error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
--> src/main.rs:26:6
|
26 | impl<T> statig::StateMachine for Machine {
| ^ unconstrained type parameter
For more information about this error, try `rustc --explain E0207`.
Okay, fair enough. So I add to the Machine
itself to constrain it:
use std::marker::PhantomData;
use statig::prelude::*;
pub struct Event;
pub enum State<T> {
Init,
Other(PhantomData<T>),
}
impl<T> statig::State<Machine<T>> for State<T> {
fn call_handler(
&mut self,
machine: &mut Machine<T>,
event: &Event,
) -> Response<Self> {
todo!()
}
}
#[derive(Default)]
pub struct Machine<T> {
_marker: PhantomData<T>,
}
impl<T> statig::StateMachine for Machine<T> {
type Event<'a> = Event;
type State = State<T>;
type Superstate<'a> = ();
const INITIAL: Self::State = State::Init;
}
fn main() {
let machine = Machine::<u32>::default().state_machine().init();
}
But now I get an even stranger error message:
error[E0309]: the parameter type `T` may not live long enough
--> src/main.rs:31:27
|
31 | type Superstate<'a> = ();
| ^^- help: consider adding a where clause: `where T: 'a`
| |
| ...so that the type `State<T>` will meet its required lifetime bounds
For more information about this error, try `rustc --explain E0309`.
If I change the line to this, as it suggests:
type Superstate<'a> = () where T: 'a;
I now get a third different error:
error[E0276]: impl has stricter requirements than trait
--> src/main.rs:31:39
|
31 | type Superstate<'a> = () where T: 'a;
| ^^ impl has extra requirement `T: 'a`
For more information about this error, try `rustc --explain E0276`.
I've tried several things to try getting around the two latter errors, but I've given up sadly. :(
I'm not sure how to get generics to work in state, apart from forking statig and removing every mention of Superstate, which is a very... hacky brute-force solution.
Do you have any ideas on things I could try? I've unfortunately run out of ideas.
It is a well documented and clean library on HSM. I would have used it in my next project but my use case demands to perform async operations inside state handler and possibly in actions.
It would be nice to have a crate feature to enable async support for such use cases. By default, it can be off.
It would be convenient if superstates supported initial states just like the state_machine does. Any possibility of adding support for this? Or perhaps adding support for "initial transitions" rather than initial states (so that you could implement an action on the transition)?
Thinking something like
#[state_machine(visualize)]
impl MyStruct { ... }
fn main() {
let mut state_machine = Blinky::default().uninitialized_state_machine().init();
let ascii_visualization: String = state_machine.visualize();
println!("{}", ascii_visualiztion);
}
Which would print something like
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Top
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│
┌────────────┴────────────┐
│ │
┌─────────────────────┐ ╔═════════════════════╗
│ Blinking │ ║ NotBlinking ║
│─────────────────────│ ╚═════════════════════╝
│ counter: &'a usize │
└─────────────────────┘
│
┌────────────┴────────────┐
│ │
╔═════════════════════╗ ╔═════════════════════╗
║ LedOn ║ ║ LedOff ║
║─────────────────────║ ║─────────────────────║
║ counter: usize ║ ║ counter: usize ║
╚═════════════════════╝ ╚═════════════════════╝
Looking to continue the discussion from #3 for refactoring the async implementation to work in both no_std
and no_alloc
environments. In particular, I would like to use this crate within an embassy-rs project.
In the previous issue, you said you had thoughts as to how you could statically allocate space for the futures. Do you still view this as a viable approach? I have not had luck in finding this done elsewhere.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.