Commits (4)
......@@ -124,6 +124,12 @@ The background pattern was downloaded from www.subtlepatterns.com
Changelog
=========
**3.5.7 (05/2021)**
- Fix generation of statecharts
**3.5.6 (05/2021)**
- observers: support renaming of continuous signals with parameters
**3.5.5 (04/2021)**
- Fix support for single input/output expressions (with no message name)
- Fix graphical location of errors for undefined states
......
......@@ -792,6 +792,11 @@ package body {process_name}_RI is''']
# dont generate anything in stop_condition functions
break
if 'renames' in signal and signal['renames'] is not None:
# don't generate anything if this is an observer signal
# (a renames clause for a continuuous signal)
continue
signame = signal.get('name', 'START')
fake_name = False
......
......@@ -654,9 +654,10 @@ class ContinuousSignal(Input):
self.priority = 0
# Set if we are in an observer to render the symbol differently
self.observer : bool = False
# Artificial set to true if it is meant to replace an input symbol
# in observers
self.artificial : bool = False
# instance of class Input when this CS is an alias of an input
self.observer_input = None
# artificial is set to True if this is an alias (for Renderer)
self.artificial = False
def trace(self):
''' Debug output for a Continuous signal '''
......
......@@ -1629,16 +1629,49 @@ def unary_expression(root, context):
return expr, errors, warnings
def io_expression(root, context):
def parse_io_expression(root, context):
''' Extract all parts of an IO expression (see io_expression below) '''
result = {
'inputString': get_input_string(root),
'kind' : root.type == lexer.INPUT_EXPRESSION and "input" or "output",
'msgName' : None,
'from' : None,
'to' : None,
'paramName': None
}
for child in root.getChildren():
if child.type == lexer.ID:
result['msgName'] = child.text
elif child.type == lexer.FROM:
result['from'] = child.getChild(0).text
elif child.type == lexer.TO:
result['to'] = child.getChild(0).text
elif child.type == lexer.IOPARAM:
# optional parameter
result['paramName'] = child.getChild(0).text
else:
raise NotImplementedError("Parsing error in io_expression")
return result
def io_expression(root, context, io_expr=None):
''' Expressions used in the context of observers (for model checking):
input
input x [from P] to F
output
output X from P
output X from P [to F]
Since this is syntactic sugar, we transform these expression into the
regular form based on the known structure of events: Observable_Event
type that is generated by kazoo.
Optional parameter io_expr allows to pass a pre-parsed string
(format: result of function parse_io_expression defined just above)
'''
errors, warnings = [], []
......@@ -1649,59 +1682,33 @@ def io_expression(root, context):
string = "present(event) = "
msg, src, dest = "", "", ""
if root.type == lexer.INPUT_EXPRESSION:
kind = "input"
direction = "in"
else:
kind = "output"
direction = "out"
string += event_kind.format(kind=kind)
param_name = ""
from_f, to_f = "", ""
for child in root.getChildren():
if child.type == lexer.ID:
msg = child.text
elif child.type == lexer.FROM:
from_f = child.getChild(0).text
string += target_option.format(kind=kind,
target="source",
function=from_f)
elif child.type == lexer.TO:
to_f = child.getChild(0).text
string += target_option.format(kind=kind,
target="dest",
function=to_f)
elif child.type == lexer.IOPARAM:
# optional parameter
# to find the type of th parameter, the easiest is to parse the
# path to the field as an expression. That will also detect errors.
# this will be done after the parsing of other elements, when
# destination field is known.
param_name = child.getChild(0).text
else:
raise NotImplementedError("In io_expression")
func = from_f if direction == "out" else to_f
if msg and not func:
if io_expr == None:
io_expr = parse_io_expression(root, context) # extract name, source, dest, etc.
direction = (io_expr['kind'] == 'input') and 'in' or 'out'
string += event_kind.format(kind=io_expr['kind'])
if io_expr['from'] is not None:
string += target_option.format(kind=io_expr['kind'],
target="source",
function=io_expr['from'])
if io_expr['to'] is not None:
string += target_option.format(kind=io_expr['kind'],
target="dest",
function=io_expr['to'])
func = io_expr['from'] if direction == "out" else io_expr['to']
if io_expr['msgName'] and not func:
# When input or output specify a message there must be a source or destination
if direction == "out":
errors.append(f"FROM clause is missing in output expression '{inputString}'")
else:
errors.append(f"TO clause is missing in input expression '{inputString}'")
elif msg:
string += msg_name.format(kind=kind,
elif io_expr['msgName']:
string += msg_name.format(kind=io_expr['kind'],
function=func,
direction=direction,
msg=msg)
msg=io_expr['msgName'])
parser = parser_init (string=string)
new_root = parser.expression()
......@@ -1716,8 +1723,8 @@ def io_expression(root, context):
# Now address the optional parameter: if set, we will create an implicit
# alias definition to the event structure where the parameter is actually
# present. If an alias of the same type already exists, raise an error
if param_name:
path=f"event.{kind}_event.event.{func}.msg_{direction}.{msg}"
if io_expr['paramName']:
path=f"event.{io_expr['kind']}_event.event.{func}.msg_{direction}.{io_expr['msgName']}"
parser = parser_init (string=path)
new_root = parser.expression()
tree = new_root.tree
......@@ -1732,7 +1739,7 @@ def io_expression(root, context):
try:
pname, ptype = list(find_basic_type(param_expr.exprType).Children.items())[0]
except (IndexError, AttributeError):
errors.append(f"No parameter expected for message {msg}")
errors.append(f"No parameter expected for message {io_expr['msgName']}")
else:
path += f".{pname}"
# We must re-parse the expression with the field name
......@@ -1746,19 +1753,18 @@ def io_expression(root, context):
# The type of the parameter is:
ptype = ptype.type
#print(f"dcl {param_name} {type_name(ptype)} renames {path};")
for var, (sort, _) in context.variables.items():
if var.lower() == param_name.lower():
if var.lower() == io_expr['paramName'].lower():
# variable already defined, does it have the same type?
if type_name(sort) != type_name(ptype):
errors.append(f"Duplicate/incompatible definition"
f" of variable '{param_name}'")
f" of variable '{io_expr['paramName']}'")
elif var.lower() in context.aliases.keys():
# Check if already defined variable is an alias,
# and if so, if it points to the same element
_, alias_expr = context.aliases[var.lower()]
if alias_expr.inputString != param_expr.inputString:
errors.append(f"Parameter name '{param_name}' "
errors.append(f"Parameter name '{io_expr['paramName']}' "
"is used in another context, but not "
f"pointing to the same content")
else:
......@@ -1766,13 +1772,13 @@ def io_expression(root, context):
# implicit parameter, as it is actually pointing
# to the event.
errors.append("A variable declaration with the "
f"same name as parameter '{param_name}' "
f"same name as parameter '{io_expr['paramName']}' "
"exists and shall be removed or renamed")
break
else:
# not found a duplicate definition -> Add an alias
context.variables[param_name.lower()] = (ptype, None)
context.aliases[param_name.lower()] = (ptype, param_expr)
context.variables[io_expr['paramName'].lower()] = (ptype, None)
context.aliases[io_expr['paramName'].lower()] = (ptype, param_expr)
return expr, errors, warnings
def expression(root, context, pos='right'):
......@@ -4151,6 +4157,7 @@ def continuous_signal(root, parent, context):
def input_part(root, parent, context):
''' Parse an INPUT - set of TASTE provided interfaces '''
# parent is a State
i = ogAST.Input()
cs = None
warnings, errors = [], []
......@@ -4174,6 +4181,7 @@ def input_part(root, parent, context):
# syntax error (CommonErrorNode) - already caught
continue
for inp_sig in context.input_signals:
# process RENAMES clause at signal level
if inp_sig['name'].lower() == inputname.text.lower():
i.inputlist.append(inp_sig['name'])
sig_param_type = inp_sig.get('type')
......@@ -4183,10 +4191,33 @@ def input_part(root, parent, context):
cs = ogAST.ContinuousSignal()
dec = ogAST.Decision()
ans = ogAST.Answer()
dec.question, _, _ = expression(inp_sig['renames'], context)
dec.inputString = dec.question.inputString
dec.line = dec.question.line
dec.charPositionInLine = dec.question.charPositionInLine
# inp_sig['renames'] is the AST entry
# (e.g. "input foo to bar")
# extact all parts:
io_expr = parse_io_expression(inp_sig['renames'], context)
if io_expr['paramName'] is not None:
# io expressions cannot have a parameter when they rename a signal
# the parameter can be specified in the INPUT part only
errors.append('Renamed expression for signals cannot have a named parameter in declaration')
# Check here is there is a parameter to the input
# If so, add it to the io expression so that when
# it is processed, an implicit variable will be
# declared for it
inputparams = [c.getChildren() for c in child.getChildren()
if c.type == lexer.PARAMS]
if len(inputparams) == 1 and len(inputparams[0]) == 1:
user_param, = inputparams[0]
io_expr['paramName'] = user_param.text
dec.question, errs, warns = io_expression(inp_sig['renames'], context, io_expr)
# possible errors: if user declared a variable for the parameter
# in observers, variables are always implicit, as they are
# in fact a field in the event structure
errors.extend(errs)
warnings.extend(warns)
#dec.inputString = dec.question.inputString
dec.inputString = i.inputString
dec.line = i.line
dec.charPositionInLine = i.charPositionInLine
dec.kind = 'question'
ans.inputString = 'true'
ans.openRangeOp = ogAST.ExprEq
......@@ -4196,8 +4227,20 @@ def input_part(root, parent, context):
ans.constant.exprType = BOOLEAN
dec.answers = [ans]
cs.trigger = dec
cs.inputString = dec.inputString
cs.inputString = dec.question.inputString
parent.continuous_signals.append(cs)
# must also be added to mapping at context level
# (see PROVIDED clause for details)
for statename in parent.statelist:
existing = context.cs_mapping.get(statename.lower(), [])
for each in existing:
if ''.join(each.inputString.lower().split()) == \
''.join(cs.inputString.lower().split()):
errors.append('Trigger for observer already defined '
'below state "{}"'.format(statename.lower()))
break
else:
context.cs_mapping[statename.lower()].append(cs)
break
else:
for timer in chain(context.timers, context.global_timers):
......@@ -4282,8 +4325,12 @@ def input_part(root, parent, context):
i.terminators = list(context.terminators[terminators:])
if cs:
cs.terminators = i.terminators
cs.artificial = True
cs.comment = i.comment
dec.comment = i.comment
cs.artificial = True # needed for graphical rendering
i.replaced_with_continuous_signal = True
# Store the parsed input too, for backends that don't support CS
cs.observer_input = i
return i, errors, warnings
......@@ -4422,10 +4469,17 @@ def state(root, parent, context):
# Add the continuous signal to a mapping at context level,
# useful for code generation. Also check for duplicates.
for statename in state_def.statelist:
if provided_part in \
context.cs_mapping.get(statename.lower(), []):
sterr.append('Continuous signal is defined more than once '
'below state "{}"'.format(statename.lower()))
existing = context.cs_mapping.get(statename.lower(), [])
for each in existing:
print (each.inputString.lower().split())
print(provided_part.inputString.lower().split())
for each in existing:
if ''.join(each.inputString.lower().split()) == \
''.join(provided_part.inputString.lower().split()):
#if provided_part in \
# context.cs_mapping.get(statename.lower(), []):
sterr.append('Continuous signal is defined more than once '
'below state "{}"'.format(statename.lower()))
else:
context.cs_mapping[statename.lower()].append(provided_part)
warnings.extend(warn)
......@@ -5818,6 +5872,7 @@ def pr_file(root):
if proc.processName.lower() == proc_name.lower():
return block
return None
proc_parent = None
for system in ast.systems:
proc_parent = rec_find_process_parent(system, processName)
if proc_parent:
......
......@@ -141,7 +141,7 @@ except ImportError:
__all__ = ['opengeode', 'SDL_Scene', 'SDL_View', 'parse']
__version__ = '3.5.5'
__version__ = '3.5.7'
if hasattr(sys, 'frozen'):
# Detect if we are running on Windows (py2exe-generated)
......