korken89 / smlang-rs

A State Machine Language DSL procedual macro for Rust
Apache License 2.0
193 stars 28 forks source link

Support for wildcard internal transitions #80

Closed dkumsh closed 2 weeks ago

dkumsh commented 3 weeks ago

This PR

The DSL supports internal transitions. Internals transition allow to accept some event and process an action and then stay in the current state. Internal transitions can be specified implicitly, e.g.

State2 + Event2 / event2_action = State2,

and now, after this change, can be specified explicitly, using underscore ('_')

State2 + Event2 / event2_action = _,

or implicitly, by omitting the target state including symbol '='.

State2 + Event2 / event2_action,

It is also possible to define wildcard implicit (or explicit using '_') internal transitions.

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,

        _ + Event2 / event2_action,
    },
}

The example above demonstrates, how you could make Event2 acceptable in any state, not covered by any of the previous transitions, and to do an action to process it.

It is equivalent to:

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,

        State2 + Event2 / event2_action = State2,
        State3 + Event2 / event2_action = State3,
        State4 + Event2 / event2_action = State4,
    },
}

See also examples/wildcard_states_and_internal_transitions.rs or examples/internal_transitions_with_data.rs for a usage example.

dkumsh commented 3 weeks ago

I'm also confused how the wild-card transition will avoid generating transitions for previously-defined wildcard states.

This actually works out of the box. I did not do anything for this, as it is covered by the wildcard transitions support, which had already been there. I only extended support to allow internal transitions including in the wildcard context. Under the hood this only means that when target state is not explicitly specified, it is assumed to be an internal transition and in that case the input state becomes the target state. For example in test_wildcard_states_and_internal_transitions() :

    statemachine! {
        transitions: {
            *State1 + Event2 = State2,
            State2 + Event3 = State3,
            _ + Event1 / increment_count,      // Internal transition (implicit: omitting target state)
            _ + Event3 / increment_count = _ , // Internal transition (explicit: using _ as target state)
        },
        derive_states: [Debug, Clone,  Copy]
    }

The macro generates the following transitions for processing Event3:

State2 + Event3 = State3,
State1 + Event3 / increment_count = State1,
State3 + Event3 / increment_count = State3,

And it is validated by the test, that Event3 in State2 does not increment the count (current state is State2, and the count is 1 before the following assertion is invoked):

assert_transition!(sm, Events::Event3, States::State3, 1);

while the same Event3 in State3, does increment

    assert_transition!(sm, Events::Event3, States::State3, 3);
dkumsh commented 3 weeks ago

I pushed the fixes.

dkumsh commented 3 weeks ago

Hi @ryan-summers , would you approve this PR or we have anything undecided ?

dkumsh commented 2 weeks ago

Hi @ryan-summers,

I have rebased due to a merge conflict. I noticed that a new callback, transition_callback, had been added. Now, the transition logic looks as follows, and I am wondering about the purpose of having two different callbacks at practically the same point, transition_callback and on_entry. Should we consider using just one?

#on_exit
#action_code
let out_state = #states_type_name::#out_state;
self.context.log_state_change(&out_state);
self.context().transition_callback(&self.state, &out_state); // transition_callback
self.state = out_state;
#on_entry    // on_entry callback
ryan-summers commented 2 weeks ago

Hi @ryan-summers,

I have rebased due to a merge conflict. I noticed that a new callback, transition_callback, had been added. Now, the transition logic looks as follows, and I am wondering about the purpose of having two different callbacks at practically the same point, transition_callback and on_entry. Should we consider using just one?

#on_exit
#action_code
let out_state = #states_type_name::#out_state;
self.context.log_state_change(&out_state);
self.context().transition_callback(&self.state, &out_state); // transition_callback
self.state = out_state;
#on_entry    // on_entry callback

The purpose of the transition_callback is that its called on all transitions, where on_entry is a unique function per entry state. That being said, there does feel like some redundancy here, specifically around log_state_change. Maybe we can simply update log_state_change to be replaced by transition_callback.

dkumsh commented 2 weeks ago

The purpose of the transition_callback is that its called on all transitions, where on_entry is a unique function per entry state. That being said, there does feel like some redundancy here, specifically around log_state_change. Maybe we can simply update log_state_change to be replaced by transition_callback.

Okay, I'll leave it to you to decide what to do with this as this is not part of this PR.

Thanks

ryan-summers commented 2 weeks ago

Thanks for the PR and patience in getting this merged while I was traveling :)