Browse Source

Fix memory protocol semantics (always write to 'clean' memory, read from 'snapshot' memory)

Joeri Exelmans 5 years ago
parent
commit
8ed7fc5ddb

+ 4 - 4
src/sccd/controller/controller.py

@@ -78,9 +78,9 @@ class Controller:
 
     # 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, interrupt: queue.Queue = queue.SimpleQueue()):
+    def run_until(self, now: Optional[Timestamp], pipe: queue.Queue):
 
-        unstable: List[Instance] = []
+        # unstable: List[Instance] = []
 
         # Helper. Put big step output events in the event queue or add them to the right output listeners.
         def process_big_step_output(events: List[OutputEvent]):
@@ -106,8 +106,8 @@ class Controller:
             for i in self.object_manager.instances:
                 stable, events = i.initialize(self.simulated_time)
                 process_big_step_output(events)
-                if not stable:
-                    unstable.append(i)
+                # if not stable:
+                #     unstable.append(i)
             print_debug("initialized. time is now %s" % str(self.get_simulated_duration()))
 
 

+ 75 - 0
src/sccd/execution/memory.py

@@ -0,0 +1,75 @@
+from typing import *
+from sccd.util.bitmap import *
+from sccd.util.debug import *
+
+class Memory:
+  def __init__(self, scope):
+    self.scope = scope
+    self.memory = [None] * scope.size()
+
+    # Write default values to storage
+    for name, variable in scope.all():
+      self.memory[variable.offset] = variable.default_value
+
+class MemorySnapshot:
+  def __init__(self, memory: Memory):
+    # self.memory = memory
+    self.actual = memory.memory
+    self.snapshot = list(self.actual)
+    self.len = len(self.actual)
+
+    self.temp_dirty = Bitmap() # positions in actual memory written to before flush_temp
+    self.round_dirty = Bitmap() # positions in actual memory written to after flush_temp and before flush_round
+
+    self.stack_use = 0
+    self.stack = [None]*1024 # Storage for local scope values. Always temporary: no memory protocol applies to them
+
+    self.scope = [memory.scope]
+
+  def refresh(self):
+    self.snapshot = list(self.actual)
+
+  def store(self, offset: int, value: Any):
+    if offset >= self.len:
+      self.stack[offset - self.len] = value
+    else:
+      self.actual[offset] = value
+      self.temp_dirty |= bit(offset)
+
+  def load(self, offset: int) -> Any:
+    if offset >= self.len:
+      return self.stack[offset - self.len]
+    else:
+      if self.temp_dirty.has(offset):
+        return self.actual[offset]
+      else:
+        return self.snapshot[offset]
+
+  def grow_stack(self, scope):
+    self.scope.append(scope)
+    self.stack_use += scope.size()
+    if self.stack_use > len(self.stack):
+      self.stack.extend([None]*len(self.stack)) # double stack
+
+  def shrink_stack(self):
+    scope = self.scope.pop()
+    self.stack_use -= scope.size()
+    if self.stack_use < len(self.stack) // 4 and len(self.stack) > 1024:
+      del self.stack[len(self.stack)//2:] # half stack
+
+  def flush_temp(self):
+    assert self.stack_use == 0 # only allowed to be called in between statement executions or expression evaluations
+    race_conditions = self.temp_dirty & self.round_dirty
+    if race_conditions:
+        raise Exception("Race condition for variables %s" % str(list(self.scope[-1].get_name(offset) for offset in race_conditions.items())))
+
+    self.round_dirty |= self.temp_dirty
+    self.temp_dirty = Bitmap() # reset
+
+  def refresh(self):
+    assert self.stack_use == 0 # only allowed to be called in between statement executions or expression evaluations
+    # The following looks more efficient, but may be in fact slower in Python:
+    # for i in self.round_dirty.items():
+    #   self.snapshot[i] = self.actual[i] # refresh snapshot
+    self.snapshot = list(self.actual) # refresh
+    self.round_dirty = Bitmap() # reset

+ 6 - 3
src/sccd/execution/round.py

@@ -54,16 +54,19 @@ class Round(ABC):
         self.name = name
         self.parent = None
         
-        self.memory = None
+        self.callbacks: List[Callable] = []
 
         self.remainder_events = [] # events enabled for the remainder of the current round
         self.next_events = [] # events enabled for the entirety of the next round
 
+    def when_done(self, callback):
+        self.callbacks.append(callback)
+
     def run(self, arenas_changed: Bitmap = Bitmap()) -> Bitmap:
         changed = self._internal_run(arenas_changed)
         if changed:
-            if self.memory:
-                self.memory.flush_round()
+            for callback in self.callbacks:
+                callback()
             self.remainder_events = self.next_events
             self.next_events = []
             print_debug("completed "+self.name)

+ 13 - 11
src/sccd/execution/statechart_instance.py

@@ -7,7 +7,7 @@ from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
 from sccd.execution.round import *
 from sccd.execution.statechart_state import *
-from sccd.execution.storage import *
+from sccd.execution.memory import *
 
 class StatechartInstance(Instance):
     def __init__(self, statechart: Statechart, object_manager):
@@ -63,29 +63,31 @@ class StatechartInstance(Instance):
             # InternalEventLifeline.QUEUE: self._big_step.add_next_event,
             InternalEventLifeline.QUEUE: lambda e: self.state.output.append(OutputEvent(e, InstancesTarget([self]))),
             InternalEventLifeline.NEXT_COMBO_STEP: combo_step.add_next_event,
-            InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event
+            InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event,
+
+            InternalEventLifeline.REMAINDER: self._big_step.add_remainder_event,
         }[semantics.internal_event_lifeline]
 
-        storage = Storage(statechart.scope)
-        gc_memory = DirtyStorage(storage)
+        memory = Memory(statechart.scope)
+        gc_memory = MemorySnapshot(memory)
 
         if semantics.enabledness_memory_protocol == MemoryProtocol.BIG_STEP:
-            self._big_step.memory = gc_memory
+            self._big_step.when_done(gc_memory.refresh)
         elif semantics.enabledness_memory_protocol == MemoryProtocol.COMBO_STEP:
-            combo_step.memory = gc_memory
+            combo_step.when_done(gc_memory.refresh)
         elif semantics.enabledness_memory_protocol == MemoryProtocol.SMALL_STEP:
-            small_step.memory = gc_memory
+            small_step.when_done(gc_memory.refresh)
 
         if semantics.assignment_memory_protocol == semantics.enabledness_memory_protocol:
             rhs_memory = gc_memory
         else:
-            rhs_memory = DirtyStorage(storage)
+            rhs_memory = MemorySnapshot(memory)
             if semantics.assignment_memory_protocol == MemoryProtocol.BIG_STEP:
-                self._big_step.memory = rhs_memory
+                self._big_step.when_done(rhs_memory.refresh)
             elif semantics.assignment_memory_protocol == MemoryProtocol.COMBO_STEP:
-                combo_step.memory = rhs_memory
+                combo_step.when_done(rhs_memory.refresh)
             elif semantics.assignment_memory_protocol == MemoryProtocol.SMALL_STEP:
-                small_step.memory = rhs_memory
+                small_step.when_done(rhs_memory.refresh)
 
         print_debug("\nRound hierarchy: " + str(self._big_step) + '\n')
 

+ 3 - 3
src/sccd/execution/statechart_state.py

@@ -91,7 +91,7 @@ class StatechartState:
         self.eventless_states -= s.gen.has_eventless_transitions
         # execute exit action(s)
         self._perform_actions(events, s.exit)
-        self.configuration_bitmap &= ~bit(s.gen.state_id)
+        self.configuration_bitmap &= ~s.gen.state_id_bitmap
             
     # execute transition action(s)
     self._perform_actions(events, t.actions)
@@ -102,7 +102,7 @@ class StatechartState:
     for s in enter_set:
         print_debug(termcolor.colored('  ENTER %s' % s.gen.full_name, 'green'))
         self.eventless_states += s.gen.has_eventless_transitions
-        self.configuration_bitmap |= bit(s.gen.state_id)
+        self.configuration_bitmap |= s.gen.state_id_bitmap
         # execute enter action(s)
         self._perform_actions(events, s.enter)
         self._start_timers(s.gen.after_triggers)
@@ -116,7 +116,7 @@ class StatechartState:
   def check_guard(self, t, events) -> bool:
       # Special case: after trigger
       if isinstance(t.trigger, AfterTrigger):
-        e = [e for e in events if e.id == t.trigger.id][0]
+        e = [e for e in events if bit(e.id) & t.trigger.bitmap][0]
         if self.timer_ids[t.trigger.after_id] != e.parameters[0]:
           return False
 

+ 0 - 58
src/sccd/execution/storage.py

