Commit d1c60c7a authored by Maxime Perrotin's avatar Maxime Perrotin
Browse files

Merge pull request #32 from maxime-esa/state_aggregation

State aggregation
parents 391e344a 0fa743fe
......@@ -148,6 +148,9 @@ The fonts are the fonts from Ubuntu, check licence in file FONT-LICENSE.TXT
1.3.0 (10/2015)
- Support for State Aggregations (parallel states)
1.2.10 (10/2015)
- Better support of renamePolicy
- Better handling of models without CIF coordinates
......@@ -72,6 +72,7 @@
import logging
import traceback
import os
from itertools import chain
from singledispatch import singledispatch
import ogAST
......@@ -181,6 +182,16 @@ LD_LIBRARY_PATH=. taste-gui -l
# In case model has nested states, flatten everything
Helper.flatten(process, sep=UNICODE_SEP)
# Process State aggregations (Parallel states)
# Find recursively in the AST all state aggregations
# Format: {'aggregation_name' : [list of ogAST.CompositeState]
aggregates = Helper.state_aggregations(process)
# Extract the list of parallel states names inside the composite states
# of state aggregations XXX add to C generator
parallel_states = Helper.parallel_states(aggregates)
# Make an maping {input: {state: transition...}} in order to easily
# generate the lookup tables for the state machine runtime
mapping = Helper.map_input_state(process)
......@@ -189,19 +200,28 @@ LD_LIBRARY_PATH=. taste-gui -l
process_level_decl = []
# Establish the list of states (excluding START states)
statelist = ', '.join(name for name in process.mapping.iterkeys()
if not name.endswith(u'START')) or 'No_State'
if statelist:
states_decl = u'type States is ({});'.format(statelist)
# Establish the list of states (excluding START states) XXX update C backend
full_statelist = list(chain(aggregates.viewkeys(),
(name for name in process.mapping.iterkeys()
if not name.endswith(u'START'))))
reduced_statelist = [s for s in full_statelist if s not in parallel_states]
if full_statelist:
process_level_decl.append(u'type States is ({});'
.format(u', '.join(full_statelist) or u'No_State'))
# Generate the code to declare process-level context
process_level_decl.extend(['type {}_Ty is'.format(LPREFIX), 'record'])
if statelist:
if full_statelist:
process_level_decl.append('state : States;')
# State aggregation: add list of substates (XXX to be added in C generator)
for substates in aggregates.viewvalues():
for each in substates:
process_level_decl.append(u'{}{}state: States;'
.format(each.statename, UNICODE_SEP))
for var_name, (var_type, def_value) in process.variables.viewitems():
if def_value:
# Expression must be a ground expression, i.e. must not
......@@ -226,6 +246,22 @@ LD_LIBRARY_PATH=. taste-gui -l
process_level_decl.append(u'{name} : constant := {val};'
.format(name=name, val=str(val)))
# Declare start procedure for aggregate states XXX add in C generator
# should create one START per "via" clause, TODO later
aggreg_start_proc = []
for name, substates in aggregates.viewitems():
proc_name = u'procedure {}{}START'.format(name, UNICODE_SEP)
aggreg_start_proc.extend([u'{} is'.format(proc_name),
for subname in substates)
aggreg_start_proc.extend([u'end {}{}START;'
.format(name, UNICODE_SEP),
# Add the declaration of the runTransition procedure, if needed
if process.transitions:
process_level_decl.append('procedure runTransition(Id: Integer);')
......@@ -322,7 +358,6 @@ package {process_name} is'''.format(process_name=process_name,
dll_api.append('end dll_set_l_{};'.format(var_name))
# Generate the the code of the procedures
inner_procedures_code = []
for proc in process.content.inner_procedures:
......@@ -336,17 +371,23 @@ package {process_name} is'''.format(process_name=process_name,
# Add the code of the procedures definitions
# Generate the code of the START procedures of state aggregations
# XXX to be added to C generator
# Add the code of the DLL interface
# Generate the code for each input signal (provided interface) and timers
print process.input_signals
for signal in process.input_signals + [
{'name': timer.lower()} for timer in process.timers]:
if signal.get('name', u'START') == u'START':
signame = signal.get('name', u'START')
if name == u'START':
pi_header = u'procedure {sig_name}'.format(sig_name=signal['name'])
pi_header = u'procedure {sig_name}'.format(sig_name=signame)
param_name = signal.get('param_name') \
or u'{}_param'.format(signal['name'])
or u'{}_param'.format(name)
# Add (optional) PI parameter (only one is possible in TASTE PI)
if 'type' in signal:
typename = type_name(signal['type'])
......@@ -354,29 +395,27 @@ package {process_name} is'''.format(process_name=process_name,
pName=param_name, sort=typename)
# Add declaration of the provided interface in the .ads file
ads_template.append(u'-- Provided interface "' + signal['name'] + '"')
ads_template.append(u'-- Provided interface "{}"'.format(signame))
ads_template.append(pi_header + ';')
ads_template.append(u'pragma export(C, {name}, "{proc}_{name}");'
.format(name=signal['name'], proc=process_name))
.format(name=signame, proc=process_name))
if simu:
# Generate code for the mini-cv template
params = [(param_name, type_name(signal['type'], use_prefix=False),
'IN')] if 'type' in signal else []
minicv.append(aadl_template(signal['name'], params, 'RI'))
minicv.append(aadl_template(signame, params, 'RI'))
pi_header += ' is'
taste_template.append('case {ctxt}.state is'.format(ctxt=LPREFIX))
for state in process.mapping.viewkeys():
if state.endswith(u'START'):
taste_template.append(u'when {state} =>'.format(state=state))
input_def = mapping[signal['name']].get(state)
def execute_transition(state):
''' Generate the code that triggers the transition for the current
state/input combination '''
input_def = mapping[signame].get(state)
# Check for nested states to call optional exit procedure
state_tree = state.split(sep)
state_tree = state.split(UNICODE_SEP)
context = process
exitlist = []
current = ''
......@@ -388,13 +427,13 @@ package {process_name} is'''.format(process_name=process_name,
if comp.exit_procedure:
context = comp
current = current + sep
current = current + UNICODE_SEP
for each in reversed(exitlist):
if trans and all(each.startswith(trans_st)
for trans_st in trans.possible_states):
.format(ref=each, sep=sep))
.format(ref=each, sep=UNICODE_SEP))
if input_def:
for inp in input_def.parameters:
......@@ -412,10 +451,48 @@ package {process_name} is'''.format(process_name=process_name,
taste_template.append('case {ctxt}.state is'.format(ctxt=LPREFIX))
def case_state(state):
''' Recursive function (in case of state aggregation) to generate
the code that calls the proper transition according
to the current state
The input name is in signame
if state.endswith(u'START'):
taste_template.append(u'when {state} =>'.format(state=state))
if state in aggregates.viewkeys():
# State aggregation:
# - find which substate manages this input
# - add a swich case on the corresponding substate
taste_template.append(u'-- this is a state aggregation')
for sub in aggregates[state]:
for par in sub.mapping.viewkeys():
if par in mapping[signame].viewkeys():
taste_template.append(u'case '
u'{ctxt}.{sub}{sep}state is'
taste_template.append('when others =>')
taste_template.append('end case;')
# Input is not managed in the state aggregation
map(case_state, reduced_statelist) # XXX update C generator
taste_template.append('when others =>')
taste_template.append('end case;')
taste_template.append(u'end {};'.format(signal['name']))
taste_template.append(u'end {};'.format(signame))
# for the .ads file, generate the declaration of the required interfaces
......@@ -680,7 +757,6 @@ def write_statement(param, newline):
code, string, local = expression(param)
if type_kind == 'OctetStringType':
# Octet string -> convert to Ada string
last_it = u""
if isinstance(param, ogAST.PrimSubstring):
range_str = u"{}'Range".format(string)
......@@ -696,7 +772,7 @@ def write_statement(param, newline):
last_it = u"({})".format(range_str)
code.extend([u"for i in {} loop".format(range_str),
.format(st=string, sep=sep, it=iterator),
u"end loop;"])
......@@ -1878,23 +1954,47 @@ def _transition(tr, **kwargs):
if tr.terminator.kind == 'next_state':
if tr.terminator.inputString.strip() != '-':
history = tr.terminator.inputString.strip() == '-'
if tr.terminator.next_is_aggregation and not history: # XXX add to C generator
code.append(u'-- Entering state aggregation {}'
# Call the START function of the state aggregation
code.append(u'{ctxt}.state := {nextState};'
code.append(u'trId := -1;')
elif not history: # tr.terminator.inputString.strip() != '-':
code.append(u'trId := ' +
unicode(tr.terminator.next_id) + u';')
if tr.terminator.next_id == -1:
code.append(u'{ctxt}.state := {nextState};'
if not tr.terminator.substate: # XXX add to C generator
code.append(u'{ctxt}.state := {nextState};'
code.append(u'{ctxt}.{sub}{sep}state :='
u' {nextState};'
# "nextstate -": switch case to re-run the entry transition
# in case of a composite state or state aggregation
if any(next_id
for next_id in tr.terminator.candidate_id.viewkeys()
if next_id != -1):
code.append('case {}.state is'.format(LPREFIX))
for nid, sta in tr.terminator.candidate_id.viewitems():
if nid != -1:
for each in sta:
code.extend([u'when {} =>'.format(each),
u'trId := {};'.format(nid)])
if tr.terminator.next_is_aggregation:
statement = u'{};'.format(nid)
statement = u'tdId := {};'.format(nid)
code.extend([u'when {} =>'
code.extend(['when others =>',
'trId := -1;',
......@@ -13,8 +13,11 @@
sorted_fields(SEQ/CHOICE) : returns the ordered list of fields
of an ASN.1 SEQUENCE or CHOICE type
state_aggregations: enrich AST with state aggregation flags,
and return the list of substates of aggregations
parallel_states: return a list of strings naming all parallel states
Copyright (c) 2012-2014 European Space Agency
Copyright (c) 2012-2015 European Space Agency
Designed and implemented by Maxime Perrotin
......@@ -33,7 +36,55 @@ import ogAST
LOG = logging.getLogger(__name__)
__all__ = ['flatten', 'rename_everything', 'inner_labels_to_floating',
'map_input_state', 'sorted_fields']
'map_input_state', 'sorted_fields', 'state_aggregations',
def state_aggregations(process):
''' Explore recursively the AST to find all state aggregations, and
return the composite states inside them
input: ogAST.Process element
output: {state_aggregation: {list of ogAST.CompositeState}
# { aggregate_name : [list of parallel states] }
aggregates = defaultdict(list)
def do_composite(comp, aggregate=''):
''' Recursively find all state aggregations in order to create
variables to store the state of each parallel state '''
for each in comp.composite_states: # CHECKME
pre = comp.statename if isinstance(comp, ogAST.StateAggregation) \
else ''
do_composite(each, pre)
if isinstance(each, ogAST.StateAggregation):
for term in comp.terminators:
if term.inputString.lower() == each.statename.lower():
term.next_is_aggregation = True
if aggregate and not isinstance(comp, ogAST.StateAggregation):
# Composite state inside a state aggregation
# Here, all the terminators inside the composite states must
# be flagged with the name of the substate so that the NEXTSTATE
# will not be using the main "context.state" variable but will
# use the parallel substate name when generating code.
for each in comp.terminators:
each.substate = comp.statename
for each in process.composite_states:
for each in process.terminators:
if each.inputString.lower() in aggregates:
each.next_is_aggregation = True
return aggregates
def parallel_states(aggregates):
''' Given a mapping obtained with state_aggregation(process), extract
all parallel states and return a list of state names '''
parallel_states = []
for name, comp in aggregates.viewitems():
for each in comp:
parallel_states.extend(name for name in each.mapping.viewkeys()
if not name.endswith(u'START'))
return parallel_states
def map_input_state(process):
......@@ -89,16 +140,20 @@ def flatten(process, sep=u'_'):
term.next_id = u'{term}{sep}{entry}_START'.format(
term=term.inputString, entry=term.entrypoint, sep=sep)
elif term.inputString.strip() == '-':
term.candidate_id = defaultdict(list)
#term.candidate_id = defaultdict(list)
for each in term.possible_states:
if each.lower() in (st.statename.lower()
for st in context.composite_states):
term.candidate_id[each + sep + u'START'] = \
for comp in context.composite_states:
if each.lower() == comp.statename.lower():
if isinstance(comp, ogAST.StateAggregation):
term.next_is_aggregation = True
term.candidate_id[each + sep + u'START'] = [each]
term.candidate_id[each + sep + u'START'] = \
[st for st in process.mapping.viewkeys()
if st.startswith(each)
and not st.endswith(u'START')]
def update_composite_state(state, process):
''' Rename inner states, recursively, and add inner transitions
......@@ -171,8 +226,12 @@ def flatten(process, sep=u'_'):
# Go recursively in inner composite states
inner.statename = prefix + inner.statename
update_composite_state(inner, process)
propagate_inputs(inner, process.mapping[inner.statename])
del process.mapping[inner.statename]
propagate_inputs(inner, process)
del process.mapping[inner.statename]
except KeyError:
# KeyError in case of state aggregation
for each in state.terminators:
# Give prefix to terminators
if each.label:
......@@ -200,7 +259,7 @@ def flatten(process, sep=u'_'):
each.inputString = prefix + each.inputString
def propagate_inputs(nested_state, inputlist):
def propagate_inputs(nested_state, context):
''' Nested states: Inputs at level N must be handled at level N-1
that is, all inputs of a composite states (the ones that allow
to exit the composite state from the outer scope) must be
......@@ -208,12 +267,14 @@ def flatten(process, sep=u'_'):
for _, val in nested_state.mapping.viewitems():
inputlist = context.mapping[nested_state.statename]
except AttributeError:
except (AttributeError, KeyError):
# KeyError in case of StateAggregation
for each in nested_state.composite_states:
# do the same recursively
propagate_inputs(each, nested_state.mapping[each.statename])
propagate_inputs(each, nested_state)
#del nested_state.mapping[each.statename]
def set_terminator_states(context, prefix=''):
......@@ -238,7 +299,7 @@ def flatten(process, sep=u'_'):
for each in process.composite_states:
update_composite_state(each, process)
propagate_inputs(each, process.mapping[each.statename])
propagate_inputs(each, process)
del process.mapping[each.statename]
# Update terminators at process level
......@@ -332,7 +332,8 @@ def _state(symbol, recursive=True, nextstate=True, composite=False, cpy=False,
# Generate code for a nested state
result = Indent()
result.append('STATE {};'.format(unicode(symbol).split()[0]))
agg = ' AGGREGATION' if not list(symbol.nested_scene.start) else ''
result.append('STATE{} {};'.format(agg, unicode(symbol).split()[0]))
Indent.indent += 1
entry_points, exit_points = [], []
......@@ -2,7 +2,7 @@
# Resource object code
# Created: Thu Oct 8 22:16:37 2015
# Created: Tue Oct 20 15:17:00 2015
# by: The Resource Compiler for PySide (Qt v4.8.6)
# WARNING! All changes made in this file will be lost!
......@@ -35,6 +35,7 @@
import logging
from collections import defaultdict
LOG = logging.getLogger(__name__)
......@@ -463,6 +464,17 @@ class Terminator(object):
self.possible_states = []
# optional composite state content (type CompositeState)
self.composite = None
# Flag to indicate if the nextstate is a state aggregation
self.next_is_aggregation = False
# If this terminator is within a state aggregation, store the name
# of the parallel substate (set by Helper.state_aggregations)
self.substate = ''
# candidate_id: {transition_id: [states]}
# field is set by, in case of "nextstate -"
# there is a list of states that set transition_id to -1 : the standard
# states ; and there are the composite states, that set a different
# id corresponding to the start transition of the state.
self.candidate_id = defaultdict(list)
def trace(self):
''' Debug output for terminators '''
......@@ -716,7 +728,7 @@ class TextArea(object):
class Automaton(object):
''' Elements contained in a process or a procedure '''
''' Elements contained in a process, procedure or composite state'''
def __init__(self, parent=None):
''' AST grouping the elements that can be rendered graphically '''
self.parent = parent
......@@ -879,6 +891,28 @@ class CompositeState(Process):
l=self.line, c=self.charPositionInLine)
class StateAggregation(CompositeState):
State Aggregation (Parallel states) are supported since SDL2000
These states can only contain (in the self.content field):
text areas
procedure definitions
composite states (including sub-state aggregations)
But no state machine definition
def __init__(self):
super(StateAggregation, self).__init__()
# List of partition connections:
# [{'outer': {'state_part_id': str, 'point': str},
# 'inner': {'state_part_id': str, 'point': str}}]
self.state_partition_connections = []
def trace(self):
''' Debug output for state aggregation '''
return u'STATE AGGREGATION {exp} ({l},{c})'.format(exp=self.statename,
l=self.line, c=self.charPositionInLine)
class Block(object):
''' AST for a BLOCK entity '''
def __init__(self):
......@@ -1893,8 +1893,11 @@ def fpar(root):
def composite_state(root, parent=None, context=None):
''' Parse a composite state definition '''
comp = ogAST.CompositeState()
''' Parse a composite state (incl. state aggregation) definition '''
if root.type == lexer.COMPOSITE_STATE:
comp = ogAST.CompositeState()
elif root.type == lexer.STATE_AGGREGATION:
comp = ogAST.StateAggregation()
errors, warnings = [], []
# Create a list of all inherited data
......@@ -1948,7 +1951,7 @@ def composite_state(root, parent=None, context=None):
# Add procedure to the context, to make it visible at scope level
elif child.type == lexer.COMPOSITE_STATE:
elif child.type in (lexer.COMPOSITE_STATE, lexer.STATE_AGGREGATION):
elif child.type == lexer.STATE:
......@@ -1956,6 +1959,10 @@ def composite_state(root, parent=None, context=None):
elif child.type == lexer.START:
elif child.type == lexer.STATE_PARTITION_CONNECTION:
# TODO (see section 11.11.2)
warnings.append(['Ignoring state partition connections',
[0, 0], []])
warnings.append(['Unsupported construct in nested state, type: {}'
'- line {} - State name: {}'
......@@ -1964,11 +1971,18 @@ def composite_state(root, parent=None, context=None):
[0 , 0], # No graphical position
if (floatings or starts) and isinstance(comp, ogAST.StateAggregation):
errors.append(['State aggregation can only contain composite state(s)',
[0, 0], []])
for each in inner_composite:
# Parse inner composite states after the text areas to make sure
# that all variables are propagated to the the inner scope
inner, err, warn = composite_state(each, parent=None,
if isinstance(comp, ogAST.StateAggregation):
# State aggregation contain only composite states, so we must
# add empty mapping information since there are no transitions
comp.mapping[inner.statename.lower()] = []
......@@ -2701,7 +2715,7 @@ def process_definition(root, parent=None, context=None):
elif child.type == lexer.COMPOSITE_STATE:
elif child.type in (lexer.COMPOSITE_STATE, lexer.STATE_AGGREGATION):
comp, err, warn = composite_state(child,
......@@ -3574,7 +3588,8 @@ def nextstate(root, context):
composite, = (comp for comp in context.composite_states
if comp.statename.lower() == next_state_id.lower())
if not composite.content.start:
if not isinstance(composite, ogAST.StateAggregation) \
and not composite.content.start:
errors.append('Composite state "{}" has no unnamed '
'START symbol'.format(composite.statename))
except ValueError:
......@@ -116,7 +116,7 @@ except ImportError:
__all__ = ['opengeode', 'SDL_Scene', 'SDL_View', 'parse']
__version__ = '1.2.10'
__version__ = '1.3.0'
if hasattr(sys, 'frozen'):
# Detect if we are running on Windows (py2exe-generated)