Browse Source

Parse tree-less parsing of expressions with Lark. Moved some stuff around.

Joeri Exelmans 5 years ago
parent
commit
8b8fbba6b1
34 changed files with 663 additions and 297 deletions
  1. 1 1
      src/sccd/runtime/debug.py
  2. 12 0
      src/sccd/runtime/expression.py
  3. 52 37
      src/sccd/runtime/round.py
  4. 47 158
      src/sccd/runtime/statechart_instance.py
  5. 149 0
      src/sccd/runtime/statechart_state.py
  6. 3 16
      src/sccd/runtime/statechart_syntax.py
  7. 57 56
      src/sccd/runtime/xml_loader2.py
  8. 10 9
      src/sccd/schema/grammar.g
  9. 80 0
      test/new_test_files/features/after/test_after.svg
  10. 41 0
      test/new_test_files/features/after/test_after.xml
  11. 134 0
      test/new_test_files/features/after/test_after_reentry.svg
  12. 48 0
      test/new_test_files/features/after/test_after_reentry.xml
  13. 0 0
      test/new_test_files/semantics/big_step_maximality/statechart_flat.svg
  14. 0 0
      test/new_test_files/semantics/big_step_maximality/statechart_flat.xml
  15. 0 0
      test/new_test_files/semantics/big_step_maximality/statechart_ortho.svg
  16. 0 0
      test/new_test_files/semantics/big_step_maximality/statechart_ortho.xml
  17. 2 2
      test/new_test_files/big_step_maximality/flat_takemany.test.xml
  18. 2 2
      test/new_test_files/big_step_maximality/flat_takeone.test.xml
  19. 2 2
      test/new_test_files/big_step_maximality/ortho_takemany.test.xml
  20. 2 2
      test/new_test_files/big_step_maximality/ortho_takeone.test.xml
  21. 0 0
      test/new_test_files/semantics/event_lifeline/statechart_flat.svg
  22. 0 0
      test/new_test_files/semantics/event_lifeline/statechart_flat.xml
  23. 0 0
      test/new_test_files/semantics/event_lifeline/statechart_ortho.svg
  24. 1 0
      test/new_test_files/models/ortho_intevent.statechart.xml
  25. 1 1
      test/new_test_files/event_lifeline/flat_nextbs.test.xml
  26. 1 1
      test/new_test_files/event_lifeline/flat_nextss_takemany.test.xml
  27. 1 1
      test/new_test_files/event_lifeline/flat_nextss_takeone.test.xml
  28. 1 1
      test/new_test_files/event_lifeline/ortho_nextbs.test.xml
  29. 2 1
      test/new_test_files/event_lifeline/ortho_nextcs_takemany.test.xml
  30. 1 1
      test/new_test_files/event_lifeline/ortho_nextcs_takeone.test.xml
  31. 1 1
      test/new_test_files/event_lifeline/ortho_nextss.test.xml
  32. 8 2
      test/render2.py
  33. 1 1
      test/test.py
  34. 3 2
      test/test2.py

+ 1 - 1
src/sccd/runtime/debug.py

@@ -1,7 +1,7 @@
 import os
 
 try:
-  DEBUG = bool(os.environ['SCCDDEBUG'])
+  DEBUG = bool(os.environ['SCCDDEBUG']=="1")
 except KeyError:
   DEBUG = False
 def print_debug(msg):

+ 12 - 0
src/sccd/runtime/expression.py

@@ -25,6 +25,9 @@ class Identifier(Expression):
     def eval(self, events, datamodel):
         return datamodel.names[self.identifier].value
 
+    def render(self):
+        return self.identifier
+
 @dataclass
 class FunctionCall(Expression):
     function: Expression
@@ -35,6 +38,9 @@ class FunctionCall(Expression):
         p = [p.eval(events, datamodel) for p in self.parameters]
         return f(*p)
 
+    def render(self):
+        return self.function.render()+'('+','.join([p.render() for p in self.parameters])+')'
+
 @dataclass
 class StringLiteral(Expression):
     string: str
@@ -42,9 +48,15 @@ class StringLiteral(Expression):
     def eval(self, events, datamodel):
         return self.string
 
+    def render(self):
+        return '"'+self.string+'"'
+
 @dataclass
 class Array(Expression):
     elements: List[Any]
 
     def eval(self, events, datamodel):
         return [e.eval(events, datamodel) for e in self.elements]
+
+    def render(self):
+        return '['+','.join([e.render() for e in self.elements])+']'

+ 52 - 37
src/sccd/runtime/round.py

@@ -5,34 +5,24 @@ from sccd.runtime.statechart_syntax import *
 from sccd.runtime.debug import print_debug
 
 class CandidatesGenerator:
-    def __init__(self, instance):
-        self.instance = instance
+    def __init__(self, reverse: bool):
+        self.reverse = reverse
         self.cache = {}
 
-class CandidatesGeneratorEventBased(CandidatesGenerator):
-    def generate(self, enabled_events: List[Event], arenas_changed: Bitmap) -> Iterable[Transition]:
-        events_bitmap = Bitmap.from_list(e.id for e in enabled_events)
-        key = (events_bitmap, arenas_changed)
-
-        candidates = self.cache.setdefault(key, [
-            t for t in self.instance.statechart.tree.transition_list
-                if (not t.trigger or events_bitmap.has(t.trigger.id)) # todo: check port
-                and (not arenas_changed.has(t.source.state_id))
-            ])
-
-        def filter_f(t):
-            return self.instance._check_source(t) and self.instance._check_guard(t, enabled_events)
-        return filter(filter_f, candidates)
-
 class CandidatesGeneratorCurrentConfigBased(CandidatesGenerator):
-    def generate(self, enabled_events: List[Event], arenas_changed: Bitmap) -> Iterable[Transition]:
-        key = (self.instance.configuration_bitmap, arenas_changed)
-
-        candidates = self.cache.setdefault(key, [
-            t for s in self.instance.configuration
-                if (not arenas_changed.has(s.state_id))
-                for t in s.transitions
-            ])
+    def generate(self, state, enabled_events: List[Event], arenas_changed: Bitmap) -> Iterable[Transition]:
+        key = (state.configuration_bitmap, arenas_changed)
+
+        try:
+            candidates = self.cache[key]
+        except KeyError:
+            candidates = self.cache[key] = [
+                t for s in state.configuration
+                    if (not arenas_changed.has(s.state_id))
+                    for t in s.transitions
+                ]
+            if self.reverse:
+                candidates.reverse()
 
         def check_trigger(t, enabled_events):
             if not t.trigger:
@@ -43,7 +33,27 @@ class CandidatesGeneratorCurrentConfigBased(CandidatesGenerator):
             return False
 
         def filter_f(t):
-            return check_trigger(t, enabled_events) and self.instance._check_guard(t, enabled_events)
+            return check_trigger(t, enabled_events) and state.check_guard(t, enabled_events)
+        return filter(filter_f, candidates)
+
+class CandidatesGeneratorEventBased(CandidatesGenerator):
+    def generate(self, state, enabled_events: List[Event], arenas_changed: Bitmap) -> Iterable[Transition]:
+        events_bitmap = Bitmap.from_list(e.id for e in enabled_events)
+        key = (events_bitmap, arenas_changed)
+
+        try:
+            candidates = self.cache[key]
+        except KeyError:
+            candidates = self.cache[key] = [
+                t for t in state.model.tree.transition_list
+                    if (not t.trigger or events_bitmap.has(t.trigger.id)) # todo: check port?
+                    and (not arenas_changed.has(t.source.state_id))
+                ]
+            if self.reverse:
+                candidates.reverse()
+
+        def filter_f(t):
+            return state.check_source(t) and state.check_guard(t, enabled_events)
         return filter(filter_f, candidates)
 
 class Round(ABC):
@@ -78,6 +88,9 @@ class Round(ABC):
         else:
             return self.remainder_events
 
+    def __repr__(self):
+        return self.name
+
 # Examples: Big step, combo step
 class SuperRound(Round):
     def __init__(self, name, subround: Round, take_one: bool):
@@ -97,24 +110,26 @@ class SuperRound(Round):
             arenas_changed |= changed
         return arenas_changed
 
+    def __repr__(self):
+        return self.name + " > " + self.subround.__repr__()
+
 class SmallStep(Round):
-    def __init__(self, name, generator: CandidatesGenerator):
+    def __init__(self, name, state, generator: CandidatesGenerator):
         super().__init__(name)
+        self.state = state
         self.generator = generator
 
     def _internal_run(self, arenas_changed: Bitmap) -> Bitmap:
         enabled_events = self.enabled_events()
-        print_debug("enabled events: " + str(enabled_events))
-        candidates = self.generator.generate(enabled_events, arenas_changed)
+        candidates = self.generator.generate(self.state, enabled_events, arenas_changed)
 
-        # candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
-        # print_debug(termcolor.colored("small step candidates: "+
-        #     str(list(map(
-        #         lambda t: reduce(lambda x,y:x+y,list(map(
-        #             lambda s: "to "+s.name,
-        #             t.targets))),
-        #         candidates))), 'blue'))
+        candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
+        if candidates:
+            print_debug("")
+            if enabled_events:
+                print_debug("events: " + str(enabled_events))
+            print_debug("candidates: " + str(candidates))
 
         for t in candidates:
-            arenas_changed |= self.generator.instance._fire_transition(t)
+            arenas_changed |= self.state.fire_transition(t)
             return arenas_changed

+ 47 - 158
src/sccd/runtime/statechart_instance.py

@@ -9,6 +9,7 @@ from sccd.runtime.debug import print_debug
 from sccd.runtime.bitmap import *
 from sccd.runtime.model import *
 from sccd.runtime.round import *
+from sccd.runtime.statechart_state import *
 
 ELSE_GUARD = "ELSE_GUARD"
 
@@ -17,19 +18,21 @@ class StatechartInstance(Instance):
         self.statechart = statechart
         self.object_manager = object_manager
 
-
         semantics = statechart.semantics
 
         if semantics.has_wildcard():
             raise Exception("Model semantics has unexpanded wildcard for some fields.")
 
-        # generator = CandidatesGeneratorEventBased(self)
-        generator = CandidatesGeneratorCurrentConfigBased(self)
-        small_step = SmallStep(termcolor.colored("small", 'blue'), generator)
+        reverse = semantics.priority == Priority.SOURCE_CHILD
+
+        # generator = CandidatesGeneratorCurrentConfigBased(reverse)
+        generator = CandidatesGeneratorEventBased(reverse)
+
+        small_step = SmallStep(termcolor.colored("small", 'blue'), None, generator)
 
         # Always add a layer of 'fairness' above our small steps, so
         # orthogonal transitions take turns fairly.
-        combo_one = SuperRound(termcolor.colored("combo_one", 'cyan'), small_step, take_one=True)
+        combo_one = SuperRound(termcolor.colored("combo_one", 'magenta'), small_step, take_one=True)
 
         if semantics.big_step_maximality == BigStepMaximality.TAKE_ONE:
             self._big_step = combo_step = combo_one # No combo steps
@@ -41,178 +44,64 @@ class StatechartInstance(Instance):
                 combo_step = combo_one
             elif semantics.combo_step_maximality == ComboStepMaximality.COMBO_TAKE_MANY:
                 # Add even more layers, basically an onion at this point.
-                combo_step = SuperRound(termcolor.colored("combo_many", 'magenta'), combo_one, take_one=False)
+                combo_step = SuperRound(termcolor.colored("combo_many", 'cyan'), combo_one, take_one=False)
 
             self._big_step = SuperRound(termcolor.colored("big_many", 'red'), combo_step, take_one=False)
 
-        if semantics.input_event_lifeline == InputEventLifeline.WHOLE:
-            self.input_event_round = self._big_step
-        elif semantics.input_event_lifeline == InputEventLifeline.FIRST_COMBO_STEP:
-            self.input_event_round = combo_step
-        elif semantics.input_event_lifeline == InputEventLifeline.FIRST_SMALL_STEP:
-            self.input_event_round = small_step
-
-        if semantics.internal_event_lifeline == InternalEventLifeline.QUEUE:
-            self.raise_internal = lambda e: self.output_events.append(OutputEvent(e, InstancesTarget([self])))
-        elif semantics.internal_event_lifeline == InternalEventLifeline.NEXT_COMBO_STEP:
-            self.raise_internal = combo_step.add_next_event
-        elif semantics.internal_event_lifeline == InternalEventLifeline.NEXT_SMALL_STEP:
-            self.raise_internal = small_step.add_next_event
-
-        print_debug("Round hierarchy:")
-        s = small_step
-        i=0
-        while s:
-            print_debug(str(i)+': '+s.name)
-            i += 1
-            s = s.parent
-
-        self.data_model = DataModel({
-            "INSTATE": Variable(self.in_state),
-        })
-
-        # these 2 fields have the same information
-        self.configuration = []
-        self.configuration_bitmap = Bitmap()
-
-        self.eventless_states = 0 # number of states in current configuration that have at least one eventless outgoing transition.
-
-        # mapping from configuration_bitmap (=int) to configuration (=List[State])
-        self.config_mem = {}
-        # mapping from history state id to states to enter if history is target of transition
-        self.history_values = {}
+        def whole(input):
+            self._big_step.remainder_events = input
+
+        def first_combo(input):
+            combo_step.remainder_events = input
+
+        def first_small(input):
+            small_step.remainder_events = input
+
+        self.set_input = {
+            InputEventLifeline.WHOLE: whole,
+            InputEventLifeline.FIRST_COMBO_STEP: first_combo,
+            InputEventLifeline.FIRST_SMALL_STEP: first_small
+        }[semantics.input_event_lifeline]
+
+        raise_internal = {
+            InternalEventLifeline.QUEUE: lambda e: self.state.output.append(OutputEvent(e, InstancesTarget([self]))),
+            InternalEventLifeline.NEXT_COMBO_STEP: combo_step.add_next_event,
+            InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event
+        }[semantics.internal_event_lifeline]
+
+        print_debug("\nRound hierarchy: " + str(self._big_step) + '\n')
+
+        self.state = StatechartState(statechart, self, raise_internal)
+
+        small_step.state = self.state
+
 
     # enter default states, generating a set of output events
     def initialize(self, now: Timestamp) -> Tuple[bool, List[OutputEvent]]:
-        self.output_events = []
-
-        states = self.statechart.tree.root.getEffectiveTargetStates(self)
-        self.configuration.extend(states)
-        self.configuration_bitmap = Bitmap.from_list(s.state_id for s in states)
-        for state in states:
-            print_debug(termcolor.colored('  ENTER %s'%state.name, 'green'))
-            self.eventless_states += state.has_eventless_transitions
-            self._perform_actions(state.enter)
-            self._start_timers(state.after_triggers)
-        stable = not self.eventless_states
-        print_debug(termcolor.colored('completed initialization (time=%d)'%now+("(stable)" if stable else ""), 'red'))
-        return (stable, self.output_events)
+        self.state.initialize()
+        stable, output = self.state.collect_output()
+
+        print_debug('completed initialization (time=%d)'%now+("(stable)" if stable else ""))
+
+        return (stable, output)
 
     # perform a big step. generating a set of output events
     def big_step(self, now: Timestamp, input_events: List[Event]) -> Tuple[bool, List[OutputEvent]]:
 
         # print_debug(termcolor.colored('attempting big step, input_events='+str(input_events), 'red'))
 
-        self.output_events = []
+        self.set_input(input_events)
 
-        self.input_event_round.remainder_events = input_events
         arenas_changed = self._big_step.run()
 
+        stable, output = self.state.collect_output()
+
         # can the next big step still contain transitions, even if there are no input events?
-        stable = not self.eventless_states or (not input_events and not arenas_changed)
+        stable |= not input_events and not arenas_changed
 
         # if arenas_changed:
         #     print_debug(termcolor.colored('completed big step (time=%d)'%now+(" (stable)" if stable else ""), 'red'))
         # else:
         #     print_debug(termcolor.colored("(stable)" if stable else "", 'red'))
 
-        return (stable, self.output_events)
-
-    def _check_guard(self, t, events) -> bool:
-        if t.guard is None:
-            return True
-        else:
-            return t.guard.eval(events, self.data_model)
-
-    def _check_source(self, t) -> bool:
-        return self.configuration_bitmap.has(t.source.state_id)
-
-    # @profile
-    def _fire_transition(self, t: Transition) -> Bitmap:
-
-        def __exitSet():
-            return [s for s in reversed(t.lca.descendants) if (s in self.configuration)]
-        
-        def __enterSet(targets):
-            target = targets[0]
-            for a in reversed(target.ancestors):
-                if a in t.source.ancestors:
-                    continue
-                else:
-                    yield a
-            for target in targets:
-                yield target
-
-        def __getEffectiveTargetStates():
-            targets = []
-            for target in t.targets:
-                for e_t in target.getEffectiveTargetStates(self):
-                    if not e_t in targets:
-                        targets.append(e_t)
-            return targets
-
-        # exit states...
-        exit_set = __exitSet()
-        for s in exit_set:
-            # remember which state(s) we were in if a history state is present
-            for h in s.history:
-                f = lambda s0: s0.ancestors and s0.parent == s
-                if isinstance(h, DeepHistoryState):
-                    f = lambda s0: not s0.descendants and s0 in s.descendants
-                self.history_values[h.state_id] = list(filter(f, self.configuration))
-        # print_debug('')
-        print_debug(termcolor.colored('transition  %s 🡪 %s'%(t.source.name, t.targets[0].name), 'green'))
-        for s in exit_set:
-            print_debug(termcolor.colored('  EXIT %s' % s.name, 'green'))
-            self.eventless_states -= s.has_eventless_transitions
-            # execute exit action(s)
-            self._perform_actions(s.exit)
-            self.configuration_bitmap &= ~Bit(s.state_id)
-                
-        # execute transition action(s)
-        self._perform_actions(t.actions)
-            
-        # enter states...
-        targets = __getEffectiveTargetStates()
-        enter_set = __enterSet(targets)
-        for s in enter_set:
-            print_debug(termcolor.colored('  ENTER %s' % s.name, 'green'))
-            self.eventless_states += s.has_eventless_transitions
-            self.configuration_bitmap |= Bit(s.state_id)
-            # execute enter action(s)
-            self._perform_actions(s.enter)
-            self._start_timers(s.after_triggers)
-        try:
-            self.configuration = self.config_mem[self.configuration_bitmap]
-        except:
-            self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.statechart.tree.state_list if self.configuration_bitmap.has(s.state_id)]
-
-        return t.arena_bitmap
-        
-    def _perform_actions(self, actions: List[Action]):
-        for a in actions:
-            if isinstance(a, RaiseInternalEvent):
-                self.raise_internal(Event(id=a.event_id, name=a.name, port="", parameters=[]))
-            elif isinstance(a, RaiseOutputEvent):
-                self.output_events.append(
-                    OutputEvent(Event(id=0, name=a.name, port=a.outport, parameters=[]),
-                    OutputPortTarget(a.outport),
-                    a.time_offset))
-
-    def _start_timers(self, triggers: List[AfterTrigger]):
-        for after in triggers:
-            self.output_events.append(OutputEvent(
-                Event(id=after.id, name=after.name, parameters=[after.nextTimerId()]),
-                target=InstancesTarget([self]),
-                time_offset=after.delay))
-
-    # Return whether the current configuration includes ALL the states given.
-    def in_state(self, state_strings: List[str]) -> bool:
-        state_ids_bitmap = Bitmap.from_list((self.statechart.tree.states[state_string].state_id for state_string in state_strings))
-        in_state = self.configuration_bitmap.has_all(state_ids_bitmap)
-        if in_state:
-            print_debug("in state"+str(state_strings))
-        else:
-            print_debug("not in state"+str(state_strings))
-        return in_state
-
+        return (stable, output)

+ 149 - 0
src/sccd/runtime/statechart_state.py

@@ -0,0 +1,149 @@
+from typing import *
+from sccd.runtime.statechart_syntax import *
+from sccd.runtime.model import Statechart
+from sccd.runtime.event import *
+from sccd.runtime.debug import print_debug
+
+# Set of current states etc.
+class StatechartState:
+
+  def __init__(self, statechart: Statechart, instance, raise_internal):
+    self.model = statechart
+    self.instance = instance
+    self.raise_internal = raise_internal
+
+    self.data_model = DataModel({
+        "INSTATE": Variable(self.in_state),
+    })
+
+    # these 2 fields have the same information
+    self.configuration: List[State] = []
+    self.configuration_bitmap: Bitmap() = Bitmap()
+
+    self.eventless_states = 0 # number of states in current configuration that have at least one eventless outgoing transition.
+
+    # mapping from configuration_bitmap (=int) to configuration (=List[State])
+    self.config_mem = {}
+    # mapping from history state id to states to enter if history is target of transition
+    self.history_values = {}
+
+    # output events accumulate here until they are collected
+    self.output = []
+
+  # enter default states
+  def initialize(self):
+    states = self.model.tree.root.getEffectiveTargetStates(self)
+    self.configuration.extend(states)
+    self.configuration_bitmap = Bitmap.from_list(s.state_id for s in states)
+    for state in states:
+        print_debug(termcolor.colored('  ENTER %s'%state.name, 'green'))
+        self.eventless_states += state.has_eventless_transitions
+        self.perform_actions(state.enter)
+        self.start_timers(state.after_triggers)
+
+  def fire_transition(self, t: Transition) -> Bitmap:
+    def __exitSet():
+        return [s for s in reversed(t.lca.descendants) if (s in self.configuration)]
+    
+    def __enterSet(targets):
+        target = targets[0]
+        for a in reversed(target.ancestors):
+            if a in t.source.ancestors:
+                continue
+            else:
+                yield a
+        for target in targets:
+            yield target
+
+    def __getEffectiveTargetStates():
+        targets = []
+        for target in t.targets:
+            for e_t in target.getEffectiveTargetStates(self):
+                if not e_t in targets:
+                    targets.append(e_t)
+        return targets
+
+    # exit states...
+    exit_set = __exitSet()
+    for s in exit_set:
+        # remember which state(s) we were in if a history state is present
+        for h in s.history:
+            f = lambda s0: s0.ancestors and s0.parent == s
+            if isinstance(h, DeepHistoryState):
+                f = lambda s0: not s0.descendants and s0 in s.descendants
+            self.history_values[h.state_id] = list(filter(f, self.configuration))
+    # print_debug('')
+    print_debug("fire " + str(t))
+    for s in exit_set:
+        print_debug(termcolor.colored('  EXIT %s' % s.name, 'green'))
+        self.eventless_states -= s.has_eventless_transitions
+        # execute exit action(s)
+        self.perform_actions(s.exit)
+        self.configuration_bitmap &= ~Bit(s.state_id)
+            
+    # execute transition action(s)
+    self.perform_actions(t.actions)
+        
+    # enter states...
+    targets = __getEffectiveTargetStates()
+    enter_set = __enterSet(targets)
+    for s in enter_set:
+        print_debug(termcolor.colored('  ENTER %s' % s.name, 'green'))
+        self.eventless_states += s.has_eventless_transitions
+        self.configuration_bitmap |= Bit(s.state_id)
+        # execute enter action(s)
+        self.perform_actions(s.enter)
+        self.start_timers(s.after_triggers)
+    try:
+        self.configuration = self.config_mem[self.configuration_bitmap]
+    except:
+        self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.model.tree.state_list if self.configuration_bitmap.has(s.state_id)]
+
+    return t.arena_bitmap
+
+  def check_guard(self, t, events) -> bool:
+      # Special case: after trigger
+      if isinstance(t.trigger, AfterTrigger):
+        e = [e for e in events if e.id == t.trigger.id][0]
+        if t.trigger.expected_id != e.parameters[0]:
+          return False
+
+      if t.guard is None:
+          return True
+      else:
+          return t.guard.eval(events, self.data_model)
+
+  def check_source(self, t) -> bool:
+      return self.configuration_bitmap.has(t.source.state_id)
+
+  def perform_actions(self, actions: List[Action]):
+      for a in actions:
+          if isinstance(a, RaiseInternalEvent):
+              self.raise_internal(Event(id=a.event_id, name=a.name, port="", parameters=[]))
+          elif isinstance(a, RaiseOutputEvent):
+              self.output.append(
+                  OutputEvent(Event(id=0, name=a.name, port=a.outport, parameters=[]),
+                  OutputPortTarget(a.outport),
+                  a.time_offset))
+
+  def start_timers(self, triggers: List[AfterTrigger]):
+      for after in triggers:
+          self.output.append(OutputEvent(
+              Event(id=after.id, name=after.name, parameters=[after.nextTimerId()]),
+              target=InstancesTarget([self.instance]),
+              time_offset=after.delay))
+
+  # Return whether the current configuration includes ALL the states given.
+  def in_state(self, state_strings: List[str]) -> bool:
+      state_ids_bitmap = Bitmap.from_list((self.model.tree.states[state_string].state_id for state_string in state_strings))
+      in_state = self.configuration_bitmap.has_all(state_ids_bitmap)
+      if in_state:
+          print_debug("in state"+str(state_strings))
+      else:
+          print_debug("not in state"+str(state_strings))
+      return in_state
+
+  def collect_output(self) -> Tuple[bool, List[OutputEvent]]:
+    output = self.output
+    self.output = []
+    return (not self.eventless_states, output)

+ 3 - 16
src/sccd/runtime/statechart_syntax.py

@@ -1,3 +1,4 @@
+import termcolor
 from dataclasses import dataclass, field
 from typing import *
 from sccd.runtime.event_queue import Timestamp
@@ -89,7 +90,7 @@ class State:
         self.exit = exit
                     
     def __repr__(self):
-        return "State(%s)" % (self.state_id)
+        return "State(\"%s\")" % (self.name)
         
 class HistoryState(State):
     def __init__(self, name):
@@ -154,18 +155,6 @@ class AfterTrigger(Trigger):
         # Only one scheduled event should be responded to, i.e. the latest one.
         self.expected_id = -1
 
-    # Guard condition always paired with an AfterTrigger
-    class Guard(Expression):
-        def __init__(self, trigger):
-            self.trigger = trigger
-
-        def eval(self, events, datamodel):
-            e = [e for e in events if e.name == self.trigger.name][0]
-            return e.parameters[0] == self.trigger.expected_id
-
-    def createGuard(self):
-        return AfterTrigger.Guard(self)
-
     def nextTimerId(self) -> int:
         self.expected_id += 1
         return self.expected_id
@@ -191,8 +180,6 @@ class Transition:
         self.trigger = trigger
         if self.trigger is None:
             self.source.has_eventless_transitions = True
-        if isinstance(self.trigger, AfterTrigger):
-            self.guard = self.trigger.createGuard()
         
     def optimize(self):
         # the least-common ancestor can be computed statically
@@ -209,7 +196,7 @@ class Transition:
         self.arena_bitmap = self.lca.descendant_bitmap.set(self.lca.state_id)
                     
     def __repr__(self):
-        return "Transition(%s, %s)" % (self.source, self.targets[0])
+        return termcolor.colored("%s 🡪 %s" % (self.source.name, self.targets[0].name), 'green')
 
 @dataclass
 class RaiseEvent(Action):

+ 57 - 56
src/sccd/runtime/xml_loader2.py

@@ -1,6 +1,6 @@
 import os
 import lxml.etree as ET
-from lark import Lark
+from lark import Lark, Transformer
 from sccd.runtime.test import *
 from sccd.runtime.model import *
 from sccd.runtime.statechart_syntax import *
@@ -8,27 +8,25 @@ from sccd.runtime.statechart_syntax import *
 import sccd.schema
 schema_dir = os.path.dirname(sccd.schema.__file__)
 
-# Grammar for parsing state references and expressions
-grammar = open(os.path.join(schema_dir,"grammar.g"))
-parser = Lark(grammar, parser="lalr", start=["state_ref", "expr"])
-
-class ParseError(Exception):
-  def __init__(self, msg):
-    self.msg = msg
-
-def load_expression(parse_node) -> Expression:
-  if parse_node.data == "func_call":
-    function = load_expression(parse_node.children[0])
-    parameters = [load_expression(e) for e in parse_node.children[1].children]
-    return FunctionCall(function, parameters)
-  elif parse_node.data == "string":
-    return StringLiteral(parse_node.children[0].value[1:-1])
-  elif parse_node.data == "identifier":
-    return Identifier(parse_node.children[0].value)
-  elif parse_node.data == "array":
-    elements = [load_expression(e) for e in parse_node.children]
-    return Array(elements)
-  raise ParseError("Can't handle expression type: "+parse_node.data)
+with open(os.path.join(schema_dir,"grammar.g")) as file:
+  grammar = file.read()
+
+# Lark transformer for parsetree-less parsing of expressions
+class ExpressionTransformer(Transformer):
+  def string(self, node):
+    return StringLiteral(node[0][1:-1])
+
+  def func_call(self, node):
+    return FunctionCall(node[0], node[1].children)
+
+  def identifier(self, node):
+    return Identifier(node[0].value)
+
+  array = Array
+
+expr_parser = Lark(grammar, parser="lalr", start=["expr"], transformer=ExpressionTransformer())
+
+state_ref_parser = Lark(grammar, parser="lalr", start=["state_ref"])
 
 # Load state tree from XML <tree> node.
 # Namespace is required for building event namespace and in/outport discovery.
@@ -45,7 +43,7 @@ def load_state_tree(namespace: ModelNamespace, tree_node) -> StateTree:
         namespace.add_outport(port)
         return RaiseOutputEvent(name=name, parameters=[], outport=port, time_offset=0)
     else:
-      raise None
+      raise Exception("Unsupported action")
 
   # parent_node: XML node containing any number of action nodes as direct children
   def load_actions(parent_node) -> List[Action]:
@@ -79,8 +77,11 @@ def load_state_tree(namespace: ModelNamespace, tree_node) -> StateTree:
           state.addChild(child)
           if child.short_name == initial:
             state.default_state = child
-    if not initial and len(state.children) == 1:
+    if tag == "state" and not initial:
+      if len(state.children) == 1:
         state.default_state = state.children[0]
+      elif len(state.children) > 1:
+        raise Exception("Line %d: <%s> with %d children: Must set 'initial' attribute." % (state_node.sourceline, tag, len(state.children)))
 
     for xml_t in state_node.findall("transition", state_node.nsmap):
       transition_nodes.append((xml_t, state))
@@ -104,23 +105,26 @@ def load_state_tree(namespace: ModelNamespace, tree_node) -> StateTree:
   # Add transitions
   next_after_id = 0
   for t_node, source in transition_nodes:
-    # Parse and find target state
-    target_string = t_node.get("target", "")
-    parse_tree = parser.parse(target_string, start="state_ref")
-    def find_state(sequence) -> State:
-      if sequence.data == "relative_path":
-        el = source
-      elif sequence.data == "absolute_path":
-        el = root
-      for item in sequence.children:
-        if item.type == "PARENT_NODE":
-          el = el.parent
-        elif item.type == "CURRENT_NODE":
-          continue
-        elif item.type == "IDENTIFIER":
-          el = [x for x in el.children if x.short_name == item.value][0]
-      return el
-    targets = [find_state(seq) for seq in parse_tree.children]
+    try:
+      # Parse and find target state
+      target_string = t_node.get("target", "")
+      parse_tree = state_ref_parser.parse(target_string, start="state_ref")
+      def find_state(sequence) -> State:
+        if sequence.data == "relative_path":
+          el = source
+        elif sequence.data == "absolute_path":
+          el = root
+        for item in sequence.children:
+          if item.type == "PARENT_NODE":
+            el = el.parent
+          elif item.type == "CURRENT_NODE":
+            continue
+          elif item.type == "IDENTIFIER":
+            el = [x for x in el.children if x.short_name == item.value][0]
+        return el
+      targets = [find_state(seq) for seq in parse_tree.children]
+    except:
+      raise Exception("Line %d: <transition> with target=\"%s\": Could not find target." % (t_node.sourceline, target_string))
 
     transition = Transition(source, targets)
 
@@ -144,11 +148,8 @@ def load_state_tree(namespace: ModelNamespace, tree_node) -> StateTree:
     # Guard
     cond = t_node.get("cond")
     if cond is not None:
-      parse_tree = parser.parse(cond, start="expr")
-      # print(parse_tree)
-      # print(parse_tree.pretty())
-      cond_expr = load_expression(parse_tree)
-      transition.setGuard(cond_expr)
+      expr = expr_parser.parse(cond, start="expr")
+      transition.setGuard(expr)
     source.addTransition(transition)
 
   # Calculate stuff like list of ancestors, descendants, etc.
@@ -178,16 +179,16 @@ def load_statechart(namespace: ModelNamespace, sc_node) -> Statechart:
   return Statechart(tree=state_tree, semantics=semantics)
 
 def load_semantics(semantics: SemanticConfiguration, semantics_node):
-  if semantics_node is not None:
-    # Use reflection to find the possible XML attributes and their values
-    for aspect in dataclasses.fields(SemanticConfiguration):
-      key = semantics_node.get(aspect.name)
-      if key is not None:
-        if key == "*":
-          setattr(semantics, aspect.name, None)
-        else:
-          value = aspect.type[key.upper()]
-          setattr(semantics, aspect.name, value)
+    if semantics_node is not None:
+      # Use reflection to find the possible XML attributes and their values
+      for aspect in dataclasses.fields(SemanticConfiguration):
+        key = semantics_node.get(aspect.name)
+        if key is not None:
+          if key == "*":
+            setattr(semantics, aspect.name, None)
+          else:
+            value = aspect.type[key.upper()]
+            setattr(semantics, aspect.name, value)
 
 # Returned list contains more than one test if the semantic configuration contains wildcard values.
 def load_test(src_file) -> List[Test]:

+ 10 - 9
src/sccd/schema/grammar.g

@@ -14,9 +14,9 @@ CURRENT_NODE: "."
 IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
 
 // target of a transition
-state_ref: _path | "(" _path ("," _path)+ ")" 
+state_ref: path | "(" path ("," path)+ ")" 
 
-_path: absolute_path | relative_path 
+?path: absolute_path | relative_path 
 absolute_path: _PATH_SEP _path_sequence
 relative_path: _path_sequence
 _path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_sequence)?
@@ -27,23 +27,24 @@ _path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_seque
 ?expr: or_expr
 
 ?or_expr: and_expr
-       | or_expr "||" and_expr -> or
+        | or_expr "||" and_expr -> or
 
 ?and_expr: atom
-        | and_expr "&&" atom   -> and
+         | and_expr "&&" atom   -> and
 
 ?atom: IDENTIFIER               -> identifier
-    | "-" atom                 -> neg
-    | "(" expr ")"             -> group
-    | literal                 
-    | func_call
-    | array
+     | "-" atom                 -> neg
+     | "(" expr ")"             -> group
+     | literal
+     | func_call
+     | array
 
 array: "[" (expr ("," expr)*)? "]"
 
 ?literal: ESCAPED_STRING -> string
         | SIGNED_NUMBER -> number
         | bool_literal -> bool
+
 ?bool_literal: TRUE | FALSE
 
 func_call: atom "(" param_list ")"

+ 80 - 0
test/new_test_files/features/after/test_after.svg

@@ -0,0 +1,80 @@
+<?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="242pt" height="231pt"
+ viewBox="0.00 0.00 242.00 231.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 227)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-227 238,-227 238,4 -4,4"/>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="117" cy="-217.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _s1 -->
+<g id="node2" class="node">
+<title>_s1</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="145,-184 89,-184 89,-148 145,-148 145,-184"/>
+<text text-anchor="start" x="110.6646" y="-162.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s1</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M101.3333,-149C101.3333,-149 132.6667,-149 132.6667,-149 138.3333,-149 144,-154.6667 144,-160.3333 144,-160.3333 144,-171.6667 144,-171.6667 144,-177.3333 138.3333,-183 132.6667,-183 132.6667,-183 101.3333,-183 101.3333,-183 95.6667,-183 90,-177.3333 90,-171.6667 90,-171.6667 90,-160.3333 90,-160.3333 90,-154.6667 95.6667,-149 101.3333,-149"/>
+</g>
+<!-- __initial&#45;&gt;_s1 -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_s1</title>
+<path fill="none" stroke="#000000" d="M117,-211.9886C117,-207.6293 117,-201.1793 117,-194.4801"/>
+<polygon fill="#000000" stroke="#000000" points="120.5001,-194.0122 117,-184.0122 113.5001,-194.0122 120.5001,-194.0122"/>
+<text text-anchor="middle" x="118.3895" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _s2 -->
+<g id="node3" class="node">
+<title>_s2</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-120 0,-120 0,-74 106,-74 106,-120"/>
+<text text-anchor="start" x="46.6646" y="-103.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s2</text>
+<text text-anchor="start" x="5.5022" y="-83.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_2</text>
+<polygon fill="#000000" stroke="#000000" points="0,-97 0,-97 106,-97 106,-97 0,-97"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-75C13,-75 93,-75 93,-75 99,-75 105,-81 105,-87 105,-87 105,-107 105,-107 105,-113 99,-119 93,-119 93,-119 13,-119 13,-119 7,-119 1,-113 1,-107 1,-107 1,-87 1,-87 1,-81 7,-75 13,-75"/>
+</g>
+<!-- _s1&#45;&gt;_s2 -->
+<g id="edge2" class="edge">
+<title>_s1&#45;&gt;_s2</title>
+<path fill="none" stroke="#000000" d="M100.1848,-147.8711C94.4188,-141.6547 87.7966,-134.5151 81.372,-127.5886"/>
+<polygon fill="#000000" stroke="#000000" points="83.8947,-125.1616 74.5281,-120.21 78.7625,-129.9219 83.8947,-125.1616"/>
+<text text-anchor="start" x="93" y="-131" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(100) &#160;&#160;</text>
+</g>
+<!-- _s3 -->
+<g id="node4" class="node">
+<title>_s3</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="234,-120 128,-120 128,-74 234,-74 234,-120"/>
+<text text-anchor="start" x="174.6646" y="-103.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s3</text>
+<text text-anchor="start" x="133.5022" y="-83.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_3</text>
+<polygon fill="#000000" stroke="#000000" points="128,-97 128,-97 234,-97 234,-97 128,-97"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M141,-75C141,-75 221,-75 221,-75 227,-75 233,-81 233,-87 233,-87 233,-107 233,-107 233,-113 227,-119 221,-119 221,-119 141,-119 141,-119 135,-119 129,-113 129,-107 129,-107 129,-87 129,-87 129,-81 135,-75 141,-75"/>
+</g>
+<!-- _s1&#45;&gt;_s3 -->
+<g id="edge3" class="edge">
+<title>_s1&#45;&gt;_s3</title>
+<path fill="none" stroke="#000000" d="M141.4695,-147.9993C144.4564,-145.4372 147.376,-142.7385 150,-140 153.4025,-136.4491 156.7352,-132.5071 159.8791,-128.4919"/>
+<polygon fill="#000000" stroke="#000000" points="162.8542,-130.3572 166.029,-120.2492 157.2437,-126.1712 162.8542,-130.3572"/>
+<text text-anchor="start" x="159" y="-131" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(200) &#160;&#160;</text>
+</g>
+<!-- _s4 -->
+<g id="node5" class="node">
+<title>_s4</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-46 0,-46 0,0 106,0 106,-46"/>
+<text text-anchor="start" x="46.6646" y="-29.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s4</text>
+<text text-anchor="start" x="5.5022" y="-9.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_4</text>
+<polygon fill="#000000" stroke="#000000" points="0,-23 0,-23 106,-23 106,-23 0,-23"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-1C13,-1 93,-1 93,-1 99,-1 105,-7 105,-13 105,-13 105,-33 105,-33 105,-39 99,-45 93,-45 93,-45 13,-45 13,-45 7,-45 1,-39 1,-33 1,-33 1,-13 1,-13 1,-7 7,-1 13,-1"/>
+</g>
+<!-- _s2&#45;&gt;_s4 -->
+<g id="edge4" class="edge">
+<title>_s2&#45;&gt;_s4</title>
+<path fill="none" stroke="#000000" d="M53,-73.9916C53,-68.476 53,-62.474 53,-56.5881"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-56.249 53,-46.2491 49.5001,-56.2491 56.5001,-56.249"/>
+<text text-anchor="start" x="53" y="-57" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(150) &#160;&#160;</text>
+</g>
+</g>
+</svg>

+ 41 - 0
test/new_test_files/features/after/test_after.xml

@@ -0,0 +1,41 @@
+<?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">
+          <transition after="100" target="/s2"/>
+          <transition after="200" target="/s3"/>
+        </state>
+        <state id="s2">
+          <onentry>
+            <raise event="in_2" port="out" />
+          </onentry>
+          <transition after="150" target="/s4"/>
+        </state>
+        <state id="s3">
+          <onentry>
+            <raise event="in_3" port="out"/>
+          </onentry>
+        </state>
+        <state id="s4">
+          <onentry>
+            <raise event="in_4" port="out"/>
+          </onentry>
+        </state>
+      </state>
+    </tree>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_2" port="out"/>
+    </big_step>
+    <big_step>
+      <event name="in_4" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 134 - 0
test/new_test_files/features/after/test_after_reentry.svg

