Selaa lähdekoodia

Abandon silly 'state scope' idea (too complex and difficult to combine with memory protocol semantics). Major revision of memory protocol implementation. Added support for function closures.

Oops
Joeri Exelmans 5 vuotta sitten
vanhempi
commit
012898370d

+ 17 - 9
src/sccd/execution/builtin_scope.py

@@ -1,33 +1,41 @@
-from sccd.syntax.scope import *
+from sccd.syntax.expression import *
 from sccd.util.debug import *
 import termcolor
 
-builtin_scope = Scope("builtin", None)
-
 def _in_state(ctx: EvalContext, state_list: List[str]) -> bool:
   from sccd.execution.statechart_state import StatechartState
-
   return StatechartState.in_state(ctx.current_state, state_list)
 
-builtin_scope.add_constant("INSTATE", _in_state, SCCDFunction([SCCDArray(SCCDString)], SCCDBool))
 
 def _log10(ctx: EvalContext, i: int) -> float:
   import math
   return math.log10(i)
 
-builtin_scope.add_constant("log10", _log10, SCCDFunction([SCCDInt], SCCDFloat))
 
 def _float_to_int(ctx: EvalContext, x: float) -> int:
   return int(x)
 
-builtin_scope.add_constant("float_to_int", _float_to_int, SCCDFunction([SCCDFloat], SCCDInt))
 
 def _log(ctx: EvalContext, s: str) -> None:
   print_debug(termcolor.colored("log: ",'blue')+s)
 
-builtin_scope.add_constant("log", _log, SCCDFunction([SCCDString]))
 
 def _int_to_str(ctx: EvalContext, i: int) -> str:
   return str(i)
 
-builtin_scope.add_constant("int_to_str", _int_to_str, SCCDFunction([SCCDInt], SCCDString))
+BuiltIn = Scope("builtin", None)
+
+BuiltIn.declare("INSTATE", SCCDFunction([SCCDArray(SCCDString)], SCCDBool), const=True)
+BuiltIn.declare("log10", SCCDFunction([SCCDInt], SCCDFloat), const=True)
+BuiltIn.declare("float_to_int", SCCDFunction([SCCDFloat], SCCDInt), const=True)
+BuiltIn.declare("log", SCCDFunction([SCCDString]), const=True)
+BuiltIn.declare("int_to_str", SCCDFunction([SCCDInt], SCCDString), const=True)
+
+def load_builtins(memory):
+  memory.push_frame(BuiltIn)
+
+  memory.current_frame().storage[0] = _in_state
+  memory.current_frame().storage[1] = _log10
+  memory.current_frame().storage[2] = _float_to_int
+  memory.current_frame().storage[3] = _log
+  memory.current_frame().storage[4] = _int_to_str

+ 138 - 31
src/sccd/execution/memory.py

@@ -1,55 +1,162 @@
 from typing import *
+from dataclasses import *
 from sccd.util.bitmap import *
 from sccd.util.debug import *
+from sccd.syntax.scope import *
 
-class Memory:
-  def __init__(self, scope):
-    self.scope = scope
-    self.storage = [None]*scope.total_size()
+@dataclass(frozen=True)
+class StackFrame:
+  # Values of variables in the frame.
+  storage: List[Any]
+
+  # The previous stack frame, forming a linked list of stack frames representing the "call stack".
+  # The parent frame will become the "current frame" when this stack frame is popped.
+  parent: Optional['StackFrame']
+
+  # The sequence of 'context' values forms another linked list, often but not always identical to the linked list of 'parent's, for accessing nonlocal variables.
+  # The 'context' is the stack frame in which the frame of the currently called function was declared.
+  # This could be a stack frame that no longer exists on "the stack", like with a function closure, or some ancestor, for a recursive function.
+  # If the current scope is not a function, this value is equal to the 'parent' field.
+  context: Optional['StackFrame']
+
+  # We need this to know where the offsets of this scope start relative to the offsets of the parent scope, and to print variable names in error messages.
+  scope: Scope
+
+  def __str__(self):
+    def short_descr(frame):
+      return "StackFrame(%s, len=%d, ...)" % (frame.scope.name, len(frame.storage)) if frame else "None"
+
+    return "StackFrame(%s, len=%d, parent=%s, context=%s)" % (self.scope.name, len(self.storage), short_descr(self.parent), "parent" if self.context is self.parent else short_descr(self.context))
+
+
+class MemoryInterface(ABC):
+
+  @abstractmethod
+  def current_frame(self) -> StackFrame:
+    pass
+
+  @abstractmethod
+  def push_frame(self, scope: Scope):
+    pass
+
+  @abstractmethod
+  def push_frame_w_context(self, scope: Scope, context: StackFrame):
+    pass
+
+  @abstractmethod
+  def pop_frame(self):
+    pass
+
+  @abstractmethod
+  def load(self, offset: int) -> Any:
+    pass
+
+  @abstractmethod
+  def store(self, offset: int, value: Any):
+    pass
+
+
+class Memory(MemoryInterface):
+
+  def __init__(self):
+    self._current_frame = None
+
+  def current_frame(self) -> StackFrame:
+    return self._current_frame
+
+  def push_frame_w_context(self, scope: Scope, context: StackFrame):
+
+    self._current_frame = StackFrame(
+      storage=[None]*scope.size(),
+      parent=self._current_frame,
+      context=context,
+      scope=scope)
+
+  # For function calls: context MAY differ from _current_frame if the function was
+  # called from a different context from where it was created.
+  # This enables function closures.
+  def push_frame(self, scope: Scope):
+    self.push_frame_w_context(scope, self._current_frame)
+
+  def pop_frame(self):
+    self._current_frame = self._current_frame.parent
+
+  def _get_frame(self, offset: int) -> Tuple[StackFrame, int]:
+    frame = self._current_frame
+    while offset < 0:
+      offset += frame.scope.parent_offset
+      frame = frame.context
+
+    return (frame, offset)
+
+  def load(self, offset: int) -> Any:
+    frame, offset = self._get_frame(offset)
+    return frame.storage[offset]
+
+  def store(self, offset: int, value: Any):
+    frame, offset = self._get_frame(offset)
+    frame.storage[offset] = value
+
+
+class MemoryPartialSnapshot(MemoryInterface):
 
-class MemorySnapshot:
   def __init__(self, memory: Memory):
-    self.actual = memory.storage
-    self.snapshot = list(memory.storage)
-    self.len = len(memory.storage)
+    self.memory = memory
+    self.frame = memory.current_frame()
 
-    self.trans_dirty = Bitmap() # positions in actual memory written to before flush_transition
-    self.round_dirty = Bitmap() # positions in actual memory written to after flush_temp and before flush_round
+    self.actual: List[Any] = self.frame.storage
+    self.snapshot: List[Any] = list(self.actual)
 
-    self.local_storage: Dict[Scope, List[List[Any]]] = {}
+    # Positions in stack frame written to by current transition.
+    self.trans_dirty = Bitmap()
 
-  def store(self, scope, offset: int, value: Any):
-    try:
-      scope_stack = self.local_storage[scope]
-      scope_stack[-1][offset] = value
-    except KeyError:
-      self.actual[offset] = value
-      self.trans_dirty |= bit(offset)
+    # Positions in stack frame written to during current big, combo or small step (depending on semantic option chosen)
+    self.round_dirty = Bitmap()
+
+  def current_frame(self) -> StackFrame:
+    return self.memory.current_frame()
+
+  def push_frame(self, scope: Scope):
+    return self.memory.push_frame(scope)
 
-  def load(self, scope, offset: int) -> Any:
-    try:
-      scope_stack = self.local_storage[scope]
-      return scope_stack[-1][offset]
-    except KeyError:
+  def push_frame_w_context(self, scope: Scope, context: StackFrame):
+    return self.memory.push_frame_w_context(scope, context)
+
+  def pop_frame(self):
+    # The frame we are snapshotting should never be popped.
+    assert self.memory.current_frame() is not self.frame
+
+    return self.memory.pop_frame()
+
+  def load(self, offset: int) -> Any:
+    frame, offset = self.memory._get_frame(offset)
+    # Sometimes read from snapshot
+    if frame is self.frame:
+      # "our" frame! :)
       if self.trans_dirty.has(offset):
         return self.actual[offset]
       else:
         return self.snapshot[offset]
+    else:
+      return frame.storage[offset]
 
-  def push_local_scope(self, scope):
-    scope_stack = self.local_storage.setdefault(scope, [])
-    scope_stack.append([None]*scope.local_size()) # append stack frame
+  def store(self, offset: int, value: Any):
+    frame, offset = self.memory._get_frame(offset)
+    # Always write to 'actual' storage
+    frame.storage[offset] = value
 
-  def pop_local_scope(self, scope):
-    scope_stack = self.local_storage[scope]
-    scope_stack.pop() # pop stack frame
+    if frame is self.frame:
+      # "our" frame! :)
+      # Remember that we wrote, such that next read during same transition will be the value we wrote.
+      self.trans_dirty |= bit(offset)
 
   def flush_transition(self):  
     race_conditions = self.trans_dirty & self.round_dirty
     if race_conditions:
-      variables = list(self.scope[-1].all_variables())
+      variables = self.frame.scope.variables
       # some variable written to twice before refresh
-      raise Exception("Race condition for variables %s" % str(list(variables[offset].name for offset in race_conditions.items())))
+      raise Exception("Race condition for variables %s" %
+          ", ".join(variables[offset].name for offset in race_conditions.items()))
 
     self.round_dirty |= self.trans_dirty
     self.trans_dirty = Bitmap() # reset

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

@@ -184,6 +184,9 @@ class SmallStep(Round):
         def get_candidates(extra_forbidden):
             nonlocal enabled_events
             enabled_events = self.enabled_events()
+            # The cost of sorting our enabled events is smaller than the benefit gained by having to loop less often over it in our transition execution code:
+            enabled_events.sort(key=lambda e: e.id)
+
             candidates = self.generator.generate(self.state, enabled_events, forbidden_arenas |  extra_forbidden)
 
             if is_debug():

+ 19 - 10
src/sccd/execution/statechart_instance.py

@@ -2,6 +2,7 @@ import termcolor
 import functools
 from typing import List, Tuple, Iterable
 from sccd.execution.instance import *
+from sccd.execution.builtin_scope import *
 from sccd.syntax.statechart import *
 from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
@@ -11,7 +12,7 @@ from sccd.execution.memory import *
 
 # Hardcoded limit on number of sub-rounds of combo and big step to detect never-ending superrounds.
 # TODO: make this configurable
-LIMIT = 1000
+LIMIT = 100
 
 class StatechartInstance(Instance):
     def __init__(self, statechart: Statechart, object_manager):
@@ -83,20 +84,28 @@ class StatechartInstance(Instance):
             InternalEventLifeline.SAME: small_step.add_remainder_event,
         }[semantics.internal_event_lifeline]
 
-        memory = Memory(statechart.scope)
-        gc_memory = MemorySnapshot(memory)
+        memory = Memory()
+        load_builtins(memory)
+        memory.push_frame(statechart.scope)
+
+        if semantics.enabledness_memory_protocol == MemoryProtocol.NONE:
+            gc_memory = memory
+        else:
+            gc_memory = MemoryPartialSnapshot(memory)
+
+            if semantics.enabledness_memory_protocol == MemoryProtocol.BIG_STEP:
+                self._big_step.when_done(gc_memory.flush_round)
+            elif semantics.enabledness_memory_protocol == MemoryProtocol.COMBO_STEP:
+                combo_step.when_done(gc_memory.flush_round)
+            elif semantics.enabledness_memory_protocol == MemoryProtocol.SMALL_STEP:
+                small_step.when_done(gc_memory.flush_round)
 
-        if semantics.enabledness_memory_protocol == MemoryProtocol.BIG_STEP:
-            self._big_step.when_done(gc_memory.flush_round)
-        elif semantics.enabledness_memory_protocol == MemoryProtocol.COMBO_STEP:
-            combo_step.when_done(gc_memory.flush_round)
-        elif semantics.enabledness_memory_protocol == MemoryProtocol.SMALL_STEP:
-            small_step.when_done(gc_memory.flush_round)
 
         if semantics.assignment_memory_protocol == semantics.enabledness_memory_protocol:
             rhs_memory = gc_memory
         else:
-            rhs_memory = MemorySnapshot(memory)
+            rhs_memory = MemoryPartialSnapshot(memory)
+
             if semantics.assignment_memory_protocol == MemoryProtocol.BIG_STEP:
                 self._big_step.when_done(rhs_memory.flush_round)
             elif semantics.assignment_memory_protocol == MemoryProtocol.COMBO_STEP:

+ 33 - 7
src/sccd/execution/statechart_state.py

@@ -48,14 +48,14 @@ class StatechartState:
         for state in states:
             print_debug(termcolor.colored('  ENTER %s'%state.gen.full_name, 'green'))
             self.eventless_states += state.gen.has_eventless_transitions
-            self.rhs_memory.push_local_scope(state.scope)
             self._perform_actions(ctx, state.enter)
             self._start_timers(state.gen.after_triggers)
 
         self.rhs_memory.flush_transition()
         self.rhs_memory.flush_round()
 
-    def fire_transition(self, events, t: Transition):
+    # events: list SORTED by event id
+    def fire_transition(self, events: List[Event], t: Transition):
         def __exitSet():
             return [s for s in reversed(t.gen.lca.gen.descendants) if (s in self.configuration)]
         
@@ -95,13 +95,14 @@ class StatechartState:
             self.eventless_states -= s.gen.has_eventless_transitions
             # execute exit action(s)
             self._perform_actions(ctx, s.exit)
-            self.rhs_memory.pop_local_scope(s.scope)
+            # self.rhs_memory.pop_local_scope(s.scope)
             self.configuration_bitmap &= ~s.gen.state_id_bitmap
                 
         # execute transition action(s)
-        self.rhs_memory.push_local_scope(t.scope) # make room for event parameters on stack
+        self.rhs_memory.push_frame(t.scope) # make room for event parameters on stack
+        self._copy_event_params_to_stack(self.rhs_memory, t, events)
         self._perform_actions(ctx, t.actions)
-        self.rhs_memory.pop_local_scope(t.scope)
+        self.rhs_memory.pop_frame()
             
         # enter states...
         targets = __getEffectiveTargetStates()
@@ -111,7 +112,6 @@ class StatechartState:
             self.eventless_states += s.gen.has_eventless_transitions
             self.configuration_bitmap |= s.gen.state_id_bitmap
             # execute enter action(s)
-            self.rhs_memory.push_local_scope(s.scope)
             self._perform_actions(ctx, s.enter)
             self._start_timers(s.gen.after_triggers)
         try:
@@ -121,19 +121,45 @@ class StatechartState:
 
         self.rhs_memory.flush_transition()
 
+    @staticmethod
+    def _copy_event_params_to_stack(memory, t, events):
+        # Both 'events' and 't.trigger.enabling' are sorted by event ID...
+        # This way we have to iterate over each of both lists at most once.
+        if t.trigger and not isinstance(t.trigger, AfterTrigger):
+            event_decls = iter(t.trigger.enabling)
+            try:
+                event_decl = next(event_decls)
+                offset = 0
+                for e in events:
+                    if e.id < event_decl.id:
+                        continue
+                    else:
+                        while e.id > event_decl.id:
+                            event_decl = next(event_decls)
+                        for p in e.params:
+                            memory.store(offset, p)
+                            offset += 1
+            except StopIteration:
+                pass
+
     def check_guard(self, t, events) -> bool:
         # Special case: after trigger
         if isinstance(t.trigger, AfterTrigger):
-            e = [e for e in events if bit(e.id) & t.trigger.bitmap][0]
+            e = [e for e in events if bit(e.id) & t.trigger.enabling_bitmap][0] # it's safe to assume the list will contain one element cause we only check a transition's guard after we know it may be enabled given the set of events
             if self.timer_ids[t.trigger.after_id] != e.params[0]:
                 return False
 
         if t.guard is None:
             return True
         else:
+            # print("evaluating guard for ", str(t))
+            self.gc_memory.push_frame(t.scope)
+            self._copy_event_params_to_stack(self.gc_memory, t, events)
             result = t.guard.eval(
                 EvalContext(current_state=self, events=events, memory=self.gc_memory))
+            self.gc_memory.pop_frame()
             self.gc_memory.flush_transition()
+            # print("done with guard for ", str(t))
             return result
 
     def check_source(self, t) -> bool:

+ 2 - 2
src/sccd/parser/expression_parser.py

@@ -111,7 +111,7 @@ class _ExpressionTransformer(Transformer):
     return (pos_events, neg_events)
 
   def event_decl(self, node):
-    event_name = node[0]
+    event_name = node[0].value
     event_id = self.globals.events.assign_id(event_name)
     return EventDecl(id=event_id, name=event_name, params_decl=node[1])
 
@@ -124,7 +124,7 @@ class _ExpressionTransformer(Transformer):
       "float": SCCDFloat,
       "dur": SCCDDuration
     }[node[1]]
-    return ParamDecl(name=node[0].value, type=type)
+    return ParamDecl(name=node[0].value, formal_type=type)
 
   def func_decl(self, node):
     return FunctionDeclaration(params_decl=node[0], body=node[1])

+ 36 - 32
src/sccd/parser/statechart_parser.py

@@ -1,7 +1,7 @@
 from typing import *
 from sccd.syntax.statechart import *
 from sccd.syntax.tree import *
-from sccd.execution import builtin_scope
+from sccd.execution.builtin_scope import *
 from sccd.parser.xml_parser import *
 from sccd.parser.expression_parser import *
 
@@ -18,7 +18,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
     if ext_file is None:
       statechart = Statechart(
         semantics=SemanticConfiguration(),
-        scope=Scope("instance", parent=builtin_scope.builtin_scope),
+        scope=Scope("instance", parent=BuiltIn),
         datamodel=None,
         inport_events={},
         event_outport={},
@@ -67,10 +67,10 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
       return [("event+", parse_event)]
 
     def parse_root(el):
-      root = State("", parent=None, scope=Scope("root", parent=statechart.scope))
+      root = State("", parent=None)
       children_dict = {}
-      transitions = []
-      next_after_id = 0
+      transitions = [] # All of the statechart's transitions accumulate here, cause we still need to find their targets, which we can't do before the entire state tree has been built. We find their targets when encoutering the </root> closing tag.
+      next_after_id = 0 # Counter for 'after' transitions within the statechart.
 
       def create_actions_parser(scope):
 
@@ -82,7 +82,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
             expr.init_rvalue(scope)
             params.append(expr)
 
-          def when_done():
+          def finish_raise():
             event_name = require_attribute(el, "event")
             try:
               port = statechart.event_outport[event_name]
@@ -97,15 +97,15 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
               # output event - no ID in global namespace
               globals.outports.assign_id(port)
               return RaiseOutputEvent(name=event_name, params=params, outport=port, time_offset=0)
-          return ([("param*", parse_param)], when_done)
+          return ([("param*", parse_param)], finish_raise)
 
         def parse_code(el):
-          def when_done():
+          def finish_code():
             block = parse_block(globals, el.text)
             # local_scope = Scope("local", scope)
             block.init_stmt(scope)
             return Code(block)
-          return ([], when_done)
+          return ([], finish_code)
 
         return {"raise": parse_raise, "code": parse_code}
 
@@ -121,11 +121,11 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
         elif len(state.children) > 1:
           raise XmlError("More than 1 child state: must set 'initial' attribute.")
 
-      def create_state_parser(parent, sibling_dict):
+      def create_state_parser(parent, sibling_dict: Dict[str, State]={}):
 
         def common(el, constructor):
           short_name = require_attribute(el, "id")
-          state = constructor(short_name, parent, Scope("state_"+short_name, statechart.scope))
+          state = constructor(short_name, parent)
 
           already_there = sibling_dict.setdefault(short_name, state)
           if already_there is not state:
@@ -141,13 +141,13 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
         def parse_state(el):
           state = common_nonpseudo(el, State)
           children_dict = {}
-          def when_done():
+          def finish_state():
             deal_with_initial(el, state, children_dict)
-          return (create_state_parser(parent=state, sibling_dict=children_dict), when_done)
+          return (create_state_parser(parent=state, sibling_dict=children_dict), finish_state)
 
         def parse_parallel(el):
           state = common_nonpseudo(el, ParallelState)
-          return create_state_parser(parent=state, sibling_dict={})
+          return create_state_parser(parent=state)
 
         def parse_history(el):
           history_type = el.get("type", "shallow")
@@ -159,14 +159,14 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
             raise XmlError("attribute 'type' must be \"shallow\" or \"deep\".")
 
         def parse_onentry(el):
-          def when_done(*actions):
+          def finish_onentry(*actions):
             parent.enter = actions
-          return (create_actions_parser(parent.scope), when_done)
+          return (create_actions_parser(statechart.scope), finish_onentry)
 
         def parse_onexit(el):
-          def when_done(*actions):
+          def finish_onexit(*actions):
             parent.exit = actions
-          return (create_actions_parser(parent.scope), when_done)
+          return (create_actions_parser(statechart.scope), finish_onexit)
 
         def parse_transition(el):
           nonlocal next_after_id
@@ -174,21 +174,24 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           if parent is root:
             raise XmlError("Root <state> cannot be source of a transition.")
 
+          scope = Scope("event_params", parent=statechart.scope)
           target_string = require_attribute(el, "target")
-          scope = Scope("event_params", parent=parent.scope)
           transition = Transition(parent, [], scope, target_string)
 
           event = el.get("event")
           if event is not None:
             positive_events, negative_events = parse_events_decl(globals, event)
 
-            def process_event_decl(e: EventDecl):
+            # Optimization: sort events by ID
+            # Allows us to save time later.
+            positive_events.sort(key=lambda e: e.id)
+
+            def add_event_params_to_scope(e: EventDecl):
               for i,p in enumerate(e.params_decl):
-                scope.add_event_parameter(param_name=p.name, type=p.type,
-                  event_name=e.name, param_offset=i)
+                p.init_param(scope)
 
             for e in itertools.chain(positive_events, negative_events):
-              process_event_decl(e)
+              add_event_params_to_scope(e)
 
             if not negative_events:
               transition.trigger = Trigger(positive_events)
@@ -204,11 +207,11 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
               after_type = after_expr.init_rvalue(scope)
               if after_type != SCCDDuration:
                 msg = "Expression is '%s' type. Expected 'Duration' type." % str(after_type)
-                if after_type == int:
+                if after_type == SCCDInt:
                   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
-              transition.trigger = AfterTrigger(globals.events.assign_id(event), event, next_after_id, after_expr)
+              event_name = "_after%d" % next_after_id # transition gets unique event name
+              transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, next_after_id, after_expr)
               next_after_id += 1
             except Exception as e:
               raise XmlError("after=\"%s\": %s" % (after, str(e))) from e
@@ -220,18 +223,19 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
               expr.init_rvalue(scope)
             except Exception as e:
               raise XmlError("cond=\"%s\": %s" % (cond, str(e))) from e
+              # raise except_msg("cond=\"%s\": " % cond, e)
             transition.guard = expr
 
-          def when_done(*actions):
+          def finish_transition(*actions):
             transition.actions = actions
             transitions.append((transition, el))
             parent.transitions.append(transition)
 
-          return (create_actions_parser(scope), when_done)
+          return (create_actions_parser(scope), finish_transition)
 
         return {"state": parse_state, "parallel": parse_parallel, "history": parse_history, "onentry": parse_onentry, "onexit": parse_onexit, "transition": parse_transition}
 
-      def when_done():
+      def finish_root():
         deal_with_initial(el, root, children_dict)
 
         for transition, t_el in transitions:
@@ -261,11 +265,11 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
 
         statechart.tree = StateTree(root)
 
-      return (create_state_parser(root, sibling_dict=children_dict), when_done)
+      return (create_state_parser(root, sibling_dict=children_dict), finish_root)
 
-    def when_done():
+    def finish_statechart():
       return statechart
 
-    return ([("semantics?", parse_semantics), ("override_semantics?", parse_semantics), ("datamodel?", parse_datamodel), ("inport*", parse_inport), ("outport*", parse_outport), ("root", parse_root)], when_done)
+    return ([("semantics?", parse_semantics), ("override_semantics?", parse_semantics), ("datamodel?", parse_datamodel), ("inport*", parse_inport), ("outport*", parse_outport), ("root", parse_root)], finish_statechart)
 
   return [("statechart", parse_statechart)]

+ 13 - 10
src/sccd/parser/xml_parser.py

@@ -2,6 +2,7 @@ from enum import *
 from typing import *
 from lxml import etree
 import termcolor
+from sccd.util.debug import *
 
 class XmlError(Exception):
   pass
@@ -11,10 +12,8 @@ class XmlErrorElement(Exception):
     super().__init__(msg)
     self.el = el
 
-# 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.
-class XmlDecoratedError(Exception):
-  def __init__(self, src_file: str, el: etree.Element, msg):
+# Returns multiline string containing fragment of src_file with 'el' XML element highlighted.
+def xml_fragment(src_file: str, el: etree.Element) -> str:
     # This is really dirty, but can't find a clean way to do this with lxml.
 
     parent = el.getparent()
@@ -47,7 +46,8 @@ class XmlDecoratedError(Exception):
         ll = termcolor.colored(ll, 'yellow')
       text.append(ll)
 
-    super().__init__("\n\n%s\n\n%s:\nline %d: <%s>: %s" % ('\n'.join(text), src_file,el.sourceline, el.tag, msg))
+    return "\n\n%s\n\n%s:\nline %d: <%s>: " % ('\n'.join(text), src_file,el.sourceline, el.tag)
+
     
 ParseElementF = Callable[[etree.Element], Optional['RulesWDone']]
 OrderedElements = List[Tuple[str, ParseElementF]]
@@ -149,6 +149,7 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
                 rules = rules[1:]
                 rules_stack[-1] = (rules, when_done)
         else:
+          print(rules)
           assert False # rule should always be a dict or list
 
         if parse_function:
@@ -177,12 +178,14 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
           # else:
           #   print("end", el.tag)
 
-    except decorate_exceptions as e:
-      raise XmlDecoratedError(src_file, el, str(e)) from e
-    except XmlError as e:
-      raise XmlDecoratedError(src_file, el, str(e)) from e
+    except (XmlError, *decorate_exceptions) as e:
+      # Assume exception occured while visiting current element 'el':
+      e.args = (xml_fragment(src_file, el) + str(e),)
+      raise
     except XmlErrorElement as e:
-      raise XmlDecoratedError(src_file, e.el, str(e)) from e
+      # Element where exception occured is part of exception object:
+      e.args = (xml_fragment(src_file, t.el) + str(e),)
+      raise
 
   results = results_stack[0] # sole stack frame remaining
   if len(results) > 0:

+ 32 - 28
src/sccd/syntax/expression.py

@@ -4,6 +4,12 @@ from dataclasses import *
 from sccd.util.duration import *
 from sccd.syntax.scope import *
 
+@dataclass
+class EvalContext:
+    current_state: 'StatechartState'
+    events: List['Event']
+    memory: 'MemoryInterface'
+
 # Thrown if the type checker encountered something illegal.
 # Not to be confused with Python's TypeError exception.
 class StaticTypeError(ModelError):
@@ -29,36 +35,35 @@ class Expression(ABC):
 class LValue(Expression):
     # Initialize the LValue as an LValue. 
     @abstractmethod
-    def init_lvalue(self, scope: Scope, expected_type: SCCDType):
+    def init_lvalue(self, scope: Scope, rhs_type: SCCDType):
         pass
 
+    # Should return offset relative to current context stack frame.
+    #   offset ∈ [0, +∞[ : variable's memory address is within current scope
+    #   offset ∈ ]-∞, 0[ : variable's memory address is in a parent scope (or better: 'context scope')
     @abstractmethod
-    def eval_lvalue(self, ctx: EvalContext) -> Variable:
+    def eval_lvalue(self, ctx: EvalContext) -> int:
         pass
 
     # LValues can also serve as expressions!
     def eval(self, ctx: EvalContext):
-        variable = self.eval_lvalue(ctx)
-        return variable.load(ctx)
+        offset = self.eval_lvalue(ctx)
+        return ctx.memory.load(offset)
 
 @dataclass
 class Identifier(LValue):
     name: str
-    variable: Optional[Variable] = None
+    offset: Optional[int] = None
 
     def init_rvalue(self, scope: Scope) -> SCCDType:
-        # assert self.variable is None
-        self.variable = scope.get(self.name)
-        # print("init rvalue", self.name, "as", self.variable)
-        return self.variable.type
+        self.offset, type = scope.get_rvalue(self.name)
+        return type
 
-    def init_lvalue(self, scope: Scope, expected_type):
-        # assert self.variable is None
-        self.variable = scope.put_variable_assignment(self.name, expected_type)
-        # print("init lvalue", self.name, "as", self.variable)
+    def init_lvalue(self, scope: Scope, type):
+        self.offset = scope.put_lvalue(self.name, type)
 
-    def eval_lvalue(self, ctx: EvalContext) -> Variable:
-        return self.variable
+    def eval_lvalue(self, ctx: EvalContext) -> int:
+        return self.offset
 
     def render(self):
         return self.name
@@ -78,10 +83,6 @@ class FunctionCall(Expression):
         formal_types = function_type.param_types
         return_type = function_type.return_type
 
-        # We always secretly pass an EvalContext object with every function call
-        # Not visible to the user.
-        # assert formal_types[0] == EvalContext
-
         actual_types = [p.init_rvalue(scope) for p in self.params]
         for i, (formal, actual) in enumerate(zip(formal_types, actual_types)):
             if formal != actual:
@@ -100,12 +101,14 @@ class FunctionCall(Expression):
 @dataclass
 class ParamDecl:
     name: str
-    type: type
-
-    variable: Optional[Variable] = None
+    formal_type: SCCDType
+    offset: Optional[int] = None
 
     def init_param(self, scope: Scope):
-        self.variable = scope.add_variable(self.name, self.type)
+        self.offset = scope.declare(self.name, self.formal_type)
+
+    def render(self):
+        return self.name + ":" + str(self.formal_type)
 
 @dataclass
 class FunctionDeclaration(Expression):
@@ -120,21 +123,22 @@ class FunctionDeclaration(Expression):
             p.init_param(self.scope)
         ret = self.body.init_stmt(self.scope)
         return_type = ret.get_return_type()
-        return SCCDFunction([p.type for p in self.params_decl], return_type)
+        return SCCDFunction([p.formal_type for p in self.params_decl], return_type)
 
     def eval(self, ctx: EvalContext):
+        context: StackFrame = ctx.memory.current_frame()
         def FUNCTION(ctx: EvalContext, *params):
-            ctx.memory.push_local_scope(self.scope)
+            ctx.memory.push_frame_w_context(self.scope, context)
             # Copy arguments to stack
             for val, p in zip(params, self.params_decl):
-                p.variable.store(ctx, val)
+                ctx.memory.store(p.offset, val)
             ret = self.body.exec(ctx)
-            ctx.memory.pop_local_scope(self.scope)
+            ctx.memory.pop_frame()
             return ret.val
         return FUNCTION
 
     def render(self) -> str:
-        return "" # todo
+        return "<func_decl>" # todo
         
 
 @dataclass

+ 59 - 167
src/sccd/syntax/scope.py

@@ -13,210 +13,102 @@ class ScopeError(ModelError):
   def __init__(self, scope, msg):
     super().__init__(msg + '\n\nCurrent scope:\n' + str(scope))
 
-@dataclass
-class EvalContext:
-    current_state: 'StatechartState'
-    events: List['Event']
-    memory: 'MemorySnapshot'
 
+# Stateless stuff we know about a variable existing within a scope.
 @dataclass(frozen=True)
-class Value(ABC):
-  _name: str
+class _Variable(ABC):
+  name: str # only used to print error messages
+  offset: int # Offset within variable's scope. Always >= 0.
   type: SCCDType
-
-  @property
-  def name(self):
-    import termcolor
-    return termcolor.colored(self._name, 'yellow')
-  
-
-  @abstractmethod
-  def is_read_only(self) -> bool:
-    pass
-
-  @abstractmethod
-  def load(self, ctx: EvalContext) -> Any:
-    pass
-    
-  @abstractmethod
-  def store(self, ctx: EvalContext, value):
-    pass
-
-# Stateless stuff we know about a variable
-@dataclass(frozen=True)
-class Variable(Value):
-  scope: 'Scope'
-  offset: int # wrt. scope variable belongs to
-
-  def is_read_only(self) -> bool:
-    return False
-
-  def load(self, ctx: EvalContext) -> Any:
-    return ctx.memory.load(self.scope, self.offset)
-
-  def store(self, ctx: EvalContext, value):
-    ctx.memory.store(self.scope, self.offset, value)
-
-  def __str__(self):
-    return "Variable(%s, type=%s, offset=%s+%d)" %(self.name, str(self.type), self.scope.name, self.offset)
-
-class EventParam(Variable):
-  def __init__(self, name, type, scope, offset, event_name, param_offset):
-    super().__init__(name, type, scope, offset)
-    self.event_name: str = event_name
-    self.param_offset: int = param_offset # offset within event parameters
-
-  def is_read_only(self) -> bool:
-    return True
-
-  def load(self, ctx: EvalContext) -> Any:
-    from_stack = Variable.load(self, ctx)
-    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 ctx.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, ctx, value)
-      return value
-
-  def store(self, ctx: EvalContext, 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(frozen=True)
-class Constant(Value):
-  value: Any
-
-  def is_read_only(self) -> bool:
-    return True
-
-  def load(self, ctx: EvalContext) -> Any:
-    return self.value
-
-  def store(self, ctx: EvalContext, value):
-    # Bug in the code: should never attempt to write to Constant
-    assert False
+  const: bool
 
 # Stateless stuff we know about a scope (= set of named values)
 class Scope:
   def __init__(self, name: str, parent: 'Scope'):
     self.name = name
     self.parent = parent
-
     if parent:
-      self.parent_offset = parent.total_size()
+      # Position of the start of this scope, seen from the parent scope
+      self.parent_offset = self.parent.size()
     else:
-      self.parent_offset = 0
+      self.parent_offset = None # value should never be used
 
     # Mapping from name to Value
-    self.named_values: Dict[str, Value] = {}
+    self.names: Dict[str, _Variable] = {}
 
     # All non-constant values, ordered by memory position
-    self.variables: List[Variable] = []
-
-  def local_size(self) -> int:
-    return len(self.variables)
-
-  def offset(self) -> int:
-    if self.parent:
-      return self.parent.total_size()
-    else:
-      return 0
+    self.variables: List[_Variable] = []
 
-  def total_size(self) -> int:
-    return self.parent_offset + len(self.variables)
 
-  def all_variables(self):
-    if self.parent:
-      return itertools.chain(self.parent.all_variables(), self.variables)
-    else:
-      return self.variables
-
-  def list_scope_names(self):
-    if self.parent:
-      return [self.name] + self.parent.list_scope_names()
-    else:
-      return [self.name]
+  def size(self) -> int:
+    return len(self.variables)
 
   def __str__(self):
     s = "  scope: '%s'\n" % self.name
+
     is_empty = True
     for v in reversed(self.variables):
-      s += "    %s: %s\n" % (v.name, str(v.type))
-      is_empty = False
-    constants = [v for v in self.named_values.values() if v not in self.variables]
-    for c in constants:
-      s += "    %s (constant): %s\n" % (c.name, str(c.type))
+      s += "    +%d: %s: %s\n" % (v.offset, v.name, str(v.type))
       is_empty = False
+
     if is_empty:
-      s += "   (empty)\n"
+      s += "   ø\n"
+
     if self.parent:
       s += self.parent.__str__()
+
     return s
 
-  def _internal_lookup(self, name: str) -> Optional[Tuple['Scope', Value]]:
+  def _internal_lookup(self, name, offset=0) -> Optional[Tuple['Scope', int, _Variable]]:
     try:
-      return (self, self.named_values[name])
+      return (self, offset, self.names[name])
     except KeyError:
       if self.parent is not None:
-        return self.parent._internal_lookup(name)
+        return self.parent._internal_lookup(name, offset - self.parent_offset)
       else:
         return None
 
-  def get(self, name: str) -> Value:
+  # Create name in this scope
+  def _internal_add(self, name, type, const) -> int:
+    offset = len(self.variables)
+    var = _Variable(name, offset, type, const)
+    self.names[name] = var
+    self.variables.append(var)
+    return offset
+
+  # This is what we do when we encounter an assignment expression:
+  # Add name to current scope if it doesn't exist yet in current or any parent scope.
+  # Or assign to existing variable, if the name already exists, if the types match.
+  # Returns offset relative to the beginning of this scope (may be a postive or negative number).
+  def put_lvalue(self, name: str, type: SCCDType) -> int:
     found = self._internal_lookup(name)
-    if not found:
-      # return None
-      raise ScopeError(self, "No variable with name '%s' found in any of scopes: %s" % (name, str(self.list_scope_names())))
-    else:
-      return found[1]
+    if found:
+      scope, scope_offset, var = found
+      if var.type == type:
+        if var.const:
+          raise ScopeError(self, "Cannot assign to %s: %s of scope '%s': Variable is constant." % (name, str(var.type), scope.name))
+        return scope_offset + var.offset
+      else:
+        raise ScopeError(self, "Cannot assign %s to %s: %s of scope '%s'" %(str(type), name, str(var.type), scope.name))
+
+    return self._internal_add(name, type, const=False)
 
-  # 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: SCCDType) -> Variable:
+  # Lookup name in this scope and its ancestors. Raises exception if not found.
+  # Returns offset relative to the beginning of this scope, just like put_lvalue, and also the type of the variable.
+  def get_rvalue(self, name: str) -> Tuple[int, SCCDType]:
     found = self._internal_lookup(name)
-    if found:
-      scope, variable = found
-      if variable.is_read_only():
-        raise ScopeError(self, "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:
-        raise ScopeError(self, "Cannot assign type %s to variable of type %s" % (expected_type, variable.type))
-      return variable
-    else:
-      variable = Variable(name, expected_type, scope=self,offset=self.local_size())
-      self.named_values[name] = variable
-      self.variables.append(variable)
-      return variable
+    if not found:
+      raise ScopeError(self, "Name '%s' not found in any scope." % name)
 
-  def _assert_name_available(self, name: str):
+    scope, scope_offset, var = found
+    return (scope_offset + var.offset, var.type)
+
+  # Attempt to declare given name in this scope. Only succeeds if name does not exist yet in any scope.
+  # Returns offset relative to this scope (always a positive number since this function only creates new variables in this scope)
+  def declare(self, name: str, type: SCCDType, const: bool = False) -> int:
     found = self._internal_lookup(name)
     if found:
-      scope, variable = found
-      raise ScopeError(self, "Name '%s' already in use in scope '%s'" % (name, scope.name))
-
-  def add_constant(self, name: str, value: Any, type: SCCDType) -> Constant:
-    self._assert_name_available(name)
-    c = Constant(name, type, value=value)
-    self.named_values[name] = c
-    return c
-
-  def add_variable(self, name: str, expected_type: SCCDType) -> Variable:
-    self._assert_name_available(name)
-    variable = Variable(name, expected_type, scope=self, offset=self.local_size())
-    self.named_values[name] = variable
-    self.variables.append(variable)
-    return variable
-
-  def add_event_parameter(self, event_name: str, param_name: str, type: SCCDType, param_offset=int) -> EventParam:
-    self._assert_name_available(param_name)
-    param = EventParam(param_name, type,
-      scope=self, offset=self.local_size(),
-      event_name=event_name, param_offset=param_offset)
-    self.named_values[param_name] = param
-    self.variables.append(param)
-    return param
+      scope, scope_offset, var = found
+      raise ScopeError(self, "Cannot declare '%s' in scope '%s': Name already exists in scope '%s'" % (name, self.name, scope.name))
+
+    return self._internal_add(name, type, const)

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

@@ -36,6 +36,7 @@ class MemoryProtocol(SemanticAspect, Enum):
   BIG_STEP = auto()
   COMBO_STEP = auto()
   SMALL_STEP = auto()
+  NONE = auto()
 
 class Priority(SemanticAspect, Enum):
   SOURCE_PARENT = auto()

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

@@ -99,9 +99,9 @@ class Assignment(Statement):
 
     def exec(self, ctx: EvalContext) -> Return:
         rhs_val = self.rhs.eval(ctx)
-        variable = self.lhs.eval_lvalue(ctx)
+        offset = self.lhs.eval_lvalue(ctx)
 
-        variable.store(ctx, rhs_val)
+        ctx.memory.store(offset, rhs_val)
 
         return DontReturn
 

+ 16 - 35
src/sccd/syntax/tree.py

@@ -8,7 +8,7 @@ from sccd.util.bitmap import *
 class State:
     short_name: str # value of 'id' attribute in XML
     parent: Optional['State'] # only None if root state
-    scope: Scope # states have their own scope: variables can be declared in <onentry>, subsequently read in guard conditions and actions.
+    # scope: Scope # states have their own scope: variables can be declared in <onentry>, subsequently read in guard conditions and actions.
 
     stable: bool = False # whether this is a stable stabe. this field is ignored if maximality semantics is not set to SYNTACTIC
 
@@ -92,7 +92,7 @@ class EventDecl:
 
     def render(self) -> str:
         if self.params_decl:
-            return self.name + '(' + ', '.join(self.params_decl) + ')'
+            return self.name + '(' + ', '.join(p.render() for p in self.params_decl) + ')'
         else:
             return self.name
 
@@ -101,57 +101,38 @@ class Trigger:
     enabling: List[EventDecl]
 
     def __post_init__(self):
+        # Optimization: Require 'enabling' to be sorted!
+        assert sorted(self.enabling, key=lambda e: e.id) == self.enabling
+
         self.enabling_bitmap = Bitmap.from_list(e.id for e in self.enabling)
 
     def check(self, events_bitmap: Bitmap) -> bool:
         return (self.enabling_bitmap & events_bitmap) == self.enabling_bitmap
 
     def render(self) -> str:
-        return ' ∧ '.join(e.name for e in self.enabling)
+        return ' ∧ '.join(e.render() for e in self.enabling)
 
 @dataclass
-class NegatedTrigger:
-    enabling: List[EventDecl]
+class NegatedTrigger(Trigger):
     disabling: List[EventDecl]
 
     def __post_init__(self):
-        self.enabling_bitmap = Bitmap.from_list(e.id for e in self.enabling)
+        Trigger.__post_init__(self)
         self.disabling_bitmap = Bitmap.from_list(e.id for e in self.disabling)
 
     def check(self, events_bitmap: Bitmap) -> bool:
-        return (self.enabling_bitmap & events_bitmap) == self.enabling_bitmap and not (self.disabling_bitmap & events_bitmap)
+        return Trigger.check(self, events_bitmap) and not (self.disabling_bitmap & events_bitmap)
 
     def render(self) -> str:
-        return ' ∧ '.join(e.name for e in self.enabling) + ' ∧ ' + ' ∧ '.join('¬'+e.name for e in self.disabling)
-
-@dataclass
-class EventTrigger:
-    id: int # event ID
-    name: str # event name
-    port: str
-
-    bitmap: Bitmap = None
-
-    def __post_init__(self):
-        self.bitmap = bit(self.id)
-
-    def check(self, events_bitmap: Bitmap) -> bool:
-        return (self.bitmap & events_bitmap) == self.bitmap
-
-    def render(self) -> str:
-        if self.port:
-            return self.port+'.'+self.name
-        else:
-            return self.name
-
-class NegatedEventTrigger(EventTrigger):
-    pass
+        return Trigger.render(self) + ' ∧ ' + ' ∧ '.join('¬'+e.render() for e in self.disabling)
 
-class AfterTrigger(EventTrigger):
-    # id: unique within the statechart
+class AfterTrigger(Trigger):
     def __init__(self, id: int, name: str, after_id: int, delay: Expression):
+        enabling = [EventDecl(id=id, name=name, params_decl=[])]
+        super().__init__(enabling)
 
-        super().__init__(id=id, name=name, port="")
+        self.id = id
+        self.name = name
         self.after_id = after_id # unique ID for AfterTrigger
         self.delay = delay
 
@@ -168,7 +149,7 @@ class Transition:
 
     guard: Optional[Expression] = None
     actions: List[Action] = field(default_factory=list)
-    trigger: Optional[EventTrigger] = None
+    trigger: Optional[Trigger] = None
 
     gen: Optional['TransitionOptimization'] = None        
                     

+ 8 - 7
src/sccd/syntax/types.py

@@ -9,10 +9,11 @@ class SCCDType(ABC):
         
     def __str__(self):
         import termcolor
-        return termcolor.colored(self._str(), 'blue')
+        # return termcolor.colored(self._str(), 'blue')
+        return self._str()
 
 @dataclass(frozen=True)
-class SCCDSimpleType(SCCDType):
+class _SCCDSimpleType(SCCDType):
     name: str
 
     def _str(self):
@@ -39,8 +40,8 @@ class SCCDArray(SCCDType):
     def _str(self):
         return "[" + str(self.element_type) + "]"
 
-SCCDBool = SCCDSimpleType("bool")
-SCCDInt = SCCDSimpleType("int")
-SCCDFloat = SCCDSimpleType("float")
-SCCDDuration = SCCDSimpleType("dur")
-SCCDString = SCCDSimpleType("str")
+SCCDBool = _SCCDSimpleType("bool")
+SCCDInt = _SCCDSimpleType("int")
+SCCDFloat = _SCCDSimpleType("float")
+SCCDDuration = _SCCDSimpleType("dur")
+SCCDString = _SCCDSimpleType("str")

+ 39 - 39
test/test_files/day_atlee/statechart_fig1_redialer.svg

@@ -4,25 +4,25 @@
 <!-- Generated by graphviz version 2.40.1 (20161225.0304)
  -->
 <!-- Title: state transitions Pages: 1 -->
-<svg width="752pt" height="394pt"
- viewBox="0.00 0.00 752.00 394.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<svg width="777pt" height="394pt"
+ viewBox="0.00 0.00 777.00 394.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 390)">
 <title>state transitions</title>
-<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-390 748,-390 748,4 -4,4"/>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-390 773,-390 773,4 -4,4"/>
 <g id="clust1" class="cluster">
 <title>cluster__Dialing</title>
-<path fill="none" stroke="#000000" stroke-width="2" d="M20,-8C20,-8 724,-8 724,-8 730,-8 736,-14 736,-20 736,-20 736,-335 736,-335 736,-341 730,-347 724,-347 724,-347 20,-347 20,-347 14,-347 8,-341 8,-335 8,-335 8,-20 8,-20 8,-14 14,-8 20,-8"/>
-<text text-anchor="start" x="353.6682" y="-328.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Dialing</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M20,-8C20,-8 749,-8 749,-8 755,-8 761,-14 761,-20 761,-20 761,-335 761,-335 761,-341 755,-347 749,-347 749,-347 20,-347 20,-347 14,-347 8,-341 8,-335 8,-335 8,-20 8,-20 8,-14 14,-8 20,-8"/>
+<text text-anchor="start" x="366.1682" y="-328.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Dialing</text>
 </g>
 <g id="clust2" class="cluster">
 <title>cluster__Dialing_Redialer</title>
-<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="436,-16 436,-309 728,-309 728,-16 436,-16"/>
-<text text-anchor="start" x="559.6668" y="-290.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Redialer</text>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="461,-16 461,-309 753,-309 753,-16 461,-16"/>
+<text text-anchor="start" x="584.6668" y="-290.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Redialer</text>
 </g>
 <g id="clust3" class="cluster">
 <title>cluster__Dialing_Dialer</title>
-<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="24,-16 24,-309 428,-309 428,-16 24,-16"/>
-<text text-anchor="start" x="210.8376" y="-290.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Dialer</text>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="24,-16 24,-309 453,-309 453,-16 24,-16"/>
+<text text-anchor="start" x="223.3376" y="-290.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">Dialer</text>
 </g>
 <!-- __initial -->
 <g id="node1" class="node">
@@ -41,49 +41,49 @@
 <!-- _Dialing_Redialer_initial -->
 <g id="node4" class="node">
 <title>_Dialing_Redialer_initial</title>
-<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="547" cy="-265.5" rx="5.5" ry="5.5"/>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="572" cy="-265.5" rx="5.5" ry="5.5"/>
 </g>
 <!-- _Dialing_Redialer_WaitForRedial -->
 <g id="node6" class="node">
 <title>_Dialing_Redialer_WaitForRedial</title>
-<polygon fill="transparent" stroke="transparent" stroke-width="2" points="602.5,-178 491.5,-178 491.5,-142 602.5,-142 602.5,-178"/>
-<text text-anchor="start" x="502.8342" y="-156.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">WaitForRedial ✓</text>
-<path fill="none" stroke="#000000" stroke-width="2" d="M503.8333,-143C503.8333,-143 590.1667,-143 590.1667,-143 595.8333,-143 601.5,-148.6667 601.5,-154.3333 601.5,-154.3333 601.5,-165.6667 601.5,-165.6667 601.5,-171.3333 595.8333,-177 590.1667,-177 590.1667,-177 503.8333,-177 503.8333,-177 498.1667,-177 492.5,-171.3333 492.5,-165.6667 492.5,-165.6667 492.5,-154.3333 492.5,-154.3333 492.5,-148.6667 498.1667,-143 503.8333,-143"/>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="627.5,-178 516.5,-178 516.5,-142 627.5,-142 627.5,-178"/>
+<text text-anchor="start" x="527.8342" y="-156.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">WaitForRedial ✓</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M528.8333,-143C528.8333,-143 615.1667,-143 615.1667,-143 620.8333,-143 626.5,-148.6667 626.5,-154.3333 626.5,-154.3333 626.5,-165.6667 626.5,-165.6667 626.5,-171.3333 620.8333,-177 615.1667,-177 615.1667,-177 528.8333,-177 528.8333,-177 523.1667,-177 517.5,-171.3333 517.5,-165.6667 517.5,-165.6667 517.5,-154.3333 517.5,-154.3333 517.5,-148.6667 523.1667,-143 528.8333,-143"/>
 </g>
 <!-- _Dialing_Redialer_initial&#45;&gt;_Dialing_Redialer_WaitForRedial -->
 <g id="edge2" class="edge">
 <title>_Dialing_Redialer_initial&#45;&gt;_Dialing_Redialer_WaitForRedial</title>
-<path fill="none" stroke="#000000" d="M547,-259.8288C547,-255.1736 547,-248.4097 547,-242.5 547,-242.5 547,-242.5 547,-195.5 547,-193.1079 547,-190.6252 547,-188.1342"/>
-<polygon fill="#000000" stroke="#000000" points="550.5001,-188.0597 547,-178.0598 543.5001,-188.0598 550.5001,-188.0597"/>
-<text text-anchor="middle" x="548.3895" y="-216" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+<path fill="none" stroke="#000000" d="M572,-259.8288C572,-255.1736 572,-248.4097 572,-242.5 572,-242.5 572,-242.5 572,-195.5 572,-193.1079 572,-190.6252 572,-188.1342"/>
+<polygon fill="#000000" stroke="#000000" points="575.5001,-188.0597 572,-178.0598 568.5001,-188.0598 575.5001,-188.0597"/>
+<text text-anchor="middle" x="573.3895" y="-216" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
 </g>
 <!-- _Dialing_Redialer_RedialDigits -->
 <g id="node5" class="node">
 <title>_Dialing_Redialer_RedialDigits</title>
-<polygon fill="transparent" stroke="transparent" stroke-width="2" points="563.5,-60 478.5,-60 478.5,-24 563.5,-24 563.5,-60"/>
-<text text-anchor="start" x="489.5026" y="-38.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">RedialDigits</text>
-<path fill="none" stroke="#000000" stroke-width="2" d="M490.8333,-25C490.8333,-25 551.1667,-25 551.1667,-25 556.8333,-25 562.5,-30.6667 562.5,-36.3333 562.5,-36.3333 562.5,-47.6667 562.5,-47.6667 562.5,-53.3333 556.8333,-59 551.1667,-59 551.1667,-59 490.8333,-59 490.8333,-59 485.1667,-59 479.5,-53.3333 479.5,-47.6667 479.5,-47.6667 479.5,-36.3333 479.5,-36.3333 479.5,-30.6667 485.1667,-25 490.8333,-25"/>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="588.5,-60 503.5,-60 503.5,-24 588.5,-24 588.5,-60"/>
+<text text-anchor="start" x="514.5026" y="-38.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">RedialDigits</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M515.8333,-25C515.8333,-25 576.1667,-25 576.1667,-25 581.8333,-25 587.5,-30.6667 587.5,-36.3333 587.5,-36.3333 587.5,-47.6667 587.5,-47.6667 587.5,-53.3333 581.8333,-59 576.1667,-59 576.1667,-59 515.8333,-59 515.8333,-59 510.1667,-59 504.5,-53.3333 504.5,-47.6667 504.5,-47.6667 504.5,-36.3333 504.5,-36.3333 504.5,-30.6667 510.1667,-25 515.8333,-25"/>
 </g>
 <!-- _Dialing_Redialer_RedialDigits&#45;&gt;_Dialing_Redialer_RedialDigits -->
 <g id="edge3" class="edge">
 <title>_Dialing_Redialer_RedialDigits&#45;&gt;_Dialing_Redialer_RedialDigits</title>
-<path fill="none" stroke="#000000" d="M563.7986,-46.856C576.0518,-46.6655 585.5,-45.0469 585.5,-42 585.5,-39.8577 580.8289,-38.4214 573.7961,-37.6913"/>
-<polygon fill="#000000" stroke="#000000" points="573.9749,-34.1959 563.7986,-37.144 573.5923,-41.1855 573.9749,-34.1959"/>
-<text text-anchor="start" x="585.5" y="-39" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[c &lt; numdigits(p)]^dial &#160;&#160;</text>
+<path fill="none" stroke="#000000" d="M588.7986,-46.856C601.0518,-46.6655 610.5,-45.0469 610.5,-42 610.5,-39.8577 605.8289,-38.4214 598.7961,-37.6913"/>
+<polygon fill="#000000" stroke="#000000" points="598.9749,-34.1959 588.7986,-37.144 598.5923,-41.1855 598.9749,-34.1959"/>
+<text text-anchor="start" x="610.5" y="-39" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[c &lt; numdigits(p)]^dial &#160;&#160;</text>
 </g>
 <!-- _Dialing_Redialer_RedialDigits&#45;&gt;_Dialing_Redialer_WaitForRedial -->
 <g id="edge4" class="edge">
 <title>_Dialing_Redialer_RedialDigits&#45;&gt;_Dialing_Redialer_WaitForRedial</title>
-<path fill="none" stroke="#000000" d="M478.4804,-56.5685C471.2377,-61.7041 466,-68.5217 466,-77.5 466,-124.5 466,-124.5 466,-124.5 466,-130.0837 472.5386,-135.3603 481.9003,-140.0534"/>
-<polygon fill="#000000" stroke="#000000" points="480.6262,-143.3174 491.1849,-144.1915 483.4759,-136.9237 480.6262,-143.3174"/>
-<text text-anchor="start" x="466" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[c == numdigits(p)] &#160;&#160;</text>
+<path fill="none" stroke="#000000" d="M503.4804,-56.5685C496.2377,-61.7041 491,-68.5217 491,-77.5 491,-124.5 491,-124.5 491,-124.5 491,-130.0837 497.5386,-135.3603 506.9003,-140.0534"/>
+<polygon fill="#000000" stroke="#000000" points="505.6262,-143.3174 516.1849,-144.1915 508.4759,-136.9237 505.6262,-143.3174"/>
+<text text-anchor="start" x="491" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[c == numdigits(p)] &#160;&#160;</text>
 </g>
 <!-- _Dialing_Redialer_WaitForRedial&#45;&gt;_Dialing_Redialer_RedialDigits -->
 <g id="edge5" class="edge">
 <title>_Dialing_Redialer_WaitForRedial&#45;&gt;_Dialing_Redialer_RedialDigits</title>
-<path fill="none" stroke="#000000" d="M593.0321,-141.7223C597.8326,-137.125 601,-131.456 601,-124.5 601,-124.5 601,-124.5 601,-77.5 601,-69.8242 588.5399,-62.6778 573.4545,-56.8384"/>
-<polygon fill="#000000" stroke="#000000" points="574.405,-53.461 563.8104,-53.3785 572.0411,-60.0498 574.405,-53.461"/>
-<text text-anchor="start" x="601" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">redial [c == 0]/p = lp ^dial &#160;&#160;</text>
+<path fill="none" stroke="#000000" d="M618.0321,-141.7223C622.8326,-137.125 626,-131.456 626,-124.5 626,-124.5 626,-124.5 626,-77.5 626,-69.8242 613.5399,-62.6778 598.4545,-56.8384"/>
+<polygon fill="#000000" stroke="#000000" points="599.405,-53.461 588.8104,-53.3785 597.0411,-60.0498 599.405,-53.461"/>
+<text text-anchor="start" x="626" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">redial [c == 0]/p = lp ^dial &#160;&#160;</text>
 </g>
 <!-- _Dialing_Dialer -->
 <!-- _Dialing_Dialer_initial -->
@@ -108,37 +108,37 @@
 <!-- _Dialing_Dialer_DialDigits -->
 <g id="node9" class="node">
 <title>_Dialing_Dialer_DialDigits</title>
-<polygon fill="transparent" stroke="transparent" stroke-width="2" points="141,-60 69,-60 69,-24 141,-24 141,-60"/>
-<text text-anchor="start" x="79.6734" y="-38.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">DialDigits</text>
-<path fill="none" stroke="#000000" stroke-width="2" d="M81.3333,-25C81.3333,-25 128.6667,-25 128.6667,-25 134.3333,-25 140,-30.6667 140,-36.3333 140,-36.3333 140,-47.6667 140,-47.6667 140,-53.3333 134.3333,-59 128.6667,-59 128.6667,-59 81.3333,-59 81.3333,-59 75.6667,-59 70,-53.3333 70,-47.6667 70,-47.6667 70,-36.3333 70,-36.3333 70,-30.6667 75.6667,-25 81.3333,-25"/>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="140,-60 68,-60 68,-24 140,-24 140,-60"/>
+<text text-anchor="start" x="78.6734" y="-38.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">DialDigits</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M80.3333,-25C80.3333,-25 127.6667,-25 127.6667,-25 133.3333,-25 139,-30.6667 139,-36.3333 139,-36.3333 139,-47.6667 139,-47.6667 139,-53.3333 133.3333,-59 127.6667,-59 127.6667,-59 80.3333,-59 80.3333,-59 74.6667,-59 69,-53.3333 69,-47.6667 69,-47.6667 69,-36.3333 69,-36.3333 69,-30.6667 74.6667,-25 80.3333,-25"/>
 </g>
 <!-- _Dialing_Dialer_DialDigits&#45;&gt;_Dialing_Dialer_DialDigits -->
 <g id="edge7" class="edge">
 <title>_Dialing_Dialer_DialDigits&#45;&gt;_Dialing_Dialer_DialDigits</title>
-<path fill="none" stroke="#000000" d="M141.25,-46.875C153.3333,-46.875 163,-45.25 163,-42 163,-39.7656 158.431,-38.2993 151.6489,-37.6011"/>
-<polygon fill="#000000" stroke="#000000" points="151.3996,-34.0861 141.25,-37.125 151.0794,-41.0788 151.3996,-34.0861"/>
-<text text-anchor="start" x="163" y="-39" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial [c &lt; 10]/lp = lp * 10 + d⁏ c = c + 1⁏ &#160;^out.out &#160;&#160;</text>
+<path fill="none" stroke="#000000" d="M140.25,-46.875C152.3333,-46.875 162,-45.25 162,-42 162,-39.7656 157.431,-38.2993 150.6489,-37.6011"/>
+<polygon fill="#000000" stroke="#000000" points="150.3996,-34.0861 140.25,-37.125 150.0794,-41.0788 150.3996,-34.0861"/>
+<text text-anchor="start" x="162" y="-39" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial(d:int) [c &lt; 10]/lp = lp * 10 + d⁏ c = c + 1⁏ &#160;^out.out &#160;&#160;</text>
 </g>
 <!-- _Dialing_Dialer_DialDigits&#45;&gt;_Dialing_Dialer_WaitForDial -->
 <g id="edge8" class="edge">
 <title>_Dialing_Dialer_DialDigits&#45;&gt;_Dialing_Dialer_WaitForDial</title>
-<path fill="none" stroke="#000000" d="M74.6899,-60.1859C70.1752,-65.0344 67,-70.8191 67,-77.5 67,-124.5 67,-124.5 67,-124.5 67,-126.9367 67.2734,-129.4132 67.7409,-131.8668"/>
+<path fill="none" stroke="#000000" d="M74.4875,-60.3273C70.0916,-65.167 67,-70.9124 67,-77.5 67,-124.5 67,-124.5 67,-124.5 67,-126.9367 67.2734,-129.4132 67.7409,-131.8668"/>
 <polygon fill="#000000" stroke="#000000" points="64.4295,-133.0164 70.5523,-141.6629 71.1579,-131.0853 64.4295,-133.0164"/>
 <text text-anchor="start" x="67" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[c == 10] &#160;&#160;</text>
 </g>
 <!-- _Dialing_Dialer_WaitForDial&#45;&gt;_Dialing_Dialer_DialDigits -->
 <g id="edge10" class="edge">
 <title>_Dialing_Dialer_WaitForDial&#45;&gt;_Dialing_Dialer_DialDigits</title>
-<path fill="none" stroke="#000000" d="M128.5115,-146.0105C144.3736,-139.9675 158,-132.4599 158,-124.5 158,-124.5 158,-124.5 158,-77.5 158,-70.3568 154.6355,-64.5595 149.602,-59.8834"/>
-<polygon fill="#000000" stroke="#000000" points="151.3797,-56.849 141.2305,-53.8085 147.2684,-62.5145 151.3797,-56.849"/>
-<text text-anchor="start" x="158" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial ∧ redial [c == 0]/lp = d⁏ c = 1⁏ &#160;^out.out &#160;&#160;</text>
+<path fill="none" stroke="#000000" d="M128.5115,-146.0105C144.3736,-139.9675 158,-132.4599 158,-124.5 158,-124.5 158,-124.5 158,-77.5 158,-69.8638 154.1829,-63.7787 148.5785,-58.9596"/>
+<polygon fill="#000000" stroke="#000000" points="150.2577,-55.8692 140.0188,-53.1463 146.3249,-61.66 150.2577,-55.8692"/>
+<text text-anchor="start" x="158" y="-98" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial(d:int) ∧ redial [c == 0]/lp = d⁏ c = 1⁏ &#160;^out.out &#160;&#160;</text>
 </g>
 <!-- _Dialing_Dialer_WaitForDial&#45;&gt;_Dialing_Dialer_WaitForDial -->
 <g id="edge9" class="edge">
 <title>_Dialing_Dialer_WaitForDial&#45;&gt;_Dialing_Dialer_WaitForDial</title>
 <path fill="none" stroke="#000000" d="M128.7649,-164.8167C141.1797,-164.5001 150.5,-162.8945 150.5,-160 150.5,-157.9648 145.8922,-156.5668 138.8407,-155.8061"/>
 <polygon fill="#000000" stroke="#000000" points="138.9618,-152.307 128.7649,-155.1833 138.5299,-159.2937 138.9618,-152.307"/>
-<text text-anchor="start" x="150.5" y="-157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial ∧ ¬redial [c &lt; 10]/c = c + 1⁏ lp = lp * 10 + d⁏ &#160;^out.out &#160;&#160;</text>
+<text text-anchor="start" x="150.5" y="-157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial(d:int) ∧ ¬redial [c &lt; 10]/c = c + 1⁏ lp = lp * 10 + d⁏ &#160;^out.out &#160;&#160;</text>
 </g>
 </g>
 </svg>

+ 0 - 51
test/test_files/features/datamodel/test_state_scope.xml

@@ -1,51 +0,0 @@
-<?xml version="1.0" ?>
-<test>
-  <statechart>
-
-    <datamodel>
-      x = 5;
-    </datamodel>
-
-    <inport name="in">
-      <event name="start"/>
-    </inport>
-
-    <outport name="out">
-      <event name="done"/>
-    </outport>
-
-    <root initial="s1">
-      <state id="s1">
-        <onentry>
-          <!-- create variable 'y' in state scope -->
-          <code> y = 6; </code>
-        </onentry>
-        <!-- condition can see 'y' -->
-        <transition  target="/s2" cond="x &lt; y"/>
-        <onexit>
-          <!-- exit actions also use state scope -->
-          <code> x = y; </code>
-        </onexit>
-      </state>
-
-      <state id="s2">
-        <!-- we cannot access variable 'y' here -->
-        <transition cond="x == 6" target=".">
-          <code> x = 7; </code>
-          <raise event="done"/>
-        </transition>
-      </state>
-    </root>
-
-  </statechart>
-
-  <input>
-    <event name="start" port="in" time="0 d"/>
-  </input>
-
-  <output>
-    <big_step>
-      <event name="done" port="out"/>
-    </big_step>
-  </output>
-</test>

+ 66 - 0
test/test_files/features/functions/test_closure.svg

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: state transitions Pages: 1 -->
+<svg width="150pt" height="241pt"
+ viewBox="0.00 0.00 150.00 241.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 237)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-237 146,-237 146,4 -4,4"/>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="71" cy="-227.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _s1 -->
+<g id="node4" class="node">
+<title>_s1</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="142,-194 0,-194 0,-148 142,-148 142,-194"/>
+<text text-anchor="start" x="64.6646" y="-177.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s1</text>
+<text text-anchor="start" x="5.8232" y="-157.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ /x = increment()</text>
+<polygon fill="#000000" stroke="#000000" points="0,-171 0,-171 142,-171 142,-171 0,-171"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-149C13,-149 129,-149 129,-149 135,-149 141,-155 141,-161 141,-161 141,-181 141,-181 141,-187 135,-193 129,-193 129,-193 13,-193 13,-193 7,-193 1,-187 1,-181 1,-181 1,-161 1,-161 1,-155 7,-149 13,-149"/>
+</g>
+<!-- __initial&#45;&gt;_s1 -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_s1</title>
+<path fill="none" stroke="#000000" d="M71,-221.876C71,-217.5252 71,-211.1081 71,-204.286"/>
+<polygon fill="#000000" stroke="#000000" points="74.5001,-204.1947 71,-194.1947 67.5001,-204.1947 74.5001,-204.1947"/>
+<text text-anchor="middle" x="72.3895" y="-205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _s3 -->
+<g id="node2" class="node">
+<title>_s3</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="126.5,-46 15.5,-46 15.5,0 126.5,0 126.5,-46"/>
+<text text-anchor="start" x="65.1646" y="-29.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s3</text>
+<text text-anchor="start" x="21.9982" y="-9.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.done</text>
+<polygon fill="#000000" stroke="#000000" points="16,-23 16,-23 127,-23 127,-23 16,-23"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M28.5,-1C28.5,-1 113.5,-1 113.5,-1 119.5,-1 125.5,-7 125.5,-13 125.5,-13 125.5,-33 125.5,-33 125.5,-39 119.5,-45 113.5,-45 113.5,-45 28.5,-45 28.5,-45 22.5,-45 16.5,-39 16.5,-33 16.5,-33 16.5,-13 16.5,-13 16.5,-7 22.5,-1 28.5,-1"/>
+</g>
+<!-- _s2 -->
+<g id="node3" class="node">
+<title>_s2</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="142,-120 0,-120 0,-74 142,-74 142,-120"/>
+<text text-anchor="start" x="64.6646" y="-103.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s2</text>
+<text text-anchor="start" x="5.8232" y="-83.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ /x = increment()</text>
+<polygon fill="#000000" stroke="#000000" points="0,-97 0,-97 142,-97 142,-97 0,-97"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-75C13,-75 129,-75 129,-75 135,-75 141,-81 141,-87 141,-87 141,-107 141,-107 141,-113 135,-119 129,-119 129,-119 13,-119 13,-119 7,-119 1,-113 1,-107 1,-107 1,-87 1,-87 1,-81 7,-75 13,-75"/>
+</g>
+<!-- _s2&#45;&gt;_s3 -->
+<g id="edge2" class="edge">
+<title>_s2&#45;&gt;_s3</title>
+<path fill="none" stroke="#000000" d="M71,-73.9916C71,-68.476 71,-62.474 71,-56.5881"/>
+<polygon fill="#000000" stroke="#000000" points="74.5001,-56.249 71,-46.2491 67.5001,-56.2491 74.5001,-56.249"/>
+<text text-anchor="start" x="71" y="-57" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[x == 2] &#160;&#160;</text>
+</g>
+<!-- _s1&#45;&gt;_s2 -->
+<g id="edge3" class="edge">
+<title>_s1&#45;&gt;_s2</title>
+<path fill="none" stroke="#000000" d="M71,-147.9916C71,-142.476 71,-136.474 71,-130.5881"/>
+<polygon fill="#000000" stroke="#000000" points="74.5001,-130.249 71,-120.2491 67.5001,-130.2491 74.5001,-130.249"/>
+<text text-anchor="start" x="71" y="-131" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[x == 1] &#160;&#160;</text>
+</g>
+</g>
+</svg>

+ 1 - 2
test/test_files/features/functions/fail_closure.xml

@@ -1,8 +1,7 @@
 <?xml version="1.0" ?>
 <test>
   <statechart>
-    <!-- closures are currently not supported -->
-    <!-- TODO: support them OR statically detect them and throw a ModelError indicating they're not supported -->
+    <!-- function closures are supported -->
     <datamodel><![CDATA[
 
       counter = func(i:int) {