Reactions

Reactions are one of the most important features of the YAKINDU statechart language. Basically everything that happens in a statechart happens in a reaction.

Reactions can be attached to states as well as to transitions. While states can have as many reactions as you wish, transitions can only have one.

The syntax follows the standards of automata theory, where the output is put into relation to the input of the automaton using the following notation (especially Mealy automata):

input / output

YAKINDU Statechart Tools uses the following syntax for all reactions:

trigger [ guard ] / effect

The possible values that can be specified as a trigger depend on the context, which is either a state or a transition.

All three components are grammatically optional, though a reaction with neither a trigger nor a guard will never happen. Reactions with no effect are useful for transitions, the slash / can be omitted as well then.

Trigger specifications

Reactions can define a trigger that needs to occur to activate the reaction. Events are possible triggers, as well as certain keywords defined in the statechart language.

Triggers can be combined with guards to further define the execution conditions of the reaction. In that case, the reaction is only executed if both the trigger occurs and the guard condition is true.

You can define multiple triggers for a reaction, separated by comma: trigger1, trigger2. For the combined trigger to fire, it is sufficient that one of the individual triggers occurs.

There are a couple of special triggers defined by the statechart language:

Examples:

  • A reaction shall occur if an event named ev1 happens: ev1 / effect
  • A reaction shall occur if ev1 or ev2 happen: ev1, ev2 / effect
  • A reaction shall occur if ev1 happened or three seconds passed: ev1, after 3s / effect

Figure "Event specifications syntax overview" shows where to put event specifications in a reaction trigger and how their syntax is defined.


Event specifications syntax overview

Event specifications syntax overview

Reaction trigger "after"

The after trigger specifies a one-shot time event.

After the specified period of time the reaction is triggered. An after trigger can be used in transitions of states as well as in local reactions of states and statecharts. The specified period of time starts when the state or statechart is entered.

after 20 s

Synopsis: after time unit

The time value is a integer literal or an expression with an integer value.

The time unit is one of:

  • s: seconds
  • ms: milliseconds
  • us: microseconds
  • ns: nanoseconds

Reaction trigger "every"

The every trigger specifies periodic time events.

The reaction is triggered recurrently after each passing of the specified period of time. An every trigger can be used in transitions as well as in local reactions of states and statecharts. The specified period of time starts when the state or statechart is entered and repeats periodically.

every 200 ms

Synopsis: every time unit

The time value is a integer literal or an expression with an integer value.

The time unit is one of:

  • s: seconds
  • ms: milliseconds
  • us: microseconds
  • ns: nanoseconds

Reaction trigger "always"

The always trigger is always true and enables a reaction to be executed in every run-to-completion step (RTC). This trigger is equivalent to the oncycle trigger.

Reaction trigger "oncycle"

The oncycle trigger is always true and enables a reaction to be executed in every run-to-completion step (RTC). This trigger is equivalent to the always trigger.

Reaction trigger "else"

The else trigger is intended to be used for the outgoing transitions of choice pseudo states to make sure that there is always an outgoing transition that can be taken. It can only be be used in transitions and implies the lowest evaluation priority for that transition. The default trigger is equivalent to the else trigger.

Reaction trigger "default"

The default trigger is intended to be used for the outgoing transitions of choice pseudo states to make sure that there is always an outgoing transition that can be taken. It can only be be used in transitions and implies the lowest evaluation priority for that transition. The default trigger is equivalent to the else trigger.

Reaction trigger "entry"

An entry trigger marks actions that are carried out when entering a state or state machine, immediately before making it active.

Please note: A self-transition, leading from a state back to itself, actually first leaves and then re-enters the state. That means that its exit effects as well as its entry effects are executed. This is in contrast to a local reaction, which is executed without leaving the active state. Thus exit and entry effects are not executed in the latter case.

Reaction trigger "exit"

An exit trigger marks actions that are carried out when exiting a state or state machine, immediately after making it inactive.

Please note: A self-transition, leading from a state back to itself, actually first leaves and then re-enters the state. That means that its exit effects as well as its entry effects are executed. This is in contrast to a local reaction, which is executed without leaving the active state. Thus exit and entry effects are not executed in the latter case.

Guard conditions

A guard condition, or simply guard, is another option that can be used to specify when a reaction should be executed. Guard conditions can be used on their own or in conjunction with a specified trigger . In the latter case, the trigger needs to be fired and the guard needs to be true for the reaction to fire its effect.

As a reminder, the complete reaction syntax is as follows:

trigger [ guard ] / effect

In the square brackets you can use any expression that evaluates to a boolean. Boolean variables, all kinds of comparisons, and operations returning a boolean fall into this category.

Please note: YAKINDU Statechart Tools does not support the concept of „truthy and falsey” seen in C, Javascript or Python. [x] is not a valid guard condition, unless the type of x is boolean.

Please consult figure "Guard condition syntax overview" to find out how a guard condition syntactically fits into a reaction.


Guard condition syntax overview

Guard condition syntax overview

Actions and reaction effects

A transition specification may contain zero, one or more actions that are executed when the transition is taken. An action could be something like assigning a value to a variable, calling an operation, or raising an event. All actions taken together are called the transition’s reaction effect, or simply effect. The complete syntax for reactions is as follows:

trigger [ guard ] / effect

This means that the effect will only be executed if the trigger fires or does not exist and the guard condition evaluates to true or does not exist and at least a trigger or a guard exist. See the sections about triggers and guard conditions.

Normally, actions are statements. Note that you can also use the conditional expression to make some constructs more elegant.

