Browse Source

Added Bitmap type. Added an alternative transition candidate generation implementation, starting from enabled events instead of the current state configuration. Candidate generation returns a generator instead of a list (better performance when there are lots of candidates, basically when there is no concurrency, candidate generation ends after the first valid candidate). Conflicting transitions (to which the Priority semantic aspect applies) are dealt with much more simply and efficiently by making assumptions about the order in which candidates are generated.

Should do benchmarks on realistic statecharts to see which candidate generation algorithm has better performance. It may even depend on the statechart.
Joeri Exelmans 5 years ago
parent
commit
cd427692c0

+ 47 - 0
src/sccd/runtime/bitmap.py

@@ -0,0 +1,47 @@
+from functools import reduce
+import math
+
+class Bitmap(int):
+  def __new__(cls, value=0, *args, **kwargs):
+    return super(cls, cls).__new__(cls, value)
+
+  # iterable: positions of bits to set.
+  @classmethod
+  def from_list(cls, iterable):
+    v = reduce(lambda x,y: x|2**y, iterable, 0) # iterable
+    return super(cls, cls).__new__(cls, v)
+
+  def __repr__(self):
+    return "Bitmap("+format(self, 'b')+")"
+
+  def __str__(self):
+    return self.__repr__()
+
+  def __or__(self, other):
+    return self.__class__(super().__or__(other))
+
+  def __and__(self, other):
+    return self.__class__(super().__and__(other))
+
+  def __invert__(self):
+    return self.__class__(super().__invert__())
+
+  def set(self, pos):
+    return self.__or__(2**pos)
+
+  def unset(self, pos):
+    return self.__and__(~2**pos)
+
+  def has(self, pos):
+    return self & 2**pos
+
+  def has_all(self, bitmap):
+    return (self | bitmap) == self
+
+  # pos of first set bit
+  def first_bit_pos(self):
+    return math.floor(math.log2(x & -x))
+
+
+def Bit(pos):
+  return Bitmap(2 ** pos)

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

@@ -4,7 +4,6 @@ from typing import Dict, List, Optional
 from sccd.runtime.event_queue import EventQueue, EventQueueDeque, Timestamp
 from sccd.runtime.event import *
 from sccd.runtime.object_manager import ObjectManager
-from sccd.runtime.infinity import INFINITY
 from sccd.runtime.debug import print_debug
 
 @dataclasses.dataclass

+ 0 - 3
src/sccd/runtime/infinity.py

@@ -1,3 +0,0 @@
-# instantiate singleton     
-INFINITY = float('inf')
-

+ 88 - 61
src/sccd/runtime/statechart_instance.py

@@ -1,13 +1,12 @@
 import termcolor
 import functools
-from typing import List, Tuple
-from enum import Enum
-from sccd.runtime.infinity import INFINITY
+from typing import List, Tuple, Iterable
 from sccd.runtime.event_queue import Timestamp
 from sccd.runtime.statechart_syntax import *
 from sccd.runtime.event import *
 from sccd.runtime.semantic_options import *
 from sccd.runtime.debug import print_debug
+from sccd.runtime.bitmap import *
 from collections import Counter
 
 ELSE_GUARD = "ELSE_GUARD"
@@ -23,7 +22,9 @@ class StatechartInstance(Instance):
 
         # these 2 fields have the same information
         self.configuration = []
-        self.configuration_bitmap = 0
+        self.configuration_bitmap = Bitmap()
+
+        self.event_mem: Dict[Bitmap, List[Transition]] = {} # mapping from set of enabled events to document-ordered list of potentially enabled transitions.
 
         self.eventless_states = 0 # number of states in current configuration that have at least one eventless outgoing transition.
 
@@ -42,7 +43,7 @@ class StatechartInstance(Instance):
     def initialize(self, now: Timestamp) -> Tuple[bool, List[OutputEvent]]:
         states = self.model.root.getEffectiveTargetStates(self)
         self.configuration.extend(states)
-        self.configuration_bitmap = sum([2**s.state_id for s in 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
@@ -58,6 +59,8 @@ class StatechartInstance(Instance):
         self._combo_step.reset()
         self._small_step.reset()
 
+        print_debug(termcolor.colored('attempt big step, input_events='+str(input_events), 'red'))
+
         while self.combo_step():
             print_debug(termcolor.colored('completed combo step', 'yellow'))
             self._big_step.has_stepped = True
@@ -93,41 +96,28 @@ class StatechartInstance(Instance):
         if self._small_step.has_stepped:
             self._small_step.next()
 
-        candidates = self._transition_candidates()
-        if candidates:
-            # print_debug(termcolor.colored("small step candidates: "+
-            #     str(list(map(
-            #         lambda t: "("+str(list(map(
-            #             lambda s: "to "+s.name,
-            #             t.targets))),
-            #         candidates))), 'blue'))
-            to_skip = set()
-            conflicting = []
-            for c1 in candidates:
-                if c1 not in to_skip:
-                    conflict = [c1]
-                    for c2 in candidates[candidates.index(c1):]:
-                        if c2.source in c1.source.ancestors or c1.source in c2.source.ancestors:
-                            conflict.append(c2)
-                            to_skip.add(c2)
-
-                    import functools
-                    conflicting.append(sorted(conflict, key=functools.cmp_to_key(__younger_than)))
+        candidates = self._transition_candidates2()
+
+        candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up
+        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'))
 
+        for c in candidates:
             if self.model.semantics.concurrency == Concurrency.SINGLE:
-                candidate = conflicting[0]
-                if self.model.semantics.priority == Priority.SOURCE_PARENT:
-                    self._fire_transition(candidate[-1])
-                else:
-                    self._fire_transition(candidate[0])
+                self._fire_transition(c)
+                self._small_step.has_stepped = True
+                break
             elif self.model.semantics.concurrency == Concurrency.MANY:
-                pass # TODO: implement
-            self._small_step.has_stepped = True
+                raise Exception("Not implemented!")
         return self._small_step.has_stepped
 
     # generate transition candidates for current small step
     # @profile
-    def _transition_candidates(self) -> List[Transition]:
+    def _transition_candidates(self) -> Iterable[Transition]:
         # 1. Get all transitions possibly enabled looking only at current configuration
         changed_bitmap = self._combo_step.changed_bitmap
         key = (self.configuration_bitmap, changed_bitmap)
@@ -135,33 +125,70 @@ class StatechartInstance(Instance):
             transitions = self.transition_mem[key]
         except KeyError:
             # outgoing transitions whose arenas don't overlap with already fired transitions
-            self.transition_mem[key] = transitions = [t for s in self.configuration if not (2**s.state_id & changed_bitmap) for t in s.transitions]
-        
-        # 2. Filter those based on guard and event trigger
-        enabled_events = self._small_step.current_events + self._combo_step.current_events
-        if self.model.semantics.input_event_lifeline == InputEventLifeline.WHOLE or (
-            not self._big_step.has_stepped and
-                (self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_COMBO_STEP or (
-                not self._combo_step.has_stepped and
-                    self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_SMALL_STEP))):
-            enabled_events += self._big_step.input_events
+            self.transition_mem[key] = transitions = [t for s in self.configuration if not changed_bitmap.has(s.state_id) for t in s.transitions]
+            if self.model.semantics.priority == Priority.SOURCE_CHILD:
+                # Transitions are already in parent -> child (depth-first) order
+                # Only the first transition of the candidates will be executed.
+                # To get SOURCE-CHILD semantics, we simply reverse the list of candidates:
+                transitions.reverse()
+
+        # 2. Filter based on guard and event trigger
+        enabled_events = self._enabled_events()
+        def filter_f(t):
+            return self._check_trigger(t, enabled_events) and self._check_guard(t, enabled_events)
         # print_debug(termcolor.colored("small step enabled events: "+str(list(map(lambda e: e.name, enabled_events))), 'blue'))
-        enabled_transitions = []
-        for t in transitions:
-            if self._is_transition_enabled(t, enabled_events, enabled_transitions):
-                enabled_transitions.append(t)
-        return enabled_transitions
+        return filter(filter_f, transitions)
+
 
-    def _is_transition_enabled(self, t, events, enabled_transitions) -> bool:
+    # Alternative implementation of candidate generation using mapping from set of enabled events to enabled transitions
+    def _transition_candidates2(self) -> Iterable[Transition]:
+        enabled_events = self._enabled_events()
+        key = Bitmap.from_list(e.id for e in enabled_events)
+        try:
+            transitions = self.event_mem[key]
+        except KeyError:
+            self.event_mem[key] = transitions = [t for t in self.model.transition_list if (not t.trigger or key.has(t.trigger.id))]
+            if self.model.semantics.priority == Priority.SOURCE_CHILD:
+                # Transitions are already in parent -> child (depth-first) order
+                # Only the first transition of the candidates will be executed.
+                # To get SOURCE-CHILD semantics, we simply reverse the list of candidates:
+                transitions.reverse()
+
+        def filter_f(t):
+            return self._check_source(t) and self._check_arena(t) and self._check_guard(t, enabled_events)
+        return filter(filter_f, transitions)
+
+    def _check_trigger(self, t, events) -> bool:
         if t.trigger is None:
-            # t.enabled_event = None
-            return (t.guard is None) or (t.guard == ELSE_GUARD and not enabled_transitions) or t.guard.eval(events, self.data_model)
+            return True
         else:
             for event in events:
-                if (t.trigger.id == event.id and (not t.trigger.port or t.trigger.port == event.port)) and ((t.guard is None) or (t.guard == ELSE_GUARD and not enabled_transitions) or t.guard.eval(events, self.data_model)):
-                    # t.enabled_event = event
+                if (t.trigger.id == event.id and (not t.trigger.port or t.trigger.port == event.port)):
                     return True
 
+    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)
+
+    def _check_arena(self, t) -> bool:
+        return not self._combo_step.changed_bitmap.has(t.source.state_id)
+
+    # List of current small step enabled events
+    def _enabled_events(self) -> List[Event]:
+        events = self._small_step.current_events + self._combo_step.current_events
+        if self.model.semantics.input_event_lifeline == InputEventLifeline.WHOLE or (
+            not self._big_step.has_stepped and
+                (self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_COMBO_STEP or (
+                not self._combo_step.has_stepped and
+                    self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_SMALL_STEP))):
+            events += self._big_step.input_events
+        return events
+
     # @profile
     def _fire_transition(self, t: Transition):
 
@@ -202,10 +229,10 @@ class StatechartInstance(Instance):
             self.eventless_states -= s.has_eventless_transitions
             # execute exit action(s)
             self._perform_actions(s.exit)
-            self.configuration_bitmap &= ~2**s.state_id
+            self.configuration_bitmap &= ~Bit(s.state_id)
         
         # combo state changed area
-        self._combo_step.changed_bitmap |= 2**t.lca.state_id
+        self._combo_step.changed_bitmap |= Bit(t.lca.state_id)
         self._combo_step.changed_bitmap |= t.lca.descendant_bitmap
         
         # execute transition action(s)
@@ -217,14 +244,14 @@ class StatechartInstance(Instance):
         for s in enter_set:
             print_debug(termcolor.colored('  ENTER %s' % s.name, 'green'))
             self.eventless_states += s.has_eventless_transitions
-            self.configuration_bitmap |= 2**s.state_id
+            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] = sorted([s for s in list(self.model.states.values()) if 2**s.state_id & self.configuration_bitmap], key=lambda s: s.state_id)
+            self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.model.state_list if self.configuration_bitmap.has(s.state_id)]
         # t.enabled_event = None
         
     # def getChildren(self, link_name):
@@ -261,8 +288,8 @@ class StatechartInstance(Instance):
 
     # Return whether the current configuration includes ALL the states given.
     def inState(self, state_strings: List[str]) -> bool:
-        state_ids_bitmap = functools.reduce(lambda x,y: x|y, [2**self.model.states[state_string].state_id for state_string in state_strings])
-        in_state = (self.configuration_bitmap | state_ids_bitmap) == self.configuration_bitmap
+        state_ids_bitmap = Bitmap.from_list((self.model.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:
@@ -288,7 +315,7 @@ class ComboStepState(object):
     def __init__(self):
         self.current_events = [] # set of enabled events during combo step
         self.next_events = [] # internal events that were raised during combo step
-        self.changed_bitmap = 0 # set of all or-states that were the arena of a triggered transition during big step.
+        self.changed_bitmap = Bitmap() # set of all or-states that were the arena of a triggered transition during big step.
         self.has_stepped = True
 
     def reset(self):
@@ -298,7 +325,7 @@ class ComboStepState(object):
     def next(self):
         self.current_events = self.next_events
         self.next_events = []
-        self.changed_bitmap = 0
+        self.changed_bitmap = Bitmap()
         self.has_stepped = False
 
     def addNextEvent(self, event):

+ 10 - 6
src/sccd/runtime/statechart_syntax.py

@@ -3,6 +3,7 @@ from typing import *
 from sccd.runtime.event_queue import Timestamp
 from sccd.runtime.expression import *
 from sccd.compiler.utils import FormattedWriter
+from sccd.runtime.bitmap import *
 
 @dataclass
 class Action:
@@ -29,7 +30,7 @@ class State:
         self.state_id = -1
         self.ancestors = []
         self.descendants = []
-        self.descendant_bitmap = 0
+        self.descendant_bitmap = Bitmap()
         self.has_eventless_transitions = False
 
     def getEffectiveTargetStates(self, instance):
@@ -43,23 +44,26 @@ class State:
     # Should only be called once for the root of the state tree,
     # after the tree has been built.
     # Returns state_id + total number of states in tree
-    def init_tree(self, state_id: int = 0, name_prefix: str = "", states = {}) -> int:
+    def init_tree(self, state_id: int = 0, name_prefix: str = "", states = {}, state_list = [], transition_list = []) -> int:
         self.state_id = state_id
         next_id = state_id + 1
         self.name = name_prefix + self.short_name if name_prefix == '/' else name_prefix + '/' + self.short_name
         states[self.name] = self
+        state_list.append(self)
+        for t in self.transitions:
+            transition_list.append(t)
         for i, c in enumerate(self.children):
             if isinstance(c, HistoryState):
                 self.history.append(c)
             c.parent = self
             c.ancestors.append(self)
             c.ancestors.extend(self.ancestors)
-            next_id = c.init_tree(next_id, self.name, states)
+            next_id = c.init_tree(next_id, self.name, states, state_list, transition_list)
         self.descendants.extend(self.children)
         for c in self.children:
             self.descendants.extend(c.descendants)
         for d in self.descendants:
-            self.descendant_bitmap |= 2**d.state_id
+            self.descendant_bitmap |= Bit(d.state_id)
         return next_id
 
     def print(self, w = FormattedWriter()):
@@ -70,6 +74,7 @@ class State:
         w.dedent()
             
     def addChild(self, child):
+        child.parent = self
         self.children.append(child)
     
     def addTransition(self, transition):
@@ -175,7 +180,6 @@ class Transition:
         self.trigger: Optional[Trigger] = None
         self.source: State = source
         self.targets: List[State] = targets
-        self.optimize()
                     
     def setGuard(self, guard):
         self.guard = guard
@@ -202,7 +206,7 @@ class Transition:
                     if a in target.ancestors:
                         self.lca = a
                         break
-        self.arena_bitmap = 2**self.lca.state_id | self.lca.descendant_bitmap
+        self.arena_bitmap = self.lca.descendant_bitmap.set(self.lca.state_id)
                     
     def __repr__(self):
         return "Transition(%s, %s)" % (self.source, self.targets[0])

+ 2 - 3
src/sccd/runtime/test_event_queue.py

@@ -1,9 +1,8 @@
 import unittest
 from event_queue import EventQueue, EventQueueDeque
-from infinity import INFINITY
 
 def add_pop(q, unit):
-  unit.assertEqual(q.earliest_timestamp(), INFINITY)
+  unit.assertEqual(q.earliest_timestamp(), None)
   q.add(10, 'a')
   q.add(11, 'b')
   q.add(11, 'c')
@@ -19,7 +18,7 @@ def add_pop(q, unit):
   unit.assertEqual(q.pop(), (11,'e'))
   unit.assertEqual(q.pop(), (11,'f'))
   unit.assertEqual(q.pop(), (11,'g'))
-  unit.assertEqual(q.earliest_timestamp(), INFINITY)
+  unit.assertEqual(q.earliest_timestamp(), None)
 
 def add_remove(q, unit):
   class X:

+ 33 - 23
src/sccd/runtime/xml_loader.py

@@ -34,10 +34,13 @@ class EventNamespace:
 # Some types immitating the types that are produced by the compiler
 @dataclass
 class Statechart:
-  _class: Any
   root: State
-  states: Dict[str, State]
-  semantics: SemanticConfiguration
+  states: Dict[str, State] # mapping from state "full name" (e.g. "/parallel/ortho1/a") to state
+  state_list: List[State] # depth-first order
+  transition_list: List[Transition] # source state depth-first order, then document order
+
+  semantics: SemanticConfiguration = SemanticConfiguration()
+  _class: Any = None
 
 @dataclass
 class Class:
@@ -72,19 +75,10 @@ def load_model(src_file) -> Tuple[Model, Optional[Test]]:
     default = c.get("default", "")
 
     scxml_node = c.find("scxml", root.nsmap)
-    root_state, states = load_tree(scxml_node, model.event_namespace)
-
-    # Semantics - We use reflection to find the xml attribute names and values
-    semantics = SemanticConfiguration()
-    for aspect in dataclasses.fields(SemanticConfiguration):
-      key = scxml_node.get(aspect.name)
-      if key is not None:
-        value = aspect.type[key.upper()]
-        setattr(semantics, aspect.name, value)
+    statechart = load_statechart(scxml_node, model.event_namespace)
 
-    _class = Class(class_name, None)
-    statechart = Statechart(_class=_class, root=root_state, states=states, semantics=semantics)
-    _class.statechart = statechart
+    _class = Class(class_name, statechart)
+    statechart._class = _class
 
     model.classes[class_name] = lambda: _class
     if default or len(classes) == 1:
@@ -127,10 +121,7 @@ def load_model(src_file) -> Tuple[Model, Optional[Test]]:
 
   return (model, test)
 
-def load_tree(scxml_node, event_namespace: EventNamespace) -> Tuple[State, Dict[str, State]]:
-
-  states: Dict[str, State] = {}
-  transitions: List[Tuple[Any, State]] = [] # List of (<transition>, State) tuples
+def load_statechart(scxml_node, event_namespace: EventNamespace) -> Statechart:
 
   def load_action(action_node) -> Optional[Action]:
     tag = ET.QName(action_node).localname
@@ -148,6 +139,7 @@ def load_tree(scxml_node, event_namespace: EventNamespace) -> Tuple[State, Dict[
   def load_actions(parent_node) -> List[Action]:
     return list(filter(lambda x: x is not None, map(lambda child: load_action(child), parent_node)))
 
+  transitions: List[Tuple[Any, State]] = [] # List of (<transition>, State) tuples
 
   # Recursively create state hierarchy from XML node
   # Adding <transition> elements to the 'transitions' list as a side effect
@@ -175,7 +167,6 @@ def load_tree(scxml_node, event_namespace: EventNamespace) -> Tuple[State, Dict[
           state.addChild(child)
           if child.short_name == initial:
             state.default_state = child
-
     if not initial and len(state.children) == 1:
         state.default_state = state.children[0]
 
@@ -194,9 +185,8 @@ def load_tree(scxml_node, event_namespace: EventNamespace) -> Tuple[State, Dict[
 
     return state
 
-  # First build a state tree
+  # Get tree from XML
   root = build_tree(scxml_node)
-  root.init_tree(0, "", states)
 
   # Add transitions
   next_after_id = 0
@@ -247,7 +237,27 @@ def load_tree(scxml_node, event_namespace: EventNamespace) -> Tuple[State, Dict[
       transition.setGuard(cond_expr)
     source.addTransition(transition)
 
-  return (root, states)
+  # Calculate stuff like list of ancestors, descendants, etc.
+  # Also get depth-first ordered lists of states and transitions (by source)
+  states: Dict[str, State] = {}
+  state_list: List[State] = []
+  transition_list: List[Transition] = []
+  root.init_tree(0, "", states, state_list, transition_list)
+
+  print(transition_list)
+
+  for t in transition_list:
+    t.optimize()
+
+  # Semantics - We use reflection to find the xml attribute names and values
+  semantics = SemanticConfiguration()
+  for aspect in dataclasses.fields(SemanticConfiguration):
+    key = scxml_node.get(aspect.name)
+    if key is not None:
+      value = aspect.type[key.upper()]
+      setattr(semantics, aspect.name, value)
+
+  return Statechart(root=root, states=states, state_list=state_list, transition_list=transition_list, semantics=semantics)
 
 class ParseError(Exception):
   def __init__(self, msg):

+ 0 - 3
test/test.py

@@ -1,12 +1,9 @@
-import os
 import unittest
 import argparse
 import threading
 import queue
-from sccd.runtime.infinity import INFINITY
 from sccd.runtime.event import Event
 from sccd.runtime.controller import Controller
-from lib.builder import Builder
 from lib.loader import Loader
 from lib.os_tools import *