@@ -1,58 +0,0 @@
-from typing import *
-from sccd.util.bitmap import *
-from sccd.util.debug import *
-
-class Storage:
-  def __init__(self, scope):
-    self.scope = scope
-    self.storage = [None] * scope.size()
-
-    # Write default values to storage
-    for name, variable in scope.all():
-      self.storage[variable.offset] = variable.default_value
-
-class DirtyStorage:
-  def __init__(self, storage: Storage):
-    self.storage = storage
-
-    self.clean = storage.storage
-
-    # Dirty storage: Values copied to clean storage when rotated.
-    self.dirty = list(self.clean)
-
-    self.temp_dirty = Bitmap() # dirty values written by current transition, that also need to be read from dirty storage in RHS values while the transition's actions are executing.
-    self.round_dirty = Bitmap() # to keep track of the values that need to be copied to clean storage at the end of a round
-
-    # Storage for local scope values. No values ever copied from here to 'clean' storage
-    self.temp_stack = [None]*1024
-
-
-  def store(self, offset: int, value: Any):
-    if offset >= len(self.clean):
-      self.temp_stack[offset - len(self.clean)] = value
-    else:
-      self.dirty[offset] = value
-      self.temp_dirty |= bit(offset)
-
-  def load(self, offset: int) -> Any:
-    if offset >= len(self.clean):
-      return self.temp_stack[offset - len(self.clean)]
-    else:
-      if self.temp_dirty.has(offset):
-        return self.dirty[offset]
-      else:
-        return self.clean[offset]
-
-  # Called after a block has executed.
-  def flush_temp(self):
-    race_conditions = self.temp_dirty & self.round_dirty
-    if race_conditions:
-        raise Exception("Race condition for variables %s" % str(list(self.storage.scope.get_name(offset) for offset in race_conditions.items())))
-
-    self.round_dirty |= self.temp_dirty
-    self.temp_dirty = Bitmap()
-
-  def flush_round(self):
-    for i in self.round_dirty.items():
-      self.clean[i] = self.dirty[i] # copy dirty values to clean storage
-    self.round_dirty = Bitmap()

+ 0 - 41
src/sccd/syntax/datamodel.py

@@ -1,41 +0,0 @@
-# from typing import *
-# from sccd.util.namespace import *
-
-# class Variable:
-#     def __init__(self, value, type):
-#         self.value = value
-#         self.type = type
-
-#     def __repr__(self):
-#       return "Var(" + str(self.type) + ' = ' + str(self.value) + ")"
-
-# class DataModel:
-#     def __init__(self):
-#         self.names: Dict[str, int] = {}
-#         self.storage = []
-
-#         # Reserved variable. This is dirty, find better solution
-#         self.create("INSTATE", None, Callable[[List[str]], bool])
-
-#     def create(self, name: str, value, _type=None) -> int:
-#         if _type is None:
-#             _type = type(value)
-#         if name in self.names:
-#             raise Exception("Name already in use.")
-#         id = len(self.storage)
-#         self.storage.append(Variable(value, _type))
-#         self.names[name] = id
-#         return id
-
-#     def lookup(self, name) -> Tuple[int, type]:
-#         id = self.names[name]
-#         var = self.storage[id]
-#         return (id, var.type)
-
-#     def set(self, name, value):
-#         id = self.names[name]
-#         var = self.storage[id]
-#         var.value = value
-
-#     def __repr__(self):
-#         return "DataModel(" + ", ".join(name + ': '+str(self.storage[offset]) for name,offset in self.names.items()) + ")"

+ 8 - 3
src/sccd/syntax/expression.py

@@ -1,15 +1,16 @@
 from abc import *
 from typing import *
 from dataclasses import *
-from sccd.syntax.scope import *
 from sccd.util.duration import *
+from sccd.syntax.scope import *
 
 # to inspect types in Python 3.7
 # Python 3.8 already has this built in
 import typing_inspect
 
 class Expression(ABC):
-    # Must be called exactly once on each expression. May throw.
+    # Must be called exactly once on each expression, before any call to eval is made.
+    # Determines the static type of the expression. May throw if there is a type error.
     # Returns static type of expression.
     @abstractmethod
     def init_rvalue(self, scope) -> type:
@@ -21,7 +22,11 @@ class Expression(ABC):
     def eval(self, current_state, events, memory):
         pass
 
+# The LValue type is any type that can serve as an expression OR an LValue (left hand of assignment)
+# Either 'init_rvalue' or 'init_lvalue' is called to initialize the LValue.
+# Then either 'eval' or 'eval_lvalue' can be called any number of times.
 class LValue(Expression):
+    # Initialize the LValue as an LValue. 
     @abstractmethod
     def init_lvalue(self, scope, expected_type: type):
         pass
@@ -30,7 +35,7 @@ class LValue(Expression):
     def eval_lvalue(self, current_state, events, memory) -> Variable:
         pass
 
-    # LValues are expressions too!
+    # LValues can also serve as expressions!
     def eval(self, current_state, events, memory):
         variable = self.eval_lvalue(current_state, events, memory)
         return memory.load(variable.offset)

+ 2 - 0
src/sccd/syntax/statechart.py

@@ -23,6 +23,8 @@ class InternalEventLifeline(SemanticOption, Enum):
   NEXT_COMBO_STEP = 1
   NEXT_SMALL_STEP = 2
 
+  REMAINDER = 3
+
 class InputEventLifeline(SemanticOption, Enum):
   WHOLE = 0
   FIRST_COMBO_STEP = 1

+ 10 - 10
src/sccd/syntax/statement.py

@@ -5,7 +5,7 @@ from sccd.syntax.expression import *
 class Statement(ABC):
     # Execution typically has side effects.
     @abstractmethod
-    def exec(self, current_state, events, datamodel):
+    def exec(self, current_state, events, memory):
         pass
 
     @abstractmethod
@@ -26,14 +26,14 @@ class Assignment(Statement):
         rhs_t = self.rhs.init_rvalue(scope)
         self.lhs.init_lvalue(scope, rhs_t)
 
-    def exec(self, current_state, events, datamodel):
-        val = self.rhs.eval(current_state, events, datamodel)
-        offset = self.lhs.eval_lvalue(current_state, events, datamodel).offset
+    def exec(self, current_state, events, memory):
+        val = self.rhs.eval(current_state, events, memory)
+        offset = self.lhs.eval_lvalue(current_state, events, memory).offset
 
         def load():
-            return datamodel.load(offset)
+            return memory.load(offset)
         def store(val):
-            datamodel.store(offset, val)
+            memory.store(offset, val)
 
         def assign():
             store(val)
@@ -66,9 +66,9 @@ class Block(Statement):
         for stmt in self.stmts:
             stmt.init_stmt(local_scope)
 
-    def exec(self, current_state, events, datamodel):
+    def exec(self, current_state, events, memory):
         for stmt in self.stmts:
-            stmt.exec(current_state, events, datamodel)
+            stmt.exec(current_state, events, memory)
 
     def render(self) -> str:
         result = ""
@@ -84,8 +84,8 @@ class ExpressionStatement(Statement):
     def init_stmt(self, scope):
         self.expr.init_rvalue(scope)
 
-    def exec(self, current_state, events, datamodel):
-        self.expr.eval(current_state, events, datamodel)
+    def exec(self, current_state, events, memory):
+        self.expr.eval(current_state, events, memory)
 
     def render(self) -> str:
         return self.expr.render()

+ 17 - 0
src/sccd/syntax/tree.py

@@ -81,6 +81,20 @@ class ParallelState(State):
                 targets.extend(c.getEffectiveTargetStates(instance))
         return targets
 
+@dataclass
+class Trigger:
+    enabling: Bitmap
+
+    def check(self, events_bitmap: Bitmap) -> bool:
+        return (self.enabling & events_bitmap) == self.enabling
+
+@dataclass
+class NegatedTrigger(Trigger):
+    disabling: Bitmap
+
+    def check(self, events_bitmap: Bitmap) -> bool:
+        return Trigger.check(self, events_bitmap) and not (self.disabling & events_bitmap)
+
 @dataclass
 class EventTrigger:
     id: int # event ID
@@ -101,6 +115,9 @@ class EventTrigger:
         else:
             return self.name
 
+class NegatedEventTrigger(EventTrigger):
+    pass
+
 class AfterTrigger(EventTrigger):
     # id: unique within the statechart
     def __init__(self, id: int, name: str, after_id: int, delay: Expression):

+ 5 - 4
test/lib/test.py

@@ -27,8 +27,8 @@ class Test(unittest.TestCase):
 
     for test in test_variants:
       print_debug('\n'+test.name)
-      pipe = queue.SimpleQueue()
-      interrupt = queue.SimpleQueue()
+      pipe = queue.Queue()
+      # interrupt = queue.Queue()
 
       controller = Controller(test.model)
 
@@ -39,7 +39,7 @@ class Test(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(None, pipe, interrupt)
+          controller.run_until(None, pipe)
         except Exception as e:
           print_debug(e)
           pipe.put(e, block=True, timeout=None)
@@ -57,7 +57,8 @@ class Test(unittest.TestCase):
 
       def fail(msg, kill=False):
         if kill:
-          interrupt.put(None)
+          pass
+          # interrupt.put(None)
         thread.join()
         def repr(output):
           return '\n'.join("%d: %s" % (i, str(big_step)) for i, big_step in enumerate(output))