Bläddra i källkod

New approach to handling event parameters and access to 'state configuration' + cleanup debris. Rust codegen broken.

Joeri Exelmans 4 år sedan
förälder
incheckning
aabbf85114

+ 15 - 3
notes.txt

@@ -4,9 +4,21 @@ Notes about the future of SCCD
 Long-term vision:
 
   - code generation, work in progress:
-    - Rust as a target. Very portable:
-      - compile to WebAssembly (SCCD in browser)
-      - call from Python, Java, ...
+    - Rust as a target. Benefits:
+      - Very portable:
+        - compile to WebAssembly (SCCD in browser!)
+        - Rust compiler can produce C-libraries: call from Python, Java, ...
+      - Very good performance
+        - Zero-cost abstractions (optimize generated code for readability)
+            -> "Have your cake, and eat it too"
+      - Safety
+
+  - improve action language:
+    - highest priority:
+      - interfacing with external code only works with Python modules
+    - less important, deal with it when it becomes a problem: 
+      - statically typed (good), but no generics (bad)
+      - user cannot define his/her own types
 
   - dynamic creation/destruction of instances (the "CD" part of SCCD)
 

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

@@ -86,8 +86,6 @@ class Identifier(LValue):
     is_lvalue: Optional[bool] = None
     is_init: Optional[bool] = None
 
-    # is_function_call_result: Optional[SCCDFunctionCallResult] = None
-
     def init_expr(self, scope: Scope) -> SCCDType:
         self.offset, self.type = scope.get_rvalue(self.name)
         self.is_init = False
@@ -98,9 +96,6 @@ class Identifier(LValue):
         return self.type
 
     def init_lvalue(self, scope: Scope, rhs_t: SCCDType, rhs: Expression) -> bool:
-        # if isinstance(rhs_t, SCCDFunctionCallResult):
-            # self.is_function_call_result = rhs_t
-            # rhs_t = rhs_t.return_type
         self.is_lvalue = True
         self.offset, self.is_init = scope.put_lvalue(self.name, rhs_t, rhs)
         return self.is_init

+ 0 - 21
src/sccd/action_lang/static/types.py

@@ -147,27 +147,6 @@ class SCCDClosureObject(SCCDType):
         return "Closure(scope=%s, func=%s)" % (self.scope.name, self.function_type._str())
 
 
-# @dataclass(frozen=True, repr=False)
-# class SCCDFunctionCallResult(SCCDType):
-#     function_type: SCCDFunction
-#     return_type: SCCDType
-
-#     def _str(self):
-#         return "CallResult(%s)" % self.return_type._str()
-
-#     def is_eq(self, other):
-#         return return_type.is_eq(other)
-
-# @dataclass(frozen=True, repr=False)
-# class SCCDScope(SCCDType):
-#     scope: 'Scope'
-
-#     def _str(self):
-#         return "Scope(%s)" % scope.name
-
-#     def is_eq(self, other):
-#         return self.scope is other.scope
-
 SCCDBool = _SCCDSimpleType("bool", eq=True, bool_cast=True)
 SCCDInt = _SCCDSimpleType("int", neg=True, summable=True, eq=True, ord=True, bool_cast=True)
 SCCDFloat = _SCCDSimpleType("float", neg=True, summable=True, eq=True, ord=True)

+ 26 - 13
src/sccd/statechart/dynamic/statechart_execution.py

@@ -33,10 +33,11 @@ class StatechartExecution:
     def initialize(self):
         self.configuration = self.statechart.tree.initial_states
 
-        ctx = EvalContext(execution=self, events=[], memory=self.rhs_memory)
         if self.statechart.datamodel is not None:
             self.statechart.datamodel.exec(self.rhs_memory)
 
+        ctx = EvalContext(memory=self.rhs_memory, execution=self, params=[])
+
         for state in self.statechart.tree.bitmap_to_states(self.configuration):
             print_debug(termcolor.colored('  ENTER %s'%state.full_name, 'green'))
             _perform_actions(ctx, state.enter)
@@ -62,7 +63,7 @@ class StatechartExecution:
                         enter_ids |= self.history_values[t.target_history_id]
                     enter_set = self.statechart.tree.bitmap_to_states(enter_ids)
 
-                ctx = EvalContext(execution=self, events=events, memory=self.rhs_memory)
+                ctx = EvalContext(memory=self.rhs_memory, execution=self, params=get_event_params(events, t.trigger))
 
                 print_debug("fire " + str(t))
 
@@ -84,11 +85,7 @@ class StatechartExecution:
 
                 # execute transition action(s)
                 with timer.Context("actions"):
-                    self.rhs_memory.push_frame(t.scope) # make room for event parameters on stack
-                    if t.trigger:
-                        t.trigger.copy_params_to_stack(events, self.rhs_memory)
                     _perform_actions(ctx, t.actions)
-                    self.rhs_memory.pop_frame()
 
                 with timer.Context("enter states"):
                     for s in enter_set:
@@ -105,17 +102,13 @@ class StatechartExecution:
             e.args = ("During execution of transition %s:\n" % str(t) +str(e),)
             raise
 
+
     def check_guard(self, t: Transition, events: List[InternalEvent]) -> bool:
         try:
             if t.guard is None:
                 return True
             else:
-                self.gc_memory.push_frame(t.scope)
-                # Guard conditions can also refer to event parameters
-                if t.trigger:
-                    t.trigger.copy_params_to_stack(events, self.gc_memory)
-                result = t.guard.eval(self.gc_memory)
-                self.gc_memory.pop_frame()
+                result = t.guard.eval(self.gc_memory)(self.gc_memory, self.configuration, *get_event_params(events, t.trigger))
                 return result
         except Exception as e:
             e.args = ("While checking guard of transition %s:\n" % str(t) + str(e),)
@@ -124,7 +117,7 @@ class StatechartExecution:
     def _start_timers(self, triggers: List[AfterTrigger]):
         for after in triggers:
             delay: Duration = after.delay.eval(
-                EvalContext(execution=self, events=[], memory=self.gc_memory))
+                EvalContext(memory=self.gc_memory, execution=self, params=[]))
             self.timer_ids[after.after_id] = self.schedule_callback(delay, InternalEvent(id=after.id, name=after.name, params=[]), [self.instance])
 
     def _cancel_timers(self, triggers: List[AfterTrigger]):
@@ -150,3 +143,23 @@ class StatechartExecution:
 def _perform_actions(ctx: EvalContext, actions: List[Action]):
     for a in actions:
         a.exec(ctx)
+
+
+def get_event_params(events: List[InternalEvent], trigger) -> List[any]:
+    # Both 'events' and 'self.enabling' are sorted by event ID,
+    # this way we have to iterate over each of both lists at most once.
+    iterator = iter(trigger.enabling)
+    params = []
+    try:
+        event_decl = next(iterator)
+        for e in events:
+            if e.id < event_decl.id:
+                continue
+            else:
+                while e.id > event_decl.id:
+                    event_decl = next(iterator)
+                for p in e.params:
+                    params.append(p)
+    except StopIteration:
+        pass
+    return params

+ 42 - 30
src/sccd/statechart/parser/xml.py

@@ -88,15 +88,36 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
       transitions = [] # All of the statechart's transitions accumulate here, cause we still need to find their targets, which we can't do before the entire state tree has been built. We find their targets when encoutering the </root> closing tag.
       after_id = 0 # After triggers need unique IDs within the scope of the statechart model
 
-      def actions_rules(scope):
+      # A transition's guard expression and action statements can read the transition's event parameters, and also possibly the current state configuration. We therefore now wrap these into a function with a bunch of parameters for those values that we want to bring into scope.
+      def wrap_transition_params(expr_or_stmt, trigger: Trigger):
+        if isinstance(expr_or_stmt, Statement):
+          # Transition's action code
+          body = expr_or_stmt
+        elif isinstance(expr_or_stmt, Expression):
+          # Transition's guard
+          body = ReturnStatement(expr=expr_or_stmt)
+        else:
+          raise Exception("Unexpected error in parser")
+        # The joy of writing expressions in abstract syntax:
+        wrapped = FunctionDeclaration(
+          params_decl=
+            # The param '@conf' (which, on purpose, is an illegal identifier in textual concrete syntax, to prevent naming collisions) will contain the statechart's configuration as a bitmap (SCCDInt). This parameter is currently only used in the expansion of the INSTATE-macro.
+            [ParamDecl(name="@conf", formal_type=SCCDInt)]
+            # Plus all the parameters of the enabling events of the transition's trigger:
+            + [param for event in trigger.enabling for param in event.params_decl],
+          body=body)
+        return wrapped
+
+      def actions_rules(scope, wrap_trigger: Trigger = EMPTY_TRIGGER):
 
         def parse_raise(el):
           params = []
           def parse_param(el):
             expr_text = require_attribute(el, "expr")
             expr = parse_expression(globals, expr_text)
-            expr.init_expr(scope)
-            params.append(expr)
+            function = wrap_transition_params(expr, trigger=wrap_trigger)
+            function.init_expr(scope)
+            params.append(function)
 
           def finish_raise():
             event_name = require_attribute(el, "event")
@@ -120,9 +141,9 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
         def parse_code(el):
           def finish_code():
             block = parse_block(globals, el.text)
-            # local_scope = Scope("local", scope)
-            block.init_stmt(scope)
-            return Code(block)
+            function = wrap_transition_params(block, trigger=wrap_trigger)
+            function.init_expr(scope)
+            return Code(function)
           return ([], finish_code)
 
         return {"raise": parse_raise, "code": parse_code}
@@ -154,7 +175,6 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
 
         def common(el, constructor):
           short_name = require_attribute(el, "id")
-          # if short_name == "":
           match = re.match("[A-Za-z_][A-Za-z_0-9]*", short_name)
           if match is None or match[0] != short_name:
             raise XmlError("invalid id")
@@ -199,20 +219,19 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
         def parse_onentry(el):
           def finish_onentry(*actions):
             parent.enter = actions
-          return (actions_rules(statechart.scope), finish_onentry)
+          return (actions_rules(scope=statechart.scope), finish_onentry)
 
         def parse_onexit(el):
           def finish_onexit(*actions):
             parent.exit = actions
-          return (actions_rules(statechart.scope), finish_onexit)
+          return (actions_rules(scope=statechart.scope), finish_onexit)
 
         def parse_transition(el):
           if parent is root:
             raise XmlError("Root cannot be source of a transition.")
 
-          scope = Scope("transition", parent=statechart.scope)
           target_string = require_attribute(el, "target")
-          transition = Transition(source=parent, target_string=target_string, scope=scope)
+          transition = Transition(source=parent, target_string=target_string)
 
           have_event_attr = False
           def parse_attr_event(event):
@@ -225,13 +244,6 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             # Allows us to save time later.
             positive_events.sort(key=lambda e: e.id)
 
-            def add_params_to_scope(e: EventDecl):
-              for i,p in enumerate(e.params_decl):
-                p.init_param(scope)
-
-            for e in itertools.chain(positive_events, negative_events):
-              add_params_to_scope(e)
-
             if not negative_events:
               transition.trigger = Trigger(positive_events)
               statechart.internal_events |= transition.trigger.enabling_bitmap
@@ -244,25 +256,25 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             if have_event_attr:
               raise XmlError("Cannot specify 'after' and 'event' at the same time.")
             after_expr = parse_expression(globals, after)
-            after_type = after_expr.init_expr(scope)
+            after_type = after_expr.init_expr(statechart.scope)
             check_duration_type(after_type)
-            # after-events should only be generated by the runtime
-            # by putting a '+' in the event name (which isn't an allowed character in the parser),
-            # we can be certain that the user will never generate a valid after-event.
-            # if DEBUG:
-            #   event_name = "+%d_%s_%s" % (after_id, parent.short_name, target_string) # transition gets unique event name
-            # else:
+            # After-events should only be generated by the runtime.
+            # By putting a '+' in the event name (which isn't an allowed character in the parser), we ensure that the user will never accidentally (careless) or purposefully (evil) generate a valid after-event.
             event_name = "+%d" % after_id
             transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, after_id, after_expr)
             statechart.internal_events |= transition.trigger.enabling_bitmap
             after_id += 1
 
           def parse_attr_cond(cond):
-            expr = parse_expression(globals, cond)
-            guard_type = expr.init_expr(scope)
-            if guard_type is not SCCDBool:
+            # Transition's guard expression
+            guard_expr = parse_expression(globals, cond)
+            guard_function = wrap_transition_params(guard_expr, transition.trigger)
+            guard_type = guard_function.init_expr(statechart.scope)
+
+            if guard_type.return_type is not SCCDBool:
               raise XmlError("Guard should be an expression evaluating to 'bool'.")
-            transition.guard = expr
+
+            transition.guard = guard_function
 
           if_attribute(el, "event", parse_attr_event)
           if_attribute(el, "after", parse_attr_after)
@@ -273,7 +285,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             transitions.append((transition, el))
             parent.transitions.append(transition)
 
-          return (actions_rules(scope), finish_transition)
+          return (actions_rules(scope=statechart.scope, wrap_trigger=transition.trigger), finish_transition)
 
         return {"state": parse_state, "parallel": parse_parallel, "history": parse_history, "onentry": parse_onentry, "onexit": parse_onexit, "transition": parse_transition}
 

+ 11 - 10
src/sccd/statechart/static/action.py

@@ -4,6 +4,7 @@ from abc import *
 from sccd.action_lang.static.expression import *
 from sccd.action_lang.static.statement import *
 from sccd.statechart.dynamic.event import *
+from sccd.util.bitmap import Bitmap
 
 @dataclass
 class SCDurationLiteral(DurationLiteral):
@@ -22,11 +23,11 @@ class SCDurationLiteral(DurationLiteral):
 
 @dataclass
 class EvalContext:
-    __slots__ = ["execution", "events", "memory"]
+    __slots__ = ["memory", "execution", "params"]
     
-    execution: 'StatechartExecution'
-    events: List['Event']
     memory: 'MemoryInterface'
+    execution: 'StatechartExecution'
+    params: List[any]
 
 @dataclass
 class Action(ABC, Visitable):
@@ -41,11 +42,11 @@ class Action(ABC, Visitable):
 @dataclass
 class RaiseEvent(Action):
     name: str
-    params: List[Expression]
+    params: List[FunctionDeclaration]
 
 
-    def _eval_params(self, memory: MemoryInterface) -> List[Any]:
-        return [p.eval(memory) for p in self.params]
+    def _eval_params(self, ctx: EvalContext) -> List[Any]:
+        return [p.eval(ctx.memory)(ctx.memory, ctx.execution.configuration, *ctx.params) for p in self.params]
 
 @dataclass
 class RaiseInternalEvent(RaiseEvent):
@@ -55,7 +56,7 @@ class RaiseInternalEvent(RaiseEvent):
         return '^'+self.name
 
     def exec(self, ctx: EvalContext):
-        params = self._eval_params(ctx.memory)
+        params = self._eval_params(ctx)
         ctx.execution.raise_internal(
             InternalEvent(id=self.event_id, name=self.name, params=params))
 
@@ -64,7 +65,7 @@ class RaiseOutputEvent(RaiseEvent):
     outport: str
 
     def exec(self, ctx: EvalContext):
-        params = self._eval_params(ctx.memory)
+        params = self._eval_params(ctx)
         ctx.execution.raise_output(
             OutputEvent(port=self.outport, name=self.name, params=params))
 
@@ -73,10 +74,10 @@ class RaiseOutputEvent(RaiseEvent):
 
 @dataclass
 class Code(Action):
-    block: Block
+    block: FunctionDeclaration
 
     def exec(self, ctx: EvalContext):
-        self.block.exec(ctx.memory)
+        self.block.eval(ctx.memory)(ctx.memory, ctx.execution.configuration, *ctx.params)
 
     def render(self) -> str:
         return '/'+self.block.render()

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

@@ -231,11 +231,11 @@ EMPTY_TRIGGER = Trigger(enabling=[])
 class Transition:
     source: State
     target_string: Optional[str]
-    scope: Scope
+    # scope: Scope
 
     target: State = None
 
-    guard: Optional[Expression] = None
+    guard: Optional[FunctionDeclaration] = None
     actions: List[Action] = field(default_factory=list)
     trigger: Trigger = EMPTY_TRIGGER