Explorar o código

Fixed bug in history values implementation. Added test that would fail before the fix.

Joeri Exelmans %!s(int64=5) %!d(string=hai) anos
pai
achega
d842777e40

+ 8 - 2
src/sccd/statechart/dynamic/statechart_execution.py

@@ -70,14 +70,20 @@ class StatechartExecution:
                 print_debug("fire " + str(t))
 
                 with timer.Context("exit states"):
+                    just_exited = None
                     for s in exit_set:
                         print_debug(termcolor.colored('  EXIT %s' % s.opt.full_name, 'green'))
-                        # remember which state(s) we were in if a history state is present
-                        for history_id, history_mask in s.opt.history:
+                        if s.opt.deep_history is not None:
+                            # s has a deep-history child:
+                            history_id, history_mask = s.opt.deep_history
                             self.history_values[history_id] = exit_ids & history_mask
+                        if s.opt.shallow_history is not None:
+                            history_id = s.opt.shallow_history
+                            self.history_values[history_id] = just_exited._effective_targets()
                         self._cancel_timers(s.opt.after_triggers)
                         _perform_actions(ctx, s.exit)
                         self.configuration &= ~s.opt.state_id_bitmap
+                        just_exited = s
 
                 # execute transition action(s)
                 with timer.Context("actions"):

+ 40 - 28
src/sccd/statechart/static/tree.py

@@ -31,18 +31,18 @@ class State(Freezable):
         if self.parent is not None:
             self.parent.children.append(self)
 
-
     # Subset of descendants that are always entered when this state is the target of a transition, minus history states.
-    def _static_target_states(self) -> Bitmap:
+
+    def _effective_targets(self) -> Bitmap:
         if self.default_state:
             # this state + recursion on 'default state'
-            return self.opt.state_id_bitmap | self.default_state._static_target_states() 
+            return self.opt.state_id_bitmap | self.default_state._effective_targets() 
         else:
             # only this state
             return self.opt.state_id_bitmap
 
     # States that are always entered when this state is part of the "enter path", but not the actual target of a transition.
-    def _static_additional_target_states(self, exclude: 'State') -> Tuple[Bitmap, List['HistoryState']]:
+    def _additional_effective_targets(self, exclude: 'State') -> Tuple[Bitmap, List['HistoryState']]:
         return self.opt.state_id_bitmap # only this state
 
     def __repr__(self):
@@ -56,22 +56,22 @@ class HistoryState(State):
 
         self.history_id: Optional[int] = None
 
-    # Set of states that may be history values.
-    @abstractmethod
-    def history_mask(self) -> Bitmap:
-        pass
+    # # Set of states that may be history values.
+    # @abstractmethod
+    # def history_mask(self) -> Bitmap:
+    #     pass
 
-    def _static_target_states(self) -> Bitmap:
+    def _effective_targets(self) -> Bitmap:
         return Bitmap()
 
-    def _static_additional_target_states(self, exclude: 'State') -> Bitmap:
+    def _additional_effective_targets(self, exclude: 'State') -> Bitmap:
         assert False # history state cannot have children and therefore should never occur in a "enter path"
 
 class ShallowHistoryState(HistoryState):
 
-    def history_mask(self) -> Bitmap:
-        # Only direct children of parent:
-        return bm_union(s.opt.state_id_bitmap for s in self.parent.children)
+    # def history_mask(self) -> Bitmap:
+    #     # Only direct children of parent:
+    #     return bm_union(s.opt.state_id_bitmap for s in self.parent.children)
 
     def __repr__(self):
         return "ShallowHistoryState(\"%s\")" % (self.short_name)
@@ -87,13 +87,13 @@ class DeepHistoryState(HistoryState):
 
 class ParallelState(State):
 
-    def _static_target_states(self) -> Bitmap:
+    def _effective_targets(self) -> Bitmap:
         # this state + recursive on all children that are not a history state
-        return bm_union(c._static_target_states() for c in self.children if not isinstance(c, HistoryState)) | self.opt.state_id_bitmap
+        return bm_union(c._effective_targets() for c in self.children if not isinstance(c, HistoryState)) | self.opt.state_id_bitmap
 
-    def _static_additional_target_states(self, exclude: 'State') -> Bitmap:
+    def _additional_effective_targets(self, exclude: 'State') -> Bitmap:
         # 
-        return self._static_target_states() & ~exclude._static_target_states()
+        return self._effective_targets() & ~exclude._effective_targets()
 
     def __repr__(self):
         return "ParallelState(\"%s\")" % (self.short_name)
@@ -210,7 +210,7 @@ class Transition(Freezable):
 
 # 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):
-    __slots__ = ["full_name", "depth", "state_id", "state_id_bitmap", "ancestors", "descendants", "history", "after_triggers"]
+    __slots__ = ["full_name", "depth", "state_id", "state_id_bitmap", "ancestors", "descendants", "effective_targets", "deep_history", "shallow_history", "after_triggers"]
     def __init__(self):
         super().__init__()
 
@@ -223,9 +223,13 @@ class StateOptimization(Freezable):
         self.ancestors: Bitmap = Bitmap()
         self.descendants: Bitmap = Bitmap()
 
-        # Tuple for each child that is HistoryState: (history-id, history mask)
-        # Typically zero or one children are a HistoryState (shallow or deep)
-        self.history: List[Tuple[int, Bitmap]] = []
+        self.effective_targets: Bitmap = Bitmap()
+
+        # If a direct child of this state is a deep history state, then "deep history" needs to be recorded when exiting this state. This value contains a tuple, with the (history-id, history_mask) of that child state.
+        self.deep_history: Optional[Tuple[int, Bitmap]] = None
+
+        # If a direct child of this state is a shallow history state, then "shallow history" needs to be recorded when exiting this state. This value is the history-id of that child state
+        self.shallow_history: Optional[int] = None
 
         # Triggers of outgoing transitions that are AfterTrigger.
         self.after_triggers: List[AfterTrigger] = []
@@ -309,7 +313,6 @@ def optimize_tree(root: State) -> StateTree:
                         state.opt.after_triggers.append(t.trigger)
                         after_triggers.append(t.trigger)
 
-
         def set_ancestors(state: State, ancestors=Bitmap()):
             state.opt.ancestors = ancestors
             return ancestors | state.opt.state_id_bitmap
@@ -319,14 +322,22 @@ def optimize_tree(root: State) -> StateTree:
             state.opt.descendants = descendants
             return state.opt.state_id_bitmap | descendants
 
-        # If a history state is entered whose parent has never been exited before, the parent's default states are entered.
+        def calculate_effective_targets(state: State, _=None):
+            # implementation of "_effective_targets"-method is recursive (slow)
+            # store the result, it is always the same:
+            state.opt.effective_targets = state._effective_targets()
+
         initial_history_values = []
         def deal_with_history(state: State, children_history):
-            state.opt.history = [(h.history_id, h.history_mask()) for h in children_history if h is not None]
+            for h in children_history:
+                if isinstance(h, DeepHistoryState):
+                    state.opt.deep_history = (h.history_id, h.history_mask())
+                elif isinstance(h, ShallowHistoryState):
+                    state.opt.shallow_history = h.history_id
 
             if isinstance(state, HistoryState):
                 state.history_id = len(initial_history_values)
-                initial_history_values.append(state.parent._static_target_states())
+                initial_history_values.append(state.parent._effective_targets())
                 return state
 
         def freeze(state: State, _=None):
@@ -344,6 +355,7 @@ def optimize_tree(root: State) -> StateTree:
             ],
             after_children=[
                 set_descendants,
+                calculate_effective_targets,
                 deal_with_history,
                 freeze,
             ])
@@ -378,10 +390,10 @@ def optimize_tree(root: State) -> StateTree:
                 if next_state_id:
                     # an intermediate state on the path from arena to target
                     next_state = state_list[next_state_id]
-                    enter_states_static |= state._static_additional_target_states(next_state)
+                    enter_states_static |= state._additional_effective_targets(next_state)
                 else:
                     # the actual target of the transition
-                    enter_states_static |= state._static_target_states()
+                    enter_states_static |= state.opt.effective_targets
                 state_id = next_state_id
 
             target_history_id = None
@@ -403,7 +415,7 @@ def optimize_tree(root: State) -> StateTree:
 
             t.freeze()
 
-        initial_states = root._static_target_states()
+        initial_states = root._effective_targets()
 
         return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, initial_history_values, initial_states)
 

+ 127 - 0
test/test_files/features/history/test_composite_shallow.svg

@@ -0,0 +1,127 @@
+<?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="298pt" height="420pt"
+ viewBox="0.00 0.00 298.00 420.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 416)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-416 294,-416 294,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster__composite</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M20,-8C20,-8 270,-8 270,-8 276,-8 282,-14 282,-20 282,-20 282,-297 282,-297 282,-303 276,-309 270,-309 270,-309 20,-309 20,-309 14,-309 8,-303 8,-297 8,-297 8,-20 8,-20 8,-14 14,-8 20,-8"/>
+<text text-anchor="start" x="117.6622" y="-290.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">composite</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster__composite_b</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M134,-16C134,-16 262,-16 262,-16 268,-16 274,-22 274,-28 274,-28 274,-259 274,-259 274,-265 268,-271 262,-271 262,-271 134,-271 134,-271 128,-271 122,-265 122,-259 122,-259 122,-28 122,-28 122,-22 128,-16 134,-16"/>
+<text text-anchor="start" x="194.6646" y="-252.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">b</text>
+</g>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="170" cy="-406.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _outer -->
+<g id="node3" class="node">
+<title>_outer</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="198,-373 142,-373 142,-337 198,-337 198,-373"/>
+<text text-anchor="start" x="156.329" y="-351.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">outer</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M154.3333,-338C154.3333,-338 185.6667,-338 185.6667,-338 191.3333,-338 197,-343.6667 197,-349.3333 197,-349.3333 197,-360.6667 197,-360.6667 197,-366.3333 191.3333,-372 185.6667,-372 185.6667,-372 154.3333,-372 154.3333,-372 148.6667,-372 143,-366.3333 143,-360.6667 143,-360.6667 143,-349.3333 143,-349.3333 143,-343.6667 148.6667,-338 154.3333,-338"/>
+</g>
+<!-- __initial&#45;&gt;_outer -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_outer</title>
+<path fill="none" stroke="#000000" d="M170,-400.9886C170,-396.6293 170,-390.1793 170,-383.4801"/>
+<polygon fill="#000000" stroke="#000000" points="173.5001,-383.0122 170,-373.0122 166.5001,-383.0122 173.5001,-383.0122"/>
+<text text-anchor="middle" x="171.3895" y="-384" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _outer2 -->
+<g id="node2" class="node">
+<title>_outer2</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-373 50,-373 50,-337 106,-337 106,-373"/>
+<text text-anchor="start" x="60.9936" y="-351.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">outer2</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M62.3333,-338C62.3333,-338 93.6667,-338 93.6667,-338 99.3333,-338 105,-343.6667 105,-349.3333 105,-349.3333 105,-360.6667 105,-360.6667 105,-366.3333 99.3333,-372 93.6667,-372 93.6667,-372 62.3333,-372 62.3333,-372 56.6667,-372 51,-366.3333 51,-360.6667 51,-360.6667 51,-349.3333 51,-349.3333 51,-343.6667 56.6667,-338 62.3333,-338"/>
+</g>
+<!-- _composite_history -->
+<g id="node7" class="node">
+<title>_composite_history</title>
+<ellipse fill="transparent" stroke="#000000" stroke-width="2" cx="34" cy="-210" rx="18" ry="18"/>
+<text text-anchor="middle" x="34" y="-206.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">H</text>
+</g>
+<!-- _outer2&#45;&gt;_composite_history -->
+<g id="edge5" class="edge">
+<title>_outer2&#45;&gt;_composite_history</title>
+<path fill="none" stroke="#000000" d="M72.4086,-336.6466C69.844,-328.2237 66.7653,-318.1058 64,-309 56.6229,-284.7083 48.2456,-257.0577 42.222,-237.1644"/>
+<polygon fill="#000000" stroke="#000000" points="45.552,-236.0843 39.3044,-227.5275 38.8523,-238.1127 45.552,-236.0843"/>
+<text text-anchor="middle" x="70.3895" y="-320" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _composite_b_s1 -->
+<g id="node11" class="node">
+<title>_composite_b_s1</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="210,-233 130,-233 130,-187 210,-187 210,-233"/>
+<text text-anchor="start" x="163.6646" y="-216.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s1</text>
+<text text-anchor="start" x="135.5072" y="-196.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">enter ^out.s1</text>
+<polygon fill="#000000" stroke="#000000" points="130,-210 130,-210 210,-210 210,-210 130,-210"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M143,-188C143,-188 197,-188 197,-188 203,-188 209,-194 209,-200 209,-200 209,-220 209,-220 209,-226 203,-232 197,-232 197,-232 143,-232 143,-232 137,-232 131,-226 131,-220 131,-220 131,-200 131,-200 131,-194 137,-188 143,-188"/>
+</g>
+<!-- _outer&#45;&gt;_composite_b_s1 -->
+<g id="edge6" class="edge">
+<title>_outer&#45;&gt;_composite_b_s1</title>
+<path fill="none" stroke="#000000" d="M170,-336.9288C170,-313.5344 170,-272.3213 170,-243.2131"/>
+<polygon fill="#000000" stroke="#000000" points="173.5001,-243.0921 170,-233.0921 166.5001,-243.0922 173.5001,-243.0921"/>
+<text text-anchor="middle" x="171.3895" y="-320" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _composite -->
+<!-- _composite_initial -->
+<g id="node5" class="node">
+<title>_composite_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="80" cy="-210" rx="5.5" ry="5.5"/>
+</g>
+<!-- _composite_a -->
+<g id="node6" class="node">
+<title>_composite_a</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="108,-141 52,-141 52,-105 108,-105 108,-141"/>
+<text text-anchor="start" x="76.6646" y="-119.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">a</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M64.3333,-106C64.3333,-106 95.6667,-106 95.6667,-106 101.3333,-106 107,-111.6667 107,-117.3333 107,-117.3333 107,-128.6667 107,-128.6667 107,-134.3333 101.3333,-140 95.6667,-140 95.6667,-140 64.3333,-140 64.3333,-140 58.6667,-140 53,-134.3333 53,-128.6667 53,-128.6667 53,-117.3333 53,-117.3333 53,-111.6667 58.6667,-106 64.3333,-106"/>
+</g>
+<!-- _composite_initial&#45;&gt;_composite_a -->
+<g id="edge2" class="edge">
+<title>_composite_initial&#45;&gt;_composite_a</title>
+<path fill="none" stroke="#000000" d="M80,-204.2917C80,-193.6876 80,-170.3024 80,-151.3237"/>
+<polygon fill="#000000" stroke="#000000" points="83.5001,-151.066 80,-141.0661 76.5001,-151.0661 83.5001,-151.066"/>
+<text text-anchor="middle" x="81.3895" y="-161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _composite_b -->
+<!-- _composite_b_initial -->
+<g id="node9" class="node">
+<title>_composite_b_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="238" cy="-210" rx="5.5" ry="5.5"/>
+</g>
+<!-- _composite_b_s2 -->
+<g id="node10" class="node">
+<title>_composite_b_s2</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="266,-70 186,-70 186,-24 266,-24 266,-70"/>
+<text text-anchor="start" x="219.6646" y="-53.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">s2</text>
+<text text-anchor="start" x="191.5072" y="-33.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">enter ^out.s2</text>
+<polygon fill="#000000" stroke="#000000" points="186,-47 186,-47 266,-47 266,-47 186,-47"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M199,-25C199,-25 253,-25 253,-25 259,-25 265,-31 265,-37 265,-37 265,-57 265,-57 265,-63 259,-69 253,-69 253,-69 199,-69 199,-69 193,-69 187,-63 187,-57 187,-57 187,-37 187,-37 187,-31 193,-25 199,-25"/>
+</g>
+<!-- _composite_b_initial&#45;&gt;_composite_b_s2 -->
+<g id="edge3" class="edge">
+<title>_composite_b_initial&#45;&gt;_composite_b_s2</title>
+<path fill="none" stroke="#000000" d="M236.7858,-204.3655C235.0326,-195.7239 232,-178.6578 232,-164 232,-164 232,-164 232,-87.5 232,-85.1011 231.8957,-82.6307 231.7146,-80.1495"/>
+<polygon fill="#000000" stroke="#000000" points="235.1758,-79.6033 230.6084,-70.0436 228.2174,-80.3651 235.1758,-79.6033"/>
+<text text-anchor="middle" x="233.3895" y="-120" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _composite_b_s1&#45;&gt;_outer2 -->
+<g id="edge4" class="edge">
+<title>_composite_b_s1&#45;&gt;_outer2</title>
+<path fill="none" stroke="#000000" d="M162.5412,-233.3378C155.1916,-254.2877 142.6513,-285.2994 126,-309 120.8494,-316.3312 114.409,-323.4458 107.9346,-329.8012"/>
+<polygon fill="#000000" stroke="#000000" points="105.2889,-327.4843 100.3939,-336.8805 110.0801,-332.5877 105.2889,-327.4843"/>
+<text text-anchor="middle" x="120.3895" y="-320" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+</g>
+</svg>

+ 49 - 0
test/test_files/features/history/test_composite_shallow.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <inport name="in">
+      <event name="start"/>
+    </inport>
+    <outport name="out">
+      <event name="s1"/>
+      <event name="s2"/>
+    </outport>
+
+    <root initial="outer">
+      <state id="outer">
+        <transition target="/composite/b/s1"/>
+      </state>
+      <state id="outer2">
+        <transition target="/composite/history"/>
+      </state>
+      <state id="composite" initial="a">
+        <state id="a"/>
+        <state id="b" initial="s2">
+          <state id="s1">
+            <onentry>
+              <raise event="s1"/>
+            </onentry>
+            <transition target="/outer2"/>
+          </state>
+          <state id="s2">
+            <onentry>
+              <raise event="s2"/>
+            </onentry>
+          </state>
+        </state>
+        <history id="history" type="shallow"/>
+      </state>
+    </root>
+  </statechart>
+
+  <input>
+    <event port="in" name="start" time="0 d"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="s1"/>
+      <event port="out" name="s2"/>
+    </big_step>
+  </output>
+</test>

test/test_files/features/history/test_history_deep.svg → test/test_files/features/history/test_deep.svg


test/test_files/features/history/test_history_deep.xml → test/test_files/features/history/test_deep.xml


test/test_files/features/history/test_history_default.svg → test/test_files/features/history/test_default.svg


test/test_files/features/history/test_history_default.xml → test/test_files/features/history/test_default.xml


test/test_files/features/history/test_history_parallel_deep.svg → test/test_files/features/history/test_parallel_deep.svg


test/test_files/features/history/test_history_parallel_deep.xml → test/test_files/features/history/test_parallel_deep.xml


test/test_files/features/history/test_history.svg → test/test_files/features/history/test_shallow.svg


test/test_files/features/history/test_history.xml → test/test_files/features/history/test_shallow.xml


+ 0 - 15
test/test_files/todo/test_todo_history.xml

@@ -1,15 +0,0 @@
-<?xml version="1.0" ?>
-<test>
-  <statechart>
-    <!-- there's a bug in calculating the history value for a shallow history state:
-         the history value will only include the exited state that is a direct child of the parent of the history state.
-         the correct behavior is to include also
-           1) if the child is a parallelstate, all of the direct child's children
-           2) if the child is a regular state, the direct child's 'default' state
-         both, recursively. -->
-    <root>
-      <state id="A">
-      </state>
-    </root>
-  </statechart>
-</test>