Просмотр исходного кода

Added some documentation. Moved Globals class to statechart.static package. Moved cd.cd module to cd.static.cd to achieve common structure with action_lang and statechart packages.

Joeri Exelmans 5 лет назад
Родитель
Сommit
b973c0f4fa

+ 2 - 1
src/sccd/action_lang/cmd/prompt.py

@@ -5,10 +5,11 @@ from sccd.action_lang.parser.text import *
 from lark.exceptions import *
 
 if __name__ == "__main__":
-  scope = Scope("interactive", parent=None)
+  scope = Scope("interactive", parent=None) # "global" scope
   memory = Memory()
   memory.push_frame(scope)
   readline.set_history_length(1000)
+
   print("Enter statements or expressions. Most statements end with ';'. Statements will be executed, expressions will be evaluated. Either can have side effects.")
   print("Examples:")
   print("  basic stuff:")

+ 3 - 4
src/sccd/action_lang/dynamic/memory.py

@@ -41,17 +41,16 @@ class Memory(MemoryInterface):
   def current_frame(self) -> StackFrame:
     return self._current_frame
 
+  # For function calls: context MAY differ from _current_frame if the function was
+  # called from a different context from where it was created.
+  # This enables function closures.
   def push_frame_w_context(self, scope: Scope, context: StackFrame):
-
     self._current_frame = StackFrame(
       storage=[None]*scope.size(),
       parent=self._current_frame,
       context=context,
       scope=scope)
 
-  # For function calls: context MAY differ from _current_frame if the function was
-  # called from a different context from where it was created.
-  # This enables function closures.
   def push_frame(self, scope: Scope):
     self.push_frame_w_context(scope, self._current_frame)
 

+ 5 - 2
src/sccd/action_lang/static/expression.py

@@ -43,12 +43,15 @@ class Expression(ABC):
     def init_expr(self, scope: Scope) -> SCCDType:
         pass
 
-    # Evaluation should NOT have side effects.
-    # Motivation is that the evaluation of a guard condition cannot have side effects.
+    # Evaluation may have side effects.
     @abstractmethod
     def eval(self, memory: MemoryInterface):
         pass
 
+    @abstractmethod
+    def render(self) -> str:
+        pass
+
 # The LValue type is any type that can serve as an expression OR an LValue (left hand of assignment)
 # Either 'init_expr' or 'init_lvalue' is called to initialize the LValue.
 # Then either 'eval' or 'eval_lvalue' can be called any number of times.

+ 1 - 0
src/sccd/action_lang/static/statement.py

@@ -80,6 +80,7 @@ class Statement(ABC):
     def exec(self, memory: MemoryInterface) -> Return:
         pass
 
+    # Looks up identifiers in the given scope, and adds new identifiers to the scope.
     @abstractmethod
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
         pass

+ 1 - 2
src/sccd/cd/parser/xml.py

@@ -1,7 +1,6 @@
 from sccd.action_lang.parser import text as action_lang_parser
 from sccd.statechart.parser.xml import *
-from sccd.cd.globals import *
-from sccd.cd.cd import *
+from sccd.cd.static.cd import *
 
 def cd_parser_rules(statechart_parser_rules, default_delta = duration(100, Microsecond)):
   globals = Globals()

+ 5 - 1
src/sccd/cd/cd.py

@@ -2,7 +2,7 @@ from abc import *
 from dataclasses import *
 from typing import *
 from sccd.statechart.static.statechart import *
-from sccd.cd.globals import *
+from sccd.statechart.static.globals import *
 
 @dataclass
 class AbstractCD(ABC):
@@ -12,6 +12,10 @@ class AbstractCD(ABC):
   def get_default_class(self) -> Statechart:
     pass
 
+  # Get the "model delta", i.e. the smallest possible duration representable.
+  def get_delta(self) -> Duration:
+    return self.globals.delta
+
 @dataclass
 class CD(AbstractCD):
   classes: Dict[str, Statechart]

+ 18 - 12
src/sccd/controller/controller.py

@@ -5,13 +5,15 @@ from sccd.controller.event_queue import *
 from sccd.statechart.dynamic.event import *
 from sccd.controller.object_manager import *
 from sccd.util.debug import print_debug
-from sccd.cd.cd import *
+from sccd.cd.static.cd import *
 
 def _dummy_output_callback(output_event):
     pass
 
-# The Controller class is a primitive that can be used to build backends of any kind:
+# The Controller class' sole responsibility is running a model.
+# Its interface is a primitive that can be used to build backends of any kind:
 # Threads, integration with existing event loop, game loop, test framework, ...
+# All methods take control of the current thread and are synchronous (blocking).
 # The Controller class itself is NOT thread-safe.
 class Controller:
     __slots__ = ["cd", "object_manager", "queue", "simulated_time", "run_until"]
@@ -25,7 +27,6 @@ class Controller:
         def __repr__(self):
             return "QueueEntry("+str(self.event)+")"
 
-
     def __init__(self, cd: AbstractCD, output_callback: Callable[[OutputEvent],None] = _dummy_output_callback):
         cd.globals.assert_ready()
         self.cd = cd
@@ -45,18 +46,18 @@ class Controller:
 
         if DEBUG:
             self.cd.print()
-            print("Model delta is %s" % str(self.cd.globals.delta))
+            print("Model delta is %s" % str(self.cd.get_delta()))
 
-        # First call to 'run_until' method initializes
+        # This is a 'hack', the attribute run_until First call to 'run_until' method initializes
         self.run_until = self._run_until_w_initialize
 
-
-    def get_model_delta(self) -> Duration:
-        return self.cd.globals.delta
-
+    # Lower-level way of adding an event to the queue
+    # See also method 'add_input'
     def schedule(self, timestamp: int, event: InternalEvent, instances: List[Instance]):
         self.queue.add(timestamp, Controller.EventQueueEntry(event, instances))
 
+    # Low-level utility function, intended to map a port name to a list of instances
+    # For now, all known ports map to all instances (i.e. all ports are broadcast ports)
     def inport_to_instances(self, port: str) -> List[Instance]:
         try:
             self.cd.globals.inports.get_id(port)
@@ -68,6 +69,8 @@ class Controller:
         # TODO: multicast event only to instances that subscribe to this port.
         return self.object_manager.instances
 
+    # Higher-level way of adding an event to the queue.
+    # See also method 'schedule'
     def add_input(self, timestamp: int, port: str, event_name: str, params = []):
         try:
             event_id = self.cd.globals.events.get_id(event_name)
@@ -79,10 +82,11 @@ class Controller:
 
         self.schedule(timestamp, event, instances)
 
-    # Get timestamp of next entry in event queue
+    # Get timestamp of earliest entry in event queue
     def next_wakeup(self) -> Optional[int]:
         return self.queue.earliest_timestamp()
 
+    # Before calling _run_until, this method should be called instead, exactly once.
     def _run_until_w_initialize(self, now: Optional[int]):
         # first run...
         # initialize the object manager, in turn initializing our default class
@@ -98,8 +102,10 @@ class Controller:
         # Let's try it out :)
         self.run_until(now)
 
-    # 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.
+    # Run until the event queue has no more events smaller than given timestamp.
+    # If no timestamp is given (now = None), the "given timestamp" is considered to be +infinity,
+    # meaning the controller will run until event queue is empty.
+    # This method is blocking.
     def _run_until(self, now: Optional[int]):
         # Actual "event loop"
         for timestamp, entry in self.queue.due(now):

+ 4 - 1
src/sccd/realtime/eventloop.py

@@ -4,6 +4,7 @@ from sccd.controller.controller import *
 
 ScheduledID = Any
 
+# The interface for declaring 3rd party event loop implementations
 @dataclass
 class EventLoopImplementation(ABC):
     @abstractmethod
@@ -21,7 +22,7 @@ class EventLoopImplementation(ABC):
 
 class EventLoop:
     def __init__(self, controller: Controller, event_loop: EventLoopImplementation, time_impl: TimeImplementation = DefaultTimeImplementation):
-        delta = controller.get_model_delta()
+        delta = controller.cd.get_delta()
         self.timer = Timer(time_impl, unit=delta) # will give us timestamps in model unit
         self.controller = controller
         self.event_loop = event_loop
@@ -29,6 +30,8 @@ class EventLoop:
         # got to convert from model unit to eventloop native unit for scheduling
         self.to_event_loop_unit = lambda x: int(get_conversion_f(delta, event_loop.time_unit())(x))
 
+        # ID of currently scheduled task.
+        # The actual type of this attribute depends on the event loop implementation.
         self.scheduled = None
 
         # Keeps the model responsive if we cannot keep up with wallclock time.

+ 9 - 2
src/sccd/realtime/time.py

@@ -25,7 +25,8 @@ if sys.version_info.minor >= 7:
         get_time=perf_counter_ns) # returns int
     DefaultTimeImplementation = PerfCounterNSTime
 
-
+# A simple "chrono" timer, using a configurable time function to measure wall-clock time passed since its start,
+# returning elapsed times in a configurable fixed unit, efficiently.
 class Timer:
     def __init__(self, impl: TimeImplementation, unit: Duration):
         self.impl = impl
@@ -36,6 +37,7 @@ class Timer:
             from_unit=self.impl.time_unit, to_unit=unit)(x))
         self.paused = True
 
+    # Start (if not paused) or continue timer (if paused)
     def start(self):
         self.started_at = self.convert(self.impl.get_time()) + self.paused_at
         self.paused = False
@@ -44,9 +46,14 @@ class Timer:
         self.paused_at = self.now()
         self.paused = True
 