If an effect shall consist of multiple actions, they must be separated by a semicolon. That means the last action is not followed by a semicolon. The syntax is:

trigger [ guard ] / action1 ; action2 ; action3 ;

A reaction in a state always has a reaction effect with at least one action.

Figure "Reaction effect syntax" shows the syntax definition.


Reaction effect syntax

Reaction effect syntax

Example: When a transition or local reaction specified as ev1 [x > 3] / x += 1; notify(x); raise ev2 is executed, it increments variable x by 1, calls the notify operation with x as parameter, and raises event ev2.

Raising and processing an event

For the syntax of event raising, see section "Raising an event".
For the syntax of event declaration, see section "Events".

When a reaction raises an event, the subsequent behaviour of the state machine is highly dependent on the execution scheme, selected by either the @CycleBased (default) or the @EventDriven annotation. In short:

  • In the cycle-based execution scheme, the raised event will be added to the events that are processed by the current run-to-completion step. However, it will only be visible „downstream” in the run cycle, i.e., active states that have not yet been executed can take the event into account. When the run cycle is completed, the event will cease to exist. It can thus not be processed in any subsequent run-to-completion step.
  • In the event-driven execution scheme, the raised event will be added to an event queue. As a consequence, it will not be visible „downstream” in the current run cycle. Instead, the event will trigger a run cycle on its own. In that run-to-completion step, it will be visible everywhere.

The following sections will give a more in-depth view on these different behaviours.

Processing an event in the cycle-based execution scheme

As explained above, an event that has been raised using the raise statement exists in the current run-to-completion step only and thus can be dealt with during the rest of that particular run-to-completion step only.

Please also note that orthogonal regions are executed sequentially and in a deterministic order.

This has remarkable consequences on where an event is „visible” and where it is not. Let’s say we have a state machine at a certain point in time where n orthogonal regions are active. The regions ri have a defined execution order: r1r2 → … → rn-1rn.

If an event e is raised in region rm, then e is available only in those regions that have not been processed yet, i. e. in ri where i > m and in.

The following example illustrates this. When the state machine is started, the situation is as shown in figure "Raising an event [1]": A and X in the parallel regions r1 and r2 are active states. Important: Region r1 is executed before r2.


Raising an event [1]

Raising an event [1]

Now event e1 occurs. It triggers transition A → B in r1. This transition’s effect raise e2 causes event e2 to be raised. This event triggers transition X → Y in r2 to fire within the same RTC.

After that, the situation is now as can be seen in figure "Raising an event [2]": substates B and Y are now active states. However, the transition B → C will not be taken, because that would have to happen in another RTC. But by then the particular e2 instance raised by A → B will be long gone, since it was restricted to the RTC it has been raised in.


Raising an event [2]

Raising an event [2]

Orthogonal regions within a composite state are always executed from left to right or from top to bottom, respectively. On the top level, where orthogonal regions can be graphically arranged at will, the execution order is defined by the statechart model’s property region priority. To inspect or change it, open the properties view and select the statechart’s definition section.

The region priorities of the example above are shown in figure "Region priorities". Change the execution order and the example will behave differently than described.


Region priorities

Region priorities

Not being able to raise an event and process it in „earlier” regions is a restriction you can overcome using variables. The idea is to set a „communication variable” to a certain value in a region that is processed „later”. During the next RTC the earlier region could act on the variables value. Another option – and in many cases the better on, is to change to the event-driven execution scheme.

Processing an event in the event-driven execution scheme

As explained above, an event that has been raised using the raise statement is appended to an event queue, and thus it is not visible in the current run-to-completion step (RTC). Since in the event-driven execution scheme an RTC always processes a single event at a time, only the event that caused the RTC to execute in the first place remains visible „downstream” in the current RTC.

The raised event will, however, trigger an RTC on its own. In that RTC, it will be visible to all regions, no matter of their execution order. The execution order as explained in the preceeding section still applies, but it is of less importance in the event-driven approach.

Events raised internally by the raise statement in the reaction of a state or transition take precedence over events that entered the state machine from the outside. This convention ensures that an external event together with all effects it causes – and in particular with all events raised internally – are processed completely, before the next external event can take charge.

Consider, for example, events a and b having entered, in that order, a state machine that is executing in event-driven mode. Since a entered the state machine first, it is processed first in an RTC. Event b is put on a queue for external events, where it has to wait until a is done. Processing of a, however, leads to another event c being raised via the raise statement. Since a is still being processed, c is, similar to b, also queued.

However, c is put on a different event queue, a queue that is reserved for internal events only. Events in the internal event queue are always processed first. The external event queue will be considered only if the internal event queue is empty. As a consequence for our example, the order of events being processed by successive run-to-completion steps is a, c, b.

Please note: There is only a single internal event queue, and all internally-raised events are appended to it. Let’s consider the example above again and let’s assume that processing of a not only raised the internal event c, but also an additional event d. Since both c and d are going to the internal event queue, the overall processing sequence is a, c, d, b. Now let’s also assume that in the course of processing c yet another event e is raised. Like all internally-raised events, it will be appended to the end of the internal event queue, i.e., behind d, not behind c, resulting in an overall execution sequence of a, c, d, e, b. In other words, the state machine is taking a breadth-first approach on internally raised events, in lieu of depth-first.

Please also note: In event-driven execution mode, there’s a looming risk of endless event processing you should be aware of. In the example above, imagine processing of event c would always raise another instance of c. In that case, the internal event queue would never get empty, the state machine would be busily processing one c after the other, without ever ending and without ever getting to b.