Browse Source

Re-implemented priority semantics: Based on priority semantics, a directed graph of orderings between transitions is explicitly built, checked for cycles, and from it, a total ordering of transitions is constructed. Transitions between whom no ordering exists are only allowed if they cannot be simultaneously enabled wrt their source states. It is now possible to switch off explicit ordering of orthogonal regions and explicit ordering of transitions with the same source state (but it's ON by default :)

Joeri Exelmans 4 years ago
parent
commit
00becf8a0d

+ 2 - 2
examples/semantics/run.py

@@ -76,8 +76,8 @@ if __name__ == "__main__":
     "/P/MemoryProtocol/MemoryProtocol/ComboStep": ("enabledness_memory_protocol", MemoryProtocol.COMBO_STEP),
     "/P/MemoryProtocol/MemoryProtocol/SmallStep": ("enabledness_memory_protocol", MemoryProtocol.SMALL_STEP),
 
-    "/P/Priority/SourceParent": ("priority", Priority.SOURCE_PARENT),
-    "/P/Priority/SourceChild": ("priority", Priority.SOURCE_CHILD),
+    "/P/Priority/SourceParent": ("priority", HierarchicalPriority.SOURCE_PARENT),
+    "/P/Priority/SourceChild": ("priority", HierarchicalPriority.SOURCE_CHILD),
   }
 
   state_id_to_semantics = {

+ 7 - 7
src/sccd/statechart/cmd/check_model.py

@@ -2,6 +2,7 @@ import argparse
 import sys
 import termcolor
 from sccd.statechart.parser.xml import *
+from sccd.statechart.static.priority import *
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(
@@ -11,12 +12,11 @@ if __name__ == "__main__":
 
     src = args.path
 
-    try:
-      path = os.path.dirname(src)
-      rules = [("statechart", statechart_parser_rules(Globals(), path, load_external=True))]
+    path = os.path.dirname(src)
+    rules = [("statechart", statechart_parser_rules(Globals(), path, load_external=True))]
 
-      statechart = parse(src, rules, decorate_exceptions=(ModelError,))
+    statechart = parse_f(src, rules)
 
-      assert isinstance(statechart, Statechart)
-    except Exception as e:
-      print(e)
+    assert isinstance(statechart, Statechart)
+
+    print("Model is OK.")

+ 22 - 8
src/sccd/statechart/dynamic/statechart_instance.py

@@ -4,11 +4,11 @@ from typing import List, Tuple, Iterable
 from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
 from sccd.statechart.static.statechart import *
+import sccd.statechart.static.priority as priority
 from sccd.statechart.dynamic.builtin_scope import *
 from sccd.statechart.dynamic.round import *
 from sccd.statechart.dynamic.statechart_execution import *
 from sccd.statechart.dynamic.memory_snapshot import *
-
 # Interface for all instances and also the Object Manager
 class Instance(ABC):
     @abstractmethod
@@ -36,14 +36,28 @@ class StatechartInstance(Instance):
         if semantics.has_multiple_variants():
             raise Exception("Cannot execute model with multiple semantics.")
 
-        priority_function = {
-            Priority.SOURCE_PARENT: priority_source_parent,
-            Priority.SOURCE_CHILD: priority_source_child,
-            Priority.ARENA_PARENT: priority_arena_parent,
-            Priority.ARENA_CHILD: priority_arena_child,
-        }[semantics.priority]
+        # Priority semantics
+
+        with timer.Context("priority"):
+            hierarchical = {
+                HierarchicalPriority.SOURCE_PARENT: priority.source_parent,
+                HierarchicalPriority.SOURCE_CHILD: priority.source_child,
+                HierarchicalPriority.ARENA_PARENT: priority.arena_parent,
+                HierarchicalPriority.ARENA_CHILD: priority.arena_child,
+            }[semantics.priority]
+
+            same_state = {
+                SameSourcePriority.NONE: priority.none,
+                SameSourcePriority.EXPLICIT: priority.explicit,
+            }[semantics.same_source_priority]
+
+            orthogonal = {
+                OrthogonalPriority.NONE: priority.none,
+                OrthogonalPriority.EXPLICIT: priority.ooss_explicit_ordering,
+            }[semantics.orthogonal_priority]
 
-        priority_ordered_transitions = priority_function(statechart.tree)
+            priority_ordered_transitions = priority.get_total_ordering(statechart.tree,
+                hierarchical, same_state, orthogonal)
 
         # strategy = CurrentConfigStrategy(priority_ordered_transitions)
         strategy = EnabledEventsStrategy(priority_ordered_transitions, statechart)

+ 151 - 0
src/sccd/statechart/static/priority.py

@@ -0,0 +1,151 @@
+from sccd.statechart.static.tree import StateTree, Transition, State, ParallelState
+from sccd.util.graph import strongly_connected_components
+from typing import *
+from sccd.util.visit_tree import visit_tree
+from sccd.util.bitmap import *
+import collections
+import itertools
+
+EdgeList = List[Tuple[Transition,Transition]]
+
+# explicit ordering of orthogonal regions
+def ooss_explicit_ordering(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    # get all outgoing transitions of state or one of its descendants
+    def get_transitions(s: State) -> List[Transition]:
+        transitions = []
+        def visit_state(s: State, _=None):
+            transitions.extend(s.transitions)
+        visit_tree(s, lambda s: s.children, before_children=[visit_state])
+        return transitions
+    # create edges between transitions in one region to another
+    def visit_parallel_state(s: State, _=None):
+        if isinstance(s, ParallelState):
+            prev = []
+            # s.children is document order
+            for region in s.children:
+                curr = get_transitions(region)
+                edges.extend(itertools.product(prev, curr))
+                prev = curr
+    visit_tree(tree.root, lambda s: s.children,
+        before_children=[visit_parallel_state])
+    return edges
+
+# explicit ordering of outgoing transitions of the same state
+def explicit(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    def visit_state(s: State, _=None):
+        prev = None
+        # s.transitions is document order
+        for t in s.transitions:
+            if prev is not None:
+                edges.append((prev, t))
+            prev = t
+    visit_tree(tree.root, lambda s: s.children,
+        before_children=[visit_state])
+    return edges
+
+# hierarchical Source-Parent ordering
+def source_parent(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    def visit_state(s: State, parent_transitions: List[Transition] = []) -> List[Transition]:
+        edges.extend(itertools.product(parent_transitions, s.transitions))
+        return s.transitions
+    visit_tree(tree.root, lambda s: s.children, before_children=[visit_state])
+    return edges
+
+# hierarchical Source-Child ordering
+def source_child(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    def visit_state(s: State, children_transitions: List[List[Transition]]) -> List[Transition]:
+        edges.extend(itertools.product(itertools.chain.from_iterable(children_transitions), s.transitions))
+        return s.transitions
+    visit_tree(tree.root, lambda s: s.children, after_children=[visit_state])
+    return edges
+
+# hierarchical Arena-Parent ordering
+def arena_parent(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    partitions = collections.defaultdict(list) # mapping of transition's arena depth to list of transitions
+    for t in tree.transition_list:
+        partitions[t.opt.arena.opt.depth].append(t)
+    ordered_partitions = sorted(partitions.items(), key=lambda tup: tup[0])
+    prev = []
+    for depth, curr in ordered_partitions:
+        edges.extend(itertools.product(prev, curr))
+        prev = curr
+    return edges
+
+# hierarchical Arena-Child ordering
+def arena_child(tree: StateTree) -> EdgeList:
+    edges: EdgeList = []
+    partitions = collections.defaultdict(list) # mapping of transition's arena depth to list of transitions
+    for t in tree.transition_list:
+        partitions[t.opt.arena.opt.depth].append(t)
+    ordered_partitions = sorted(partitions.items(), key=lambda tup: -tup[0])
+    prev = []
+    for depth, curr in ordered_partitions:
+        edges.extend(itertools.product(prev, curr))
+        prev = curr
+    return edges
+
+# no priority
+def none(tree: StateTree) -> EdgeList:
+    return []
+
+# Apply the graph-yielding functions to the given statetree, and merge their results into a single graph
+def get_graph(tree: StateTree, *priorities: Callable[[StateTree], EdgeList]) -> EdgeList:
+    edges = list(itertools.chain.from_iterable(p(tree) for p in priorities))
+    return edges
+
+# Checks whether the 'priorities' given yield a valid ordering of transitions in the statechart.
+# Returns list of all transitions in statechart, ordered by priority (high -> low).
+def get_total_ordering(tree: StateTree, *priorities: Callable[[StateTree], EdgeList]) -> List[Transition]:
+    edges = get_graph(tree, *priorities)
+    scc = strongly_connected_components(edges)
+    if len(scc) != len(tree.transition_list):
+        # Priority graph contains cycles
+        for component in scc:
+            if len(component) > 1:
+                raise Exception("Nondeterminism! Cycle among transition priorities: " + str(component))
+
+    total_ordering = []
+
+    # Raises an exception if the given set of transitions can potentially be enabled simulatenously, wrt. their source states in the state tree.
+    def check_unordered(transitions):
+        pairs = itertools.combinations(transitions, 2)
+        for t1, t2 in pairs:
+            # LCA of their sources
+            lca_id = bm_highest_bit(t1.source.opt.ancestors & t2.source.opt.ancestors)
+            lca = tree.state_list[lca_id]
+            # Transitions are orthogonal to each other (LCA is And-state):
+            if isinstance(lca, ParallelState):
+                raise Exception("Nondeterminism! No priority between orthogonal transitions: " + str(highest_priority))
+            # Their source states are the same state or ancestors of one another:
+            if lca_id == t1.source.opt.state_id or lca_id == t2.source.opt.state_id:
+                raise Exception("Nondeterminism! No priority between ancestral transitions: " + str(highest_priority))
+
+    remaining_transitions = set(tree.transition_list)
+    remaining_edges = edges
+    while len(remaining_edges) > 0:
+        # 1. Find set of highest-priority transitions (= the ones that only have outgoing edges)
+        # Such a set must exist, because we've already assured that are no cycles in the graph.
+        highs = set()
+        lows = set()
+        for high, low in remaining_edges:
+            highs.add(high)
+            lows.add(low)
+        highest_priority = highs - lows
+        # 2. Check if the transitions in this set are allowed to have equal priority.
+        check_unordered(highest_priority) # may raise Exception
+        # 3. All good. Add the transitions in the highest-priority set in any order to the total ordering
+        total_ordering.extend(highest_priority)
+        # 4. Remove the transitions of the highest-priority set from the graph, and repeat.
+        remaining_edges = [(high,low) for high, low in remaining_edges if high not in highest_priority]
+        remaining_transitions -= highest_priority
+
+    # Finally, there may be transitions that occur in the priority graph only as vertices, e.g. in flat statecharts:
+    check_unordered(remaining_transitions)
+    total_ordering.extend(remaining_transitions)
+
+    return total_ordering

+ 126 - 0
src/sccd/statechart/static/semantic_configuration.py

@@ -0,0 +1,126 @@
+from enum import *
+from dataclasses import *
+from typing import *
+import itertools
+
+class SemanticAspect:
+  def __str__(self):
+    # Override default: only print field, not the type (e.g. "TAKE_ONE" instead of "BigStepMaximality.TAKE_ONE")
+    return self.name
+
+  __repr__ = __str__
+
+class Maximality(SemanticAspect, Enum):
+  TAKE_ONE = auto()
+  SYNTACTIC = auto()
+  TAKE_MANY = auto()
+
+  # We define an ordering TAKE_ONE < SYNTACTIC < TAKE_MANY
+
+  def __lt__(self, other):
+    if self == other:
+      return False
+    if other == Maximality.TAKE_MANY:
+      return True
+    if other == Maximality.SYNTACTIC:
+      return self == Maximality.TAKE_ONE
+    return False
+
+  def __le__(self, other):
+    return self == other or self < other
+
+  def __gt__(self, other):
+    return not (self <= other)
+
+  def __ge__(self, other):
+    return not (self < other)
+
+class InternalEventLifeline(SemanticAspect, Enum):
+  QUEUE = auto()
+  NEXT_COMBO_STEP = auto()
+  NEXT_SMALL_STEP = auto()
+
+  REMAINDER = auto()
+  SAME = auto()
+
+class InputEventLifeline(SemanticAspect, Enum):
+  WHOLE = auto()
+  FIRST_COMBO_STEP = auto()
+  FIRST_SMALL_STEP = auto()
+
+class MemoryProtocol(SemanticAspect, Enum):
+  BIG_STEP = auto()
+  COMBO_STEP = auto()
+  SMALL_STEP = auto()
+  # NONE = auto()
+
+class HierarchicalPriority(SemanticAspect, Enum):
+  SOURCE_PARENT = auto()
+  SOURCE_CHILD = auto()
+
+  ARENA_PARENT = auto()
+  ARENA_CHILD = auto()
+
+class OrthogonalPriority(SemanticAspect, Enum):
+  NONE = auto()
+  EXPLICIT = auto()
+
+class SameSourcePriority(SemanticAspect, Enum):
+  NONE = auto()
+  EXPLICIT = auto()
+
+class Concurrency(SemanticAspect, Enum):
+  SINGLE = auto()
+  MANY = auto()
+
+_T = TypeVar('_T', bound=SemanticAspect)
+SemanticChoice = Union[_T, List[_T]]
+
+@dataclass
+class SemanticConfiguration:
+  # All semantic aspects and their default values.
+  # Every field can be set to a list of multiple options, or just a value.
+
+  # The names of the fields of this class are used when parsing semantic options in the XML input format.
+
+  # The following is the default configuration. Changing these values changes SCCD's default semantics:
+  big_step_maximality: SemanticChoice[Maximality] = Maximality.TAKE_MANY
+  combo_step_maximality: SemanticChoice[Maximality] = Maximality.TAKE_ONE
+  internal_event_lifeline: SemanticChoice[InternalEventLifeline] = InternalEventLifeline.NEXT_COMBO_STEP
+  input_event_lifeline: SemanticChoice[InputEventLifeline] = InputEventLifeline.FIRST_COMBO_STEP
+  enabledness_memory_protocol: SemanticChoice[MemoryProtocol] = MemoryProtocol.COMBO_STEP
+  assignment_memory_protocol: SemanticChoice[MemoryProtocol] = MemoryProtocol.COMBO_STEP
+
+  priority: SemanticChoice[HierarchicalPriority] = HierarchicalPriority.SOURCE_PARENT
+  orthogonal_priority: SemanticChoice[OrthogonalPriority] = OrthogonalPriority.EXPLICIT
+  same_source_priority: SemanticChoice[SameSourcePriority] = SameSourcePriority.EXPLICIT
+  
+  concurrency: SemanticChoice[Concurrency] = Concurrency.SINGLE
+
+  def __str__(self):
+    s = ""
+    for f in fields(self):
+      s += "\n  %s: %s" % (f.name, getattr(self, f.name))
+    return s
+
+
+  @classmethod
+  # def get_fields(cls) -> Iterator[Tuple[str, SemanticAspect]]:
+  def get_fields(cls) -> Dict[str, SemanticAspect]:
+    return {f.name: type(f.default) for  f in fields(cls)}
+
+  # Whether multiple options are set for any aspect.
+  def has_multiple_variants(self) -> bool:
+    for f in fields(self):
+      if isinstance(getattr(self, f.name), list):
+        return True
+    return False
+
+  # Get all possible combinations for aspects with multiple options (as a list) set.
+  # Calling has_multiple_variants on resulting objects will return False.
+  def generate_variants(self) -> List['SemanticConfiguration']:
+    my_fields = fields(self)
+    chosen_options = ([item] if not isinstance(item,list) else item for item in (getattr(self, f.name) for f in my_fields))
+    variants = itertools.product(*chosen_options)
+
+    return [SemanticConfiguration(**{f.name: o for f,o in zip(my_fields, variant)}) for variant in variants]

+ 1 - 111
src/sccd/statechart/static/statechart.py

@@ -1,116 +1,6 @@
-from enum import *
-from dataclasses import *
-from typing import *
-import itertools
 from sccd.statechart.static.tree import *
 from sccd.action_lang.static.scope import *
-
-class SemanticAspect:
-  def __str__(self):
-    # Override default: only print field, not the type (e.g. "TAKE_ONE" instead of "BigStepMaximality.TAKE_ONE")
-    return self.name
-
-  __repr__ = __str__
-
-class Maximality(SemanticAspect, Enum):
-  TAKE_ONE = auto()
-  SYNTACTIC = auto()
-  TAKE_MANY = auto()
-
-  # We define an ordering TAKE_ONE < SYNTACTIC < TAKE_MANY
-
-  def __lt__(self, other):
-    if self == other:
-      return False
-    if other == Maximality.TAKE_MANY:
-      return True
-    if other == Maximality.SYNTACTIC:
-      return self == Maximality.TAKE_ONE
-    return False
-
-  def __le__(self, other):
-    return self == other or self < other
-
-  def __gt__(self, other):
-    return not (self <= other)
-
-  def __ge__(self, other):
-    return not (self < other)
-
-class InternalEventLifeline(SemanticAspect, Enum):
-  QUEUE = auto()
-  NEXT_COMBO_STEP = auto()
-  NEXT_SMALL_STEP = auto()
-
-  REMAINDER = auto()
-  SAME = auto()
-
-class InputEventLifeline(SemanticAspect, Enum):
-  WHOLE = auto()
-  FIRST_COMBO_STEP = auto()
-  FIRST_SMALL_STEP = auto()
-
-class MemoryProtocol(SemanticAspect, Enum):
-  BIG_STEP = auto()
-  COMBO_STEP = auto()
-  SMALL_STEP = auto()
-  # NONE = auto()
-
-class Priority(SemanticAspect, Enum):
-  SOURCE_PARENT = auto()
-  SOURCE_CHILD = auto()
-
-  ARENA_PARENT = auto()
-  ARENA_CHILD = auto()
-
-class Concurrency(SemanticAspect, Enum):
-  SINGLE = auto()
-  MANY = auto()
-
-_T = TypeVar('_T', bound=SemanticAspect)
-SemanticChoice = Union[_T, List[_T]]
-
-@dataclass
-class SemanticConfiguration:
-  # All semantic aspects and their default values.
-  # Every field can be set to a list of multiple options, or just a value.
-
-  # The following is the default configuration. Changing these values changes SCCD's default semantics:
-  big_step_maximality: SemanticChoice[Maximality] = Maximality.TAKE_MANY
-  combo_step_maximality: SemanticChoice[Maximality] = Maximality.TAKE_ONE
-  internal_event_lifeline: SemanticChoice[InternalEventLifeline] = InternalEventLifeline.NEXT_COMBO_STEP
-  input_event_lifeline: SemanticChoice[InputEventLifeline] = InputEventLifeline.FIRST_COMBO_STEP
-  enabledness_memory_protocol: SemanticChoice[MemoryProtocol] = MemoryProtocol.COMBO_STEP
-  assignment_memory_protocol: SemanticChoice[MemoryProtocol] = MemoryProtocol.COMBO_STEP
-  priority: SemanticChoice[Priority] = Priority.SOURCE_PARENT
-  concurrency: SemanticChoice[Concurrency] = Concurrency.SINGLE
-
-  def __str__(self):
-    s = ""
-    for f in fields(self):
-      s += "\n  %s: %s" % (f.name, getattr(self, f.name))
-    return s
-
-
-  @classmethod
-  def get_fields(cls) -> Iterator[Tuple[str, SemanticAspect]]:
-    return ((f.name, type(f.default)) for  f in fields(cls))
-
-  # Whether multiple options are set for any aspect.
-  def has_multiple_variants(self) -> bool:
-    for f in fields(self):
-      if isinstance(getattr(self, f.name), list):
-        return True
-    return False
-
-  # Get all possible combinations for aspects with multiple options (as a list) set.
-  # Calling has_multiple_variants on resulting objects will return False.
-  def generate_variants(self) -> List['SemanticConfiguration']:
-    my_fields = fields(self)
-    chosen_options = ([item] if not isinstance(item,list) else item for item in (getattr(self, f.name) for f in my_fields))
-    variants = itertools.product(*chosen_options)
-
-    return [SemanticConfiguration(**{f.name: o for f,o in zip(my_fields, variant)}) for variant in variants]
+from sccd.statechart.static.semantic_configuration import *
 
 @dataclass
 class Statechart(Freezable):

+ 2 - 89
src/sccd/statechart/static/tree.py

@@ -206,6 +206,8 @@ class Transition(Freezable):
     def __str__(self):
         return termcolor.colored("%s 🡪 %s" % (self.source.opt.full_name, self.targets[0].opt.full_name), 'green')
 
+    __repr__ = __str__
+
 
 # Simply a collection of read-only fields, generated during "optimization" for each state, inferred from the model, i.e. the hierarchy of states and transitions
 class StateOptimization(Freezable):
@@ -420,92 +422,3 @@ def optimize_tree(root: State) -> StateTree:
         initial_states = root._effective_targets()
 
         return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, initial_history_values, initial_states)
-
-
-def priority_source_parent(tree: StateTree) -> List[Transition]:
-    # Tree's transition list already ordered parent-first
-    return tree.transition_list
-
-# The following 3 priority implementations all do a stable sort with a partial order-key
-
-def priority_source_child(tree: StateTree) -> List[Transition]:
-    return sorted(tree.transition_list, key=lambda t: -t.source.opt.depth)
-
-def priority_arena_parent(tree: StateTree) -> List[Transition]:
-    return sorted(tree.transition_list, key=lambda t: t.opt.arena.opt.depth)
-
-def priority_arena_child(tree: StateTree) -> List[Transition]:
-    return sorted(tree.transition_list, key=lambda t: -t.opt.arena.opt.depth)
-
-
-def concurrency_arena_orthogonal(tree: StateTree):
-    with timer.Context("concurrency_arena_orthogonal"):
-        import collections
-        # arena_to_transition = collections.defaultdict(list)
-        # for t in tree.transition_list:
-        #     arena_to_transition[t.opt.arena].append(t)
-
-        # def sets(state: State):
-        #     sets = []
-        #     for t in arena_to_transition[state]:
-        #         sets.append(set((t,)))
-
-        #     for c in state.children:
-
-        #     return sets
-
-
-        # s = sets(tree.root)
-
-        # print(s)
-
-        sets = {}
-        unique_sets = []
-        for t1, t2 in itertools.combinations(tree.transition_list, r=2):
-            if not (t1.opt.arena_bitmap & t2.opt.arena_bitmap):
-                if t1 in sets:
-                    sets[t1].add(t2)
-                    sets[t2] = sets[t1]
-                elif t2 in sets:
-                    sets[t2].add(t1)
-                    sets[t1] = sets[t2]
-                else:
-                    s = set((t1,t2))
-                    sets[t1] = s
-                    sets[t2] = s
-                    unique_sets.append(s)
-                    print('added', s)
-                    
-
-        print((unique_sets))
-
-        # concurrent_set = itertools.chain.from_iterable(itertools.combinations(ls,r) for r in range(len(ls)+1))
-        # print(concurrent_set)
-
-        # import collections
-        # nonoverlapping = collections.defaultdict(list)
-        # for t1,t2 in itertools.combinations(tree.transition_list, r=2):
-        #     if not (t1.opt.arena_bitmap & t2.opt.arena_bitmap):
-        #         nonoverlapping[t1].append(t2)
-        #         nonoverlapping[t2].append(t1)
-
-        # for t, ts in nonoverlapping.items():
-        #     print(str(t), "does not overlap with", ",".join(str(t) for t in ts))
-
-        # print(len(nonoverlapping), "nonoverlapping pairs of transitions")
-
-def concurrency_src_dst_orthogonal(tree: StateTree):
-    with timer.Context("concurrency_src_dst_orthogonal"):
-        import collections
-        nonoverlapping = collections.defaultdict(list)
-        for t1,t2 in itertools.combinations(tree.transition_list, r=2):
-            lca_src = tree.state_list[bm_highest_bit(t1.source.opt.ancestors & t2.source.opt.ancestors)]
-            lca_dst = tree.state_list[bm_highest_bit(t1.targets[0].opt.ancestors & t2.targets[0].opt.ancestors)]
-            if isinstance(lca_src, ParallelState) and isinstance(lca_dst, ParallelState):
-                nonoverlapping[t1].append(t2)
-                nonoverlapping[t2].append(t1)
-
-        for t, ts in nonoverlapping.items():
-            print(str(t), "does not overlap with", ",".join(str(t) for t in ts))
-
-        print(len(nonoverlapping), "nonoverlapping pairs of transitions")

+ 50 - 0
src/sccd/util/graph.py

@@ -0,0 +1,50 @@
+# Tarjan's strongly connected components algorithm
+# Taken from https://stackoverflow.com/a/6575693 and adopted to Python3
+
+from itertools import chain
+from collections import defaultdict
+from typing import *
+
+Edge = Tuple[Any,Any]
+
+class _Graph:
+    def __init__(self, edges: List[Edge]):
+        edges = list(list(x) for x in edges)
+        self.edges = edges
+        self.vertices = set(chain(*edges))
+        self.tails = defaultdict(list)
+        for head, tail in self.edges:
+            self.tails[head].append(tail)
+
+def strongly_connected_components(edges: List[Edge]):
+    graph = _Graph(edges)
+    counter = 0
+    count = dict()
+    lowlink = dict()
+    stack = []
+    connected_components = []
+
+    def strong_connect(head):
+        nonlocal counter
+        lowlink[head] = count[head] = counter = counter + 1
+        stack.append(head)
+
+        for tail in graph.tails[head]:
+            if tail not in count:
+                strong_connect(tail)
+                lowlink[head] = min(lowlink[head], lowlink[tail])
+            elif count[tail] < count[head]:
+                if tail in stack:
+                    lowlink[head] = min(lowlink[head], count[tail])
+
+        if lowlink[head] == count[head]:
+            component = []
+            while stack and count[stack[-1]] >= count[head]:
+                component.append(stack.pop())
+            connected_components.append(component)
+
+    for v in graph.vertices:
+        if v not in count:
+            strong_connect(v)
+
+    return connected_components

+ 1 - 1
test/test_files/semantics/priority/statechart_priority_hierarchical.xml

@@ -17,7 +17,7 @@
       <state id="s2" initial="s3">
         <state id="s3">
           <transition target="/s1/s5">
-            <!-- with source-child, this transition is preferred above the other
+            <!-- with source-child, and same-source-priority 'explicit'  this transition is preferred above the other
                  outgoing ones, because this one occurs first in the document -->
             <raise event="source_child"/>
           </transition>

+ 2 - 1
test/test_files/semantics/priority/test_arena_child.xml

@@ -3,7 +3,8 @@
   <statechart src="statechart_priority_hierarchical.xml">
     <override_semantics
       big_step_maximality="take_one"
-      priority="arena_child"/>
+      priority="arena_child"
+      same_source_priority="none"/>
   </statechart>
   <input>
       <event name="start" port="in" time="0 d"/>

+ 2 - 1
test/test_files/semantics/priority/test_arena_parent.xml

@@ -3,7 +3,8 @@
   <statechart src="statechart_priority_hierarchical.xml">
     <override_semantics
       big_step_maximality="take_one"
-      priority="arena_parent"/>
+      priority="arena_parent"
+      same_source_priority="none"/>
   </statechart>
   <input>
       <event name="start" port="in" time="0 d"/>