Bläddra i källkod

Add EvalContext data class to group parameters passed to Expression.eval, Statement.exec, Variable.load and Variable.store.

Joeri Exelmans 5 år sedan
förälder
incheckning
19ac39f437

+ 2 - 2
README.md

@@ -4,7 +4,7 @@
 
 ### Mandatory
 
-* Python >= 3.6 or PyPy >= 7.3.0 (Compatible with Python 3.6)
+* CPython >= 3.6 or PyPy >= 7.3.0 (Compatible with Python 3.6)
 * The following packages from PyPi:
   * [lark-parser](https://github.com/lark-parser/lark) for parsing state references and action code
   * [lxml](https://lxml.de/) for parsing the SCCD XML input format
@@ -14,4 +14,4 @@
 
 ### Optional
 
-* [state-machine-cat](https://github.com/sverweij/state-machine-cat) to render statecharts as SVG images.
+* [state-machine-cat](https://github.com/sverweij/state-machine-cat) to render statecharts as SVG images. Runs on NodeJS, installable from NPM.

+ 12 - 0
src/sccd/execution/builtin_scope.py

@@ -0,0 +1,12 @@
+import math
+from sccd.execution.statechart_state import *
+
+
+builtin_scope = Scope("builtin", None)
+
+def _in_state(ctx: EvalContext, state_list: List[str]) -> bool:
+  return StatechartState.in_state(ctx.current_state, state_list)
+
+builtin_scope.add_function("INSTATE", _in_state)
+
+builtin_scope.add_function("log10", lambda _1,_2,_3,i: math.log10(i))

+ 6 - 9
src/sccd/execution/statechart_state.py

@@ -6,12 +6,6 @@ from sccd.util.bitmap import *
 from sccd.syntax.scope import *
 
 
-def _in_state(current_state, events, memory, state_list: List[str]) -> bool:
-  return StatechartState.in_state(current_state, state_list)
-
-builtin_scope = Scope("builtin", None)
-builtin_scope.add_function("INSTATE", _in_state)
-
 # Set of current states etc.
 class StatechartState:
 
@@ -122,7 +116,8 @@ class StatechartState:
       if t.guard is None:
           return True
       else:
-          result = t.guard.eval(self, events, self.gc_memory)
+          result = t.guard.eval(
+            EvalContext(current_state=self, events=events, memory=self.gc_memory))
           self.gc_memory.flush_temp()
           return result
 
@@ -139,12 +134,14 @@ class StatechartState:
                   OutputPortTarget(a.outport),
                   a.time_offset))
           elif isinstance(a, Code):
-              a.block.exec(self, events, self.rhs_memory)
+              a.block.exec(
+                EvalContext(current_state=self, events=events, memory=self.rhs_memory))
               self.rhs_memory.flush_temp()
 
   def _start_timers(self, triggers: List[AfterTrigger]):
       for after in triggers:
-          delay: Duration = after.delay.eval(self, [], self.gc_memory)
+          delay: Duration = after.delay.eval(
+            EvalContext(current_state=self, events=[], memory=self.gc_memory))
           timer_id = self._next_timer_id(after)
           self.gc_memory.flush_temp()
           self.output.append(OutputEvent(

+ 5 - 3
src/sccd/parser/statechart_parser.py

@@ -4,7 +4,9 @@ from sccd.parser.expression_parser import *
 from sccd.syntax.statechart import *
 from sccd.syntax.tree import *
 from sccd.execution.event import *
-from sccd.execution import statechart_state
+from sccd.execution import builtin_scope
+
+_blank_eval_context = EvalContext(current_state=None, events=[], memory=None)
 
 # An Exception that occured while visiting an XML element.
 # It will show a fragment of the source file and the line number of the error.
@@ -435,7 +437,7 @@ class StatechartParser(TreeParser):
 
     parsed = parse_expression(globals, expr=expr)
     rhs_type = parsed.init_rvalue(scope)
-    val = parsed.eval(None, [], None)
+    val = parsed.eval(_blank_eval_context)
     scope.add_variable_w_initial(name=id, initial=val)
 
   def start_datamodel(self, el):
@@ -452,7 +454,7 @@ class StatechartParser(TreeParser):
     ext_file = el.get("src")
     statechart = None
     if ext_file is None:
-      statechart = StatechartVariableSemantics(tree=None, semantics=VariableSemantics(), scope=Scope("instance", parent=statechart_state.builtin_scope))
+      statechart = StatechartVariableSemantics(tree=None, semantics=VariableSemantics(), scope=Scope("instance", parent=builtin_scope.builtin_scope))
     elif self.load_external:
       ext_file_path = os.path.join(os.path.dirname(src_file), ext_file)
       self.statecharts.push([])

+ 52 - 54
src/sccd/syntax/expression.py

@@ -4,24 +4,25 @@ from dataclasses import *
 from sccd.util.duration import *
 from sccd.syntax.scope import *
 
-# to inspect types in Python 3.6 and 3.7
+# to inspect types in Python 3.6 and 3.7, we rely on a backporting package
 # Python 3.8 already has this in its 'typing' module
 import sys
 if sys.version_info.minor < 8:
     from typing_inspect import get_args
 
+
 class Expression(ABC):
     # 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:
+    def init_rvalue(self, scope: Scope) -> type:
         pass
 
     # Evaluation should NOT have side effects.
     # Motivation is that the evaluation of a guard condition cannot have side effects.
     @abstractmethod
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         pass
 
 # The LValue type is any type that can serve as an expression OR an LValue (left hand of assignment)
@@ -30,43 +31,40 @@ class Expression(ABC):
 class LValue(Expression):
     # Initialize the LValue as an LValue. 
     @abstractmethod
-    def init_lvalue(self, scope, expected_type: type):
+    def init_lvalue(self, scope: Scope, expected_type: type):
         pass
 
     @abstractmethod
-    def eval_lvalue(self, current_state, events, memory) -> Variable:
+    def eval_lvalue(self, ctx: EvalContext) -> Variable:
         pass
 
     # LValues can also serve as expressions!
-    def eval(self, current_state, events, memory):
-        variable = self.eval_lvalue(current_state, events, memory)
-        return variable.load(events, memory)
-
+    def eval(self, ctx: EvalContext):
+        variable = self.eval_lvalue(ctx)
+        return variable.load(ctx)
 
 @dataclass
 class Identifier(LValue):
     name: str
-
     variable: Optional[Variable] = None
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         assert self.variable is None
         self.variable = scope.get(self.name)
         # print("init rvalue", self.name, "as", self.variable)
         return self.variable.type
 
-    def init_lvalue(self, scope, expected_type):
+    def init_lvalue(self, scope: Scope, expected_type):
         assert self.variable is None
         self.variable = scope.put_variable_assignment(self.name, expected_type)
         # print("init lvalue", self.name, "as", self.variable)
 
-    def eval_lvalue(self, current_state, events, memory) -> Variable:
+    def eval_lvalue(self, ctx: EvalContext) -> Variable:
         return self.variable
 
     def render(self):
         return self.name
 
-
 @dataclass
 class FunctionCall(Expression):
     function: Expression
@@ -74,7 +72,7 @@ class FunctionCall(Expression):
 
     type: Optional[type] = None
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         function_type = self.function.init_rvalue(scope)
         if not isinstance(function_type, Callable):
             raise Exception("Function call: Expression '%s' is not callable" % self.function.render())
@@ -87,11 +85,11 @@ class FunctionCall(Expression):
                 raise Exception("Function call: Actual types '%s' differ from formal types '%s'" % (actual_types, formal_types))
         return self.type
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         # print(self.function)
-        f = self.function.eval(current_state, events, memory)
-        p = [p.eval(current_state, events, memory) for p in self.parameters]
-        return f(current_state, events, memory, *p)
+        f = self.function.eval(ctx)
+        p = [p.eval(ctx) for p in self.parameters]
+        return f(ctx, *p)
 
     def render(self):
         return self.function.render()+'('+','.join([p.render() for p in self.parameters])+')'
@@ -101,10 +99,10 @@ class FunctionCall(Expression):
 class StringLiteral(Expression):
     string: str
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return str
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         return self.string
 
     def render(self):
@@ -115,10 +113,10 @@ class StringLiteral(Expression):
 class IntLiteral(Expression):
     i: int 
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return int
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         return self.i
 
     def render(self):
@@ -128,10 +126,10 @@ class IntLiteral(Expression):
 class BoolLiteral(Expression):
     b: bool 
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return bool
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         return self.b
 
     def render(self):
@@ -141,10 +139,10 @@ class BoolLiteral(Expression):
 class DurationLiteral(Expression):
     d: Duration
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return Duration
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         return self.d
 
     def render(self):
@@ -156,7 +154,7 @@ class Array(Expression):
 
     type: Optional[type] = None
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         for e in self.elements:
             t = e.init_rvalue(scope)
             if self.type and self.type != t:
@@ -165,8 +163,8 @@ class Array(Expression):
 
         return List[self.type]
 
-    def eval(self, current_state, events, memory):
-        return [e.eval(current_state, events, memory) for e in self.elements]
+    def eval(self, ctx: EvalContext):
+        return [e.eval(ctx) for e in self.elements]
 
     def render(self):
         return '['+','.join([e.render() for e in self.elements])+']'
@@ -177,11 +175,11 @@ class Array(Expression):
 class Group(Expression):
     subexpr: Expression
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return self.subexpr.init_rvalue(scope)
 
-    def eval(self, current_state, events, memory):
-        return self.subexpr.eval(current_state, events, memory)
+    def eval(self, ctx: EvalContext):
+        return self.subexpr.eval(ctx)
 
     def render(self):
         return '('+self.subexpr.render()+')'
@@ -192,14 +190,14 @@ class BinaryExpression(Expression):
     operator: str # token name from the grammar.
     rhs: Expression
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         lhs_t = self.lhs.init_rvalue(scope)
         rhs_t = self.rhs.init_rvalue(scope)
         if lhs_t != rhs_t:
             raise Exception("Mixed LHS and RHS types in '%s' expression: %s and %s" % (self.operator, str(lhs_t), str(rhs_t)))
         return lhs_t
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         
         return {
             # "AND": lambda x,y: x and y,
@@ -218,21 +216,21 @@ class BinaryExpression(Expression):
             # "MOD": lambda x,y: x % y,
             # "EXP": lambda x,y: x ** y,
 
-            "and": lambda x,y: x.eval(current_state, events, memory) and y.eval(current_state, events, memory),
-            "or": lambda x,y: x.eval(current_state, events, memory) or y.eval(current_state, events, memory),
-            "==": lambda x,y: x.eval(current_state, events, memory) == y.eval(current_state, events, memory),
-            "!=": lambda x,y: x.eval(current_state, events, memory) != y.eval(current_state, events, memory),
-            ">": lambda x,y: x.eval(current_state, events, memory) > y.eval(current_state, events, memory),
-            ">=": lambda x,y: x.eval(current_state, events, memory) >= y.eval(current_state, events, memory),
-            "<": lambda x,y: x.eval(current_state, events, memory) < y.eval(current_state, events, memory),
-            "<=": lambda x,y: x.eval(current_state, events, memory) <= y.eval(current_state, events, memory),
-            "+": lambda x,y: x.eval(current_state, events, memory) + y.eval(current_state, events, memory),
-            "-": lambda x,y: x.eval(current_state, events, memory) - y.eval(current_state, events, memory),
-            "*": lambda x,y: x.eval(current_state, events, memory) * y.eval(current_state, events, memory),
-            "/": lambda x,y: x.eval(current_state, events, memory) / y.eval(current_state, events, memory),
-            "//": lambda x,y: x.eval(current_state, events, memory) // y.eval(current_state, events, memory),
-            "%": lambda x,y: x.eval(current_state, events, memory) % y.eval(current_state, events, memory),
-            "**": lambda x,y: x.eval(current_state, events, memory) ** y.eval(current_state, events, memory),
+            "and": lambda x,y: x.eval(ctx) and y.eval(ctx),
+            "or": lambda x,y: x.eval(ctx) or y.eval(ctx),
+            "==": lambda x,y: x.eval(ctx) == y.eval(ctx),
+            "!=": lambda x,y: x.eval(ctx) != y.eval(ctx),
+            ">": lambda x,y: x.eval(ctx) > y.eval(ctx),
+            ">=": lambda x,y: x.eval(ctx) >= y.eval(ctx),
+            "<": lambda x,y: x.eval(ctx) < y.eval(ctx),
+            "<=": lambda x,y: x.eval(ctx) <= y.eval(ctx),
+            "+": lambda x,y: x.eval(ctx) + y.eval(ctx),
+            "-": lambda x,y: x.eval(ctx) - y.eval(ctx),
+            "*": lambda x,y: x.eval(ctx) * y.eval(ctx),
+            "/": lambda x,y: x.eval(ctx) / y.eval(ctx),
+            "//": lambda x,y: x.eval(ctx) // y.eval(ctx),
+            "%": lambda x,y: x.eval(ctx) % y.eval(ctx),
+            "**": lambda x,y: x.eval(ctx) ** y.eval(ctx),
         }[self.operator](self.lhs, self.rhs) # Borrow Python's lazy evaluation
 
     def render(self):
@@ -243,13 +241,13 @@ class UnaryExpression(Expression):
     operator: str # token value from the grammar.
     expr: Expression
 
-    def init_rvalue(self, scope) -> type:
+    def init_rvalue(self, scope: Scope) -> type:
         return self.expr.init_rvalue(scope)
 
-    def eval(self, current_state, events, memory):
+    def eval(self, ctx: EvalContext):
         return {
-            "not": lambda x: not x.eval(current_state, events, memory),
-            "-": lambda x: - x.eval(current_state, events, memory),
+            "not": lambda x: not x.eval(ctx),
+            "-": lambda x: - x.eval(ctx),
         }[self.operator](self.expr)
 
     def render(self):

+ 19 - 14
src/sccd/syntax/scope.py

@@ -4,6 +4,11 @@ from dataclasses import *
 from inspect import signature
 import itertools
 
+@dataclass
+class EvalContext:
+    current_state: 'StatechartState'
+    events: List['Event']
+    memory: 'MemorySnapshot'
 
 @dataclass
 class Value(ABC):
@@ -15,11 +20,11 @@ class Value(ABC):
     pass
 
   @abstractmethod
-  def load(self, events, memory) -> Any:
+  def load(self, ctx: EvalContext) -> Any:
     pass
     
   @abstractmethod
-  def store(self, memory, value):
+  def store(self, ctx: EvalContext, value):
     pass
 
 # Stateless stuff we know about a variable
@@ -31,11 +36,11 @@ class Variable(Value):
   def is_read_only(self) -> bool:
     return False
 
-  def load(self, events, memory) -> Any:
-    return memory.load(self.offset)
+  def load(self, ctx: EvalContext) -> Any:
+    return ctx.memory.load(self.offset)
 
-  def store(self, memory, value):
-    memory.store(self.offset, value)
+  def store(self, ctx: EvalContext, value):
+    ctx.memory.store(self.offset, value)
 
 class EventParam(Variable):
   def __init__(self, name, type, offset, event_name, param_offset):
@@ -46,20 +51,20 @@ class EventParam(Variable):
   def is_read_only(self) -> bool:
     return True
 
-  def load(self, events, memory) -> Any:
-    from_stack = Variable.load(self, events, memory)
+  def load(self, ctx: EvalContext) -> Any:
+    from_stack = Variable.load(self, ctx)
     if from_stack is not None:
       return from_stack
     else:
       # find event in event list and get the parameter we're looking for
-      e = [e for e in events if e.name == self.event_name][0]
+      e = [e for e in ctx.events if e.name == self.event_name][0]
       value = e.params[self.param_offset]
       # "cache" the parameter value on our reserved stack position so the next
       # 'load' will be faster
-      Variable.store(self, memory, value)
+      Variable.store(self, ctx)
       return value
 
-  def store(self, memory, value):
+  def store(self, ctx: EvalContext, value):
     # Bug in the code: should never attempt to write to EventParam
     assert False
 
@@ -72,10 +77,10 @@ class Constant(Value):
   def is_read_only(self) -> bool:
     return True
 
-  def load(self, events, memory) -> Any:
+  def load(self, ctx: EvalContext) -> Any:
     return self.value
 
-  def store(self, memory, value):
+  def store(self, ctx: EvalContext, value):
     # Bug in the code: should never attempt to write to Constant
     assert False
 
@@ -185,7 +190,7 @@ class Scope:
   def add_function(self, name: str, function: Callable) -> Constant:
     sig = signature(function)
     return_type = sig.return_annotation
-    args = list(sig.parameters.values())[3:]
+    args = list(sig.parameters.values())[1:] # hide 'EvalContext' parameter to user
     param_types = [a.annotation for a in args]
     function_type = Callable[param_types, return_type]
     

+ 28 - 23
src/sccd/syntax/statement.py

@@ -5,11 +5,11 @@ from sccd.syntax.expression import *
 class Statement(ABC):
     # Execution typically has side effects.
     @abstractmethod
-    def exec(self, current_state, events, memory):
+    def exec(self, ctx: EvalContext):
         pass
 
     @abstractmethod
-    def init_stmt(self, scope):
+    def init_stmt(self, scope: Scope):
         pass
 
     @abstractmethod
@@ -22,31 +22,29 @@ class Assignment(Statement):
     operator: str # token value from the grammar.
     rhs: Expression
 
-    def init_stmt(self, scope):
+    def init_stmt(self, scope: Scope):
         rhs_t = self.rhs.init_rvalue(scope)
         self.lhs.init_lvalue(scope, rhs_t)
 
-    def exec(self, current_state, events, memory):
-        val = self.rhs.eval(current_state, events, memory)
-        variable = self.lhs.eval_lvalue(current_state, events, memory)
+    def exec(self, ctx: EvalContext):
+        rhs_val = self.rhs.eval(ctx)
+        variable = self.lhs.eval_lvalue(ctx)
 
         def load():
-            return variable.load(events, memory)
-            # return memory.load(offset)
+            return variable.load(ctx)
         def store(val):
-            variable.store(memory, val)
-            # memory.store(offset, val)
+            variable.store(ctx, val)
 
         def assign():
-            store(val)
+            store(rhs_val)
         def increment():
-            store(load() + val)
+            store(load() + rhs_val)
         def decrement():
-            store(load() - val)
+            store(load() - rhs_val)
         def multiply():
-            store(load() * val)
+            store(load() * rhs_val)
         def divide():
-            store(load() / val)
+            store(load() / rhs_val)
 
         {
             "=": assign,
@@ -64,16 +62,16 @@ class Block(Statement):
     stmts: List[Statement]
     scope: Optional[Scope] = None
 
-    def init_stmt(self, scope):
+    def init_stmt(self, scope: Scope):
         self.scope = Scope("local", scope)
         for stmt in self.stmts:
             stmt.init_stmt(self.scope)
 
-    def exec(self, current_state, events, memory):
-        memory.grow_stack(self.scope)
+    def exec(self, ctx: EvalContext):
+        ctx.memory.grow_stack(self.scope)
         for stmt in self.stmts:
-            stmt.exec(current_state, events, memory)
-        memory.shrink_stack()
+            stmt.exec(ctx)
+        ctx.memory.shrink_stack()
 
     def render(self) -> str:
         result = ""
@@ -86,11 +84,18 @@ class Block(Statement):
 class ExpressionStatement(Statement):
     expr: Expression
 
-    def init_stmt(self, scope):
+    def init_stmt(self, scope: Scope):
         self.expr.init_rvalue(scope)
 
-    def exec(self, current_state, events, memory):
-        self.expr.eval(current_state, events, memory)
+    def exec(self, ctx: EvalContext):
+        self.expr.eval(ctx)
 
     def render(self) -> str:
         return self.expr.render()
+
+@dataclass
+class ReturnStatement(Statement):
+    expr: Expression
+
+    def init_stmt(self, scope: Scope):
+        pass