Include code in statecharts¶
Python code evaluator¶
A statechart can specify code that needs to be executed under some circumstances. For example, the preamble of a statechart, the guard or action of a transition or the on entry and on exit of a state may all contain code.
In Sismic, these pieces of code can be evaluated and executed by Evaluator
instances.
By default, when an interpreter is created, a PythonEvaluator
is created and allows
the interpreter to evaluate and execute Python code contained in a statechart.
Alternatively, a DummyEvaluator
that always evaluates conditions to True
and
silently ignores actions can be used, but is clearly of less interest.
In the following, we will implicitly assume that the code evaluator is an instance of PythonEvaluator
.
Context of the Python code evaluator¶
When a code evaluator is created or provided to an interpreter, all the variables that are defined or used by the
statechart are stored in an execution context. This context is exposed through the context
attribute of the interpreter and can be seen as a mapping between variable names and their values.
When a piece of code contained in a statechart has to be evaluated or executed, the context of the evaluator is used to
populate the local and global variables that are available for this piece of code.
As an example, consider the following partial statechart definition.
statechart:
# ...
preamble: |
x = 1
y = 0
root state:
name: s1
on entry: x += 1
When an interpreter is created for this statechart, its preamble is executed and the context of the code evaluator is
populated with {'x': 1, 'y': 0}
. When the statechart is further executed (initialized), and its root state
s1 is entered, the code x += 1
contained in the on entry
field of s1 is then executed in this context.
After execution, the context is {'x': 2, 'y': 0}
.
The default code evaluator uses a global context, meaning that all variables that are defined in the statechart are exposed by the evaluator when a piece of code has to be evaluated or executed. The main limitation of this approach is that you cannot have distinct variables with a same name in different states or, in other words, there is only one scope for all your variables.
The preamble of a statechart can be used to provide default values for some variables. However, the preamble is part of
the statechart and as such, cannot be used to parametrize the statechart. To circumvent this, an initial context
can be specified when a PythonEvaluator
is created. For convenience, this initial context
can also be passed to the constructor of an Interpreter
.
Considered the following toy example:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
yaml = """statechart:
name: example
preamble:
x = DEFAULT_X
root state:
name: s
"""
statechart = import_from_yaml(yaml)
Notice that variable DEFAULT_X
is used in the preamble but not defined. The statechart expects this
variable to be provided in the initial context, as illustrated next:
interpreter = Interpreter(statechart, initial_context={'DEFAULT_X': 1})
We can check that the value of x
is 1
by accessing the context
attribute of the interpreter:
assert interpreter.context['x'] == 1
Omitting to provide the DEFAULT_X
variable in the initial context leads to an error, as an unknown
variable is accessed by the preamble:
try:
Interpreter(statechart)
except Exception as e:
print(e)
"name 'DEFAULT_X' is not defined" occurred while executing "x = DEFAULT_X"
It could be tempting to define a default value for x
in the preamble and overriding this
value by providing an initial context where x
is defined. However, the initial context of an
interpreter is set before executing the preamble of a statechart. As a consequence, if a variable
is defined both in the initial context and the preamble, its value will be overridden by the preamble.
Consider the following example where x
is both defined in the initial context and the preamble:
yaml = """statechart:
name: example
preamble:
x = 1
root state:
name: s
"""
statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2})
assert interpreter.context['x'] == 1
The value of x
is eventually set to 1
.
While the initial context provided to the interpreter defined the value of x
to 2
, the code
contained in the preamble overrode its value. If you want to make use of the initial context to
somehow parametrize the execution of the statechart while still providing default values for
these parameters, you should either check the existence of the variables before setting their values
or rely on the setdefault
function that is exposed by the Python code evaluator when a piece of
code is executed (not only in the preamble).
This function can be used to define (and return) a variable, very similarly to the
setdefault
method of a dictionary. Using this function, we can easily rewrite the preamble
of our statechart to deal with the optional default values of x
(and y
and z
in this
example):
yaml = """statechart:
name: example
preamble: |
x = setdefault('x', 1)
setdefault('y', 1) # Value is affected to y implicitly
setdefault('z', 1) # Value is affected to z implicitly
root state:
name: s
on entry: print(x, y, z)
"""
statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2, 'z': 3})
interpreter.execute()
2 1 3
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.
Predefined variables and functions¶
When a piece of code is evaluated or executed, the default Python code evaluator enriches its local context with several predefined variables and functions. These predefined objects depend on the situation triggering a code evaluation or a code execution (entry or exit actions, guard evaluation, transition action, …).
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.
This evaluator exposes some additional functions/variables:
- On both code execution and code evaluation:
- A time: float value that represents the current time exposed by interpreter clock.
- 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. Raised events are propagated to bound statecharts as external events and to the current statechart as internal event. If delay is provided, a delayed event is created.
- A notify(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises a meta-event with it. Meta-events are only sent to bound property statecharts.
- If the code is related to a transition, the event: Event that fires the transition is exposed.
- A setdefault(name:str, value: Any) -> Any function that defines and returns variable name in the global scope if it is not yet defined.
- On guard or contract evaluation:
- If the code is related to a transition, an event: Optional[Event] variable is exposed. This variable contains the currently considered event, or None.
- 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.
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__
Anatomy of a code evaluator¶
Note
This section explains which are the methods that are called during the execution or evaluation of a piece of code, and is mainly useful if you plan to write your own statechart code interpreter.
An Evaluator
subclass must at lest implement the following methods and attributes:
-
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: Return type: 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: 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
]
Note
None of those two methods are actually called by the interpreter during the execution of a statechart.
These methods are fallback methods that are used by other methods that are implicitly called depending on what is
currently being processed in the statechart. The documentation of Evaluator
covers this:
-
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
-
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
-
evaluate_guard
(transition, event=None) Evaluate the guard for given transition.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: Optional
[bool
]Returns: truth value of code
- transition (
-
execute_action
(transition, event=None) 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 (
Optional
[Event
]) – instance of Event if any
Return type: List
[Event
]Returns: a list of sent events
- transition (
-
execute_on_entry
(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 stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_on_exit
(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 stateReturn 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, if any
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, if any
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, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions