浏览代码

Split 'statechart syntax' classes (basically just data classes describing the statechart model: State, Transition, ...) from 'statechart instance' (describing an instance of a statechart, with a set of current states, execution logic, etc.)

Joeri Exelmans 5 年之前
父节点
当前提交
6327a97aa9

+ 3 - 2
src/sccd/compiler/generic_generator.py

@@ -44,8 +44,9 @@ class GenericGenerator(Visitor):
 
         self.writer.addMultiLineComment(header)
         self.writer.addVSpace()
-        self.writer.addInclude(([GLC.RuntimeModuleIdentifier(), "statecharts_core"]))
         self.writer.addInclude(([GLC.RuntimeModuleIdentifier(), "event"]))
+        self.writer.addInclude(([GLC.RuntimeModuleIdentifier(), "statechart_syntax"]))
+        self.writer.addInclude(([GLC.RuntimeModuleIdentifier(), "statechart_instance"]))
         if class_diagram.top.strip():
             self.writer.addRawCode(class_diagram.top)
         self.writer.addVSpace()
@@ -256,7 +257,7 @@ class GenericGenerator(Visitor):
             GLC.FunctionCall(
                 GLC.Property(
                     GLC.SelfProperty("root"),
-                    "fixTree"
+                    "optimize"
                 )
             )
         )

+ 1 - 9
src/sccd/compiler/sccd_constructs.py

@@ -503,7 +503,6 @@ class StateChartNode(Visitable):
                 self.has_timers = True
         
         #TODO: Remove this...
-        self.optimizeTransitions()
         self.generateChildren(xml_element)    
         self.calculateDefaults(xml_element)
             
@@ -555,14 +554,7 @@ class StateChartNode(Visitable):
             self.exit_action = ExitAction(self, on_exits[0])    
         else :
             self.exit_action = ExitAction(self)
-            
-    def optimizeTransitions(self):
-        """If a transition with no trigger and no guard is found then it is considered as the only transition."""
-        try:
-            self.transitions = [next(t for t in self.transitions if (t.isUCTransition() and not t.hasGuard()))]
-        except StopIteration:
-            pass
-    
+
     def generateChildren(self, xml):
         children_names = []
         for child_xml in list(xml) :

+ 30 - 32
src/sccd/runtime/controller.py

@@ -39,17 +39,13 @@ class Controller:
             # potentially responding to the event
             self.queue.add(self.simulated_time+time_offset, Controller.EventQueueEntry(event, self.object_manager.instances))
 
-    # The following 2 methods are the basis of any kind of event loop,
-    # regardless of the platform ()
-
     # Get timestamp of next entry in event queue
     def next_wakeup(self) -> Optional[Timestamp]:
         return self.queue.earliest_timestamp()
 
-    # Run until given timestamp.
-    # Simulation continues until there are no more due events wrt timestamp and until all instances are stable.
-    # Output generated while running is written to 'pipe' so it can be heard by another thread.
-    def run_until(self, now: Timestamp, pipe: queue.Queue):
+    # Run until the event queue has no more due events wrt given timestamp and until all instances are stable.
+    # If no timestamp is given (now = None), run until event queue is empty.
+    def run_until(self, now: Optional[Timestamp], pipe: queue.Queue):
 
         unstable: List[Instance] = []
 
@@ -68,28 +64,24 @@ class Controller:
                 pipe.put(pipe_events, block=True, timeout=None)
 
         # Helper. Let all unstable instances execute big steps until they are stable
-        def run_unstable():
-            had_unstable = False
+        def stabilize():
             while unstable:
-                had_unstable = True
                 for i in reversed(range(len(unstable))):
                     instance = unstable[i]
                     stable, output = instance.big_step(self.simulated_time, [])
                     process_big_step_output(output)
                     if stable:
                         del unstable[i]
-            if had_unstable:
-                print_debug("all instances stabilized.")
-                pass
+            else:
+                return
+            print_debug("all instances stabilized.")
 
-        if now < self.simulated_time:
-            raise Exception("Simulated time can only increase!")
 
         if not self.initialized:
             self.initialized = True
             # first run...
             # initialize the object manager, in turn initializing our default class
-            # and add the generated events to the queue
+            # and adding the generated events to the queue
             for i in self.object_manager.instances:
                 stable, events = i.initialize(self.simulated_time)
                 process_big_step_output(events)
@@ -97,21 +89,27 @@ class Controller:
                     unstable.append(i)
 
         # Actual "event loop"
-        for timestamp, entry in self.queue.due(now):
-            # check if there's a time leap
-            if timestamp is not self.simulated_time:
-                # before every "time leap", continue to run instances until they are stable.
-                run_unstable()
-                # make time leap
-                self.simulated_time = timestamp
-            # run all instances for whom there are events
-            for instance in entry.targets:
-                stable, output = instance.big_step(timestamp, [entry.event])
-                process_big_step_output(output)
-                if not stable:
-                    unstable.append(instance)
-
-        # continue to run instances until all are stable
-        run_unstable()
+        # TODO: What is are the right semantics for this loop?
+        # Should we stabilize every object after it has made a big step?
+        # Should we only stabilize when there are no more events?
+        # Should we never stabilize?
+        # Should this be a semantic option?
+        while unstable or self.queue.is_due(now):
+            # 1. Handle events
+            for timestamp, entry in self.queue.due(now):
+                # check if there's a time leap
+                if timestamp is not self.simulated_time:
+                    # before every "time leap", continue to run instances until they are stable.
+                    stabilize()
+                    # make time leap
+                    self.simulated_time = timestamp
+                # run all instances for whom there are events
+                for instance in entry.targets:
+                    stable, output = instance.big_step(timestamp, [entry.event])
+                    process_big_step_output(output)
+                    if not stable:
+                        unstable.append(instance)
+            # 2. No more due events -> stabilize
+            stabilize()
 
         self.simulated_time = now

+ 5 - 2
src/sccd/runtime/event_queue.py

@@ -51,10 +51,13 @@ class EventQueue(Generic[Item]):
             if item[2] not in self.removed:
                 return (timestamp, item[2])
 
+    def is_due(self, timestamp: Optional[Timestamp]) -> bool:
+        return len(self.queue) and (timestamp == None or self.queue[0][0] <= timestamp)
+
     # Safe to call on empty queue
     # Safe to call other methods on the queue while the returned generator exists
-    def due(self, timestamp: Timestamp) -> Generator[Tuple[Timestamp, Item], None, None]:
-        while len(self.queue) and self.queue[0][0] <= timestamp:
+    def due(self, timestamp: Optional[Timestamp]) -> Generator[Tuple[Timestamp, Item], None, None]:
+        while self.is_due(timestamp):
             yield self.pop()
 
 # Alternative implementation: A heapq with unique entries for each timestamp, and a deque with items for each timestamp.

+ 17 - 44
src/sccd/runtime/object_manager.py

@@ -3,34 +3,7 @@ import abc
 from typing import List, Tuple
 from sccd.runtime.event import Instance, Event, OutputEvent, InstancesTarget
 from sccd.runtime.event_queue import Timestamp
-from sccd.runtime.statecharts_core import StatechartInstance
-
-class RuntimeException(Exception):
-    """
-    Base class for runtime exceptions.
-    """
-    def __init__(self, message):
-        self.message = message
-    def __str__(self):
-        return repr(self.message)
-
-class AssociationException(RuntimeException):
-    """
-    Exception class thrown when an error occurs in a CRUD operation on associations.
-    """
-    pass
-
-class AssociationReferenceException(RuntimeException):
-    """
-    Exception class thrown when an error occurs when resolving an association reference.
-    """
-    pass
-
-class ParameterException(RuntimeException):
-    """
-    Exception class thrown when an error occurs when passing parameters.
-    """
-    pass
+from sccd.runtime.statechart_instance import StatechartInstance
 
 # TODO: Clean this mess up. Look at all object management operations and see how they can be improved.
 class ObjectManager(Instance):
@@ -73,22 +46,22 @@ class ObjectManager(Instance):
                 pass
         return True, output
 
-    def _assoc_ref(self, input_string) -> List[Tuple[str,int]]:
-        if len(input_string) == 0:
-            raise AssociationReferenceException("Empty association reference.")
-        path_string =  input_string.split("/")
-        result = []
-        for piece in path_string:
-            match = ObjectManager._regex_pattern.match(piece)
-            if match:
-                name = match.group(1)
-                index = match.group(2)
-                if index is None:
-                    index = -1
-                result.append((name,int(index)))
-            else:
-                raise AssociationReferenceException("Invalid entry in association reference. Input string: " + input_string)
-        return result
+    # def _assoc_ref(self, input_string) -> List[Tuple[str,int]]:
+    #     if len(input_string) == 0:
+    #         raise AssociationReferenceException("Empty association reference.")
+    #     path_string =  input_string.split("/")
+    #     result = []
+    #     for piece in path_string:
+    #         match = ObjectManager._regex_pattern.match(piece)
+    #         if match:
+    #             name = match.group(1)
+    #             index = match.group(2)
+    #             if index is None:
+    #                 index = -1
+    #             result.append((name,int(index)))
+    #         else:
+    #             raise AssociationReferenceException("Invalid entry in association reference. Input string: " + input_string)
+    #     return result
             
     def _handle_broadcast(self, timestamp, parameters) -> OutputEvent:
         if len(parameters) != 2:

+ 94 - 279
src/sccd/runtime/statecharts_core.py

@@ -9,67 +9,13 @@ from typing import List, Tuple
 from enum import Enum
 from sccd.runtime.infinity import INFINITY
 from sccd.runtime.event_queue import Timestamp
+from sccd.runtime.statechart_syntax import *
 from sccd.runtime.event import Event, OutputEvent, Instance, InstancesTarget
 from sccd.runtime.debug import print_debug
 from collections import Counter
 
 ELSE_GUARD = "ELSE_GUARD"
 
-
-class Association(object):
-    """
-    Class representing an SCCD association.
-    """
-    def __init__(self, to_class, min_card, max_card):
-        """
-        Constructor
-        
-       :param to_class: the name of the target class
-       :param min_card: the minimal cardinality
-       :param max_card: the maximal cardinality
-        """
-        self.to_class = to_class
-        self.min_card = min_card
-        self.max_card = max_card
-        self.instances = {} # maps index (as string) to instance
-        self.instances_to_ids = {}
-        self.size = 0
-        self.next_id = 0        
-
-    def allowedToAdd(self):
-        return self.max_card == -1 or self.size < self.max_card
-        
-    def allowedToRemove(self):
-        return self.min_card == -1 or self.size > self.min_card
-        
-    def addInstance(self, instance):
-        if self.allowedToAdd():
-            new_id = self.next_id
-            self.next_id += 1
-            self.instances[new_id] = instance
-            self.instances_to_ids[instance] = new_id
-            self.size += 1
-            return new_id
-        else:
-            raise AssociationException("Not allowed to add the instance to the association.")
-        
-    def removeInstance(self, instance):
-        if self.allowedToRemove():
-            index = self.instances_to_ids[instance]
-            del self.instances[index]
-            del self.instances_to_ids[instance]
-            self.size -= 1
-            return index
-        else:
-            raise AssociationException("Not allowed to remove the instance from the association.")
-        
-    def getInstance(self, index):
-        try:
-            return self.instances[index]
-        except IndexError:
-            raise AssociationException("Invalid index for fetching instance(s) from association.")
-
-
 class StatechartSemantics:
     # Big Step Maximality
     TakeOne = 0
@@ -104,214 +50,8 @@ class StatechartSemantics:
         self.input_event_lifeline = self.FirstComboStep
         self.priority = self.SourceParent
         self.concurrency = self.Single
-
-class State:
-    def __init__(self, state_id, name, obj):
-        self.state_id = state_id
-        self.name = name
-        self.obj = obj
-        
-        self.ancestors = []
-        self.descendants = []
-        self.descendant_bitmap = 0
-        self.children = []
-        self.parent = None
-        self.enter = None
-        self.exit = None
-        self.default_state = None
-        self.transitions = []
-        self.history = []
-        self.has_eventless_transitions = False
-        
-    def getEffectiveTargetStates(self, instance):
-        targets = [self]
-        if self.default_state:
-            targets.extend(self.default_state.getEffectiveTargetStates(instance))
-        return targets
-        
-    def fixTree(self):
-        for c in self.children:
-            if isinstance(c, HistoryState):
-                self.history.append(c)
-            c.parent = self
-            c.ancestors.append(self)
-            c.ancestors.extend(self.ancestors)
-            c.fixTree()
-        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
-            
-    def addChild(self, child):
-        self.children.append(child)
-    
-    def addTransition(self, transition):
-        self.transitions.append(transition)
-        
-    def setEnter(self, enter):
-        self.enter = enter
-        
-    def setExit(self, exit):
-        self.exit = exit
-                    
-    def __repr__(self):
-        return "State(%s)" % (self.state_id)
-        
-class HistoryState(State):
-    def __init__(self, state_id, name, obj):
-        State.__init__(self, state_id, name, obj)
-        
-class ShallowHistoryState(HistoryState):
-    def __init__(self, state_id, name, obj):
-        HistoryState.__init__(self, state_id, name, obj)
-        
-    def getEffectiveTargetStates(self, instance):
-        if self.state_id in instance.history_values:
-            targets = []
-            for hv in instance.history_values[self.state_id]:
-                targets.extend(hv.getEffectiveTargetStates(instance))
-            return targets
-        else:
-            # TODO: is it correct that in this case, the parent itself is also entered?
-            return self.parent.getEffectiveTargetStates(instance)
-        
-class DeepHistoryState(HistoryState):
-    def __init__(self, state_id, name, obj):
-        HistoryState.__init__(self, state_id, name, obj)
-        
-    def getEffectiveTargetStates(self, instance):
-        if self.state_id in instance.history_values:
-            return instance.history_values[self.state_id]
-        else:
-            # TODO: is it correct that in this case, the parent itself is also entered?
-            return self.parent.getEffectiveTargetStates(instance)
-        
-class ParallelState(State):
-    def __init__(self, state_id, name, obj):
-        State.__init__(self, state_id, name, obj)
-        
-    def getEffectiveTargetStates(self, instance):
-        targets = [self]
-        for c in self.children:
-            if not isinstance(c, HistoryState):
-                targets.extend(c.getEffectiveTargetStates(instance))
-        return targets
-    
-class Transition:
-    def __init__(self, source, targets):
-        self.guard = None
-        self.action = None
-        self.trigger = None
-        self.source = source
-        self.targets = targets
-        self.enabled_event = None # the event that enabled this transition
-        self.optimize()
-    
-    def isEnabled(self, instance, events, enabled_transitions):
-        if self.trigger is None:
-            self.enabled_event = None
-            return (self.guard is None) or (self.guard == ELSE_GUARD and not enabled_transitions) or self.guard(instance, [])
-        else:
-            for event in events:
-                if (self.trigger.name == event.name and (not self.trigger.port or self.trigger.port == event.port)) and ((self.guard is None) or (self.guard == ELSE_GUARD and not enabled_transitions) or self.guard(event.parameters)):
-                    self.enabled_event = event
-                    return True
     
-    # @profile
-    def fire(self, instance):
-
-        def __exitSet():
-            return [s for s in reversed(self.lca.descendants) if (s in instance.configuration)]
-        
-        def __enterSet(targets):
-            target = targets[0]
-            for a in reversed(target.ancestors):
-                if a in self.source.ancestors:
-                    continue
-                else:
-                    yield a
-            for target in targets:
-                yield target
-
-        def __getEffectiveTargetStates():
-            targets = []
-            for target in self.targets:
-                for e_t in target.getEffectiveTargetStates(instance):
-                    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
-                instance.history_values[h.state_id] = list(filter(f, instance.configuration))
-        print_debug('')
-        print_debug(termcolor.colored('transition %s:  %s 🡪 %s'%(instance.model._class.name, self.source.name, self.targets[0].name), 'green'))
-        for s in exit_set:
-            print_debug(termcolor.colored('  EXIT %s' % s.name, 'green'))
-            instance.eventless_states -= s.has_eventless_transitions
-            # execute exit action(s)
-            if s.exit:
-                s.exit(instance)
-            instance.configuration_bitmap &= ~2**s.state_id
-        
-        # combo state changed area
-        instance._combo_step.changed_bitmap |= 2**self.lca.state_id
-        instance._combo_step.changed_bitmap |= self.lca.descendant_bitmap
         
-        # execute transition action(s)
-        if self.action:
-            self.action(instance, self.enabled_event.parameters if self.enabled_event else [])
-            
-        # enter states...
-        targets = __getEffectiveTargetStates()
-        enter_set = __enterSet(targets)
-        for s in enter_set:
-            print_debug(termcolor.colored('  ENTER %s' % s.name, 'green'))
-            instance.eventless_states += s.has_eventless_transitions
-            instance.configuration_bitmap |= 2**s.state_id
-            # execute enter action(s)
-            if s.enter:
-                s.enter(instance)
-        try:
-            instance.configuration = instance.config_mem[instance.configuration_bitmap]
-        except:
-            instance.configuration = instance.config_mem[instance.configuration_bitmap] = sorted([s for s in list(instance.model.states.values()) if 2**s.state_id & instance.configuration_bitmap], key=lambda s: s.state_id)
-        self.enabled_event = None
-                
-    def setGuard(self, guard):
-        self.guard = guard
-        
-    def setAction(self, action):
-        self.action = action
-    
-    def setTrigger(self, trigger):
-        self.trigger = trigger
-        if self.trigger is None:
-            self.source.has_eventless_transitions = True
-        
-    def optimize(self):
-        # the least-common ancestor can be computed statically
-        if self.source in self.targets[0].ancestors:
-            self.lca = self.source
-        else:
-            self.lca = self.source.parent
-            target = self.targets[0]
-            if self.source.parent != target.parent: # external
-                for a in self.source.ancestors:
-                    if a in target.ancestors:
-                        self.lca = a
-                        break
-                    
-    def __repr__(self):
-        return "Transition(%s, %s)" % (self.source, self.targets[0])
-
 class StatechartInstance(Instance):
     def __init__(self, model, object_manager):
         self.model = model
@@ -327,7 +67,7 @@ class StatechartInstance(Instance):
         self.transition_mem = {}
         # 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 = {}
 
         self._big_step = BigStepState()
@@ -411,29 +151,93 @@ class StatechartInstance(Instance):
             if self.model.semantics.concurrency == StatechartSemantics.Single:
                 candidate = conflicting[0]
                 if self.model.semantics.priority == StatechartSemantics.SourceParent:
-                    candidate[-1].fire(self)
+                    self._fire_transition(candidate[-1])
+                    # candidate[-1].fire(self)
                 else:
-                    candidate[0].fire(self)
+                    self._fire_transition(candidate[0])
+                    # candidate[0].fire(self)
             elif self.model.semantics.concurrency == StatechartSemantics.Many:
                 pass # TODO: implement
             self._small_step.has_stepped = True
         return self._small_step.has_stepped
 
-    def getChildren(self, link_name):
-        traversal_list = self.controller.object_manager.processAssociationReference(link_name)
-        return [i["instance"] for i in self.controller.object_manager.getInstances(self, traversal_list)]
+    # @profile
+    def _fire_transition(self, t: Transition):
 
-    def getSingleChild(self, link_name):
-        return self.getChildren(link_nameself.controller)[0] # assume this will return a single child...
+        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 processBigStepOutput(self):
-        # print("processBigStepOutput:", self._big_step.output_events_port)
-        self.controller.outputBigStep(self._big_step.output_events_port)
-        for e in self._big_step.output_events_om:
-            self.controller.object_manager.addEvent(e)
+        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 🡪 %s'%(self.model._class.name, 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)
+            if s.exit:
+                s.exit(self)
+            self.configuration_bitmap &= ~2**s.state_id
+        
+        # combo state changed area
+        self._combo_step.changed_bitmap |= 2**t.lca.state_id
+        self._combo_step.changed_bitmap |= t.lca.descendant_bitmap
+        
+        # execute transition action(s)
+        if t.action:
+            t.action(self, t.enabled_event.parameters if t.enabled_event else [])
             
-    def inState(self, state_strings):
+        # 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 |= 2**s.state_id
+            # execute enter action(s)
+            if s.enter:
+                s.enter(self)
+        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)
+        t.enabled_event = None
+        
+
+    # def getChildren(self, link_name):
+    #     traversal_list = self.controller.object_manager.processAssociationReference(link_name)
+    #     return [i["instance"] for i in self.controller.object_manager.getInstances(self, traversal_list)]
+
+    # def getSingleChild(self, link_name):
+    #     return self.getChildren(link_nameself.controller)[0] # assume this will return a single child...
+
+    # Return whether the current configuration includes ALL the states given.
+    def inState(self, state_strings: List[str]) -> bool:
         state_ids = 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) == self.configuration_bitmap
         if in_state:
@@ -444,7 +248,7 @@ class StatechartInstance(Instance):
 
     # generate transition candidates for current small step
     # @profile
-    def _transition_candidates(self):
+    def _transition_candidates(self) -> List[Transition]:
         changed_bitmap = self._combo_step.changed_bitmap
         key = (self.configuration_bitmap, changed_bitmap)
         try:
@@ -461,10 +265,21 @@ class StatechartInstance(Instance):
             enabled_events += self._big_step.input_events
         enabled_transitions = []
         for t in transitions:
-            if t.isEnabled(self, enabled_events, enabled_transitions):
+            if self._is_transition_enabled(t, enabled_events, enabled_transitions):
+            # if t.isEnabled(self, enabled_events, enabled_transitions):
                 enabled_transitions.append(t)
         return enabled_transitions
 
+    def _is_transition_enabled(self, t, events, enabled_transitions) -> 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(self, [])
+        else:
+            for event in events:
+                if (t.trigger.name == event.name 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(event.parameters)):
+                    t.enabled_event = event
+                    return True
+
     def _raiseInternalEvent(self, event):
         if self.model.semantics.internal_event_lifeline == StatechartSemantics.NextSmallStep:
             self._small_step.addNextEvent(event)
@@ -529,11 +344,11 @@ class SmallStepState(object):
 
 class Maximality(Enum):
     TAKE_ONE = 0
-    TAKE_MANY = 2
+    TAKE_MANY = 1
 
 class Round:
     def __init__(self, maximality: Maximality):
-        self.changed_bitmap
+        self.changed_bitmap: int
         self.current_events: List[Event] = []
         self.next_events: List[Event] = []
         self.has_stepped: bool = True

+ 131 - 0
src/sccd/runtime/statechart_syntax.py

@@ -0,0 +1,131 @@
+class State:
+    def __init__(self, state_id, name, obj):
+        self.state_id = state_id
+        self.name = name
+        self.obj = obj
+        
+        self.parent = None
+        self.children = []
+        self.default_state = None
+        self.transitions = []
+        self.enter = None
+        self.exit = None
+        self.history = [] # list of history states that are children
+
+        # optimization stuff
+        self.ancestors = []
+        self.descendants = []
+        self.descendant_bitmap = 0
+        self.has_eventless_transitions = False
+
+    def getEffectiveTargetStates(self, instance):
+        targets = [self]
+        if self.default_state:
+            targets.extend(self.default_state.getEffectiveTargetStates(instance))
+        return targets
+        
+    def optimize(self):
+        for c in self.children:
+            if isinstance(c, HistoryState):
+                self.history.append(c)
+            c.parent = self
+            c.ancestors.append(self)
+            c.ancestors.extend(self.ancestors)
+            c.optimize()
+        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
+            
+    def addChild(self, child):
+        self.children.append(child)
+    
+    def addTransition(self, transition):
+        self.transitions.append(transition)
+        
+    def setEnter(self, enter):
+        self.enter = enter
+        
+    def setExit(self, exit):
+        self.exit = exit
+                    
+    def __repr__(self):
+        return "State(%s)" % (self.state_id)
+        
+class HistoryState(State):
+    def __init__(self, state_id, name, obj):
+        State.__init__(self, state_id, name, obj)
+        
+class ShallowHistoryState(HistoryState):
+    def __init__(self, state_id, name, obj):
+        HistoryState.__init__(self, state_id, name, obj)
+        
+    def getEffectiveTargetStates(self, instance):
+        if self.state_id in instance.history_values:
+            targets = []
+            for hv in instance.history_values[self.state_id]:
+                targets.extend(hv.getEffectiveTargetStates(instance))
+            return targets
+        else:
+            # TODO: is it correct that in this case, the parent itself is also entered?
+            return self.parent.getEffectiveTargetStates(instance)
+        
+class DeepHistoryState(HistoryState):
+    def __init__(self, state_id, name, obj):
+        HistoryState.__init__(self, state_id, name, obj)
+        
+    def getEffectiveTargetStates(self, instance):
+        if self.state_id in instance.history_values:
+            return instance.history_values[self.state_id]
+        else:
+            # TODO: is it correct that in this case, the parent itself is also entered?
+            return self.parent.getEffectiveTargetStates(instance)
+        
+class ParallelState(State):
+    def __init__(self, state_id, name, obj):
+        State.__init__(self, state_id, name, obj)
+        
+    def getEffectiveTargetStates(self, instance):
+        targets = [self]
+        for c in self.children:
+            if not isinstance(c, HistoryState):
+                targets.extend(c.getEffectiveTargetStates(instance))
+        return targets
+
+class Transition:
+    def __init__(self, source, targets):
+        self.guard = None
+        self.action = None
+        self.trigger = None
+        self.source = source
+        self.targets = targets
+        self.enabled_event = None # the event that enabled this transition
+        self.optimize()
+                    
+    def setGuard(self, guard):
+        self.guard = guard
+        
+    def setAction(self, action):
+        self.action = action
+    
+    def setTrigger(self, trigger):
+        self.trigger = trigger
+        if self.trigger is None:
+            self.source.has_eventless_transitions = True
+        
+    def optimize(self):
+        # the least-common ancestor can be computed statically
+        if self.source in self.targets[0].ancestors:
+            self.lca = self.source
+        else:
+            self.lca = self.source.parent
+            target = self.targets[0]
+            if self.source.parent != target.parent: # external
+                for a in self.source.ancestors:
+                    if a in target.ancestors:
+                        self.lca = a
+                        break
+                    
+    def __repr__(self):
+        return "Transition(%s, %s)" % (self.source, self.targets[0])

+ 5 - 3
test/render.py

@@ -5,7 +5,7 @@ import multiprocessing
 from lib.os_tools import *
 from lib.builder import *
 from sccd.compiler.utils import FormattedWriter
-from sccd.runtime.statecharts_core import *
+from sccd.runtime.statechart_syntax import *
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(
@@ -33,6 +33,7 @@ if __name__ == '__main__':
       print("No input files specified.")      
       print()
       parser.print_usage()
+      exit()
 
 
     def process(src):
@@ -129,6 +130,7 @@ if __name__ == '__main__':
         if not args.keep_smcat:
           os.remove(smcat_target)
 
-    with multiprocessing.Pool(processes=args.pool_size) as pool:
-      print("Created a pool of %d processes."%args.pool_size)
+    pool_size = min(args.pool_size, len(srcs))
+    with multiprocessing.Pool(processes=pool_size) as pool:
+      print("Created a pool of %d processes."%pool_size)
       pool.map(process, srcs)

+ 1 - 1
test/test.py

@@ -38,7 +38,7 @@ class PyTestCase(unittest.TestCase):
             try:
                 # Run as-fast-as-possible, always advancing time to the next item in event queue, no sleeping.
                 # The call returns when the event queue is empty and therefore the simulation is finished.
-                controller.run_until(INFINITY, pipe)
+                controller.run_until(None, pipe)
             except Exception as e:
                 pipe.put(e, block=True, timeout=None)
                 return