Explorar o código

Extend semantic option wildcard mechanism to comma-separated lists of options. Parse transition event attribute as comma-separated list of (possibly negated) events with parameters. Event parameters added to transition scope. Input port ignored when generating ID for input/internal event (need this for Day & Atlee's examples)

Joeri Exelmans %!s(int64=5) %!d(string=hai) anos
pai
achega
33cf1867c5

+ 1 - 1
src/sccd/controller/controller.py

@@ -50,7 +50,7 @@ class Controller:
                 raise Exception("No such port: '%s'" % input.port) from e
 
             try:
-                event_id = self.model.globals.events.get_id(input.port + '.' + input.name)
+                event_id = self.model.globals.events.get_id(input.name)
             except KeyError as e:
                 raise Exception("No such event: '%s'" % input.name) from e
 

+ 8 - 12
src/sccd/execution/memory.py

@@ -5,18 +5,13 @@ from sccd.util.debug import *
 class Memory:
   def __init__(self, scope):
     self.scope = scope
-    self.memory = [None] * scope.size()
-
-    # Write default values to storage
-    for name, variable in scope.all():
-      self.memory[variable.offset] = variable.default_value
+    self.storage = [v.initial for v in scope.all_variables()]
 
 class MemorySnapshot:
   def __init__(self, memory: Memory):
-    # self.memory = memory
-    self.actual = memory.memory
-    self.snapshot = list(self.actual)
-    self.len = len(self.actual)
+    self.actual = memory.storage
+    self.snapshot = list(memory.storage)
+    self.len = len(memory.storage)
 
     self.temp_dirty = Bitmap() # positions in actual memory written to before flush_temp
     self.round_dirty = Bitmap() # positions in actual memory written to after flush_temp and before flush_round
@@ -45,18 +40,19 @@ class MemorySnapshot:
 
   def grow_stack(self, scope):
     self.scope.append(scope)
-    self.stack.extend([None]*scope.localsize())
+    self.stack.extend([None]*scope.local_size())
 
   def shrink_stack(self):
     scope = self.scope.pop()
-    del self.stack[-scope.localsize():]
+    del self.stack[-scope.local_size():]
 
   def flush_temp(self):
     assert len(self.stack) == 0 # only allowed to be called in between statement executions or expression evaluations
     
     race_conditions = self.temp_dirty & self.round_dirty
     if race_conditions:
-        raise Exception("Race condition for variables %s" % str(list(self.scope[-1].get_name(offset) for offset in race_conditions.items())))
+      # some variable written to twice before refresh
+      raise Exception("Race condition for variables %s" % str(list(self.scope[-1].get_name(offset) for offset in race_conditions.items())))
 
     self.round_dirty |= self.temp_dirty
     self.temp_dirty = Bitmap() # reset

+ 5 - 3
src/sccd/execution/statechart_instance.py

@@ -15,9 +15,6 @@ class StatechartInstance(Instance):
 
         semantics = statechart.semantics
 
-        if semantics.has_wildcard():
-            raise Exception("Model semantics has unexpanded wildcard for some fields.")
-
         reverse = semantics.priority == Priority.SOURCE_CHILD
 
         generator = CandidatesGeneratorCurrentConfigBased(reverse)
@@ -42,7 +39,12 @@ class StatechartInstance(Instance):
                 # Add even more layers, basically an onion at this point.
                 combo_step = SuperRound(termcolor.colored("combo_many", 'cyan'), combo_one, take_one=False, limit=1000)
 
+            else:
+                raise Exception("Unsupported option: %s" % semantics.combo_step_maximality)
+
             self._big_step = SuperRound(termcolor.colored("big_many", 'red'), combo_step, take_one=False, limit=1000)
+        else:
+            raise Exception("Unsupported option: %s" % semantics.big_step_maximality)
 
         def whole(input):
             self._big_step.remainder_events = input

+ 2 - 3
src/sccd/execution/statechart_state.py

@@ -6,12 +6,11 @@ from sccd.util.bitmap import *
 from sccd.syntax.scope import *
 
 
-def _in_state(current_state, events, memory, state_list):
+def _in_state(current_state, events, memory, state_list: List[str]) -> bool:
   return StatechartState.in_state(current_state, state_list)
 
 builtin_scope = Scope("builtin", None)
-builtin_scope.names["INSTATE"] = Variable(offset=0, type=Callable[[List[str]], bool], default_value=_in_state)
-
+builtin_scope.add_function("INSTATE", _in_state)
 
 # Set of current states etc.
 class StatechartState:

+ 37 - 9
src/sccd/parser/expression_parser.py

@@ -3,15 +3,13 @@ from lark import Lark, Transformer
 from sccd.syntax.statement import *
 from sccd.model.globals import *
 from sccd.syntax.scope import *
+from sccd.syntax.tree import *
 
 _grammar_dir = os.path.join(os.path.dirname(__file__), "grammar")
 
 with open(os.path.join(_grammar_dir,"action_language.g")) as file:
   _action_lang_grammar = file.read()
 
-with open(os.path.join(_grammar_dir,"state_ref.g")) as file:
-  _state_ref_grammar = file.read()
-
 
 # Lark transformer for parsetree-less parsing of expressions
 class _ExpressionTransformer(Transformer):
@@ -81,25 +79,55 @@ class _ExpressionTransformer(Transformer):
   def expression_stmt(self, node):
     return ExpressionStatement(node[0])
 
+  def event_decl_list(self, node):
+    pos_events = []
+    neg_events = []
+
+    for n in node:
+      if n.data == "pos":
+        pos_events.append(n.children[0])
+      elif n.data == "neg":
+        neg_events.append(n.children[0])
+
+    return (pos_events, neg_events)
+
+  def event_decl(self, node):
+    return EventDecl(name=node[0], params=node[1])
+
+  event_params = list
+
+  def event_param_decl(self, node):
+    type = {
+      "int": int,
+      "str": str,
+      "Duration": Duration
+    }[node[1]]
+    return Param(name=node[0], type=type)
+
 # Global variables so we don't have to rebuild our parser every time
 # Obviously not thread-safe
 _transformer = _ExpressionTransformer()
-_action_lang_parser = Lark(_action_lang_grammar, parser="lalr", start=["expr", "block", "duration"], transformer=_transformer)
-_state_ref_parser = Lark(_state_ref_grammar, parser="lalr", start=["state_ref"])
+_parser = Lark(_action_lang_grammar, parser="lalr", start=["expr", "block", "duration", "event_decl_list", "state_ref", "semantic_choice"], transformer=_transformer)
 
 # Exported functions:
 
 def parse_expression(globals: Globals, expr: str) -> Expression:
   _transformer.globals = globals
-  return _action_lang_parser.parse(expr, start="expr")
+  return _parser.parse(expr, start="expr")
 
 def parse_duration(globals: Globals, expr:str) -> Duration:
   _transformer.globals = globals
-  return _action_lang_parser.parse(expr, start="duration")
+  return _parser.parse(expr, start="duration")
 
 def parse_block(globals: Globals, block: str) -> Statement:
   _transformer.globals = globals
-  return _action_lang_parser.parse(block, start="block")
+  return _parser.parse(block, start="block")
+
+def parse_events_decl(events_decl: str):
+  return _parser.parse(events_decl, start="event_decl_list")
 
 def parse_state_ref(state_ref: str):
-  return _state_ref_parser.parse(state_ref, start="state_ref")
+  return _parser.parse(state_ref, start="state_ref")
+
+def parse_semantic_choice(choice: str):
+  return _parser.parse(choice, start="semantic_choice")

+ 43 - 4
src/sccd/parser/grammar/action_language.g

@@ -1,11 +1,44 @@
 // Grammar file for Lark-parser
 
-// Parsing expressions
-
 %import common.WS
 %ignore WS
 %import common.ESCAPED_STRING
 
+// state id, variable name, event name
+IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
+
+// Parsing target of a transition as a sequence of nodes
+
+_PATH_SEP: "/" 
+PARENT_NODE: ".." 
+CURRENT_NODE: "." 
+
+// target of a transition
+state_ref: path | "(" path ("," path)+ ")" 
+
+?path: absolute_path | relative_path 
+absolute_path: _PATH_SEP _path_sequence
+relative_path: _path_sequence
+_path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_sequence)?
+
+
+// Event declaration parsing
+
+event_decl_list: neg_event_decl ("," neg_event_decl)*
+
+?neg_event_decl: event_decl -> pos
+               | "not" event_decl -> neg
+
+?event_decl: IDENTIFIER event_params
+
+event_params: ( "(" event_param_decl ("," event_param_decl)* ")" )?
+
+?event_param_decl: IDENTIFIER ":" TYPE
+
+TYPE: "int" | "str" | "Duration"
+
+
+// Expression parsing
 
 // We use the same operators and operator precedence rules as Python
 
@@ -41,8 +74,6 @@
      | func_call
      | array
 
-IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
-
 func_call: atom "(" param_list ")"
 param_list: ( expr ("," expr)* )?  -> params
 
@@ -117,3 +148,11 @@ INCREMENT: "+="
 DECREMENT: "-="
 MULTIPLY: "*="
 DIVIDE: "/="
+
+
+
+// Semantic option parsing
+
+WILDCARD: "*"
+?semantic_choice: WILDCARD -> wildcard
+                | IDENTIFIER ("," IDENTIFIER)* -> list

+ 0 - 21
src/sccd/parser/grammar/state_ref.g

@@ -1,21 +0,0 @@
-// Grammar file for Lark-parser
-
-// Parsing target of a transition as a sequence of nodes
-
-%import common.WS
-%ignore WS
-%import common.ESCAPED_STRING
-
-
-_PATH_SEP: "/" 
-PARENT_NODE: ".." 
-CURRENT_NODE: "." 
-IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
-
-// target of a transition
-state_ref: path | "(" path ("," path)+ ")" 
-
-?path: absolute_path | relative_path 
-absolute_path: _PATH_SEP _path_sequence
-relative_path: _path_sequence
-_path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_sequence)?

+ 50 - 17
src/sccd/parser/statechart_parser.py

@@ -10,6 +10,8 @@ from sccd.execution import statechart_state
 # It will show a fragment of the source file and the line number of the error.
 class XmlLoadError(Exception):
   def __init__(self, src_file: str, el: etree.Element, msg):
+    # This is really dirty, but can't find a clean way to do this with lxml.
+
     parent = el.getparent()
     if parent is None:
       parent = el
@@ -239,19 +241,52 @@ class StateParser(ActionParser):
     self.state.require().exit = actions
 
   def start_transition(self, el):
+    parent_scope = self.scope.require()
+    globals = self.globals.require()
     if self.state.require().parent is None:
       raise Exception("Root <state> cannot be source of a transition.")
 
+    scope = Scope("transition", parent=parent_scope)
+
+    event = el.get("event")
+
+    if event is not None:
+      positive_events, negative_events = parse_events_decl(event)
+
+      positive_bitmap = Bitmap()
+      negative_bitmap = Bitmap()
+
+      def process_event_decl(e: EventDecl):
+        for i,p in enumerate(e.params):
+          scope.add_event_parameter(event_name=e.name, param_name=p.name, type=p.type, param_offset=i)
+
+      for e in positive_events:
+        event_id = globals.events.assign_id(e.name)
+        positive_bitmap |= bit(event_id)
+        process_event_decl(e)
+
+      for e in negative_events:
+        event_id = globals.events.assign_id(e.name)
+        negative_bitmap |= bit(event_id)
+        process_event_decl(e)
+
+      if not negative_bitmap:
+        trigger = Trigger(positive_bitmap)
+      else:
+        trigger = NegatedTrigger(positive_bitmap, negative_bitmap)
+
+    self.scope.push(scope)
     self.actions.push([])
 
   def end_transition(self, el):
     transitions = self.transitions.require()
+    scope = self.scope.pop()
     actions = self.actions.pop()
     source = self.state.require()
 
     # Parse <transition> element not until all states have been parsed,
     # and state tree constructed.
-    transitions.append((el, source, actions))
+    transitions.append((el, source, actions, scope))
 
 # Parses <tree> element and all its children.
 # In practice, this can't really parse a <tree> element because any encountered expression may contain identifiers pointing to model variables declared outside the <tree> element. To parse a <tree>, either manually add those variables to the 'scope', or, more likely, use a StatechartParser instance which will do this for you while parsing the <datamodel> node.
@@ -283,7 +318,7 @@ class TreeParser(StateParser):
     # Only now that our tree structure is complete can we resolve 'target' states of transitions.
     next_after_id = 0
     transitions = self.transitions.pop()
-    for t_el, source, actions in transitions:
+    for t_el, source, actions, t_scope in transitions:
       target_string = t_el.get("target")
       event = t_el.get("event")
       port = t_el.get("port")
@@ -326,7 +361,7 @@ class TreeParser(StateParser):
           self._raise(t_el, "Can only specify one of attributes 'after', 'event'.", None)
         try:
           after_expr = parse_expression(globals, expr=after)
-          after_type = after_expr.init_rvalue(scope)
+          after_type = after_expr.init_rvalue(t_scope)
           if after_type != Duration:
             msg = "Expression is '%s' type. Expected 'Duration' type." % str(after_type)
             if after_type == int:
@@ -338,10 +373,7 @@ class TreeParser(StateParser):
         except Exception as e:
           self._raise(t_el, "after=\"%s\": %s" % (after, str(e)), e)
       elif event is not None:
-        if port is None:
-          event_id = globals.events.assign_id(event)
-        else:
-          event_id = globals.events.assign_id(port + '.' + event)
+        event_id = globals.events.assign_id(event)
         trigger = EventTrigger(event_id, event, port)
         globals.inports.assign_id(port)
       else:
@@ -353,7 +385,7 @@ class TreeParser(StateParser):
       if cond is not None:
         try:
           expr = parse_expression(globals, expr=cond)
-          expr.init_rvalue(scope)
+          expr.init_rvalue(t_scope)
         except Exception as e:
           self._raise(t_el, "cond=\"%s\": %s" % (cond, str(e)), e)
         transition.guard = expr
@@ -376,13 +408,14 @@ class StatechartParser(TreeParser):
     statechart = self.statechart.require()
     # Use reflection to find the possible XML attributes and their values
     for aspect in dataclasses.fields(Semantics):
-      key = el.get(aspect.name)
-      if key is not None:
-        if key == "*":
-          setattr(statechart.semantics, aspect.name, None)
-        else:
-          value = aspect.type[key.upper()]
-          setattr(statechart.semantics, aspect.name, value)
+      text = el.get(aspect.name)
+      if text is not None:
+        result = parse_semantic_choice(text)
+        if result.data == "wildcard":
+          setattr(statechart.semantics, aspect.name, list(aspect.type))
+        elif result.data == "list":
+          options = [aspect.type[token.value.upper()] for token in result.children]
+          setattr(statechart.semantics, aspect.name, options)
 
   def end_semantics(self, el):
     self._internal_end_semantics(el)
@@ -403,7 +436,7 @@ class StatechartParser(TreeParser):
     parsed = parse_expression(globals, expr=expr)
     rhs_type = parsed.init_rvalue(scope)
     val = parsed.eval(None, [], None)
-    scope.add(id, val)
+    scope.add_variable_w_initial(name=id, initial=val)
 
   def start_datamodel(self, el):
     statechart = self.statechart.require()
@@ -419,7 +452,7 @@ class StatechartParser(TreeParser):
     ext_file = el.get("src")
     statechart = None
     if ext_file is None:
-      statechart = Statechart(tree=None, semantics=Semantics(), scope=Scope("instance", parent_scope=statechart_state.builtin_scope))
+      statechart = StatechartVariableSemantics(tree=None, semantics=VariableSemantics(), scope=Scope("instance", parent=statechart_state.builtin_scope))
     elif self.load_external:
       ext_file_path = os.path.join(os.path.dirname(src_file), ext_file)
       self.statecharts.push([])

+ 2 - 2
src/sccd/syntax/expression.py

@@ -40,7 +40,7 @@ class LValue(Expression):
     # LValues can also serve as expressions!
     def eval(self, current_state, events, memory):
         variable = self.eval_lvalue(current_state, events, memory)
-        return memory.load(variable.offset)
+        return variable.load(events, memory)
 
 
 @dataclass
@@ -57,7 +57,7 @@ class Identifier(LValue):
 
     def init_lvalue(self, scope, expected_type):
         assert self.variable is None
-        self.variable = scope.put(self.name, expected_type)
+        self.variable = scope.put_variable_assignment(self.name, expected_type)
         # print("init lvalue", self.name, "as", self.variable)
 
     def eval_lvalue(self, current_state, events, memory) -> Variable:

+ 156 - 50
src/sccd/syntax/scope.py

@@ -1,88 +1,194 @@
+from abc import *
 from typing import *
 from dataclasses import *
+from inspect import signature
 import itertools
 
+
+@dataclass
+class Value(ABC):
+  name: str
+  type: type
+
+  @abstractmethod
+  def is_read_only(self) -> bool:
+    pass
+
+  @abstractmethod
+  def load(self, events, memory) -> Any:
+    pass
+    
+  @abstractmethod
+  def store(self, memory, value):
+    pass
+
 # Stateless stuff we know about a variable
 @dataclass
-class Variable:
-  # offset in memory
+class Variable(Value):
   offset: int
-  # type of variable
-  type: type
+  initial: Any = None
+
+  def is_read_only(self) -> bool:
+    return False
+
+  def load(self, events, memory) -> Any:
+    return memory.load(self.offset)
+
+  def store(self, memory, value):
+    memory.store(self.offset, value)
+
+class EventParam(Variable):
+  def __init__(self, name, type, offset, event_name, param_offset):
+    super().__init__(name, type, offset)
+    self.event_name: str = event_name
+    self.param_offset: int = param_offset
+
+  def is_read_only(self) -> bool:
+    return True
+
+  def load(self, events, memory) -> Any:
+    from_stack = Variable.load(self, events, memory)
+    if from_stack is not None:
+      return from_stack
+    else:
+      # find event in event list and get the parameter we're looking for
+      e = [e for e in events if e.name == self.event_name][0]
+      value = e.params[self.param_offset]
+      # "cache" the parameter value on our reserved stack position so the next
+      # 'load' will be faster
+      Variable.store(self, memory, value)
+      return value
+
+  def store(self, memory, value):
+    # Bug in the code: should never attempt to write to EventParam
+    assert False
+
+# Constants are special: their values are stored in the object itself, not in
+# any instance's "memory"
+@dataclass
+class Constant(Value):
+  value: Any
+
+  def is_read_only(self) -> bool:
+    return True
+
+  def load(self, events, memory) -> Any:
+    return self.value
 
-  default_value: Any = None
+  def store(self, memory, value):
+    # Bug in the code: should never attempt to write to Constant
+    assert False
 
-# Stateless stuff we know about a scope (= set of variable names)
+# Stateless stuff we know about a scope (= set of named values)
 class Scope:
-  def __init__(self, name: str, parent_scope: 'Scope'):
+  def __init__(self, name: str, parent: 'Scope'):
     self.name = name
-    self.parent_scope = parent_scope
-    if parent_scope:
-      self.start_offset = parent_scope.start_offset + len(parent_scope.names)
+    self.parent = parent
+    self.frozen = False
+
+    if parent:
+      self.parent_offset = parent.total_size()
+      self.parent.frozen = True
     else:
-      self.start_offset = 0
-    self.names: Dict[str, Variable] = {}
-    self.variables: List[str] = []
+      self.parent_offset = 0
+
+    # Mapping from name to Value
+    self.named_values: Dict[str, Value] = {}
+
+    # All non-constant values, ordered by memory position
+    self.variables: List[Value] = []
 
-  def localsize(self) -> int:
+  def local_size(self) -> int:
     return len(self.variables)
 
-  def size(self) -> int:
-    return self.start_offset + len(self.variables)
+  def total_size(self) -> int:
+    return self.parent_offset + len(self.variables)
 
-  def all(self):
-    if self.parent_scope:
-      return itertools.chain(self.parent_scope.all(), self.names.items())
+  def all_variables(self):
+    if self.parent:
+      return itertools.chain(self.parent.all_variables(), self.variables)
     else:
-      return self.names.items()
+      return self.variables
 
-  def get_name(self, offset):
-    if offset >= self.start_offset:
-      return self.variables[offset - self.start_offset]
+  def list_scope_names(self):
+    if self.parent:
+      return [self.name] + self.parent.list_scope_names()
     else:
-      return self.parent_scope.name(offset)
+      return [self.name]
 
-  def _internal_lookup(self, name: str) -> Optional[Tuple['Scope', Variable]]:
+  def _internal_lookup(self, name: str) -> Optional[Tuple['Scope', Value]]:
     try:
-      return (self, self.names[name])
+      return (self, self.named_values[name])
     except KeyError:
-      if self.parent_scope is not None:
-        return self.parent_scope._internal_lookup(name)
-
-  def assert_available(self, name: str):
-    found = self._internal_lookup(name)
-    if found is not None:
-      scope, variable = found
-      raise Exception("Name '%s' already in use in scope '%s'" % (name, scope.name))
+      if self.parent is not None:
+        return self.parent._internal_lookup(name)
+      else:
+        return None
 
-  def get(self, name: str) -> Variable:
+  def get(self, name: str) -> Value:
     found = self._internal_lookup(name)
     if not found:
       # return None
-      raise Exception("No variable with name '%s' found in any scope." % name)
+      raise Exception("No variable with name '%s' found in any of scopes: %s" % (name, str(self.list_scope_names())))
     else:
       return found[1]
 
-  def put(self, name: str, expected_type: type) -> Variable:
-    found = self._internal_lookup(name)
+  # Add name to scope if it does not exist yet, otherwise return existing Variable for name.
+  # This is done when encountering an assignment statement in a block.
+  def put_variable_assignment(self, name: str, expected_type: type) -> Variable:
+    assert not self.frozen
 
+    found = self._internal_lookup(name)
     if found:
       scope, variable = found
+      if variable.is_read_only():
+        raise Exception("Cannot assign to name '%s': Is read-only value of type '%s' in scope '%s'" %(name, str(variable.type), scope.name))
       if variable.type == expected_type:
-        # name in use, but type matches
         return variable
-      else:
-        raise Exception("Cannot use name '%s' as LValue of type '%s' in scope '%s'. Name already refers to variable of type '%s' in scope '%s'" % (name, str(expected_type), self.name, str(variable.type), scope.name))
     else:
-      # name still available: add it to this scope
-      variable = Variable(offset=self.size(), type=expected_type)
-      self.names[name] = variable
-      self.variables.append(name)
+      variable = Variable(name=name, type=expected_type, offset=self.total_size())
+      self.named_values[name] = variable
+      self.variables.append(variable)
       return variable
 
-  def add(self, name: str, default_value) -> Variable:
-    self.assert_available(name)
-    expected_type = type(default_value)
-    variable = self.put(name, expected_type)
-    variable.default_value = default_value
+  def _assert_name_available(self, name: str):
+    found = self._internal_lookup(name)
+    if found:
+      scope, variable = found
+      raise Exception("Name '%s' already in use in scope '%s'" % (name, scope.name))
+
+  def add_constant(self, name: str, value) -> Constant:
+    assert not self.frozen
+    self._assert_name_available(name)
+    c = Constant(name=name, type=type(value), value=value)
+    self.named_values[name] = c
+    return c
+
+  def add_variable_w_initial(self, name: str, initial: Any) -> Variable:
+    assert not self.frozen
+    self._assert_name_available(name)
+    variable = Variable(name=name, type=type(initial), offset=self.total_size(), initial=initial)
+    self.named_values[name] = variable
+    self.variables.append(variable)
     return variable
+
+  def add_event_parameter(self, event_name: str, param_name: str, type: type, param_offset=int) -> EventParam:
+    assert not self.frozen
+    self._assert_name_available(param_name)
+    param = EventParam(
+      name=param_name, type=type, offset=self.total_size(),
+      event_name=event_name, param_offset=param_offset)
+    self.named_values[param_name] = param
+    self.variables.append(param)
+    return param
+
+  def add_function(self, name: str, function: Callable) -> Constant:
+    sig = signature(function)
+    return_type = sig.return_annotation
+    args = list(sig.parameters.values())[3:]
+    param_types = [a.annotation for a in args]
+    function_type = Callable[param_types, return_type]
+    
+    c = Constant(name=name, type=function_type, value=function)
+    self.named_values[name] = c
+    return c

+ 27 - 17
src/sccd/syntax/statechart.py

@@ -13,6 +13,7 @@ class SemanticOption:
 class BigStepMaximality(SemanticOption, Enum):
   TAKE_ONE = 0
   TAKE_MANY = 1
+  SYNTACTIC = 2
 
 class ComboStepMaximality(SemanticOption, Enum):
   COMBO_TAKE_ONE = 0
@@ -24,6 +25,7 @@ class InternalEventLifeline(SemanticOption, Enum):
   NEXT_SMALL_STEP = 2
 
   REMAINDER = 3
+  SAME = 5
 
 class InputEventLifeline(SemanticOption, Enum):
   WHOLE = 0
@@ -54,23 +56,31 @@ class Semantics:
   priority: Priority = Priority.SOURCE_PARENT
   concurrency: Concurrency = Concurrency.SINGLE
 
-  # Check if any field has been set to None.
-  def has_wildcard(self):
-    for field in fields(self):
-      if getattr(self, field.name) is None:
-        return True
-    return False
-
-  # List of mappings from field name to value for that field.
-  # Each mapping in the list can be used as parameter to the dataclasses.replace function
-  # to create a new semantic configuration with the changes applied.
-  def wildcard_cart_product(self) -> Iterable[Mapping[str, Any]]:
-    wildcard_fields = []
-    for field in fields(self):
-      if getattr(self, field.name) is None:
-        wildcard_fields.append(field)
-    types = (field.type for field in wildcard_fields)
-    return ({wildcard_fields[i].name: option for i,option in enumerate(configuration)} for configuration in itertools.product(*types))
+# Semantics with multiple options per field
+@dataclass
+class VariableSemantics:
+  big_step_maximality: List[BigStepMaximality] = field(default_factory=lambda:[BigStepMaximality.TAKE_MANY])
+  combo_step_maximality: List[ComboStepMaximality] = field(default_factory=lambda:[ComboStepMaximality.COMBO_TAKE_ONE])
+  internal_event_lifeline: List[InternalEventLifeline] = field(default_factory=lambda:[InternalEventLifeline.NEXT_COMBO_STEP])
+  input_event_lifeline: List[InputEventLifeline] = field(default_factory=lambda:[InputEventLifeline.FIRST_COMBO_STEP])
+  enabledness_memory_protocol: List[MemoryProtocol] = field(default_factory=lambda:[MemoryProtocol.COMBO_STEP])
+  assignment_memory_protocol: List[MemoryProtocol] = field(default_factory=lambda:[MemoryProtocol.COMBO_STEP])
+  priority: List[Priority] = field(default_factory=lambda:[Priority.SOURCE_PARENT])
+  concurrency: List[Concurrency] = field(default_factory=lambda:[Concurrency.SINGLE])
+
+  # Get all possible combinations
+  def generate_variants(self) -> List[Semantics]:
+    my_fields = fields(self)
+    chosen_options = (getattr(self, f.name) for f in my_fields)
+    variants = itertools.product(*chosen_options)
+
+    return [Semantics(**{f.name: o for f,o in zip(my_fields, variant)}) for variant in variants]
+
+@dataclass
+class StatechartVariableSemantics:
+  tree: StateTree
+  semantics: VariableSemantics
+  scope: Scope
 
 @dataclass
 class Statechart:

+ 5 - 3
src/sccd/syntax/statement.py

@@ -28,12 +28,14 @@ class Assignment(Statement):
 
     def exec(self, current_state, events, memory):
         val = self.rhs.eval(current_state, events, memory)
-        offset = self.lhs.eval_lvalue(current_state, events, memory).offset
+        variable = self.lhs.eval_lvalue(current_state, events, memory)
 
         def load():
-            return memory.load(offset)
+            return variable.load(events, memory)
+            # return memory.load(offset)
         def store(val):
-            memory.store(offset, val)
+            variable.store(memory, val)
+            # memory.store(offset, val)
 
         def assign():
             store(val)

+ 10 - 13
src/sccd/syntax/test_scope.py

@@ -6,22 +6,22 @@ class TestScope(unittest.TestCase):
 
   def test_scope(self):
     
-    builtin = Scope("builtin", parent_scope=None)
+    builtin = Scope("builtin", parent=None)
 
     # Lookup LHS value (creating it in the current scope if not found)
 
-    variable = builtin.put("in_state", Callable[[List[str]], bool])
+    variable = builtin.put_variable_assignment("in_state", Callable[[List[str]], bool])
     self.assertEqual(variable.offset, 0)
 
-    globals = Scope("globals", parent_scope=builtin)
-    variable = globals.put("x", int)
+    globals = Scope("globals", parent=builtin)
+    variable = globals.put_variable_assignment("x", int)
     self.assertEqual(variable.offset, 1)
 
-    variable = globals.put("in_state", Callable[[List[str]], bool])
+    variable = globals.put_variable_assignment("in_state", Callable[[List[str]], bool])
     self.assertEqual(variable.offset, 0)
 
-    local = Scope("local", parent_scope=globals)
-    variable = local.put("x", int)
+    local = Scope("local", parent=globals)
+    variable = local.put_variable_assignment("x", int)
     self.assertEqual(variable.offset, 1)
 
     # Lookup RHS value (returning None if not found)
@@ -31,9 +31,6 @@ class TestScope(unittest.TestCase):
     # name 'y' doesn't exist in any scope
     self.assertRaises(Exception, lambda: local.get("y"))
 
-    # Cannot use 'in_state' as string LValue, already another type
-    self.assertRaises(Exception, lambda: local.get("in_state", str))
-
-    self.assertEqual(builtin.size(), 1)
-    self.assertEqual(globals.size(), 2)
-    self.assertEqual(local.size(), 2)
+    self.assertEqual(builtin.total_size(), 1)
+    self.assertEqual(globals.total_size(), 2)
+    self.assertEqual(local.total_size(), 2)

+ 19 - 0
src/sccd/syntax/tree.py

@@ -81,6 +81,25 @@ class ParallelState(State):
                 targets.extend(c.getEffectiveTargetStates(instance))
         return targets
 
+@dataclass
+class Param:
+    name: str 
+    type: type
+
+    def render(self) -> str:
+        return self.name + ': ' + str(self.type)
+
+@dataclass
+class EventDecl:
+    name: str
+    params: List[Param]
+
+    def render(self) -> str:
+        if self.params:
+            return self.name + '(' + ', '.join(self.params) + ')'
+        else:
+            return self.name
+
 @dataclass
 class Trigger:
     enabling: Bitmap

+ 9 - 4
test/lib/test_parser.py

@@ -93,20 +93,25 @@ class TestParser(StatechartParser):
 
     globals.process_durations()
 
+    variants = statechart.semantics.generate_variants()
+
     def variant_description(i, variant) -> str:
       if not variant:
         return ""
-      return "Variant %d: %s" % (i, ", ".join(str(val) for val in variant.values()))
+      text = "Semantic variant %d of %d:" % (i+1, len(variants))
+      for f in fields(variant):
+        text += "\n  %s: %s" % (f.name, getattr(variant, f.name))
+      return text
 
     # Generate test variants for all semantic wildcards filled in
-    tests.extend( 
+    tests.extend(
       TestVariant(
         name=variant_description(i, variant),
         model=SingleInstanceModel(
           globals,
-          Statechart(tree=statechart.tree, scope=statechart.scope, semantics=dataclasses.replace(statechart.semantics, **variant))),
+          Statechart(tree=statechart.tree, scope=statechart.scope, semantics=variant)),
         input=input,
         output=output)
 
-      for i, variant in enumerate(statechart.semantics.wildcard_cart_product())
+      for i, variant in enumerate(variants)
     )

+ 7 - 4
test/test_files/day_atlee/statechart_fig1_redailer.xml

@@ -1,10 +1,13 @@
 <?xml version="1.0" ?>
 <statechart>
-  <semantics input_event_lifeline="whole"/>
+  <semantics
+    big_step_maximality="syntactic"
+    concurrency="many"
+    input_event_lifeline="whole"/>
 
   <datamodel>
     <var id="c" expr="0"/>
-    <var id="lp" expr="0"/>
+    <var id="lp" expr="1234567890"/>
     <var id="p" expr="0"/>
 
     <func id="digit(i:int, pos:int)">
@@ -20,7 +23,7 @@
     <state>
       <parallel id="Dialing">
         <state id="Dialer" initial="WaitForDial">
-          <state id="WaitForDial">
+          <state id="WaitForDial" stable="true">
             <!-- t1 -->
             <transition event="dial(d:int), not redial" cond="c &lt; 10" target=".">
               <code>
@@ -53,7 +56,7 @@
         </state>
 
         <state id="Redialer" initial="WaitForRedial">
-          <state id="WaitForRedial">
+          <state id="WaitForRedial" stable="true">
             <!-- t5 -->
             <transition event="redial" cond="c == 0" target="../RedialDigits">
               <code>

+ 17 - 0
test/test_files/day_atlee/test_07_redialer_same.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="statechart_fig1_redialer.xml">
+    <override_semantics
+      internal_event_lifeline="same"/>
+  </statechart>
+
+  <input>
+    <input_event port="in" name="redial" time="0 d"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="done"/>
+    </big_step>
+  </output>
+</test>

+ 1 - 1
test/test_files/features/after/test_after.xml

@@ -3,7 +3,7 @@
   <statechart>
     <!-- after events are always received as input events in a later big step -->
     <semantics
-        big_step_maximality="*"
+        big_step_maximality="take_one, take_many"
         combo_step_maximality="*"/>
     <tree>
       <state initial="s1">

+ 1 - 1
test/test_files/semantics/event_lifeline/test_flat_nextbs.xml

@@ -2,7 +2,7 @@
 <test>
   <statechart src="statechart_flat.xml">
     <override_semantics
-      big_step_maximality="*"
+      big_step_maximality="take_one, take_many"
       internal_event_lifeline="queue"
       input_event_lifeline="*"/>
   </statechart>

+ 1 - 1
test/test_files/semantics/event_lifeline/test_ortho_nextbs.xml

@@ -2,7 +2,7 @@
 <test>
   <statechart src="statechart_ortho.xml">
     <override_semantics
-      big_step_maximality="*"
+      big_step_maximality="take_one, take_many"
       internal_event_lifeline="queue"
       input_event_lifeline="whole"/>
   </statechart>

+ 1 - 1
test/test_files/semantics/event_lifeline/test_ortho_nextss.xml

@@ -2,7 +2,7 @@
 <test>
   <statechart src="statechart_ortho.xml">
     <override_semantics
-      big_step_maximality="*"
+      big_step_maximality="take_one, take_many"
       internal_event_lifeline="next_small_step"
       input_event_lifeline="*"/>
   </statechart>