فهرست منبع

Implemented Enabledness Memory Protocol semantic options.

Joeri Exelmans 5 سال پیش
والد
کامیت
45c38688c2

+ 36 - 0
src/sccd/execution/memory.py

@@ -0,0 +1,36 @@
+from typing import *
+from sccd.util.bitmap import *
+from sccd.util.debug import *
+
+class Memory:
+  def __init__(self, scope):
+    self.storage = [None] * scope.size()
+    self.dirty = Bitmap(0)
+
+    # Write default values to storage
+    for name, variable in scope.all():
+      self.storage[variable.offset] = variable.default_value
+
+    self.dirty_storage = list(self.storage)
+
+  def store(self, offset: int, value: Any):
+    # Grow storage if needed
+    if offset >= len(self.storage):
+      n = offset-len(self.storage)+1
+      self.storage.extend([None]*n)
+      self.dirty_storage.extend([None]*n)
+
+    offset_bit = bit(offset)
+    if self.dirty & offset_bit:
+      print_debug("Warning: Race condition at offset %d" % offset)
+    self.dirty_storage[offset] = value
+    self.dirty |= offset_bit
+
+  def load(self, offset: int) -> Any:
+    return self.storage[offset]
+
+  def rotate(self):
+    if self.dirty:
+      self.storage = self.dirty_storage # move list
+      self.dirty_storage = list(self.storage) # copy list
+      self.dirty = Bitmap(0)

+ 3 - 0
src/sccd/execution/round.py

@@ -60,6 +60,7 @@ class Round(ABC):
     def __init__(self, name):
         self.name = name
         self.parent = None
+        self.memory = None
 
         self.remainder_events = [] # events enabled for the remainder of the current round
         self.next_events = [] # events enabled for the entirety of the next round
@@ -67,6 +68,8 @@ class Round(ABC):
     def run(self, arenas_changed: Bitmap = Bitmap()) -> Bitmap:
         changed = self._internal_run(arenas_changed)
         if changed:
+            if self.memory:
+                self.memory.rotate()
             self.remainder_events = self.next_events
             self.next_events = []
             print_debug("completed "+self.name)

+ 11 - 1
src/sccd/execution/statechart_instance.py

@@ -7,11 +7,14 @@ from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
 from sccd.execution.round import *
 from sccd.execution.statechart_state import *
+from sccd.execution.memory import *
 
 class StatechartInstance(Instance):
     def __init__(self, statechart: Statechart, object_manager):
         self.object_manager = object_manager
 
+        memory = Memory(scope=statechart.scope)
+
         semantics = statechart.semantics
 
         if semantics.has_wildcard():
@@ -63,9 +66,16 @@ class StatechartInstance(Instance):
             InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event
         }[semantics.internal_event_lifeline]
 
+        if semantics.enabledness_memory_protocol == EnablednessMemoryProtocol.GC_SMALL_STEP:
+            small_step.memory = memory
+        elif semantics.enabledness_memory_protocol == EnablednessMemoryProtocol.GC_COMBO_STEP:
+            combo_step.memory = memory
+        elif semantics.enabledness_memory_protocol == EnablednessMemoryProtocol.GC_BIG_STEP:
+            self._big_step.memory = memory
+
         print_debug("\nRound hierarchy: " + str(self._big_step) + '\n')
 
-        self.state = StatechartState(statechart, self, raise_internal)
+        self.state = StatechartState(statechart, self, memory, raise_internal)
 
         small_step.state = self.state
 

+ 15 - 9
src/sccd/execution/statechart_state.py

@@ -3,19 +3,25 @@ from sccd.syntax.statechart import *
 from sccd.execution.event import *
 from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
+from sccd.syntax.scope import *
+
+
+def _in_state(current_state, events, memory, state_list):
+  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)
+
 
 # Set of current states etc.
 class StatechartState:
 
-  def __init__(self, statechart: Statechart, instance, raise_internal):
+  def __init__(self, statechart: Statechart, instance, memory, raise_internal):
     self.model = statechart
     self.instance = instance
+    self.memory = memory
     self.raise_internal = raise_internal
 
-    self.data_model = statechart.datamodel
-
-    self.data_model.set("INSTATE", self.in_state)
-
     # these 2 fields have the same information
     self.configuration: List[State] = []
     self.configuration_bitmap: Bitmap() = Bitmap()
@@ -96,7 +102,7 @@ class StatechartState:
         self._start_timers(s.gen.after_triggers)
     try:
         self.configuration = self.config_mem[self.configuration_bitmap]
-    except:
+    except KeyError:
         self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.model.tree.state_list if self.configuration_bitmap.has(s.gen.state_id)]
 
     return t.gen.arena_bitmap
@@ -111,7 +117,7 @@ class StatechartState:
       if t.guard is None:
           return True
       else:
-          return t.guard.eval(events, self.data_model)
+          return t.guard.eval(self, events, self.memory)
 
   def check_source(self, t) -> bool:
       return self.configuration_bitmap.has(t.source.gen.state_id)
@@ -126,11 +132,11 @@ class StatechartState:
                   OutputPortTarget(a.outport),
                   a.time_offset))
           elif isinstance(a, Code):
-              a.block.exec(events, self.data_model)
+              a.block.exec(self, events, self.memory)
 
   def _start_timers(self, triggers: List[AfterTrigger]):
       for after in triggers:
-          delay: Duration = after.delay.eval([], self.data_model)
+          delay: Duration = after.delay.eval(self, [], self.memory)
           self.output.append(OutputEvent(
               Event(id=after.id, name=after.name, parameters=[after.nextTimerId()]),
               target=InstancesTarget([self.instance]),

+ 7 - 10
src/sccd/parser/expression_parser.py

@@ -2,6 +2,7 @@ import os
 from lark import Lark, Transformer
 from sccd.syntax.statement import *
 from sccd.model.globals import *
+from sccd.syntax.scope import *
 
 _grammar_dir = os.path.join(os.path.dirname(__file__), "grammar")
 
@@ -17,7 +18,6 @@ class _ExpressionTransformer(Transformer):
   def __init__(self):
     super().__init__()
     self.globals: Globals = None
-    self.datamodel: DataModel = None
 
   array = Array
 
@@ -34,11 +34,7 @@ class _ExpressionTransformer(Transformer):
 
   def identifier(self, node):
     name = node[0].value
-    try:
-      offset, type = self.datamodel.lookup(name)
-    except Exception as e:
-      raise Exception("Unknown variable '%s'" % name) from e
-    return Identifier(name, offset, type)
+    return Identifier(name)
 
   def binary_expr(self, node):
     return BinaryExpression(node[0], node[1].value, node[2])
@@ -82,6 +78,9 @@ class _ExpressionTransformer(Transformer):
     self.globals.durations.append(d)
     return d
 
+  def expression_stmt(self, node):
+    return ExpressionStatement(node[0])
+
 # Global variables so we don't have to rebuild our parser every time
 # Obviously not thread-safe
 _transformer = _ExpressionTransformer()
@@ -90,18 +89,16 @@ _state_ref_parser = Lark(_state_ref_grammar, parser="lalr", start=["state_ref"])
 
 # Exported functions:
 
-def parse_expression(globals: Globals, datamodel, expr: str) -> Expression:
+def parse_expression(globals: Globals, expr: str) -> Expression:
   _transformer.globals = globals
-  _transformer.datamodel = datamodel
   return _action_lang_parser.parse(expr, start="expr")
 
 def parse_duration(globals: Globals, expr:str) -> Duration:
   _transformer.globals = globals
   return _action_lang_parser.parse(expr, start="duration")
 
-def parse_block(globals: Globals, datamodel, block: str) -> Statement:
+def parse_block(globals: Globals, block: str) -> Statement:
   _transformer.globals = globals
-  _transformer.datamodel = datamodel
   return _action_lang_parser.parse(block, start="block")
 
 def parse_state_ref(state_ref: str):

+ 2 - 1
src/sccd/parser/grammar/action_language.g

@@ -99,9 +99,10 @@ TIME_D: "d" // for zero-duration
 
 // Statement parsing
 
-block: stmt (";" stmt)*
+block: (stmt ";")*
 
 ?stmt: assignment
+     | expr -> expression_stmt
 
 assignment: lhs assign_operator expr
 

+ 43 - 39
src/sccd/parser/statechart_parser.py

@@ -4,6 +4,7 @@ from sccd.parser.expression_parser import *
 from sccd.syntax.statechart import *
 from sccd.syntax.tree import *
 from sccd.execution.event import *
+from sccd.execution import statechart_state
 
 # An Exception that occured while visiting an XML element.
 # It will show a fragment of the source file and the line number of the error.
@@ -13,43 +14,34 @@ class XmlLoadError(Exception):
     if parent is None:
       parent = el
 
-    lines = etree.tostring(parent).decode('utf-8').strip().split('\n')
-    nbr_highlighted_lines = len(etree.tostring(el).decode('utf-8').strip().split('\n'))
+    with open(src_file, 'r') as file:
+      lines = file.read().split('\n')
+      numbered_lines = list(enumerate(lines, 1))
+
+    parent_lines = etree.tostring(parent).decode('utf-8').strip().split('\n')
+    el_lines = etree.tostring(el).decode('utf-8').strip().split('\n')
     text = []
 
     parent_firstline = parent.sourceline
-    parent_lastline = parent.sourceline + len(lines) - 1
+    parent_lastline = parent.sourceline + len(parent_lines) - 1
 
     el_firstline = el.sourceline
-    el_lastline = el.sourceline + nbr_highlighted_lines - 1
-
-    numbered_lines = list(zip(range(parent.sourceline, parent.sourceline + len(lines)), lines))
+    el_lastline = el.sourceline + len(el_lines) - 1
 
-    from_line = max(parent_firstline, el_firstline - 5)
-    to_line = min(parent_lastline, el_lastline + 5)
+    # numbered_lines = list(zip(range(parent.sourceline, parent.sourceline + len(lines)), lines))
 
-    if from_line == parent_firstline+1:
-      from_line = parent_firstline
-    if to_line == parent_lastline-1:
-      to_line = parent_lastline
+    from_line = max(parent_firstline, el_firstline - 4)
+    to_line = min(parent_lastline, el_lastline + 4)
 
     def f(tup):
       return from_line <= tup[0] <= to_line
 
-    if from_line != parent_firstline:
-      text.append("%4d: %s" % (parent_firstline, lines[0]))
-      text.append("     ...")
-
     for linenumber, line in filter(f, numbered_lines):
       ll = "%4d: %s" % (linenumber, line)
       if el_firstline <= linenumber <= el_lastline:
         ll = termcolor.colored(ll, 'yellow')
       text.append(ll)
 
-    if to_line != parent_lastline:
-      text.append("     ...")
-      text.append("%4d: %s" % (parent_lastline, lines[-1]))
-
     super().__init__("\n\n%s\n\n%s:\nline %d: <%s>: %s" % ('\n'.join(text), src_file,el.sourceline, el.tag, msg))
     
     # self.src_file = src_file
@@ -132,7 +124,7 @@ class ActionParser(XmlParser):
   def __init__(self):
     super().__init__()
     self.globals = XmlParser.Context("globals")
-    self.datamodel = XmlParser.Context("datamodel")
+    self.scope = XmlParser.Context("scope")
     self.actions = XmlParser.Context("actions")
 
   def end_raise(self, el):
@@ -151,10 +143,11 @@ class ActionParser(XmlParser):
 
   def end_code(self, el):
     globals = self.globals.require()
-    datamodel = self.datamodel.require()
+    scope = self.scope.require()
     actions = self.actions.require()
 
-    block = parse_block(globals, datamodel, block=el.text)
+    block = parse_block(globals, block=el.text)
+    block.init_stmt(scope)
     a = Code(block)
     actions.append(a)
 
@@ -267,14 +260,14 @@ class TreeParser(StateParser):
 
   def start_tree(self, el):
     statechart = self.statechart.require()
-    self.datamodel.push(statechart.datamodel)
+    self.scope.push(statechart.scope)
     self.transitions.push([])
     self.state_children.push({})
 
   def end_tree(self, el):
     statechart = self.statechart.require()
     globals = self.globals.require()
-    datamodel = self.datamodel.pop()
+    scope = self.scope.pop()
 
     root_states = self.state_children.pop()
     if len(root_states) == 0:
@@ -321,11 +314,19 @@ class TreeParser(StateParser):
 
       # Trigger
       if after is not None:
-        after_expr = parse_expression(globals, datamodel, expr=after)
-        # print(after_expr)
-        event = "_after%d" % next_after_id # transition gets unique event name
-        next_after_id += 1
-        trigger = AfterTrigger(globals.events.assign_id(event), event, after_expr)
+        try:
+          after_expr = parse_expression(globals, expr=after)
+          after_type = after_expr.init_rvalue(scope)
+          if after_type != Duration:
+            msg = "Expression is '%s' type. Expected 'Duration' type." % str(after_type)
+            if after_type == int:
+              msg += "\n Hint: Did you forget a duration unit sufix? ('s', 'ms', ...)"
+            raise Exception(msg)
+          event = "_after%d" % next_after_id # transition gets unique event name
+          next_after_id += 1
+          trigger = AfterTrigger(globals.events.assign_id(event), event, after_expr)
+        except Exception as e:
+          self._raise(t_el, "after=\"%s\": %s" % (after, str(e)), e)
       elif event is not None:
         trigger = Trigger(globals.events.assign_id(event), event, port)
         globals.inports.assign_id(port)
@@ -337,9 +338,10 @@ class TreeParser(StateParser):
       # Guard
       if cond is not None:
         try:
-          expr = parse_expression(globals, datamodel, expr=cond)
+          expr = parse_expression(globals, expr=cond)
+          expr.init_rvalue(scope)
         except Exception as e:
-          self._raise(t_el, "Condition '%s': %s" % (cond, str(e)), e)
+          self._raise(t_el, "cond=\"%s\": %s" % (cond, str(e)), e)
         transition.guard = expr
       source.transitions.append(transition)
 
@@ -378,19 +380,22 @@ class StatechartParser(TreeParser):
 
   def end_var(self, el):
     globals = self.globals.require()
-    datamodel = self.datamodel.require()
+    scope = self.scope.require()
 
     id = el.get("id")
     expr = el.get("expr")
-    parsed = parse_expression(globals, datamodel, expr=expr)
-    datamodel.create(id, parsed.eval([], datamodel))
+
+    parsed = parse_expression(globals, expr=expr)
+    rhs_type = parsed.init_rvalue(scope)
+    val = parsed.eval(None, [], None)
+    scope.put_lvalue_default(id, val)
 
   def start_datamodel(self, el):
     statechart = self.statechart.require()
-    self.datamodel.push(statechart.datamodel)
+    self.scope.push(statechart.scope)
 
   def end_datamodel(self, el):
-    self.datamodel.pop()
+    self.scope.pop()
 
   # <statechart>
 
@@ -399,8 +404,7 @@ class StatechartParser(TreeParser):
     ext_file = el.get("src")
     statechart = None
     if ext_file is None:
-      statechart = Statechart(
-        tree=None, semantics=Semantics(), datamodel=DataModel())
+      statechart = Statechart(tree=None, semantics=Semantics(), scope=Scope("instance", wider_scope=statechart_state.builtin_scope))
     elif self.load_external:
       ext_file_path = os.path.join(os.path.dirname(src_file), ext_file)
       self.statecharts.push([])

+ 41 - 41
src/sccd/syntax/datamodel.py

@@ -1,41 +1,41 @@
-from typing import *
-from sccd.util.namespace import *
-
-class Variable:
-    def __init__(self, value, type):
-        self.value = value
-        self.type = type
-
-    def __repr__(self):
-      return "Var(" + str(self.type) + ' = ' + str(self.value) + ")"
-
-class DataModel:
-    def __init__(self):
-        self.names: Dict[str, int] = {}
-        self.storage = []
-
-        # Reserved variable. This is dirty, find better solution
-        self.create("INSTATE", None, Callable[[List[str]], bool])
-
-    def create(self, name: str, value, _type=None) -> int:
-        if _type is None:
-            _type = type(value)
-        if name in self.names:
-            raise Exception("Name already in use.")
-        id = len(self.storage)
-        self.storage.append(Variable(value, _type))
-        self.names[name] = id
-        return id
-
-    def lookup(self, name) -> Tuple[int, type]:
-        id = self.names[name]
-        var = self.storage[id]
-        return (id, var.type)
-
-    def set(self, name, value):
-        id = self.names[name]
-        var = self.storage[id]
-        var.value = value
-
-    def __repr__(self):
-        return "DataModel(" + ", ".join(name + ': '+str(self.storage[offset]) for name,offset in self.names.items()) + ")"
+# from typing import *
+# from sccd.util.namespace import *
+
+# class Variable:
+#     def __init__(self, value, type):
+#         self.value = value
+#         self.type = type
+
+#     def __repr__(self):
+#       return "Var(" + str(self.type) + ' = ' + str(self.value) + ")"
+
+# class DataModel:
+#     def __init__(self):
+#         self.names: Dict[str, int] = {}
+#         self.storage = []
+
+#         # Reserved variable. This is dirty, find better solution
+#         self.create("INSTATE", None, Callable[[List[str]], bool])
+
+#     def create(self, name: str, value, _type=None) -> int:
+#         if _type is None:
+#             _type = type(value)
+#         if name in self.names:
+#             raise Exception("Name already in use.")
+#         id = len(self.storage)
+#         self.storage.append(Variable(value, _type))
+#         self.names[name] = id
+#         return id
+
+#     def lookup(self, name) -> Tuple[int, type]:
+#         id = self.names[name]
+#         var = self.storage[id]
+#         return (id, var.type)
+
+#     def set(self, name, value):
+#         id = self.names[name]
+#         var = self.storage[id]
+#         var.value = value
+
+#     def __repr__(self):
+#         return "DataModel(" + ", ".join(name + ': '+str(self.storage[offset]) for name,offset in self.names.items()) + ")"

+ 102 - 84
src/sccd/syntax/expression.py

@@ -1,7 +1,7 @@
 from abc import *
 from typing import *
 from dataclasses import *
-from sccd.syntax.datamodel import *
+from sccd.syntax.scope import *
 from sccd.util.duration import *
 
 # to inspect types in Python 3.7
@@ -9,169 +9,190 @@ from sccd.util.duration import *
 import typing_inspect
 
 class Expression(ABC):
+    # Must be called exactly once on each expression. May throw.
+    # Returns static type of expression.
+    @abstractmethod
+    def init_rvalue(self, scope) -> type:
+        pass
+
     # Evaluation should NOT have side effects.
     # Motivation is that the evaluation of a guard condition cannot have side effects.
     @abstractmethod
-    def eval(self, events, datamodel):
+    def eval(self, current_state, events, memory):
         pass
 
-    # Types of expressions are statically checked in SCCD.
+class LValue(Expression):
     @abstractmethod
-    def get_static_type(self) -> type:
+    def init_lvalue(self, scope, expected_type: type):
         pass
 
-class LHS(Expression):
     @abstractmethod
-    def lhs(self, events, datamodel) -> Variable:
+    def eval_lvalue(self, current_state, events, memory) -> Variable:
         pass
 
-    # LHS types are expressions too!
-    def eval(self, events, datamodel):
-        return self.lhs(events, datamodel).value
+    # LValues are expressions too!
+    def eval(self, current_state, events, memory):
+        variable = self.eval_lvalue(current_state, events, memory)
+        return memory.load(variable.offset)
 
 
 @dataclass
-class Identifier(LHS):
+class Identifier(LValue):
     name: str
-    offset: int # offset in datamodel storage
-    type: type
 
-    def lhs(self, events, datamodel) -> Variable:
-        return datamodel.storage[self.offset]
+    variable: Optional[Variable] = None
+
+    def init_rvalue(self, scope) -> type:
+        assert self.variable is None
+        self.variable = scope.get_rvalue(self.name)
+        # print("init rvalue", self.name, "as", self.variable)
+        return self.variable.type
+
+    def init_lvalue(self, scope, expected_type):
+        assert self.variable is None
+        self.variable = scope.put_lvalue(self.name, expected_type)
+        # print("init lvalue", self.name, "as", self.variable)
+
+    def eval_lvalue(self, current_state, events, memory) -> Variable:
+        return self.variable
 
     def render(self):
         return self.name
 
-    def get_static_type(self) -> type:
-        return self.type
 
 @dataclass
 class FunctionCall(Expression):
     function: Expression
     parameters: List[Expression]
 
-    def __post_init__(self):
-        formal_types, return_type = typing_inspect.get_args(self.function.get_static_type())
+    type: Optional[type] = None
+
+    def init_rvalue(self, scope) -> type:
+        function_type = self.function.init_rvalue(scope)
+        if not isinstance(function_type, Callable):
+            raise Exception("Function call: Expression '%s' is not callable" % self.function.render())
+        formal_types, return_type = typing_inspect.get_args(function_type)
         self.type = return_type
 
-        actual_types = [p.get_static_type() for p in self.parameters]
+        actual_types = [p.init_rvalue(scope) for p in self.parameters]
         for formal, actual in zip(formal_types, actual_types):
             if formal != actual:
                 raise Exception("Function call: Actual types '%s' differ from formal types '%s'" % (actual_types, formal_types))
+        return self.type
 
-    def eval(self, events, datamodel):
+    def eval(self, current_state, events, memory):
         # print(self.function)
-        f = self.function.eval(events, datamodel)
-        p = [p.eval(events, datamodel) for p in self.parameters]
-        return f(*p)
+        f = self.function.eval(current_state, events, memory)
+        p = [p.eval(current_state, events, memory) for p in self.parameters]
+        return f(current_state, events, memory, *p)
 
     def render(self):
         return self.function.render()+'('+','.join([p.render() for p in self.parameters])+')'
 
-    def get_static_type(self) -> type:
-        return self.type
 
 @dataclass
 class StringLiteral(Expression):
     string: str
 
-    def eval(self, events, datamodel):
+    def init_rvalue(self, scope) -> type:
+        return str
+
+    def eval(self, current_state, events, memory):
         return self.string
 
     def render(self):
         return '"'+self.string+'"'
 
-    def get_static_type(self) -> type:
-        return str
 
 @dataclass
 class IntLiteral(Expression):
     i: int 
 
-    def eval(self, events, datamodel):
+    def init_rvalue(self, scope) -> type:
+        return int
+
+    def eval(self, current_state, events, memory):
         return self.i
 
     def render(self):
         return str(self.i)
 
-    def get_static_type(self) -> type:
-        return int
-
 @dataclass
 class BoolLiteral(Expression):
     b: bool 
 
-    def eval(self, events, datamodel):
+    def init_rvalue(self, scope) -> type:
+        return bool
+
+    def eval(self, current_state, events, memory):
         return self.b
 
     def render(self):
         return "true" if self.b else "false"
 
-    def get_static_type(self) -> type:
-        return bool
-
 @dataclass
 class DurationLiteral(Expression):
     d: Duration
 
-    def eval(self, events, datamodel):
+    def init_rvalue(self, scope) -> type:
+        return Duration
+
+    def eval(self, current_state, events, memory):
         return self.d
 
     def render(self):
         return str(self.d)
 
-    def get_static_type(self) -> type:
-        return int
-
 @dataclass
 class Array(Expression):
     elements: List[Any]
-    t: type = None
 
-    def __post_init__(self):
+    type: Optional[type] = None
+
+    def init_rvalue(self, scope) -> type:
         for e in self.elements:
-            t = e.get_static_type()
-            if self.t and self.t != t:
-                raise Exception("Mixed element types in Array expression: %s and %s" % (str(self.t), str(t)))
-            self.t = t
+            t = e.init_rvalue(scope)
+            if self.type and self.type != t:
+                raise Exception("Mixed element types in Array expression: %s and %s" % (str(self.type), str(t)))
+            self.type = t
+
+        return List[self.type]
 
-    def eval(self, events, datamodel):
-        return [e.eval(events, datamodel) for e in self.elements]
+    def eval(self, current_state, events, memory):
+        return [e.eval(current_state, events, memory) for e in self.elements]
 
     def render(self):
         return '['+','.join([e.render() for e in self.elements])+']'
 
-    def get_static_type(self) -> type:
-        return List[self.t]
-
 # Does not add anything semantically, but ensures that when rendering an expression,
 # the parenthesis are not lost
 @dataclass
 class Group(Expression):
     subexpr: Expression
 
-    def eval(self, events, datamodel):
-        return self.subexpr.eval(events, datamodel)
+    def init_rvalue(self, scope) -> type:
+        return self.subexpr.init_rvalue(scope)
+
+    def eval(self, current_state, events, memory):
+        return self.subexpr.eval(current_state, events, memory)
 
     def render(self):
         return '('+self.subexpr.render()+')'
 
-    def get_static_type(self) -> type:
-        return subexpr.get_static_type()
-
 @dataclass
 class BinaryExpression(Expression):
     lhs: Expression
     operator: str # token name from the grammar.
     rhs: Expression
 
-    def __post_init__(self):
-        lhs_t = self.lhs.get_static_type()
-        rhs_t = self.rhs.get_static_type()
+    def init_rvalue(self, scope) -> type:
+        lhs_t = self.lhs.init_rvalue(scope)
+        rhs_t = self.rhs.init_rvalue(scope)
         if lhs_t != rhs_t:
             raise Exception("Mixed LHS and RHS types in '%s' expression: %s and %s" % (self.operator, str(lhs_t), str(rhs_t)))
+        return lhs_t
 
-    def eval(self, events, datamodel):
+    def eval(self, current_state, events, memory):
         
         return {
             # "AND": lambda x,y: x and y,
@@ -190,42 +211,39 @@ class BinaryExpression(Expression):
             # "MOD": lambda x,y: x % y,
             # "EXP": lambda x,y: x ** y,
 
-            "and": lambda x,y: x.eval(events, datamodel) and y.eval(events, datamodel),
-            "or": lambda x,y: x.eval(events, datamodel) or y.eval(events, datamodel),
-            "==": lambda x,y: x.eval(events, datamodel) == y.eval(events, datamodel),
-            "!=": lambda x,y: x.eval(events, datamodel) != y.eval(events, datamodel),
-            ">": lambda x,y: x.eval(events, datamodel) > y.eval(events, datamodel),
-            ">=": lambda x,y: x.eval(events, datamodel) >= y.eval(events, datamodel),
-            "<": lambda x,y: x.eval(events, datamodel) < y.eval(events, datamodel),
-            "<=": lambda x,y: x.eval(events, datamodel) <= y.eval(events, datamodel),
-            "+": lambda x,y: x.eval(events, datamodel) + y.eval(events, datamodel),
-            "-": lambda x,y: x.eval(events, datamodel) - y.eval(events, datamodel),
-            "*": lambda x,y: x.eval(events, datamodel) * y.eval(events, datamodel),
-            "/": lambda x,y: x.eval(events, datamodel) / y.eval(events, datamodel),
-            "//": lambda x,y: x.eval(events, datamodel) // y.eval(events, datamodel),
-            "%": lambda x,y: x.eval(events, datamodel) % y.eval(events, datamodel),
-            "**": lambda x,y: x.eval(events, datamodel) ** y.eval(events, datamodel),
+            "and": lambda x,y: x.eval(current_state, events, memory) and y.eval(current_state, events, memory),
+            "or": lambda x,y: x.eval(current_state, events, memory) or y.eval(current_state, events, memory),
+            "==": lambda x,y: x.eval(current_state, events, memory) == y.eval(current_state, events, memory),
+            "!=": lambda x,y: x.eval(current_state, events, memory) != y.eval(current_state, events, memory),
+            ">": lambda x,y: x.eval(current_state, events, memory) > y.eval(current_state, events, memory),
+            ">=": lambda x,y: x.eval(current_state, events, memory) >= y.eval(current_state, events, memory),
+            "<": lambda x,y: x.eval(current_state, events, memory) < y.eval(current_state, events, memory),
+            "<=": lambda x,y: x.eval(current_state, events, memory) <= y.eval(current_state, events, memory),
+            "+": lambda x,y: x.eval(current_state, events, memory) + y.eval(current_state, events, memory),
+            "-": lambda x,y: x.eval(current_state, events, memory) - y.eval(current_state, events, memory),
+            "*": lambda x,y: x.eval(current_state, events, memory) * y.eval(current_state, events, memory),
+            "/": lambda x,y: x.eval(current_state, events, memory) / y.eval(current_state, events, memory),
+            "//": lambda x,y: x.eval(current_state, events, memory) // y.eval(current_state, events, memory),
+            "%": lambda x,y: x.eval(current_state, events, memory) % y.eval(current_state, events, memory),
+            "**": lambda x,y: x.eval(current_state, events, memory) ** y.eval(current_state, events, memory),
         }[self.operator](self.lhs, self.rhs) # Borrow Python's lazy evaluation
 
     def render(self):
         return self.lhs.render() + ' ' + self.operator + ' ' + self.rhs.render()
 
-    def get_static_type(self) -> type:
-        return self.lhs.get_static_type()
-
 @dataclass
 class UnaryExpression(Expression):
     operator: str # token value from the grammar.
     expr: Expression
 
-    def eval(self, events, datamodel):
+    def init_rvalue(self, scope) -> type:
+        return self.expr.init_rvalue(scope)
+
+    def eval(self, current_state, events, memory):
         return {
-            "not": lambda x: not x.eval(events, datamodel),
-            "-": lambda x: - x.eval(events, datamodel),
+            "not": lambda x: not x.eval(current_state, events, memory),
+            "-": lambda x: - x.eval(current_state, events, memory),
         }[self.operator](self.expr)
 
     def render(self):
         return self.operator + ' ' + self.expr.render()
-
-    def get_static_type(self) -> type:
-        return self.expr.get_static_type()

+ 77 - 0
src/sccd/syntax/scope.py

@@ -0,0 +1,77 @@
+from typing import *
+from dataclasses import *
+import itertools
+
+# Stateless stuff we know about a variable
+@dataclass
+class Variable:
+  # offset in memory
+  offset: int
+  # type of variable
+  type: type
+
+  default_value: Any = None
+
+# Stateless stuff we know about a scope (= set of variable names)
+class Scope:
+  def __init__(self, name: str, wider_scope: 'Scope'):
+    self.name = name
+    self.wider_scope = wider_scope
+    if wider_scope:
+      self.start_offset = wider_scope.start_offset + len(wider_scope.names)
+    else:
+      self.start_offset = 0
+    self.names: Dict[str, Variable] = {}
+
+  def size(self) -> int:
+    return self.start_offset + len(self.names)
+
+  def all(self):
+    if self.wider_scope:
+      return itertools.chain(self.wider_scope.all(), self.names.items())
+    else:
+      return self.names.items()
+
+  def _internal_lookup(self, name: str) -> Optional[Tuple['Scope', Variable]]:
+    try:
+      return (self, self.names[name])
+    except KeyError:
+      if self.wider_scope is not None:
+        return self.wider_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))
+
+  def get_rvalue(self, name: str) -> Variable:
+    found = self._internal_lookup(name)
+    if not found:
+      # return None
+      raise Exception("No variable with name '%s' found in any scope." % name)
+    else:
+      return found[1]
+
+  def put_lvalue(self, name: str, expected_type: type) -> Variable:
+    found = self._internal_lookup(name)
+
+    if found:
+      scope, variable = found
+      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
+      return variable
+
+  def put_lvalue_default(self, name: str, default_value) -> Variable:
+    self.assert_available(name)
+    expected_type = type(default_value)
+    variable = self.put_lvalue(name, expected_type)
+    variable.default_value = default_value
+    return variable

+ 14 - 1
src/sccd/syntax/statechart.py

@@ -3,6 +3,7 @@ from dataclasses import *
 from typing import *
 import itertools
 from sccd.syntax.tree import *
+from sccd.syntax.scope import *
 
 class SemanticOption:
 
@@ -27,6 +28,16 @@ class InputEventLifeline(SemanticOption, Enum):
   FIRST_COMBO_STEP = 1
   FIRST_SMALL_STEP = 2
 
+class EnablednessMemoryProtocol(SemanticOption, Enum):
+  GC_BIG_STEP = 0
+  GC_COMBO_STEP = 1
+  GC_SMALL_STEP = 2
+
+class AssignmentMemoryProtocol(SemanticOption, Enum):
+  RHS_BIG_STEP = 0
+  RHS_COMBO_STEP = 0
+  RHS_SMALL_STEP = 0
+
 class Priority(SemanticOption, Enum):
   SOURCE_PARENT = 0
   SOURCE_CHILD = 1
@@ -41,6 +52,8 @@ class Semantics:
   combo_step_maximality: ComboStepMaximality = ComboStepMaximality.COMBO_TAKE_ONE
   internal_event_lifeline: InternalEventLifeline = InternalEventLifeline.NEXT_COMBO_STEP
   input_event_lifeline: InputEventLifeline = InputEventLifeline.FIRST_COMBO_STEP
+  enabledness_memory_protocol: EnablednessMemoryProtocol = EnablednessMemoryProtocol.GC_COMBO_STEP
+  assignment_memory_protocol: AssignmentMemoryProtocol = AssignmentMemoryProtocol.RHS_COMBO_STEP
   priority: Priority = Priority.SOURCE_PARENT
   concurrency: Concurrency = Concurrency.SINGLE
 
@@ -66,4 +79,4 @@ class Semantics:
 class Statechart:
   tree: StateTree
   semantics: Semantics
-  datamodel: DataModel
+  scope: Scope

+ 48 - 25
src/sccd/syntax/statement.py

@@ -5,35 +5,42 @@ from sccd.syntax.expression import *
 class Statement(ABC):
     # Execution typically has side effects.
     @abstractmethod
-    def exec(self, events, datamodel):
+    def exec(self, current_state, events, datamodel):
+        pass
+
+    @abstractmethod
+    def init_stmt(self, scope):
         pass
 
 @dataclass
 class Assignment(Statement):
-    lhs: LHS
+    lhs: LValue
     operator: str # token value from the grammar.
     rhs: Expression
 
-    def __post_init__(self):
-        lhs_t = self.lhs.get_static_type()
-        rhs_t = self.rhs.get_static_type()
-        if lhs_t != rhs_t:
-            raise Exception("Assignment: LHS type '%s' differs from RHS type '%s'." % (str(lhs_t), str(rhs_t)))
-
-    def exec(self, events, datamodel):
-        rhs = self.rhs.eval(events, datamodel)
-        lhs = self.lhs.lhs(events, datamodel)
-
-        def assign(x,y):
-            x.value = y
-        def increment(x,y):
-            x.value += y
-        def decrement(x,y):
-            x.value -= y
-        def multiply(x,y):
-            x.value *= y
-        def divide(x,y):
-            x.value /= y
+    def init_stmt(self, scope):
+        rhs_t = self.rhs.init_rvalue(scope)
+        self.lhs.init_lvalue(scope, rhs_t)
+
+    def exec(self, current_state, events, datamodel):
+        val = self.rhs.eval(current_state, events, datamodel)
+        offset = self.lhs.eval_lvalue(current_state, events, datamodel).offset
+
+        def load():
+            return datamodel.load(offset)
+        def store(val):
+            datamodel.store(offset, val)
+
+        def assign():
+            store(val)
+        def increment():
+            store(load() + val)
+        def decrement():
+            store(load() - val)
+        def multiply():
+            store(load() * val)
+        def divide():
+            store(load() / val)
 
         {
             "=": assign,
@@ -41,12 +48,28 @@ class Assignment(Statement):
             "-=": decrement,
             "*=": multiply,
             "/=": divide,
-        }[self.operator](lhs, rhs)
+        }[self.operator]()
 
 @dataclass
 class Block(Statement):
     stmts: List[Statement]
 
-    def exec(self, events, datamodel):
+    def init_stmt(self, scope):
+        local_scope = Scope("local", scope)
         for stmt in self.stmts:
-            stmt.exec(events, datamodel)
+            stmt.init_stmt(local_scope)
+
+    def exec(self, current_state, events, datamodel):
+        for stmt in self.stmts:
+            stmt.exec(current_state, events, datamodel)
+
+# e.g. a function call
+@dataclass
+class ExpressionStatement(Statement):
+    expr: Expression
+
+    def init_stmt(self, scope):
+        self.expr.init_rvalue(scope)
+
+    def exec(self, current_state, events, datamodel):
+        self.expr.eval(current_state, events, datamodel)

+ 45 - 0
src/sccd/syntax/test_scope.py

@@ -0,0 +1,45 @@
+import unittest
+from scope import *
+from typing import *
+
+class TestScope(unittest.TestCase):
+
+  def test_scope(self):
+    
+    builtin = Scope("builtin", wider_scope=None)
+
+    # Lookup LHS value (creating it in the current scope if not found)
+
+    variable = builtin.lookup_lhs("in_state", Callable[[List[str]], bool])
+
+    self.assertEqual(variable.offset, 0)
+
+    globals = Scope("globals", wider_scope=builtin)
+
+    variable = globals.lookup_lhs("x", int)
+
+    self.assertEqual(variable.offset, 1)
+
+    variable = globals.lookup_lhs("in_state", Callable[[List[str]], bool])
+
+    self.assertEqual(variable.offset, 0)
+
+    local = Scope("local", wider_scope=globals)
+
+    variable = local.lookup_lhs("x", int)
+
+    self.assertEqual(variable.offset, 1)
+
+    # Lookup RHS value (returning None if not found)
+
+    variable = local.lookup_rhs("x")
+
+    self.assertEqual(variable.offset, 1)
+
+    found = local.lookup_rhs("y")
+
+    self.assertIs(found, None)
+
+    # Cannot use 'in_state' as string LValue, already another type
+
+    self.assertRaises(Exception, lambda: local.lookup_lhs("in_state", str))

+ 2 - 1
test/lib/os_tools.py

@@ -1,4 +1,5 @@
 import os
+import termcolor
 from typing import List, Callable, Set
 
 filter_any = lambda x: True
@@ -27,7 +28,7 @@ def get_files(paths: List[str], filter: Callable[[str], bool] = filter_any) -> L
         elif os.path.isfile(p):
             add_file(p)
         else:
-            print("%s: not a file or a directory, skipped." % p)
+            print(termcolor.colored("%s: not a file or a directory, skipped." % p, 'yellow'))
 
     return src_files
 

+ 1 - 1
test/lib/test.py

@@ -54,7 +54,7 @@ class Test(unittest.TestCase):
       thread.join()
       def repr(output):
         return '\n'.join("%d: %s" % (i, str(big_step)) for i, big_step in enumerate(output))
-      self.fail(msg + "\n\nActual:\n" + repr(actual) + ("\n(killed)" if kill else "") + "\n\nExpected:\n" + repr(expected))
+      self.fail(msg + "\n\nActual:\n" + repr(actual) + ("\n(possibly more output, instance killed)" if kill else "") + "\n\nExpected:\n" + repr(expected))
 
     while True:
       data = pipe.get(block=True, timeout=None)

+ 1 - 1
test/lib/test_parser.py

@@ -97,7 +97,7 @@ class TestParser(StatechartParser):
         name=src_file + variant_description(i, variant),
         model=SingleInstanceModel(
           globals,
-          Statechart(tree=statechart.tree, datamodel=deepcopy(statechart.datamodel), semantics=dataclasses.replace(statechart.semantics, **variant))),
+          Statechart(tree=statechart.tree, scope=statechart.scope, semantics=dataclasses.replace(statechart.semantics, **variant))),
         input=input,
         output=output)
 

+ 19 - 0
test/test_files/features/after/fail_duration_type.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <!-- after events are always received as input events in a later big step -->
+    <semantics
+        big_step_maximality="*"
+        combo_step_maximality="*"/>
+    <tree>
+      <state initial="s1">
+        <state id="s1">
+          <!-- '100' is not a duration -->
+          <transition after="100" target="/s2"/>
+        </state>
+        <state id="s2"/>
+      </state>
+    </tree>
+  </statechart>
+  <output/>
+</test>

+ 4 - 2
test/test_files/features/datamodel/fail_static_types.xml

@@ -11,8 +11,10 @@
       <state initial="a">
         <state id="a">
           <onentry>
-            <!-- illegal assignment, LHS is int, RHS is string -->
-            <code> x = "hello" </code>
+            <code>
+              greeting = "hello";
+              x = greeting; <!-- 'x' is already declared as integer at instance level -->
+            </code>
           </onentry>
         </state>
       </state>

+ 20 - 0
test/test_files/features/datamodel/fail_unique_var.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+
+    <datamodel>
+      <var id="x" expr="42"/>
+
+      <!-- illegal, 'x' already in use -->
+      <var id="x" expr="43"/>
+    </datamodel>
+
+    <tree>
+      <state/>
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 3 - 3
test/test_files/features/datamodel/test_cond.xml

@@ -3,13 +3,13 @@
   <statechart>
     <semantics/>
     <datamodel>
-      <var id="x" expr="0"/>
-      <var id="y" expr="x"/><!-- this is allowed, value of x copied to y -->
+      <var id="x" expr="42"/>
+      <!-- <var id="y" expr="x"/>this is allowed, value of x copied to y -->
     </datamodel>
     <tree>
       <state initial="start">
         <state id="start">
-          <transition event="e" port="in" target="/done" cond="y == 0">
+          <transition event="e" port="in" target="/done" cond="x == 42">
             <raise event="done" port="out"/>
           </transition>
         </state>

+ 2 - 2
test/test_files/features/datamodel/test_guard_action.xml

@@ -5,14 +5,14 @@
       big_step_maximality="take_many"/>
     <datamodel>
       <var id="x" expr="0"/>
-      <var id="y" expr="x"/><!-- this is allowed, y is also 0 -->
+      <!-- <var id="y" expr="x"/>this is allowed, y is also 0 -->
     </datamodel>
     <tree>
       <state initial="counting">
         <state id="counting">
           <transition event="e" port="in" target="."/>
           <transition cond="x &lt; 3" target=".">
-            <code> x += 1 </code>
+            <code> x += 1; </code>
             <raise event="inc" port="out"/>
           </transition>
           <transition cond="x == 3" target="../done"/>

+ 31 - 0
test/test_files/semantics/memory_protocol/statechart_enabledness.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" ?>
+<statechart>
+  <datamodel>
+    <var id="i" expr="0"/>
+  </datamodel>
+  <tree>
+    <state>
+      <parallel id="p">
+        <state id="increment">
+          <state id="a">
+            <transition port="in" event="e" target="." cond='not INSTATE(["/p/status/done"])'>
+              <raise port="out" event="inc"/>
+              <code> i += 1; </code>
+            </transition>
+          </state>
+        </state>
+
+        <state id="status" initial="counting">
+          <state id="counting">
+            <transition cond="i == 2" target="../done"/>
+          </state>
+          <state id="done">
+            <onentry>
+              <raise port="out" event="done"/>
+            </onentry>
+          </state>
+        </state>
+      </parallel>
+    </state>
+  </tree>
+</statechart>

+ 30 - 0
test/test_files/semantics/memory_protocol/test_gcbig.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="statechart_enabledness.xml">
+    <override_semantics
+      big_step_maximality="take_one"
+      enabledness_memory_protocol="gc_big_step"/>
+  </statechart>
+
+  <input>
+    <input_event port="in" name="e" time="0 d"/>
+    <input_event port="in" name="e" time="0 d"/>
+    <input_event port="in" name="e" time="0 d"/>
+    <input_event port="in" name="e" time="0 d"/>
+    <input_event port="in" name="e" time="0 d"/>
+    <input_event port="in" name="e" time="0 d"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="inc"/>
+    </big_step>
+    <big_step>
+      <event port="out" name="inc"/>
+    </big_step>
+    <big_step>
+      <event port="out" name="inc"/>
+      <event port="out" name="done"/>
+    </big_step>
+  </output>
+</test>

+ 22 - 0
test/test_files/semantics/memory_protocol/test_gccombo.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="statechart_enabledness.xml">
+    <override_semantics
+      big_step_maximality="take_many"
+      input_event_lifeline="whole"
+      enabledness_memory_protocol="gc_combo_step"/>
+  </statechart>
+
+  <input>
+    <input_event port="in" name="e" time="0 d"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="inc"/>
+      <event port="out" name="inc"/>
+      <event port="out" name="inc"/>
+      <event port="out" name="done"/>
+    </big_step>
+  </output>
+</test>

+ 21 - 0
test/test_files/semantics/memory_protocol/test_gcsmall.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="statechart_enabledness.xml">
+    <override_semantics
+      big_step_maximality="take_many"
+      input_event_lifeline="whole"
+      enabledness_memory_protocol="gc_small_step"/>
+  </statechart>
+  
+  <input>
+    <input_event port="in" name="e" time="0 d"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="inc"/>
+      <event port="out" name="inc"/>
+      <event port="out" name="done"/>
+    </big_step>
+  </output>
+</test>