+    # The number returned will be the wall-clock time elapsed since the call to start(),
+    # divided by the 'unit' passed to the constructor of this object, minus of course
+    # the time elapsed while the timer was 'paused'.
     def now(self) -> int:
         assert not self.paused
         return self.convert(self.impl.get_time()) - self.started_at
 
-    def is_paused(self):
+    def is_paused(self) -> bool:
         return self.paused
+
+    # Note: We could add a reset() method, but we simply don't need it for our purposes :)

+ 3 - 2
src/sccd/statechart/parser/text.py

@@ -2,7 +2,7 @@ import os
 from lark import Lark
 from sccd.action_lang.parser import text as action_lang
 from sccd.statechart.static.tree import *
-from sccd.cd.globals import *
+from sccd.statechart.static.globals import *
 
 _grammar_dir = os.path.dirname(__file__)
 
@@ -11,13 +11,14 @@ with open(os.path.join(_grammar_dir, "statechart.g")) as file:
 
 
 # Lark transformer for parsetree-less parsing of expressions
+# Extends action language's ExpressionTransformer
 class StatechartTransformer(action_lang.ExpressionTransformer):
   def __init__(self):
     super().__init__()
     self.globals: Globals = None
 
 
-  # override
+  # override: all durations must be added to 'globals'
   def duration_literal(self, node):
     val = int(node[0])
     suffix = node[1]

+ 3 - 0
src/sccd/statechart/static/action.py

@@ -7,6 +7,9 @@ from sccd.statechart.dynamic.event import *
 
 @dataclass
 class SCDurationLiteral(DurationLiteral):
+    # optimization: to save us from runtime duration conversions,
+    # all durations in statechart are divided by model delta,
+    # and evaluate to integer instead of duration type.
     opt: Optional[int] = None
 
     # override

+ 7 - 1
src/sccd/cd/globals.py

@@ -1,3 +1,6 @@
+# TODO: move this module to 'statechart.static' !
+# even though the 'globals' applies to the 'class diagram' (module 'cd'), the statechart is aware of it, and moving this module would make for a cleaner 'import' hierarchy
+
 from typing import *
 from sccd.util.namespace import *
 from sccd.util.duration import *
@@ -17,10 +20,13 @@ class Globals:
     self.durations: List[SCDurationLiteral] = []
 
     # The smallest unit for all durations in the model.
+    # Upon simulation, all timestamps are multiples of this value.
     # Calculated after all expressions have been parsed, based on all DurationLiterals.
     self.delta: Optional[Duration] = None
 
-  # delta: if set, this will be the model delta. otherwise, model delta will be the GCD of all durations registered.
+  # parameter delta: if set, this will be the model delta.
+  # otherwise, model delta will be the GCD of all durations registered.
+  # typically, a 'delta' of 100us to 1ms is desirable because this will also be the 'precision' of input event timestamps.
   def init_durations(self, delta: Optional[Duration]):
     gcd_delta = gcd(*(d.d for d in self.durations))
 

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

@@ -204,7 +204,7 @@ class Transition(Freezable):
         return termcolor.colored("%s 🡪 %s" % (self.source.opt.full_name, self.targets[0].opt.full_name), 'green')
 
 
-# Data that is generated for each state.
+# 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"]
     def __init__(self):
@@ -219,7 +219,8 @@ class StateOptimization(Freezable):
         self.ancestors: Bitmap = Bitmap()
         self.descendants: Bitmap = Bitmap()
 
-        # Tuple for each children that is HistoryState: (history-id, history mask)
+        # 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]] = []
 
         # Triggers of outgoing transitions that are AfterTrigger.

+ 1 - 1
src/sccd/test/run.py

@@ -5,7 +5,7 @@ import queue
 import functools
 from sccd.util.os_tools import *
 from sccd.util.debug import *
-from sccd.cd.cd import *
+from sccd.cd.static.cd import *
 from sccd.controller.controller import *
 from sccd.test.xml import *
 from sccd.util import timer

+ 2 - 2
src/sccd/test/xml.py

@@ -1,7 +1,7 @@
 from sccd.statechart.parser.xml import *
-from sccd.cd.globals import *
+from sccd.statechart.static.globals import *
 from sccd.statechart.dynamic.event import InternalEvent
-from sccd.cd.cd import *
+from sccd.cd.static.cd import *
 
 _empty_scope = Scope("test", parent=None)
 

+ 1 - 1
src/sccd/util/duration.py

@@ -245,7 +245,7 @@ def gcd(*iterable: Iterable[Duration]) -> Duration:
   return functools.reduce(gcd_pair, iterable, _zero)
 
 # Useful for efficiently converting many values from some fixed unit to some other fixed unit.
-def get_conversion_f(from_unit: Duration, to_unit: Duration):
+def get_conversion_f(from_unit: Duration, to_unit: Duration) -> Callable[[int], int]:
   if from_unit is _zero or to_unit is _zero:
     raise Exception("Cannot convert between zero-duration units")