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

Support for wildcard internal transitions #80

Merged
merged 7 commits into from
Jul 22, 2024
Merged

Support for wildcard internal transitions #80

merged 7 commits into from
Jul 22, 2024

Conversation

dkumsh
Copy link

@dkumsh dkumsh commented Jul 12, 2024

This PR

  • adds support for wildcard and implicit internal transitions
  • renames custom_guard_error flag to custom_error (as the flag is not specific to the guards anymore)
  • fixes minor bug: invalid Ident with trailing space, breaking IDE rendering the macro (the fix trims the string before converting it to Ident)
  • re-ordering on_exit/on_entry hooks (see justification here )

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.

Copy link
Collaborator

@ryan-summers ryan-summers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor comments. I'm also confused how the wild-card transition will avoid generating transitions for previously-defined wildcard states. For example:

statemachine! {
    *State1 + Event = State2,
    _ + Event,
}

How do we ensure that there is no State1 + Event = State1 transition generated, since there's already a state+event combo for this?

README.md Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
macros/src/codegen.rs Show resolved Hide resolved
macros/src/parser/mod.rs Outdated Show resolved Hide resolved
@dkumsh
Copy link
Author

dkumsh commented Jul 15, 2024

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
Copy link
Author

dkumsh commented Jul 15, 2024

I pushed the fixes.

@dkumsh dkumsh requested a review from ryan-summers July 15, 2024 19:29
tests/test.rs Outdated Show resolved Hide resolved
@dkumsh
Copy link
Author

dkumsh commented Jul 18, 2024

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

@dkumsh
Copy link
Author

dkumsh commented Jul 19, 2024

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
Copy link
Collaborator

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.

macros/src/codegen.rs Outdated Show resolved Hide resolved
@dkumsh
Copy link
Author

dkumsh commented Jul 21, 2024

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
Copy link
Collaborator

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

@ryan-summers ryan-summers merged commit 9da1c1c into korken89:master Jul 22, 2024
6 checks passed
@dkumsh dkumsh deleted the feat/wildcard-internal-transitions branch July 22, 2024 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants