Include code in statecharts

A statechart can specify code that needs to be executed under some circumstances. For example, the preamble property on a statechart, the guard or action on a transition or the on entry and on exit properties for a state may all contain code.

In Sismic, these pieces of code can be evaluated and executed by Evaluator instances.

Built-in Python code evaluator

By default, Sismic provides two built-in Evaluator subclasses:

  • A default PythonEvaluator that allows the statecharts to execute Python code directly.
  • A DummyEvaluator that always evaluates to True and silently executes nothing when it is called. Its context is an empty dictionary.

The key point to understand how an evaluator works is the concept of context, which is a dictionary-like structure that contains the data that is exposed to the code fragments contained in the statechart (ie. override __locals__).

As an example, consider the following partial statechart definition.

statechart:
  # ...
  preamble: |
    x = 1
    y = 0
  root state:
    name: s1
    on entry: x += 1

When the statechart is initialized, the context of the PythonEvaluator is {'x': 1, 'y': 0}. When s1 is entered, the code will be evaluated with this context. After the execution of x += 1, the context associates 2 to x.

More precisely, every state and every transition has a specific evaluation context. The code associated with a state is executed in a local context which is composed of local variables and every variable that is defined in the context of the parent state. The context of a transition is built upon the context of its source state.

Note

While you have full access to an ancestor’s context, the converse is not true: every variable that is defined in a context is NOT visible by any other context, except the ones that are nested.

When a PythonEvaluator instance is initialized, an initial context can be specified:

from sismic.code import PythonEvaluator
import math as my_favorite_module

evaluator = PythonEvaluator(initial_context={'x': 1, 'math': my_favorite_module})

For convenience, the initial context can be directly provided to the constructor of an Interpreter.

Note

The initial context is evaluated before any code contained in the statechart. As a consequence, this implies that if a same variable name is used both in the initial context and in the YAML, the value set in the initial context will be overridden by the value set in the YAML definition.

yaml = """statechart:
  name: example
  preamble:
    x = 1
  root state:
    name: s
"""

statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2})
print(interpreter.context['x'])

In this example, the value of x in the statechart is set to 1 while the initial context sets its value to 2. However, as the initial context is evaluated before the statechart, the value of x is 1.

This is a perfectly normal, expected behavior. If you want to define variables in your statechart that can be overridden by an initial context, you should check this variable does not exist in locals(). For example, using

if not 'x' in locals():
    x = 1

or equivalently,

x = locals().get('x', 1)

Warning

Under the hood, a Python evaluator makes use of eval() and exec() with global and local contexts. This can lead to some weird issues with variable scope (as in list comprehensions or lambda’s). See this question on Stackoverflow for more information.

Features of the built-in Python evaluator

Depending on the situation (state entered, guard evaluation, etc.), the context is populated with additional entries. These entries are covered in the docstring of a PythonEvaluator:

class sismic.code.PythonEvaluator(interpreter=None, *, initial_context=None)

A code evaluator that understands Python.

Depending on the method that is called, the context can expose additional values:

  • On both code execution and code evaluation:
    • A time: float value that represents the current time exposed by the interpreter.
    • An active(name: str) -> bool Boolean function that takes a state name and return True if and only if this state is currently active, ie. it is in the active configuration of the Interpreter instance that makes use of this evaluator.
  • On code execution:
    • A send(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises an internal event with it.
    • If the code is related to a transition, the event: Event that fires the transition is exposed.
  • On guard or contract evaluation:
    • If the code is related to a transition, the event: Event that fires the transition is exposed.
  • On guard or contract (except preconditions) evaluation:
    • An after(sec: float) -> bool Boolean function that returns True if and only if the source state was entered more than sec seconds ago. The time is evaluated according to Interpreter’s clock.
    • A idle(sec: float) -> bool Boolean function that returns True if and only if the source state did not fire a transition for more than sec ago. The time is evaluated according to Interpreter’s clock.
  • On contract (except preconditions) evaluation:
    • A variable __old__ that has an attribute x for every x in the context when either the state was entered (if the condition involves a state) or the transition was processed (if the condition involves a transition). The value of __old__.x is a shallow copy of x at that time.
  • On contract evaluation:
    • A sent(name: str) -> bool function that takes an event name and return True if an event with the same name was sent during the current step.
    • A received(name: str) -> bool function that takes an event name and return True if an event with the same name is currently processed in this step.

If an exception occurred while executing or evaluating a piece of code, it is propagated by the evaluator.

Each piece of code is executed with (a partially isolated) local context. Every state and every transition has a specific execution context. The code associated with a state is executed in a local context which is composed of local variables and every variable that is defined in the context of the parent state (and so one until the root context is reached). The context of a transition is built upon the context of its source state. The specific context of a state is available through the context_for method of a PythonEvaluator.

Parameters:
  • interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
  • initial_context (Optional[Mapping[str, Any]]) – a dictionary that will be used as __locals__

Note

The documentation below explains how an evaluator is organized and what does the default built-in Python evaluator. Readers that are not interested in tuning existing evaluators or creating new ones can skip this part of the documentation.

Anatomy of a code evaluator

An Evaluator must provide two main methods and an attribute:

Evaluator._evaluate_code(code, *, additional_context=None)

Generic method to evaluate a piece of code. This method is a fallback if one of the other evaluate_* methods is not overridden.

Parameters:
  • code (str) – code to evaluate
  • additional_context (Optional[Mapping[str, Any]]) – an optional additional context
Return type:

bool

Returns:

truth value of code

Evaluator._execute_code(code, *, additional_context=None)

Generic method to execute a piece of code. This method is a fallback if one of the other execute_* methods is not overridden.

Parameters:
  • code (str) – code to execute
  • additional_context (Optional[Mapping[str, Any]]) – an optional additional context
Return type:

List[Event]

Returns:

a list of sent events

Evaluator.context

The context of this evaluator. A context is a dict-like mapping between variables and values that is expected to be exposed when the code is evaluated.

Return type:Mapping[str, Any]

None of the two methods is actually called by the interpreter during the execution of a statechart. These methods are fallback methods, meaning they are implicitly called when one of the following methods is not defined in a concrete evaluator instance:

class sismic.code.Evaluator(interpreter=None, *, initial_context=None)

Abstract base class for any evaluator.

An instance of this class defines what can be done with piece of codes contained in a statechart (condition, action, etc.).

Notice that the execute_* methods are called at each step, even if there is no code to execute. This allows the evaluator to keep track of the states that are entered or exited, and of the transitions that are processed.

Parameters:
  • interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
  • initial_context (Optional[Mapping[str, Any]]) – an optional dictionary to populate the context
on_step_starts(event=None)

Called each time the interpreter starts a macro step.

Parameters:event (Optional[Event]) – Optional processed event
Return type:None
execute_statechart(statechart)

Execute the initial code of a statechart. This method is called at the very beginning of the execution.

Parameters:statechart (Statechart) – statechart to consider
Return type:List[Event]
Returns:a list of sent events
evaluate_guard(transition, event)

Evaluate the guard for given transition.

Parameters:
  • transition (Transition) – the considered transition
  • event (Event) – instance of Event if any
Return type:

bool

Returns:

truth value of code

execute_action(transition, event)

Execute the action for given transition. This method is called for every transition that is processed, even those with no action.

Parameters:
  • transition (Transition) – the considered transition
  • event (Event) – instance of Event if any
Return type:

List[Event]

Returns:

a list of sent events

execute_onentry(state)

Execute the on entry action for given state. This method is called for every state that is entered, even those with no on_entry.

Parameters:state (StateMixin) – the considered state
Return type:List[Event]
Returns:a list of sent events
execute_onexit(state)

Execute the on exit action for given state. This method is called for every state that is exited, even those with no on_exit.

Parameters:state (StateMixin) – the considered state
Return type:List[Event]
Returns:a list of sent events
evaluate_preconditions(obj, event=None)

Evaluate the preconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.

Parameters:
  • obj – the considered state or transition
  • event (Optional[Event]) – an optional Event instance, in the case of a transition
Return type:

Iterable[str]

Returns:

list of unsatisfied conditions

evaluate_invariants(obj, event=None)

Evaluate the invariants for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.

Parameters:
  • obj – the considered state or transition
  • event (Optional[Event]) – an optional Event instance, in the case of a transition
Return type:

Iterable[str]

Returns:

list of unsatisfied conditions

evaluate_postconditions(obj, event=None)

Evaluate the postconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.

Parameters:
  • obj – the considered state or transition
  • event (Optional[Event]) – an optional Event instance, in the case of a transition
Return type:

Iterable[str]

Returns:

list of unsatisfied conditions

initialize_sequential_conditions(state)

Initialize sequential conditions.

Parameters:state (StateMixin) – for given state.
Return type:None
update_sequential_conditions(state)

Update sequential conditions, and return a list of already unsatisfied conditions.

Parameters:state (StateMixin) – for given state
Return type:Iterable[str]
Returns:a list of already unsatisfied conditions.
evaluate_sequential_conditions(state)

Evaluate sequential conditions, and return a list of unsatisfied conditions.

Parameters:state (StateMixin) – for given state
Return type:Iterable[str]
Returns:a list of unsatisfied conditions.

In order to understand how the evaluator works, the documentation of the Evaluator mentions the following important statements:

  • Methods execute_onentry() and execute_onexit() are called respectively when a state is entered or exited, even if this state does not define a on_entry or on_exit attribute.
  • Method execute_action() is called when a transition is processed, even if the transition does not define any action.

This allows the evaluator to keep track of the states that are entered or exited, and of the transitions that are processed.