This section shows a quasi-UML state machine snipet and the equivalent SMC code. I use the word "quasi" because SMC is not directly derived from UML or Harel state machines. That means that there are capabilities in UML that are not in SMC and vice versa. See the SMC FAQ for why this is.
Instantiating a Finite State Machine
Care must be taken when instantiating an SMC-generated finite state machine. The application class passes a its reference to the FSM context and this reference is used when FSM actions are performed. It is safe to instantiation an FSM within a constructor but unsafe to enter the start state while in a constructor because the start state entry actions will call your application object before its constructor has completed.
Prior to SMC v. 6, the FSM constructor incorrectly used the this pointer which meant instantiating the FSM within the application constructor could lead to incorrect behavior. SMC v. 6 corrects this problem and it is now safe to instantiate the FSM within your application constructor.
The enterStartState method should be
called only once after instantiating the finite state
machine and prior to issuing any transition. This
method is unprotected and does not prevent its being
called multiple times. If
enterStartState is called after the
first transition, then incorrect behavior may occur.
|
A Simple Transition
// State
Idle
{
// Trans Next State Actions
Run Running {}
}
|
A state and transition names must have the form "[A-Za-z_][A-Za-z0-9_]*".
A Jump Transition
// State
Idle
{
// Trans Next State Actions
Run jump(Running) {}
}
|
The Jump transition is equivalent to the Simple Transition and is provided since this transition is used in an Augmented Transition Network.
In a future SMC release, the Jump transition will become the only way for make an end state outside the current map. With the syntax : jump(AnotherMap::NextState) |
An External Loopback Transition
// State
Idle
{
// Trans Next State Actions
Timeout Idle {}
}
|
An external loopback does leave the current state and comes back to it. This means that the state's exit and entry actions are executed. This is in contrast to an internal loopback transition.
An Internal Loopback Transition
// State
Idle
{
// Trans Next State Actions
Timeout nil {}
}
|
Using "nil" as the next state causes the transition to remain in the current state and not leave it. This means that the state's exit and entry actions are not executed. This is in contrast to an external loopback transition.
A Transition with Actions
// State
Idle
{
// Trans
Run
// Next State
Running
// Actions
{
StopTimer("Idle");
DoWork();
}
}
|
- A transition's actions must be enclosed in a "{ }" pair.
- The action's form is "[A-Za-z][A-Za-z0-9_-]*(<argument list>)". The argument list must be either empty or consist of comma-separated literals. Examples of literals are: integers (positive or negative, decimal, octal or hexadecimal), floating point, strings enclosed in double quotes, constants and transition arguments.
- Actions must be member functions in the %class class and accessible by the state machine. Usually that means public member functions in C++ or package in Java.
- An integer number (e.g. 1234).
- A float number (e.g. 12.34).
- A string (e.g. "abcde").
- A transition argument.
- A constant, #define or global variable.
-
An independent subroutine or method call
(e.g. event.getType()).
Note: this subroutine/method call may also include arguments.
If you want to call a method in the
context class,
then use the ctxt
variable. For example,
if the context class contains a method
getName()
and you want to call it inside
an action's argument list, then write
ctxt.getName()
.
Go here for sample
code using the ctxt
variable.
Note: Only use ctxt
inside argument
lists and transition guards.
Transition Guards
// State
Idle
{
// Trans
Run
// Guard condition
[ctxt.isProcessorAvailable() == true &&
ctxt.getConnection().isOpen() == true]
// Next State
Running
// Actions
{
StopTimer("Idle");
DoWork();
}
Run nil {RejectRequest();}
}
|
The guard must contain a condition that is valid target language source code - that is, it would be a valid "if" statement. Your guard may contain &&s, ||s, comparison operators (==, <, etc.) and nested expressions. SMC copies your guard condition verbatim into the generated output.
Note: If your are calling a context class
method, then you must prefix the method with
ctxt
- SMC will not append
ctxt
for you.
If the guard condition evaluates to true, then the transition is taken. If the guard condition evaluates to false, then one of the following occurs (ordered by precedence):
- If the state has another guarded transition with the same name and arguments, that transition's guard is checked.
- Failing that, If the state has another unguarded transition with the same name and argument list, that transition is taken.
- If none of the above, then the default transition logic is followed.
A state may have multiple transitions with the same name and argument list as long as they all have unique guards. When a state does have multiple transitions with the same name, care must be taken when ordering them. The state machine compiler will check the transitions in the same top-to-bottom order that you use except for the unguarded version. That will always be taken only if all the guarded versions fail. Guard ordering is only important if the guards are not mutually exclusive, i.e., it is possible for multiple guards to evaluate to true for the same event.
Allowable argument types for a transition guard are the same as for a transition action.
Transtion Arguments
// State
Idle
{
// Transition
Run(msg: const Message&)
// Guard condition
[ctxt.isProcessorAvailable() == true &&
msg.isValid() == true]
// Next State
Running
// Actions
{
StopTimer("Idle");
DoWork(msg);
}
Run(msg: const Message&)
// Next State Actions
nil {RejectRequest(msg);}
}
|
Note: When using transition guards and
transition arguments, multiple instances of the same
transition must have the same argument list. Just as with
C++ and Java methods, the transitions
Run(msg: const Message&)
and
Run()
are not the same transition.
Failure to use the identical argument list when defining
the same transition with multiple guards will result in
incorrect code being generated.
While Tcl is a type-less language, Tcl does distinguish between call-by-value and call-by-reference. By default SMC will generate call-by-value Tcl code if the transition argument has no specified type. But you may use the artificial types "value" or "reference".
If your Tcl-targeted FSM has a transition:
then the generated Tcl is:
If your Tcl-targeted FSM has a transition:
then the generated Tcl is:
The method workOn
must upvar
the task parameter:
While Lua/Python/Ruby is a dynamically typed language and does not use types for function parameter definitions, you could provide a optional data type for transition arguments. This "data type" is ignored when generating the target Lua/Python/Ruby code. I suggest using meaningful type names.
While Groovy/PHP gives the choice between static and dynamic typing, you could provide a optional data type for transition arguments. In this case, the type is used when generating the target Groovy/PHP code.
The PHP variable syntax is like Perl (named with '$').
While Perl is a dynamically typed language and does not use types for function parameter definitions, you could provide a optional data type for transition arguments. This "data type" is ignored when generating the target Perl code. I suggest using meaningful type names.
Only Perl scalar values (ie, named with '$') are allowed.
Entry and Exit Actions
// State
Idle
Entry {StartTimer("Idle", 1); CheckQueue();}
Exit {StopTimer("Idle");}
{
// Transitions
}
|
When a transition leaves a state, it executes the state's exit actions before any of the transition actions. When a transition enters a state, it executes the state's entry actions. A transition executes actions in this order:
- "From" state's exit actions.
- Set the current state to null.
- The transition actions in the same order as defined in the .sm file.
- Set the current state to the "to" state.
- "To" state's entry actions.
enterStartState
method which executes the
start state's entry acitons. It is now up to the
application to call the start method after instantiating
the finite state machine context. If it is not
appropriate to execute the entry actions upon start up,
then do not call enterStartState
. You are
not required to call this method to set the finite state
machine start state. That is done when the FSM is
instantiated. This method is used only to execute the
start state's entry actions.
If you do call this method, be sure to do it outside of
the context class' constructor. This is because entry
actions call context class methods. If you call
enterStateState
from within you context
class' constructor, the context instance will be
referenced before it has completed initializing which is
a bad thing to do.
enterStartState
does not protect against
being called multiple times.
It should be called at most once and prior to issuing any
transitions. Failure to follow this requirement may
result in inappropriate finite state machine behavior.
Whether a state's Entry and Exit actions are executed depends on the type of transition taken. The following table shows which transitions execute the "from" state's Exit actions and which transitions execute the "to" state's Entry actions.
Transition Type |
Execute "From" State's Exit Actions? |
Execute "To" State's Entry Actions? |
---|---|---|
Simple Transition | Yes. | Yes. |
External Loopback Transition | Yes. | Yes. |
Internal Loopback Transition | No. | No. |
Push Transition | No. | Yes. |
Pop Transition | Yes. | No. |
WARNING! Entry and exit actions are not supported for the Default state which is not an actual state. See more in the "Default Transitions" section.
From this point on, SMC diverges from UML. SMC uses the idea of multiple machines and pushing and popping states as way of breaking complicated behavior up into simpler parts. UML acheives much the same by grouping states into superstates. They may be equivalent in ability but I find the idea of pushing to a new state easier to understand because it is similar to the subroutine call. |
Push Transition
Running
{
Blocked push(WaitMap::Blocked) {GetResource();}
}
|
Note: The end state does not have to be in another map - it could be in the same %map construct. Conversely, a plain transition's end state may be in another map. But chances are that you will set up maps so that you will push to another map's state and simple transitions will stay within the same map. You use multiple maps for the same reason you create multiple subroutines: to separate out functionality into easy-to-understand pieces.
This causes the state machine to:
-
Transition to the
BlockPop
state. -
Execute the
BlockPop
entry actions. -
Push to the
WaitMap::Blocked
state. -
Execute the
WaitMap::Blocked
entry actions.
When WaitMap
issues a
pop transition, control will
return to BlockPop
and the pop transition
is issued from there.
Use this new syntax when a state has two different transitions which push to the same state but need to handle the pop transition differently. For example:
Pop Transition
Waiting
{
Granted pop(OK) {cleanUp();}
Denied pop(FAILED) {cleanUp();}
}
|
The pop transition differs from the simple and push transition in that:
- The end state is not specified. That is because the pop transition will return to whatever state issued the corresponding push.
- There pop transition has an optional argument: a transition name.
In the above example, if the resource request is granted, the state machine returns to the corresponding state that did the push and then takes that state's OK transition. If the request is denied, the same thing happens except the FAILED transition is taken. The code for the corresponding push transition is:
As of SMC v. 1.2.0, additional arguments may be added
after the pop transition's transition argument.
These additional arguments are like any others passed
to an action and will be passed into the named
transition. Following the above example, given the
pop transition pop(FAILED, errorCode, reason)
,
then the FAILED
should be coded as:
Default Transitions
What happens if a state receives a transition that is not defined in that state? SMC has two separate mechanisms for handling that situation.
The first is the "Default" state. Every %map may have a special state named "Default" (the uppercase D is significant). Like all other states, the Default state contains transitions.
Default state transitions may have guards and arguments features as non-default transitions. This means the Default state may contain multiple guarded and one unguarded definition for the same transition.
The second mechanism is the "Default" transition. This is placed inside a state and is used to back up all transitions.
Because any transition can fall through to the Default transition, Default transitions:
- May not have an argument list.
- A Default transition may take a guard.
- Putting a Default transition in the Default state means that all transitions will be handled - it is the transition definition of last resort.
Transition Precedence
Transition definitions have the following precedence:
- Guarded transition
- Unguarded transition
- The Default state's guarded definition.
- The Default state's unguarded definition.
- The current state's guarded Default transition.
- The current state's unguarded Default transition.
- The Default state's guarded Default transition.
- The Default state's unguarded Default transition.
Since SMC does not force you to specify a Default state or a Default transition, it is possible that there is no transition defined. If SMC falls through this list, it will throw a "Transition Undefined" exception. |