Browse Source

Action language components almost completely independent of statechart components. Reorganize project directory structure.

Joeri Exelmans 5 years ago
parent
commit
5c90ba0c93
39 changed files with 381 additions and 374 deletions
  1. 0 0
      src/sccd/action_lang/__init__.py
  2. 0 0
      src/sccd/action_lang/dynamic/__init__.py
  3. 2 36
      src/sccd/execution/memory.py
  4. 4 1
      src/sccd/parser/grammar/action_language.g
  5. 6 6
      src/sccd/parser/expression_parser.py
  6. 0 0
      src/sccd/action_lang/static/__init__.py
  7. 3 0
      src/sccd/action_lang/static/exceptions.py
  8. 101 72
      src/sccd/syntax/expression.py
  9. 17 10
      src/sccd/syntax/scope.py
  10. 20 20
      src/sccd/syntax/statement.py
  11. 2 2
      src/sccd/syntax/types.py
  12. 1 1
      src/sccd/controller/controller.py
  13. 1 1
      src/sccd/controller/object_manager.py
  14. 0 41
      src/sccd/execution/builtin_scope.py
  15. 1 1
      src/sccd/execution/instance.py
  16. 1 1
      src/sccd/legacy/xml_loader.py
  17. 1 1
      src/sccd/model/globals.py
  18. 1 1
      src/sccd/model/model.py
  19. 0 0
      src/sccd/statechart/__init__.py
  20. 0 0
      src/sccd/statechart/dynamic/__init__.py
  21. 38 0
      src/sccd/statechart/dynamic/builtin_scope.py
  22. 0 0
      src/sccd/statechart/dynamic/event.py
  23. 2 2
      src/sccd/execution/round.py
  24. 24 44
      src/sccd/execution/statechart_state.py
  25. 19 15
      src/sccd/execution/statechart_instance.py
  26. 58 50
      src/sccd/parser/statechart_parser.py
  27. 0 0
      src/sccd/statechart/static/__init__.py
  28. 15 8
      src/sccd/syntax/action.py
  29. 2 2
      src/sccd/syntax/statechart.py
  30. 28 4
      src/sccd/syntax/tree.py
  31. 0 36
      src/sccd/syntax/test_scope.py
  32. 10 2
      src/sccd/parser/xml_parser.py
  33. 1 1
      test/legacy_render.py
  34. 8 5
      test/lib/test_parser.py
  35. 5 1
      test/render.py
  36. 4 4
      test/test_files/day_atlee/statechart_fig7_dialer.svg
  37. 4 4
      test/test_files/features/datamodel/test_cond.svg
  38. 0 2
      test/test_files/features/functions/test_closure.xml
  39. 2 0
      test/test_files/features/functions/test_functions.xml

src/sccd/syntax/__init__.py → src/sccd/action_lang/__init__.py


+ 0 - 0
src/sccd/action_lang/dynamic/__init__.py


+ 2 - 36
src/sccd/execution/memory.py

@@ -2,8 +2,9 @@ from typing import *
 from dataclasses import *
 from sccd.util.bitmap import *
 from sccd.util.debug import *
-from sccd.syntax.scope import *
+from sccd.action_lang.static.scope import *
 from sccd.execution.exceptions import *
+from sccd.action_lang.static.expression import *
 
 @dataclass(frozen=True)
 class StackFrame:
@@ -29,41 +30,6 @@ class StackFrame:
 
     return "StackFrame(%s, len=%d, parent=%s, context=%s)" % (self.scope.name, len(self.storage), short_descr(self.parent), "parent" if self.context is self.parent else short_descr(self.context))
 
-
-class MemoryInterface(ABC):
-
-  @abstractmethod
-  def current_frame(self) -> StackFrame:
-    pass
-
-  @abstractmethod
-  def push_frame(self, scope: Scope):
-    pass
-
-  @abstractmethod
-  def push_frame_w_context(self, scope: Scope, context: StackFrame):
-    pass
-
-  @abstractmethod
-  def pop_frame(self):
-    pass
-
-  @abstractmethod
-  def load(self, offset: int) -> Any:
-    pass
-
-  @abstractmethod
-  def store(self, offset: int, value: Any):
-    pass
-
-  @abstractmethod
-  def flush_transition(self, read_only: bool = False):
-    pass
-
-  @abstractmethod
-  def flush_round(self):
-    pass
-
 class Memory(MemoryInterface):
 
   def __init__(self):

+ 4 - 1
src/sccd/parser/grammar/action_language.g

@@ -11,7 +11,7 @@ IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
 
 _PATH_SEP: "/" 
 PARENT_NODE: ".." 
-CURRENT_NODE: "." 
+CURRENT_NODE: "."
 
 // target of a transition
 state_ref: path | "(" path ("," path)+ ")" 
@@ -156,6 +156,9 @@ MULTIPLY: "*="
 DIVIDE: "/="
 FLOORDIVIDE: "//="
 
+COMMENT: "#" /(.)*/ "\n"
+%ignore COMMENT
+
 
 // Semantic option parsing
 

+ 6 - 6
src/sccd/parser/expression_parser.py

@@ -1,18 +1,18 @@
 import os
 from lark import Lark, Transformer
-from sccd.syntax.statement import *
+from sccd.action_lang.static.statement import *
 from sccd.model.globals import *
-from sccd.syntax.scope import *
-from sccd.syntax.tree import *
+from sccd.action_lang.static.scope import *
+from sccd.statechart.static.tree import *
 
-_grammar_dir = os.path.join(os.path.dirname(__file__), "grammar")
+_grammar_dir = os.path.dirname(__file__)
 
 with open(os.path.join(_grammar_dir,"action_language.g")) as file:
   _action_lang_grammar = file.read()
 
 
 # Lark transformer for parsetree-less parsing of expressions
-class _ExpressionTransformer(Transformer):
+class ExpressionTransformer(Transformer):
   def __init__(self):
     super().__init__()
     self.globals: Globals = None
@@ -131,7 +131,7 @@ class _ExpressionTransformer(Transformer):
 
 # Global variables so we don't have to rebuild our parser every time
 # Obviously not thread-safe
-_transformer = _ExpressionTransformer()
+_transformer = ExpressionTransformer()
 _parser = Lark(_action_lang_grammar, parser="lalr", start=["expr", "block", "duration", "event_decl_list", "func_decl", "state_ref", "semantic_choice"], transformer=_transformer)
 
 # Exported functions:

+ 0 - 0
src/sccd/action_lang/static/__init__.py


+ 3 - 0
src/sccd/action_lang/static/exceptions.py

@@ -0,0 +1,3 @@
+# Superclass for all "user errors", errors in the model being loaded.
+class ModelError(Exception):
+  pass

+ 101 - 72
src/sccd/syntax/expression.py

@@ -2,13 +2,42 @@ from abc import *
 from typing import *
 from dataclasses import *
 from sccd.util.duration import *
-from sccd.syntax.scope import *
+from sccd.action_lang.static.scope import *
+
+class MemoryInterface(ABC):
+
+  @abstractmethod
+  def current_frame(self) -> 'StackFrame':
+    pass
+
+  @abstractmethod
+  def push_frame(self, scope: Scope):
+    pass
+
+  @abstractmethod
+  def push_frame_w_context(self, scope: Scope, context: 'StackFrame'):
+    pass
+
+  @abstractmethod
+  def pop_frame(self):
+    pass
+
+  @abstractmethod
+  def load(self, offset: int) -> Any:
+    pass
+
+  @abstractmethod
+  def store(self, offset: int, value: Any):
+    pass
+
+  @abstractmethod
+  def flush_transition(self, read_only: bool = False):
+    pass
+
+  @abstractmethod
+  def flush_round(self):
+    pass
 
-@dataclass
-class EvalContext:
-    current_state: 'StatechartState'
-    events: List['Event']
-    memory: 'MemoryInterface'
 
 # Thrown if the type checker encountered something illegal.
 # Not to be confused with Python's TypeError exception.
@@ -20,17 +49,17 @@ class Expression(ABC):
     # 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: Scope) -> SCCDType:
+    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.
     @abstractmethod
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         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.
+# 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.
 class LValue(Expression):
     # Initialize the LValue as an LValue. 
@@ -42,27 +71,27 @@ class LValue(Expression):
     #   offset ∈ [0, +∞[ : variable's memory address is within current scope
     #   offset ∈ ]-∞, 0[ : variable's memory address is in a parent scope (or better: 'context scope')
     @abstractmethod
-    def eval_lvalue(self, ctx: EvalContext) -> int:
+    def eval_lvalue(self) -> int:
         pass
 
-    # LValues can also serve as expressions!
-    def eval(self, ctx: EvalContext):
-        offset = self.eval_lvalue(ctx)
-        return ctx.memory.load(offset)
+    # Any type that is an LValue can also serve as an expression!
+    def eval(self, memory: MemoryInterface):
+        offset = self.eval_lvalue()
+        return memory.load(offset)
 
 @dataclass
 class Identifier(LValue):
     name: str
     offset: Optional[int] = None
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         self.offset, type = scope.get_rvalue(self.name)
         return type
 
     def init_lvalue(self, scope: Scope, type):
         self.offset = scope.put_lvalue(self.name, type)
 
-    def eval_lvalue(self, ctx: EvalContext) -> int:
+    def eval_lvalue(self) -> int:
         return self.offset
 
     def render(self):
@@ -75,24 +104,24 @@ class FunctionCall(Expression):
 
     type: Optional[type] = None
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
-        function_type = self.function.init_rvalue(scope)
+    def init_expr(self, scope: Scope) -> SCCDType:
+        function_type = self.function.init_expr(scope)
         if not isinstance(function_type, SCCDFunction):
             raise StaticTypeError("Function call: Expression '%s' is not a function" % self.function.render())
 
         formal_types = function_type.param_types
         return_type = function_type.return_type
 
-        actual_types = [p.init_rvalue(scope) for p in self.params]
+        actual_types = [p.init_expr(scope) for p in self.params]
         for i, (formal, actual) in enumerate(zip(formal_types, actual_types)):
             if formal != actual:
                 raise StaticTypeError("Function call, argument %d: %s is not expected type %s, instead is %s" % (i, self.params[i].render(), str(formal), str(actual)))
         return return_type
 
-    def eval(self, ctx: EvalContext):
-        f = self.function.eval(ctx)
-        p = [p.eval(ctx) for p in self.params]
-        return f(ctx, *p)
+    def eval(self, memory: MemoryInterface):
+        f = self.function.eval(memory)
+        p = [p.eval(memory) for p in self.params]
+        return f(memory, *p)
 
     def render(self):
         return self.function.render()+'('+','.join([p.render() for p in self.params])+')'
@@ -116,7 +145,7 @@ class FunctionDeclaration(Expression):
     body: 'Statement'
     scope: Optional[Scope] = None
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         self.scope = Scope("function", scope)
         # Reserve space for arguments on stack
         for p in self.params_decl:
@@ -125,30 +154,30 @@ class FunctionDeclaration(Expression):
         return_type = ret.get_return_type()
         return SCCDFunction([p.formal_type for p in self.params_decl], return_type)
 
-    def eval(self, ctx: EvalContext):
-        context: StackFrame = ctx.memory.current_frame()
-        def FUNCTION(ctx: EvalContext, *params):
-            ctx.memory.push_frame_w_context(self.scope, context)
+    def eval(self, memory: MemoryInterface):
+        context: 'StackFrame' = memory.current_frame()
+        def FUNCTION(memory: MemoryInterface, *params):
+            memory.push_frame_w_context(self.scope, context)
             # Copy arguments to stack
             for val, p in zip(params, self.params_decl):
-                ctx.memory.store(p.offset, val)
-            ret = self.body.exec(ctx)
-            ctx.memory.pop_frame()
+                memory.store(p.offset, val)
+            ret = self.body.exec(memory)
+            memory.pop_frame()
             return ret.val
         return FUNCTION
 
     def render(self) -> str:
-        return "<func_decl>" # todo
+        return "func(%s) [...]" % ", ".join(p.render() for p in self.params_decl) # todo
         
 
 @dataclass
 class StringLiteral(Expression):
     string: str
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         return SCCDString
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         return self.string
 
     def render(self):
@@ -159,10 +188,10 @@ class StringLiteral(Expression):
 class IntLiteral(Expression):
     i: int 
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         return SCCDInt
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         return self.i
 
     def render(self):
@@ -172,10 +201,10 @@ class IntLiteral(Expression):
 class BoolLiteral(Expression):
     b: bool 
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         return SCCDBool
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         return self.b
 
     def render(self):
@@ -185,10 +214,10 @@ class BoolLiteral(Expression):
 class DurationLiteral(Expression):
     d: Duration
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         return SCCDDuration
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         return self.d
 
     def render(self):
@@ -200,17 +229,17 @@ class Array(Expression):
 
     element_type: Optional[SCCDType] = None
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
+    def init_expr(self, scope: Scope) -> SCCDType:
         for e in self.elements:
-            t = e.init_rvalue(scope)
+            t = e.init_expr(scope)
             if self.element_type and self.element_type != t:
                 raise StaticTypeError("Mixed element types in Array expression: %s and %s" % (str(self.element_type), str(t)))
             self.element_type = t
 
         return SCCDArray(self.element_type)
 
-    def eval(self, ctx: EvalContext):
-        return [e.eval(ctx) for e in self.elements]
+    def eval(self, memory: MemoryInterface):
+        return [e.eval(memory) for e in self.elements]
 
     def render(self):
         return '['+','.join([e.render() for e in self.elements])+']'
@@ -221,11 +250,11 @@ class Array(Expression):
 class Group(Expression):
     subexpr: Expression
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
-        return self.subexpr.init_rvalue(scope)
+    def init_expr(self, scope: Scope) -> SCCDType:
+        return self.subexpr.init_expr(scope)
 
-    def eval(self, ctx: EvalContext):
-        return self.subexpr.eval(ctx)
+    def eval(self, memory: MemoryInterface):
+        return self.subexpr.eval(memory)
 
     def render(self):
         return '('+self.subexpr.render()+')'
@@ -236,9 +265,9 @@ class BinaryExpression(Expression):
     operator: str # token name from the grammar.
     rhs: Expression
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
-        lhs_t = self.lhs.init_rvalue(scope)
-        rhs_t = self.rhs.init_rvalue(scope)
+    def init_expr(self, scope: Scope) -> SCCDType:
+        lhs_t = self.lhs.init_expr(scope)
+        rhs_t = self.rhs.init_expr(scope)
 
         def comparison_type():
             same_type()
@@ -278,24 +307,24 @@ class BinaryExpression(Expression):
             "**":  same_type(),
         }[self.operator]
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         
         return {
-            "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),
+            "and": lambda x,y: x.eval(memory) and y.eval(memory),
+            "or": lambda x,y: x.eval(memory) or y.eval(memory),
+            "==": lambda x,y: x.eval(memory) == y.eval(memory),
+            "!=": lambda x,y: x.eval(memory) != y.eval(memory),
+            ">": lambda x,y: x.eval(memory) > y.eval(memory),
+            ">=": lambda x,y: x.eval(memory) >= y.eval(memory),
+            "<": lambda x,y: x.eval(memory) < y.eval(memory),
+            "<=": lambda x,y: x.eval(memory) <= y.eval(memory),
+            "+": lambda x,y: x.eval(memory) + y.eval(memory),
+            "-": lambda x,y: x.eval(memory) - y.eval(memory),
+            "*": lambda x,y: x.eval(memory) * y.eval(memory),
+            "/": lambda x,y: x.eval(memory) / y.eval(memory),
+            "//": lambda x,y: x.eval(memory) // y.eval(memory),
+            "%": lambda x,y: x.eval(memory) % y.eval(memory),
+            "**": lambda x,y: x.eval(memory) ** y.eval(memory),
         }[self.operator](self.lhs, self.rhs) # Borrow Python's lazy evaluation
 
     def render(self):
@@ -306,17 +335,17 @@ class UnaryExpression(Expression):
     operator: str # token value from the grammar.
     expr: Expression
 
-    def init_rvalue(self, scope: Scope) -> SCCDType:
-        expr_type = self.expr.init_rvalue(scope)
+    def init_expr(self, scope: Scope) -> SCCDType:
+        expr_type = self.expr.init_expr(scope)
         return {
             "not": SCCDBool,
             "-":   expr_type,
         }[self.operator]
 
-    def eval(self, ctx: EvalContext):
+    def eval(self, memory: MemoryInterface):
         return {
-            "not": lambda x: not x.eval(ctx),
-            "-": lambda x: - x.eval(ctx),
+            "not": lambda x: not x.eval(memory),
+            "-": lambda x: - x.eval(memory),
         }[self.operator](self.expr)
 
     def render(self):

+ 17 - 10
src/sccd/syntax/scope.py

@@ -2,12 +2,10 @@ from abc import *
 from typing import *
 from dataclasses import *
 from inspect import signature
-from sccd.syntax.types import *
+from sccd.action_lang.static.types import *
+from sccd.action_lang.static.exceptions import *
 import itertools
 
-# Superclass for all "user errors", errors in the model being loaded.
-class ModelError(Exception):
-  pass
 
 class ScopeError(ModelError):
   def __init__(self, scope, msg):
@@ -17,11 +15,20 @@ class ScopeError(ModelError):
 # Stateless stuff we know about a variable existing within a scope.
 @dataclass(frozen=True)
 class _Variable(ABC):
-  name: str # only used to print error messages
+  _name: str # only used to print error messages
   offset: int # Offset within variable's scope. Always >= 0.
   type: SCCDType
   const: bool
 
+  @property
+  def name(self):
+    import termcolor
+    return termcolor.colored(self._name, 'yellow')
+
+  def __str__(self):
+    return "+%d: %s%s: %s" % (self.offset, "(const) "if self.const else "", self.name, str(self.type))
+
+
 # Stateless stuff we know about a scope (= set of named values)
 class Scope:
   def __init__(self, name: str, parent: 'Scope'):
@@ -48,11 +55,11 @@ class Scope:
 
     is_empty = True
     for v in reversed(self.variables):
-      s += "    +%d: %s: %s\n" % (v.offset, v.name, str(v.type))
+      s += "    %s\n" % str(v)
       is_empty = False
 
     if is_empty:
-      s += "   ø\n"
+      s += "    ø\n"
 
     if self.parent:
       s += self.parent.__str__()
@@ -86,10 +93,10 @@ class Scope:
       scope, scope_offset, var = found
       if var.type == type:
         if var.const:
-          raise ScopeError(self, "Cannot assign to %s: %s of scope '%s': Variable is constant." % (name, str(var.type), scope.name))
+          raise ScopeError(self, "Cannot assign to %s: %s of scope '%s': Variable is constant." % (var.name, str(var.type), scope.name))
         return scope_offset + var.offset
       else:
-        raise ScopeError(self, "Cannot assign %s to %s: %s of scope '%s'" %(str(type), name, str(var.type), scope.name))
+        raise ScopeError(self, "Cannot assign %s to %s: %s of scope '%s'" %(str(type), var.name, str(var.type), scope.name))
 
     return self._internal_add(name, type, const=False)
 
@@ -109,6 +116,6 @@ class Scope:
     found = self._internal_lookup(name)
     if found:
       scope, scope_offset, var = found
-      raise ScopeError(self, "Cannot declare '%s' in scope '%s': Name already exists in scope '%s'" % (name, self.name, scope.name))
+      raise ScopeError(self, "Cannot declare '%s' in scope '%s': Name already exists in scope '%s'" % (var.name, self.name, scope.name))
 
     return self._internal_add(name, type, const)

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

@@ -1,5 +1,5 @@
 from typing import *
-from sccd.syntax.expression import *
+from sccd.action_lang.static.expression import *
 
 @dataclass(frozen=True)
 class Return:
@@ -76,7 +76,7 @@ AlwaysReturns = lambda t: ReturnBehavior(ReturnBehavior.When.ALWAYS, t)
 class Statement(ABC):
     # Execution typically has side effects.
     @abstractmethod
-    def exec(self, ctx: EvalContext) -> Return:
+    def exec(self, memory: MemoryInterface) -> Return:
         pass
 
     @abstractmethod
@@ -93,15 +93,15 @@ class Assignment(Statement):
     rhs: Expression
 
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
-        rhs_t = self.rhs.init_rvalue(scope)
+        rhs_t = self.rhs.init_expr(scope)
         self.lhs.init_lvalue(scope, rhs_t)
         return NeverReturns
 
-    def exec(self, ctx: EvalContext) -> Return:
-        rhs_val = self.rhs.eval(ctx)
-        offset = self.lhs.eval_lvalue(ctx)
+    def exec(self, memory: MemoryInterface) -> Return:
+        rhs_val = self.rhs.eval(memory)
+        offset = self.lhs.eval_lvalue()
 
-        ctx.memory.store(offset, rhs_val)
+        memory.store(offset, rhs_val)
 
         return DontReturn
 
@@ -124,9 +124,9 @@ class Block(Statement):
             so_far = ReturnBehavior.sequence(so_far, now_what)            
         return so_far
 
-    def exec(self, ctx: EvalContext) -> Return:
+    def exec(self, memory: MemoryInterface) -> Return:
         for stmt in self.stmts:
-            ret = stmt.exec(ctx)
+            ret = stmt.exec(memory)
             if ret.ret:
                 break
         return ret
@@ -143,11 +143,11 @@ class ExpressionStatement(Statement):
     expr: Expression
 
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
-        self.expr.init_rvalue(scope)
+        self.expr.init_expr(scope)
         return NeverReturns
 
-    def exec(self, ctx: EvalContext) -> Return:
-        self.expr.eval(ctx)
+    def exec(self, memory: MemoryInterface) -> Return:
+        self.expr.eval(memory)
         return DontReturn
 
     def render(self) -> str:
@@ -158,11 +158,11 @@ class ReturnStatement(Statement):
     expr: Expression
 
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
-        t = self.expr.init_rvalue(scope)
+        t = self.expr.init_expr(scope)
         return AlwaysReturns(t)
 
-    def exec(self, ctx: EvalContext) -> Return:
-        val = self.expr.eval(ctx)
+    def exec(self, memory: MemoryInterface) -> Return:
+        val = self.expr.eval(memory)
         return DoReturn(val)
 
     def render(self) -> str:
@@ -175,7 +175,7 @@ class IfStatement(Statement):
     else_body: Optional[Statement] = None
 
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
-        cond_t = self.cond.init_rvalue(scope)
+        cond_t = self.cond.init_expr(scope)
         if_ret = self.if_body.init_stmt(scope)
         if self.else_body is None:
             else_ret = NeverReturns
@@ -183,12 +183,12 @@ class IfStatement(Statement):
             else_ret = self.else_body.init_stmt(scope)
         return ReturnBehavior.combine_branches(if_ret, else_ret)
 
-    def exec(self, ctx: EvalContext) -> Return:
-        val = self.cond.eval(ctx)
+    def exec(self, memory: MemoryInterface) -> Return:
+        val = self.cond.eval(memory)
         if val:
-            return self.if_body.exec(ctx)
+            return self.if_body.exec(memory)
         elif self.else_body is not None:
-            return self.else_body.exec(ctx)
+            return self.else_body.exec(memory)
         return DontReturn
 
     def render(self) -> str:

+ 2 - 2
src/sccd/syntax/types.py

@@ -9,8 +9,8 @@ class SCCDType(ABC):
         
     def __str__(self):
         import termcolor
-        # return termcolor.colored(self._str(), 'blue')
-        return self._str()
+        return termcolor.colored(self._str(), 'cyan')
+        # return self._str()
 
 @dataclass(frozen=True)
 class _SCCDSimpleType(SCCDType):

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

@@ -2,7 +2,7 @@ import queue
 import dataclasses
 from typing import Dict, List, Optional
 from sccd.controller.event_queue import *
-from sccd.execution.event import *
+from sccd.statechart.dynamic.event import *
 from sccd.controller.object_manager import *
 from sccd.util.debug import print_debug
 from sccd.model.model import *

+ 1 - 1
src/sccd/controller/object_manager.py

@@ -1,7 +1,7 @@
 import re
 import abc
 from typing import List, Tuple
-from sccd.execution.statechart_instance import *
+from sccd.statechart.dynamic.statechart_instance import *
 
 # TODO: Clean this mess up. Look at all object management operations and see how they can be improved.
 class ObjectManager(Instance):

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

@@ -1,41 +0,0 @@
-from sccd.syntax.expression import *
-from sccd.util.debug import *
-import termcolor
-
-def _in_state(ctx: EvalContext, state_list: List[str]) -> bool:
-  from sccd.execution.statechart_state import StatechartState
-  return StatechartState.in_state(ctx.current_state, state_list)
-
-
-def _log10(ctx: EvalContext, i: int) -> float:
-  import math
-  return math.log10(i)
-
-
-def _float_to_int(ctx: EvalContext, x: float) -> int:
-  return int(x)
-
-
-def _log(ctx: EvalContext, s: str) -> None:
-  print_debug(termcolor.colored("log: ",'blue')+s)
-
-
-def _int_to_str(ctx: EvalContext, i: int) -> str:
-  return str(i)
-
-BuiltIn = Scope("builtin", None)
-
-BuiltIn.declare("INSTATE", SCCDFunction([SCCDArray(SCCDString)], SCCDBool), const=True)
-BuiltIn.declare("log10", SCCDFunction([SCCDInt], SCCDFloat), const=True)
-BuiltIn.declare("float_to_int", SCCDFunction([SCCDFloat], SCCDInt), const=True)
-BuiltIn.declare("log", SCCDFunction([SCCDString]), const=True)
-BuiltIn.declare("int_to_str", SCCDFunction([SCCDInt], SCCDString), const=True)
-
-def load_builtins(memory):
-  memory.push_frame(BuiltIn)
-
-  memory.current_frame().storage[0] = _in_state
-  memory.current_frame().storage[1] = _log10
-  memory.current_frame().storage[2] = _float_to_int
-  memory.current_frame().storage[3] = _log
-  memory.current_frame().storage[4] = _int_to_str

+ 1 - 1
src/sccd/execution/instance.py

@@ -1,6 +1,6 @@
 from abc import *
 from typing import *
-from sccd.execution.event import *
+from sccd.statechart.dynamic.event import *
 from sccd.execution.timestamp import *
 
 # Interface for all instances and also the Object Manager

+ 1 - 1
src/sccd/legacy/xml_loader.py

@@ -6,7 +6,7 @@ from typing import List, Any, Optional
 import lxml.etree as ET
 from lark import Lark
 
-from sccd.syntax.statechart import *
+from sccd.statechart.static.statechart import *
 from sccd.controller.controller import *
 import sccd.schema
 

+ 1 - 1
src/sccd/model/globals.py

@@ -1,7 +1,7 @@
 # from dataclasses import *
 import termcolor
 from typing import *
-from sccd.syntax.expression import *
+from sccd.action_lang.static.expression import *
 from sccd.util.namespace import *
 from sccd.util.duration import *
 from sccd.util.debug import *

+ 1 - 1
src/sccd/model/model.py

@@ -1,7 +1,7 @@
 from abc import *
 from dataclasses import *
 from typing import *
-from sccd.syntax.statechart import *
+from sccd.statechart.static.statechart import *
 from sccd.model.globals import *
 
 @dataclass

+ 0 - 0
src/sccd/statechart/__init__.py


+ 0 - 0
src/sccd/statechart/dynamic/__init__.py


+ 38 - 0
src/sccd/statechart/dynamic/builtin_scope.py

@@ -0,0 +1,38 @@
+from sccd.action_lang.static.expression import *
+from sccd.util.debug import *
+
+BuiltIn = Scope("builtin", None)
+
+BuiltIn.declare("INSTATE", SCCDFunction([SCCDArray(SCCDString)], SCCDBool), const=True)
+BuiltIn.declare("log10", SCCDFunction([SCCDInt], SCCDFloat), const=True)
+BuiltIn.declare("float_to_int", SCCDFunction([SCCDFloat], SCCDInt), const=True)
+BuiltIn.declare("log", SCCDFunction([SCCDString]), const=True)
+BuiltIn.declare("int_to_str", SCCDFunction([SCCDInt], SCCDString), const=True)
+
+def load_builtins(memory: MemoryInterface, state):
+  memory.push_frame(BuiltIn)
+
+  def in_state(memory: MemoryInterface, state_list: List[str]) -> bool:
+    return state.in_state(state_list)
+
+  def log10(memory: MemoryInterface, i: int) -> float:
+    import math
+    return math.log10(i)
+
+  def float_to_int(memory: MemoryInterface, x: float) -> int:
+    return int(x)
+
+  def log(memory: MemoryInterface, s: str) -> None:
+    import termcolor
+    print_debug(termcolor.colored("log: ",'blue')+s)
+
+  def int_to_str(memory: MemoryInterface, i: int) -> str:
+    return str(i)
+
+  frame = memory.current_frame()
+
+  frame.storage[0] = in_state
+  frame.storage[1] = log10
+  frame.storage[2] = float_to_int
+  frame.storage[3] = log
+  frame.storage[4] = int_to_str

src/sccd/execution/event.py → src/sccd/statechart/dynamic/event.py


+ 2 - 2
src/sccd/execution/round.py

@@ -1,7 +1,7 @@
 from typing import *
-from sccd.execution.event import *
+from sccd.statechart.dynamic.event import *
 from sccd.util.bitmap import *
-from sccd.syntax.tree import *
+from sccd.statechart.static.tree import *
 from sccd.util.debug import *
 from sccd.execution.exceptions import *
 

+ 24 - 44
src/sccd/execution/statechart_state.py

@@ -1,21 +1,22 @@
 from typing import *
-from sccd.syntax.statechart import *
-from sccd.execution.event import *
+from sccd.statechart.static.statechart import *
+from sccd.statechart.dynamic.event import *
 from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
-from sccd.syntax.scope import *
+from sccd.action_lang.static.scope import *
 from sccd.execution.exceptions import *
 
-
 # Set of current states etc.
-class StatechartState:
+class StatechartExecution:
 
-    def __init__(self, statechart: Statechart, instance, gc_memory, rhs_memory, raise_internal):
-        self.model = statechart
+    def __init__(self, statechart: Statechart, instance):
+        self.statechart = statechart
         self.instance = instance
-        self.gc_memory = gc_memory
-        self.rhs_memory = rhs_memory
-        self.raise_internal = raise_internal
+
+        self.gc_memory = None
+        self.rhs_memory = None
+        self.raise_internal = None
+        self.raise_next_bs = None
 
         # these 2 fields have the same information
         self.configuration: List[State] = []
@@ -38,13 +39,13 @@ class StatechartState:
 
     # enter default states
     def initialize(self):
-        states = self.model.tree.root.getEffectiveTargetStates(self)
+        states = self.statechart.tree.root.getEffectiveTargetStates(self)
         self.configuration.extend(states)
         self.configuration_bitmap = Bitmap.from_list(s.gen.state_id for s in states)
 
         ctx = EvalContext(current_state=self, events=[], memory=self.rhs_memory)
-        if self.model.datamodel is not None:
-            self.model.datamodel.exec(ctx)
+        if self.statechart.datamodel is not None:
+            self.statechart.datamodel.exec(self.rhs_memory)
 
         for state in states:
             print_debug(termcolor.colored('  ENTER %s'%state.gen.full_name, 'green'))
@@ -103,7 +104,8 @@ class StatechartState:
                     
             # execute transition action(s)
             self.rhs_memory.push_frame(t.scope) # make room for event parameters on stack
-            self._copy_event_params_to_stack(self.rhs_memory, t, events)
+            if t.trigger:
+                t.trigger.copy_params_to_stack(ctx)
             self._perform_actions(ctx, t.actions)
             self.rhs_memory.pop_frame()
                 
@@ -120,34 +122,13 @@ class StatechartState:
             try:
                 self.configuration = self.config_mem[self.configuration_bitmap]
             except KeyError:
-                self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.model.tree.state_list if self.configuration_bitmap & s.gen.state_id_bitmap]
+                self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.statechart.tree.state_list if self.configuration_bitmap & s.gen.state_id_bitmap]
 
             self.rhs_memory.flush_transition()
         except SCCDRuntimeException as e:
             e.args = ("During execution of transition %s:\n" % str(t) +str(e),)
             raise
 
-    @staticmethod
-    def _copy_event_params_to_stack(memory, t, events):
-        # Both 'events' and 't.trigger.enabling' are sorted by event ID...
-        # This way we have to iterate over each of both lists at most once.
-        if t.trigger and not isinstance(t.trigger, AfterTrigger):
-            event_decls = iter(t.trigger.enabling)
-            try:
-                event_decl = next(event_decls)
-                offset = 0
-                for e in events:
-                    if e.id < event_decl.id:
-                        continue
-                    else:
-                        while e.id > event_decl.id:
-                            event_decl = next(event_decls)
-                        for p in e.params:
-                            memory.store(offset, p)
-                            offset += 1
-            except StopIteration:
-                pass
-
     def check_guard(self, t, events) -> bool:
         try:
             # Special case: after trigger
@@ -159,10 +140,12 @@ class StatechartState:
             if t.guard is None:
                 return True
             else:
+                ctx = EvalContext(current_state=self, events=events, memory=self.gc_memory)
                 self.gc_memory.push_frame(t.scope)
-                self._copy_event_params_to_stack(self.gc_memory, t, events)
-                result = t.guard.eval(
-                    EvalContext(current_state=self, events=events, memory=self.gc_memory))
+                # Guard conditions can also refer to event parameters
+                if t.trigger:
+                    t.trigger.copy_params_to_stack(ctx)
+                result = t.guard.eval(self.gc_memory)
                 self.gc_memory.pop_frame()
                 return result
         except SCCDRuntimeException as e:
@@ -182,10 +165,7 @@ class StatechartState:
             delay: Duration = after.delay.eval(
                 EvalContext(current_state=self, events=[], memory=self.gc_memory))
             timer_id = self._next_timer_id(after)
-            self.output.append(OutputEvent(
-                Event(id=after.id, name=after.name, params=[timer_id]),
-                target=InstancesTarget([self.instance]),
-                time_offset=delay))
+            self.raise_next_bs(Event(id=after.id, name=after.name, params=[timer_id]), delay)
 
     def _next_timer_id(self, trigger: AfterTrigger):
         self.timer_ids[trigger.after_id] += 1
@@ -193,7 +173,7 @@ class StatechartState:
 
     # Return whether the current configuration includes ALL the states given.
     def in_state(self, state_strings: List[str]) -> bool:
-        state_ids_bitmap = Bitmap.from_list((self.model.tree.state_dict[state_string].gen.state_id for state_string in state_strings))
+        state_ids_bitmap = Bitmap.from_list((self.statechart.tree.state_dict[state_string].gen.state_id for state_string in state_strings))
         in_state = self.configuration_bitmap.has_all(state_ids_bitmap)
         # if in_state:
         #     print_debug("in state"+str(state_strings))

+ 19 - 15
src/sccd/execution/statechart_instance.py

@@ -2,13 +2,13 @@ import termcolor
 import functools
 from typing import List, Tuple, Iterable
 from sccd.execution.instance import *
-from sccd.execution.builtin_scope import *
-from sccd.syntax.statechart import *
+from sccd.statechart.dynamic.builtin_scope import *
+from sccd.statechart.static.statechart import *
 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.memory import *
+from sccd.statechart.dynamic.round import *
+from sccd.statechart.dynamic.statechart_execution import *
+from sccd.action_lang.dynamic.memory import *
 
 # Hardcoded limit on number of sub-rounds of combo and big step to detect never-ending superrounds.
 # TODO: make this configurable
@@ -18,6 +18,8 @@ class StatechartInstance(Instance):
     def __init__(self, statechart: Statechart, object_manager):
         self.object_manager = object_manager
 
+        self.execution = StatechartExecution(statechart, self)
+
         semantics = statechart.semantics
 
         if semantics.has_multiple_variants():
@@ -31,7 +33,7 @@ class StatechartInstance(Instance):
 
         # Big step + combo step maximality semantics
 
-        small_step = SmallStep(termcolor.colored("small", 'blue'), None, generator,
+        small_step = SmallStep(termcolor.colored("small", 'blue'), self.execution, generator,
             concurrency=semantics.concurrency==Concurrency.MANY)
 
 
@@ -79,9 +81,11 @@ class StatechartInstance(Instance):
             InputEventLifeline.FIRST_SMALL_STEP: first_small
         }[semantics.input_event_lifeline]
 
+        raise_nextbs = lambda e, time_offset: self.execution.output.append(OutputEvent(e, InstancesTarget([self]), time_offset))
+
         raise_internal = {
             # InternalEventLifeline.QUEUE: self._big_step.add_next_event,
-            InternalEventLifeline.QUEUE: lambda e: self.state.output.append(OutputEvent(e, InstancesTarget([self]))),
+            InternalEventLifeline.QUEUE: lambda e: raise_nextbs(e, 0),
             InternalEventLifeline.NEXT_COMBO_STEP: combo_step.add_next_event,
             InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event,
 
@@ -92,7 +96,7 @@ class StatechartInstance(Instance):
         # Memory protocol semantics
 
         memory = Memory()
-        load_builtins(memory)
+        load_builtins(memory, self.execution)
         memory.push_frame(statechart.scope)
 
         rhs_memory = MemoryPartialSnapshot("RHS", memory)
@@ -115,20 +119,20 @@ class StatechartInstance(Instance):
 
         print_debug("\nRound hierarchy: " + str(self._big_step) + '\n')
 
-        self.state = StatechartState(statechart, self, gc_memory, rhs_memory, raise_internal)
-
-        # Chicken and egg problem
-        small_step.state = self.state
+        self.execution.gc_memory = gc_memory
+        self.execution.rhs_memory = rhs_memory
+        self.execution.raise_internal = raise_internal
+        self.execution.raise_next_bs = raise_nextbs
 
 
     # enter default states, generating a set of output events
     def initialize(self, now: Timestamp) -> List[OutputEvent]:
-        self.state.initialize()
-        return self.state.collect_output()
+        self.execution.initialize()
+        return self.execution.collect_output()
 
     # perform a big step. generating a set of output events
     def big_step(self, now: Timestamp, input_events: List[Event]) -> List[OutputEvent]:
         # print_debug('attempting big step, input_events='+str(input_events))
         self.set_input(input_events)
         self._big_step.run_and_cycle_events()
-        return self.state.collect_output()
+        return self.execution.collect_output()

+ 58 - 50
src/sccd/parser/statechart_parser.py

@@ -1,17 +1,23 @@
 from typing import *
-from sccd.syntax.statechart import *
-from sccd.syntax.tree import *
-from sccd.execution.builtin_scope import *
-from sccd.parser.xml_parser import *
-from sccd.parser.expression_parser import *
+from sccd.statechart.static.statechart import *
+from sccd.statechart.static.tree import *
+from sccd.statechart.dynamic.builtin_scope import *
+from sccd.util.xml_parser import *
+from sccd.action_lang.parser.parser import *
 
 class SkipFile(Exception):
   pass
 
-_blank_eval_context = EvalContext(current_state=None, events=[], memory=None)
-
 parse_f = functools.partial(parse, decorate_exceptions=(ModelError,))
 
+def check_duration_type(type):
+  if type != SCCDDuration:
+    msg = "Expression is '%s' type. Expected 'Duration' type." % str(type)
+    if type == SCCDInt:
+      msg += "\n Hint: Did you forget a duration unit sufix? ('s', 'ms', ...)"
+    raise Exception(msg)
+
+
 def create_statechart_parser(globals, src_file, load_external = True, parse = parse_f) -> Rules:
   def parse_statechart(el):
     ext_file = el.get("src")
@@ -34,8 +40,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
     def parse_semantics(el):
       # Use reflection to find the possible XML attributes and their values
       for aspect_name, aspect_type in SemanticConfiguration.get_fields():
-        text = el.get(aspect_name)
-        if text is not None:
+        def parse_semantic_attribute(text):
           result = parse_semantic_choice(text)
           if result.data == "wildcard":
             semantic_choice = list(aspect_type) # all options
@@ -45,6 +50,8 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
             semantic_choice = semantic_choice[0]
           setattr(statechart.semantics, aspect_name, semantic_choice)
 
+        if_attribute(el, aspect_name, parse_semantic_attribute)
+
     def parse_datamodel(el):
       body = parse_block(globals, el.text)
       body.init_stmt(statechart.scope)
@@ -70,7 +77,8 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
       root = State("", parent=None)
       children_dict = {}
       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.
-      next_after_id = 0 # Counter for 'after' transitions within the statechart.
+      after_triggers = [] # After triggers accumulate here
+      # next_after_id = 0 # Counter for 'after' transitions within the statechart.
 
       def create_actions_parser(scope):
 
@@ -79,7 +87,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           def parse_param(el):
             expr_text = require_attribute(el, "expr")
             expr = parse_expression(globals, expr_text)
-            expr.init_rvalue(scope)
+            expr.init_expr(scope)
             params.append(expr)
 
           def finish_raise():
@@ -110,16 +118,23 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
         return {"raise": parse_raise, "code": parse_code}
 
       def deal_with_initial(el, state, children_dict):
-        initial = el.get("initial")
-        if initial is not None:
+        have_initial = False
+
+        def parse_attr_initial(initial):
+          nonlocal have_initial
+          have_initial = True
           try:
             state.default_state = children_dict[initial]
           except KeyError as e:
-            raise XmlError("initial=\"%s\": not a child." % (initial)) from e
-        elif len(state.children) == 1:
-          state.default_state = state.children[0]
-        elif len(state.children) > 1:
-          raise XmlError("More than 1 child state: must set 'initial' attribute.")
+            raise XmlError("Not a child.") from e
+
+        if_attribute(el, "initial", parse_attr_initial)
+
+        if not have_initial:
+          if len(state.children) == 1:
+            state.default_state = state.children[0]
+          elif len(state.children) > 1:
+            raise XmlError("More than 1 child state: must set 'initial' attribute.")
 
       def create_state_parser(parent, sibling_dict: Dict[str, State]={}):
 
@@ -169,8 +184,6 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           return (create_actions_parser(statechart.scope), finish_onexit)
 
         def parse_transition(el):
-          nonlocal next_after_id
-          
           if parent is root:
             raise XmlError("Root <state> cannot be source of a transition.")
 
@@ -178,54 +191,49 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           target_string = require_attribute(el, "target")
           transition = Transition(parent, [], scope, target_string)
 
-          event = el.get("event")
-          if event is not None:
+          have_event_attr = False
+          def parse_attr_event(event):
+            nonlocal have_event_attr
+            have_event_attr = True
+
             positive_events, negative_events = parse_events_decl(globals, event)
 
             # Optimization: sort events by ID
             # Allows us to save time later.
             positive_events.sort(key=lambda e: e.id)
 
-            def add_event_params_to_scope(e: EventDecl):
+            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_event_params_to_scope(e)
+              add_params_to_scope(e)
 
             if not negative_events:
               transition.trigger = Trigger(positive_events)
             else:
               transition.trigger = NegatedTrigger(positive_events, negative_events)
 
-          after = el.get("after")
-          if after is not None:
-            if event is not None:
+          def parse_attr_after(after):
+            if have_event_attr:
               raise XmlError("Cannot specify 'after' and 'event' at the same time.")
-            try:
-              after_expr = parse_expression(globals, after)
-              after_type = after_expr.init_rvalue(scope)
-              if after_type != SCCDDuration:
-                msg = "Expression is '%s' type. Expected 'Duration' type." % str(after_type)
-                if after_type == SCCDInt:
-                  msg += "\n Hint: Did you forget a duration unit sufix? ('s', 'ms', ...)"
-                raise Exception(msg)
-              event_name = "_after%d" % next_after_id # transition gets unique event name
-              transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, next_after_id, after_expr)
-              next_after_id += 1
-            except Exception as e:
-              raise XmlError("after=\"%s\": %s" % (after, str(e))) from e
-
-          cond = el.get("cond")
-          if cond is not None:
-            try:
-              expr = parse_expression(globals, cond)
-              expr.init_rvalue(scope)
-            except Exception as e:
-              raise XmlError("cond=\"%s\": %s" % (cond, str(e))) from e
-              # raise except_msg("cond=\"%s\": " % cond, e)
+            after_expr = parse_expression(globals, after)
+            after_type = after_expr.init_expr(scope)
+            check_duration_type(after_type)
+            after_id = len(after_triggers)
+            event_name = "_after%d" % after_id # transition gets unique event name
+            transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, after_id, after_expr)
+            after_triggers.append(transition.trigger)
+
+          def parse_attr_cond(cond):
+            expr = parse_expression(globals, cond)
+            expr.init_expr(scope)
             transition.guard = expr
 
+          if_attribute(el, "event", parse_attr_event)
+          if_attribute(el, "after", parse_attr_after)
+          if_attribute(el, "cond", parse_attr_cond)
+
           def finish_transition(*actions):
             transition.actions = actions
             transitions.append((transition, el))
@@ -263,7 +271,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           except Exception as e:
             raise XmlErrorElement(t_el, "Could not find target '%s'." % (transition.target_string)) from e
 
-        statechart.tree = StateTree(root)
+        statechart.tree = StateTree(root, after_triggers)
 
       return (create_state_parser(root, sibling_dict=children_dict), finish_root)
 

+ 0 - 0
src/sccd/statechart/static/__init__.py


+ 15 - 8
src/sccd/syntax/action.py

@@ -1,9 +1,15 @@
 from typing import *
 from dataclasses import *
 from abc import *
-from sccd.syntax.expression import *
-from sccd.syntax.statement import *
-from sccd.execution.event import *
+from sccd.action_lang.static.expression import *
+from sccd.action_lang.static.statement import *
+from sccd.statechart.dynamic.event import *
+
+@dataclass
+class EvalContext:
+    current_state: 'StatechartState'
+    events: List['Event']
+    memory: 'MemoryInterface'
 
 @dataclass
 class Action(ABC):
@@ -24,14 +30,15 @@ class RaiseEvent(Action):
     def render(self) -> str:
         return '^'+self.name
 
-    def _eval_params(self, ctx: EvalContext) -> List[Any]:
-        return [p.eval(ctx) for p in self.params]
+    def _eval_params(self, memory: MemoryInterface) -> List[Any]:
+        return [p.eval(memory) for p in self.params]
+
 @dataclass
 class RaiseInternalEvent(RaiseEvent):
     event_id: int
 
     def exec(self, ctx: EvalContext):
-        params = self._eval_params(ctx)
+        params = self._eval_params(ctx.memory)
         ctx.current_state.raise_internal(
             Event(id=self.event_id, name=self.name, port="", params=params))
 
@@ -41,7 +48,7 @@ class RaiseOutputEvent(RaiseEvent):
     time_offset: int
 
     def exec(self, ctx: EvalContext):
-        params = self._eval_params(ctx)
+        params = self._eval_params(ctx.memory)
         ctx.current_state.output.append(
             OutputEvent(Event(id=0, name=self.name, port=self.outport, params=params),
                     OutputPortTarget(self.outport),
@@ -55,7 +62,7 @@ class Code(Action):
     block: Block
 
     def exec(self, ctx: EvalContext):
-        self.block.exec(ctx)
+        self.block.exec(ctx.memory)
 
     def render(self) -> str:
         return '/'+self.block.render()

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

@@ -2,8 +2,8 @@ from enum import *
 from dataclasses import *
 from typing import *
 import itertools
-from sccd.syntax.tree import *
-from sccd.syntax.scope import *
+from sccd.statechart.static.tree import *
+from sccd.action_lang.static.scope import *
 
 class SemanticAspect:
   def __str__(self):

+ 28 - 4
src/sccd/syntax/tree.py

@@ -1,6 +1,6 @@
 import termcolor
 from typing import *
-from sccd.syntax.action import *
+from sccd.statechart.static.action import *
 from sccd.util.bitmap import *
 
 
@@ -112,6 +112,25 @@ class Trigger:
     def render(self) -> str:
         return ' ∧ '.join(e.render() for e in self.enabling)
 
+    def copy_params_to_stack(self, ctx: EvalContext):
+        # Both 'ctx.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(self.enabling)
+        try:
+            event_decl = next(iterator)
+            offset = 0
+            for e in ctx.events:
+                if e.id < event_decl.id:
+                    continue
+                else:
+                    while e.id > event_decl.id:
+                        event_decl = next(iterator)
+                    for p in e.params:
+                        ctx.memory.store(offset, p)
+                        offset += 1
+        except StopIteration:
+            pass
+
 @dataclass
 class NegatedTrigger(Trigger):
     disabling: List[EventDecl]
@@ -139,6 +158,11 @@ class AfterTrigger(Trigger):
     def render(self) -> str:
         return "after("+self.delay.render()+")"
 
+    # An 'after'-event also has 1 parameter, but it is not accessible to the user,
+    # hence the override.
+    def copy_params_to_stack(self, ctx: EvalContext):
+        pass
+
 @dataclass
 class Transition:
     source: State
@@ -167,11 +191,11 @@ class StateTree:
 
     # root: The root state of a state,transition tree structure with with all fields filled in,
     #       except the 'gen' fields. This function will fill in the 'gen' fields.
-    def __init__(self, root: State):
+    def __init__(self, root: State, after_triggers: List[AfterTrigger]):
         self.state_dict = {} # mapping from 'full name' to State
         self.state_list = [] # depth-first list of states
         self.transition_list = [] # all transitions in the tree, sorted by source state, depth-first
-        self.after_triggers = []
+        self.after_triggers = after_triggers
         self.stable_bitmap = Bitmap() # bitmap of state IDs of states that are stable. Only used for SYNTACTIC-maximality semantics.
 
         next_id = 0
@@ -203,7 +227,7 @@ class StateTree:
                     has_eventless_transitions = True
                 elif isinstance(t.trigger, AfterTrigger):
                     after_triggers.append(t.trigger)
-                    self.after_triggers.append(t.trigger)
+                    # self.after_triggers.append(t.trigger)
 
             for c in state.children:
                 init_state(c, full_name, [state] + ancestors)

+ 0 - 36
src/sccd/syntax/test_scope.py

@@ -1,36 +0,0 @@
-import unittest
-from scope import *
-from typing import *
-
-class TestScope(unittest.TestCase):
-
-  def test_scope(self):
-    
-    builtin = Scope("builtin", parent=None)
-
-    # Lookup LHS value (creating it in the current scope if not found)
-
-    variable = builtin.put_variable_assignment("in_state", Callable[[List[str]], bool])
-    self.assertEqual(variable.offset, 0)
-
-    globals = Scope("globals", parent=builtin)
-    variable = globals.put_variable_assignment("x", int)
-    self.assertEqual(variable.offset, 1)
-
-    variable = globals.put_variable_assignment("in_state", Callable[[List[str]], bool])
-    self.assertEqual(variable.offset, 0)
-
-    local = Scope("local", parent=globals)
-    variable = local.put_variable_assignment("x", int)
-    self.assertEqual(variable.offset, 1)
-
-    # Lookup RHS value (returning None if not found)
-    variable = local.get("x")
-    self.assertEqual(variable.offset, 1)
-
-    # name 'y' doesn't exist in any scope
-    self.assertRaises(Exception, lambda: local.get("y"))
-
-    self.assertEqual(builtin.total_size(), 1)
-    self.assertEqual(globals.total_size(), 2)
-    self.assertEqual(local.total_size(), 2)

+ 10 - 2
src/sccd/parser/xml_parser.py

@@ -184,7 +184,7 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
       raise
     except XmlErrorElement as e:
       # Element where exception occured is part of exception object:
-      e.args = (xml_fragment(src_file, t.el) + str(e),)
+      e.args = (xml_fragment(src_file, e.el) + str(e),)
       raise
 
   results = results_stack[0] # sole stack frame remaining
@@ -194,5 +194,13 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
 def require_attribute(el, attr):
   val = el.get(attr)
   if val is None:
-    raise XmlError("missing required attribute '%s'" % attr)
+    raise XmlErrorElement(el, "missing required attribute '%s'" % attr)
   return val
+
+def if_attribute(el, attr, callback):
+  val = el.get(attr)
+  if val is not None:
+    try:
+      callback(val)
+    except Exception as e:
+      raise XmlErrorElement(el, "attribute %s=\"%s\": %s" % (attr, val, str(e))) from e

+ 1 - 1
test/legacy_render.py

@@ -5,7 +5,7 @@ import multiprocessing
 from lib.os_tools import *
 from lib.loader import *
 from sccd.compiler.utils import FormattedWriter
-from sccd.syntax.statechart import *
+from sccd.statechart.static.statechart import *
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(

+ 8 - 5
test/lib/test_parser.py

@@ -1,7 +1,7 @@
-from sccd.parser.statechart_parser import *
+from sccd.statechart.parser.parser import *
 from sccd.model.globals import *
 from sccd.controller.controller import InputEvent
-from sccd.execution.event import Event
+from sccd.statechart.dynamic.event import Event
 from sccd.model.model import *
 
 @dataclass
@@ -23,8 +23,11 @@ def create_test_parser(create_statechart_parser):
         name = require_attribute(el, "name")
         port = require_attribute(el, "port")
         time = require_attribute(el, "time")
-        duration = parse_duration(globals, time)
-        input.append(InputEvent(name=name, port=port, params=[], time_offset=duration))
+        time_expr = parse_expression(globals, time)
+        time_type = time_expr.init_expr(scope=None)
+        check_duration_type(time_type)
+        time_val = time_expr.eval(memory=None)
+        input.append(InputEvent(name=name, port=port, params=[], time_offset=time_val))
 
       return [("event+", parse_input_event)]
 
@@ -42,7 +45,7 @@ def create_test_parser(create_statechart_parser):
           def parse_param(el):
             val_text = require_attribute(el, "val")
             val_expr = parse_expression(globals, val_text)
-            val = val_expr.eval(EvalContext(current_state=None, events=[], memory=None))
+            val = val_expr.eval(memory=None)
             params.append(val)
 
           return [("param*", parse_param)]

+ 5 - 1
test/render.py

@@ -2,9 +2,10 @@ import argparse
 import sys
 import subprocess
 import multiprocessing
+import os
 from lib.os_tools import *
 from sccd.util.indenting_writer import *
-from sccd.parser.statechart_parser import *
+from sccd.statechart.parser.parser import *
 import lxml.etree as ET
 
 if __name__ == '__main__':
@@ -34,6 +35,9 @@ if __name__ == '__main__':
       parser.print_usage()
       exit()
 
+    # From this point on, disable terminal colors as we write output files
+    os.environ["ANSI_COLORS_DISABLED"] = "1"
+
     def process(src):
       try:
         parse_statechart = create_statechart_parser(Globals(), src, load_external=False)[0][1]

+ 4 - 4
test/test_files/day_atlee/statechart_fig7_dialer.svg

@@ -4,11 +4,11 @@
 <!-- Generated by graphviz version 2.40.1 (20161225.0304)
  -->
 <!-- Title: state transitions Pages: 1 -->
-<svg width="225pt" height="83pt"
- viewBox="0.00 0.00 224.92 83.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<svg width="250pt" height="83pt"
+ viewBox="0.00 0.00 250.47 83.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 79)">
 <title>state transitions</title>
-<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-79 220.918,-79 220.918,4 -4,4"/>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-79 246.471,-79 246.471,4 -4,4"/>
 <!-- __initial -->
 <g id="node1" class="node">
 <title>__initial</title>
@@ -33,7 +33,7 @@
 <title>_D&#45;&gt;_D</title>
 <path fill="none" stroke="#000000" d="M56.0183,-23.9381C67.888,-24.3856 78,-22.4063 78,-18 78,-14.9707 73.2205,-13.0885 66.3762,-12.3533"/>
 <polygon fill="#000000" stroke="#000000" points="66.1128,-8.8446 56.0183,-12.0619 65.9158,-15.8419 66.1128,-8.8446"/>
-<text text-anchor="start" x="78" y="-15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial [c &lt; 10]/c = c + 1 ^out.out &#160;&#160;</text>
+<text text-anchor="start" x="78" y="-15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dial(d:int) [c &lt; 10]/c = c + 1 ^out.out &#160;&#160;</text>
 </g>
 </g>
 </svg>

+ 4 - 4
test/test_files/features/datamodel/test_cond.svg

@@ -4,11 +4,11 @@
 <!-- Generated by graphviz version 2.40.1 (20161225.0304)
  -->
 <!-- Title: state transitions Pages: 1 -->
-<svg width="135pt" height="147pt"
- viewBox="0.00 0.00 135.19 147.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<svg width="127pt" height="147pt"
+ viewBox="0.00 0.00 126.85 147.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 143)">
 <title>state transitions</title>
-<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-143 131.188,-143 131.188,4 -4,4"/>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-143 122.85,-143 122.85,4 -4,4"/>
 <!-- __initial -->
 <g id="node1" class="node">
 <title>__initial</title>
@@ -40,7 +40,7 @@
 <title>_start&#45;&gt;_done</title>
 <path fill="none" stroke="#000000" d="M28,-63.8314C28,-58.4728 28,-52.4735 28,-46.6262"/>
 <polygon fill="#000000" stroke="#000000" points="31.5001,-46.4363 28,-36.4363 24.5001,-46.4363 31.5001,-46.4363"/>
-<text text-anchor="start" x="28" y="-47" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">e [x == 42]^out.done &#160;&#160;</text>
+<text text-anchor="start" x="28" y="-47" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">[x == 42]^out.done &#160;&#160;</text>
 </g>
 </g>
 </svg>

+ 0 - 2
test/test_files/features/functions/test_closure.xml

@@ -28,8 +28,6 @@
     <root initial="s1">
       <state id="s1">
         <onentry>
-          <!-- calling 'increment', the statement "i += 1" will cause a runtime error
-               since the stack frame of 'counter', containing variable 'i' is no longer there -->
           <code>
             x = increment();
           </code>

+ 2 - 0
test/test_files/features/functions/test_functions.xml

@@ -11,6 +11,8 @@
         return float_to_int(log10(i)) + 1;
       };
 
+      parameterless = func {};
+
       ok = numdigits(123) == 3 and digit(123, 1) == 2;
     </datamodel>