@@ -0,0 +1,134 @@
+<?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="479pt" height="404pt"
+ viewBox="0.00 0.00 479.00 404.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 400)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-400 475,-400 475,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster__p</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M20,-8C20,-8 451,-8 451,-8 457,-8 463,-14 463,-20 463,-20 463,-345 463,-345 463,-351 457,-357 451,-357 451,-357 20,-357 20,-357 14,-357 8,-351 8,-345 8,-345 8,-20 8,-20 8,-14 14,-8 20,-8"/>
+<text text-anchor="start" x="232.1646" y="-338.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">p</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster__p_o0</title>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="122,-16 122,-319 455,-319 455,-16 122,-16"/>
+<text text-anchor="start" x="282.3292" y="-300.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">o0</text>
+</g>
+<g id="clust3" class="cluster">
+<title>cluster__p_o1</title>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="24,-21 24,-319 114,-319 114,-21 24,-21"/>
+<text text-anchor="start" x="62.8292" y="-300.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">o1</text>
+</g>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="16" cy="-390.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p -->
+<!-- __initial&#45;&gt;_p -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_p</title>
+<path fill="none" stroke="#000000" d="M16,-384.9533C16,-380.7779 16,-374.5043 16,-367.0332"/>
+<polygon fill="#000000" stroke="#000000" points="19.5001,-366.9971 16,-356.9971 12.5001,-366.9972 19.5001,-366.9971"/>
+<text text-anchor="middle" x="17.3895" y="-368" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o0 -->
+<!-- _p_o0_initial -->
+<g id="node4" class="node">
+<title>_p_o0_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="348" cy="-275.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p_o0_a -->
+<g id="node5" class="node">
+<title>_p_o0_a</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="376,-188 320,-188 320,-152 376,-152 376,-188"/>
+<text text-anchor="start" x="344.6646" y="-166.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">a</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M332.3333,-153C332.3333,-153 363.6667,-153 363.6667,-153 369.3333,-153 375,-158.6667 375,-164.3333 375,-164.3333 375,-175.6667 375,-175.6667 375,-181.3333 369.3333,-187 363.6667,-187 363.6667,-187 332.3333,-187 332.3333,-187 326.6667,-187 321,-181.3333 321,-175.6667 321,-175.6667 321,-164.3333 321,-164.3333 321,-158.6667 326.6667,-153 332.3333,-153"/>
+</g>
+<!-- _p_o0_initial&#45;&gt;_p_o0_a -->
+<g id="edge2" class="edge">
+<title>_p_o0_initial&#45;&gt;_p_o0_a</title>
+<path fill="none" stroke="#000000" d="M348,-269.8288C348,-265.1736 348,-258.4097 348,-252.5 348,-252.5 348,-252.5 348,-205.5 348,-203.1079 348,-200.6252 348,-198.1342"/>
+<polygon fill="#000000" stroke="#000000" points="351.5001,-198.0597 348,-188.0598 344.5001,-198.0598 351.5001,-198.0597"/>
+<text text-anchor="middle" x="349.3895" y="-226" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o0_b -->
+<g id="node6" class="node">
+<title>_p_o0_b</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="278,-70 172,-70 172,-24 278,-24 278,-70"/>
+<text text-anchor="start" x="221.6646" y="-53.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">b</text>
+<text text-anchor="start" x="177.5022" y="-33.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_b</text>
+<polygon fill="#000000" stroke="#000000" points="172,-47 172,-47 278,-47 278,-47 172,-47"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M185,-25C185,-25 265,-25 265,-25 271,-25 277,-31 277,-37 277,-37 277,-57 277,-57 277,-63 271,-69 265,-69 265,-69 185,-69 185,-69 179,-69 173,-63 173,-57 173,-57 173,-37 173,-37 173,-31 179,-25 185,-25"/>
+</g>
+<!-- _p_o0_a&#45;&gt;_p_o0_b -->
+<g id="edge3" class="edge">
+<title>_p_o0_a&#45;&gt;_p_o0_b</title>
+<path fill="none" stroke="#000000" d="M319.918,-167.7853C265.7222,-163.1318 152,-151.3584 152,-134.5 152,-134.5 152,-134.5 152,-87.5 152,-78.3329 156.4562,-71.2234 163.1666,-65.7143"/>
+<polygon fill="#000000" stroke="#000000" points="165.5053,-68.3661 171.9476,-59.9549 161.6661,-62.5128 165.5053,-68.3661"/>
+<text text-anchor="start" x="152" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(100) [INSTATE([&quot;/p/o1/x&quot;])] &#160;&#160;</text>
+</g>
+<!-- _p_o0_c -->
+<g id="node7" class="node">
+<title>_p_o0_c</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="447,-70 341,-70 341,-24 447,-24 447,-70"/>
+<text text-anchor="start" x="391" y="-53.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">c</text>
+<text text-anchor="start" x="346.8376" y="-33.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_c</text>
+<polygon fill="#000000" stroke="#000000" points="341,-47 341,-47 447,-47 447,-47 341,-47"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M354,-25C354,-25 434,-25 434,-25 440,-25 446,-31 446,-37 446,-37 446,-57 446,-57 446,-63 440,-69 434,-69 434,-69 354,-69 354,-69 348,-69 342,-63 342,-57 342,-57 342,-37 342,-37 342,-31 348,-25 354,-25"/>
+</g>
+<!-- _p_o0_a&#45;&gt;_p_o0_c -->
+<g id="edge4" class="edge">
+<title>_p_o0_a&#45;&gt;_p_o0_c</title>
+<path fill="none" stroke="#000000" d="M376.1541,-159.659C386.1638,-154.0322 395,-145.8506 395,-134.5 395,-134.5 395,-134.5 395,-87.5 395,-85.1262 394.9826,-82.6744 394.9524,-80.2065"/>
+<polygon fill="#000000" stroke="#000000" points="398.4505,-80.0625 394.7681,-70.1282 391.4517,-80.1906 398.4505,-80.0625"/>
+<text text-anchor="start" x="395" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(150) &#160;&#160;</text>
+</g>
+<!-- _p_o0_b&#45;&gt;_p_o0_a -->
+<g id="edge5" class="edge">
+<title>_p_o0_b&#45;&gt;_p_o0_a</title>
+<path fill="none" stroke="#000000" d="M278.2342,-59.6571C311.4764,-68.3378 348,-79.6594 348,-87.5 348,-134.5 348,-134.5 348,-134.5 348,-136.8921 348,-139.3748 348,-141.8658"/>
+<polygon fill="#000000" stroke="#000000" points="344.5001,-141.9402 348,-151.9402 351.5001,-141.9403 344.5001,-141.9402"/>
+<text text-anchor="middle" x="349.3895" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o1 -->
+<!-- _p_o1_initial -->
+<g id="node9" class="node">
+<title>_p_o1_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="60" cy="-275.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p_o1_x -->
+<g id="node10" class="node">
+<title>_p_o1_x</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="88,-188 32,-188 32,-152 88,-152 88,-188"/>
+<text text-anchor="start" x="57" y="-166.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">x</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M44.3333,-153C44.3333,-153 75.6667,-153 75.6667,-153 81.3333,-153 87,-158.6667 87,-164.3333 87,-164.3333 87,-175.6667 87,-175.6667 87,-181.3333 81.3333,-187 75.6667,-187 75.6667,-187 44.3333,-187 44.3333,-187 38.6667,-187 33,-181.3333 33,-175.6667 33,-175.6667 33,-164.3333 33,-164.3333 33,-158.6667 38.6667,-153 44.3333,-153"/>
+</g>
+<!-- _p_o1_initial&#45;&gt;_p_o1_x -->
+<g id="edge6" class="edge">
+<title>_p_o1_initial&#45;&gt;_p_o1_x</title>
+<path fill="none" stroke="#000000" d="M60,-269.8288C60,-265.1736 60,-258.4097 60,-252.5 60,-252.5 60,-252.5 60,-205.5 60,-203.1079 60,-200.6252 60,-198.1342"/>
+<polygon fill="#000000" stroke="#000000" points="63.5001,-198.0597 60,-188.0598 56.5001,-198.0598 63.5001,-198.0597"/>
+<text text-anchor="middle" x="61.3895" y="-226" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o1_y -->
+<g id="node11" class="node">
+<title>_p_o1_y</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="88,-65 32,-65 32,-29 88,-29 88,-65"/>
+<text text-anchor="start" x="57" y="-43.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">y</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M44.3333,-30C44.3333,-30 75.6667,-30 75.6667,-30 81.3333,-30 87,-35.6667 87,-41.3333 87,-41.3333 87,-52.6667 87,-52.6667 87,-58.3333 81.3333,-64 75.6667,-64 75.6667,-64 44.3333,-64 44.3333,-64 38.6667,-64 33,-58.3333 33,-52.6667 33,-52.6667 33,-41.3333 33,-41.3333 33,-35.6667 38.6667,-30 44.3333,-30"/>
+</g>
+<!-- _p_o1_x&#45;&gt;_p_o1_y -->
+<g id="edge7" class="edge">
+<title>_p_o1_x&#45;&gt;_p_o1_y</title>
+<path fill="none" stroke="#000000" d="M55.7033,-151.6741C54.7416,-146.1833 54,-140.1255 54,-134.5 54,-134.5 54,-134.5 54,-87.5 54,-83.4573 54.2962,-79.2119 54.7569,-75.0534"/>
+<polygon fill="#000000" stroke="#000000" points="58.2353,-75.4511 56.1661,-65.0603 51.3039,-74.4736 58.2353,-75.4511"/>
+<text text-anchor="start" x="54" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">after(250) &#160;&#160;</text>
+</g>
+</g>
+</svg>

+ 48 - 0
test/new_test_files/features/after/test_after_reentry.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics
+        big_step_maximality="take_many"
+        combo_step_maximality="combo_take_one"/>
+    <tree>
+      <state>
+        <parallel id="p">
+          <state id="o0" initial="a">
+            <state id="a">
+              <transition after="100" cond='INSTATE(["/p/o1/x"])' target="../b"/>
+              <transition after="150" target="../c"/>
+            </state>
+            <state id="b">
+              <onentry>
+                <raise event="in_b" port="out"/>
+              </onentry>
+              <transition target="../a"/>
+            </state>
+            <state id="c">
+              <onentry>
+                <raise event="in_c" port="out"/>
+              </onentry>
+            </state>
+          </state>
+          <state id="o1" initial="x">
+            <state id="x">
+              <transition after="250" target="../y"/>
+            </state>
+            <state id="y"/>
+          </state>
+        </parallel>
+      </state>
+    </tree>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+    </big_step>
+    <big_step>
+      <event name="in_b" port="out"/>
+    </big_step>
+    <big_step>
+      <event name="in_c" port="out"/>
+    </big_step>
+  </output>
+</test>

test/new_test_files/models/flat.statechart.svg → test/new_test_files/semantics/big_step_maximality/statechart_flat.svg


test/new_test_files/models/flat.statechart.xml → test/new_test_files/semantics/big_step_maximality/statechart_flat.xml


test/new_test_files/models/ortho.statechart.svg → test/new_test_files/semantics/big_step_maximality/statechart_ortho.svg


test/new_test_files/models/ortho.statechart.xml → test/new_test_files/semantics/big_step_maximality/statechart_ortho.xml


+ 2 - 2
test/new_test_files/big_step_maximality/flat_takemany.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/flat.statechart.xml">
+  <statechart src="statechart_flat.xml">
     <override_semantics
       big_step_maximality="take_many"/>
   </statechart>
@@ -11,4 +11,4 @@
       <event name="in_c" port="out"/>
     </big_step>
   </output>
-</test>
+</test>

+ 2 - 2
test/new_test_files/big_step_maximality/flat_takeone.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/flat.statechart.xml">
+  <statechart src="statechart_flat.xml">
     <override_semantics
       big_step_maximality="take_one"/>
   </statechart>
@@ -13,4 +13,4 @@
       <event name="in_c" port="out"/>
     </big_step>
   </output>
-</test>
+</test>

+ 2 - 2
test/new_test_files/big_step_maximality/ortho_takemany.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="take_many"/>
   </statechart>
@@ -12,4 +12,4 @@
       <event name="in_f" port="out"/>
     </big_step>
   </output>
-</test>
+</test>

+ 2 - 2
test/new_test_files/big_step_maximality/ortho_takeone.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="take_one"/>
   </statechart>
@@ -14,4 +14,4 @@
       <event name="in_f" port="out"/>
     </big_step>
   </output>
-</test>
+</test>

test/new_test_files/models/flat_intevent.statechart.svg → test/new_test_files/semantics/event_lifeline/statechart_flat.svg


test/new_test_files/models/flat_intevent.statechart.xml → test/new_test_files/semantics/event_lifeline/statechart_flat.xml


test/new_test_files/models/ortho_intevent.statechart.svg → test/new_test_files/semantics/event_lifeline/statechart_ortho.svg


+ 1 - 0
test/new_test_files/models/ortho_intevent.statechart.xml

@@ -1,3 +1,4 @@
+<?xml version="1.0" ?>
 <statechart>
   <tree>
     <state>

+ 1 - 1
test/new_test_files/event_lifeline/flat_nextbs.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/flat_intevent.statechart.xml">
+  <statechart src="statechart_flat.xml">
     <override_semantics
       big_step_maximality="*"
       internal_event_lifeline="queue"/>

+ 1 - 1
test/new_test_files/event_lifeline/flat_nextss_takemany.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/flat_intevent.statechart.xml">
+  <statechart src="statechart_flat.xml">
     <override_semantics
       big_step_maximality="take_many"
       internal_event_lifeline="next_small_step"/>

+ 1 - 1
test/new_test_files/event_lifeline/flat_nextss_takeone.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/flat_intevent.statechart.xml">
+  <statechart src="statechart_flat.xml">
     <override_semantics
       big_step_maximality="take_one"
       internal_event_lifeline="next_small_step"/>

+ 1 - 1
test/new_test_files/event_lifeline/ortho_nextbs.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho_intevent.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="*"
       internal_event_lifeline="queue"/>

+ 2 - 1
test/new_test_files/event_lifeline/ortho_nextcs_takemany.test.xml

@@ -1,8 +1,9 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho_intevent.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="take_many"
+      combo_step_maximality="*"
       internal_event_lifeline="next_combo_step"/>
   </statechart>
   <output>

+ 1 - 1
test/new_test_files/event_lifeline/ortho_nextcs_takeone.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho_intevent.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="take_one"
       internal_event_lifeline="next_combo_step"/>

+ 1 - 1
test/new_test_files/event_lifeline/ortho_nextss.test.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="../models/ortho_intevent.statechart.xml">
+  <statechart src="statechart_ortho.xml">
     <override_semantics
       big_step_maximality="*"
       internal_event_lifeline="next_small_step"/>

+ 8 - 2
test/render2.py

@@ -18,7 +18,8 @@ if __name__ == '__main__':
     parser.add_argument('--pool-size', metavar='INT', type=int, default=multiprocessing.cpu_count()+1, help="Number of worker processes. Default = CPU count + 1.")
     args = parser.parse_args()
 
-    srcs = get_files(args.path, filter=lambda file: file.endswith(".statechart.xml"))
+    srcs = get_files(args.path,
+      filter=lambda file: (file.startswith("statechart_") or file.startswith("test_")) and file.endswith(".xml"))
 
     if len(srcs):
       if not args.no_svg:
@@ -35,7 +36,9 @@ if __name__ == '__main__':
 
     def process(src):
       statechart_node = ET.parse(src).getroot()
-      tree_node = statechart_node.find("tree")
+      tree_node = statechart_node.find(".//tree")
+      if tree_node is None:
+        return # no tree here :(
       tree = load_state_tree(ModelNamespace(), tree_node)
 
       target_path = lambda ext: os.path.join(args.output_dir, dropext(src)+ext)
@@ -62,6 +65,7 @@ if __name__ == '__main__':
         def __init__(self, source, targets):
           self.source = source
           self.targets = targets
+          self.guard = None
           self.trigger = None
           self.actions = []
 
@@ -111,6 +115,8 @@ if __name__ == '__main__':
         label = ""
         if t.trigger:
           label += t.trigger.render()
+        if t.guard:
+          label += ' ['+t.guard.render()+']'
         if t.actions:
           raises = [a for a in t.actions if isinstance(a, RaiseEvent)]
           label += ','.join([r.render() for r in raises])

+ 1 - 1
test/test.py

@@ -88,7 +88,7 @@ class PyTestCase(unittest.TestCase):
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(
         description="Run SCCD tests.",
-        epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of state machines.")
+        epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of the runtime.")
     parser.add_argument('path', metavar='PATH', type=str, nargs='*', help="Tests to run. Can be a XML file or a directory. If a directory, it will be recursively scanned for XML files.")
     parser.add_argument('--build-dir', metavar='BUILD_DIR', type=str, default='build', help="Directory for built tests. Defaults to 'build'")
     args = parser.parse_args()

+ 3 - 2
test/test2.py

@@ -6,12 +6,13 @@ from sccd.runtime.xml_loader2 import *
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(
         description="Run SCCD tests.",
-        epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of state machines.")
+        epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of the runtime.")
     parser.add_argument('path', metavar='PATH', type=str, nargs='*', help="Tests to run. Can be a XML file or a directory. If a directory, it will be recursively scanned for XML files.")
     parser.add_argument('--build-dir', metavar='BUILD_DIR', type=str, default='build', help="Directory for built tests. Defaults to 'build'")
     args = parser.parse_args()
 
-    src_files = get_files(args.path, filter=lambda file: file.endswith(".test.xml"))
+    src_files = get_files(args.path,
+        filter=lambda file: file.startswith("test_") and file.endswith(".xml"))
 
     suite = unittest.TestSuite()
     for src_file in src_files: