Browse Source

Got rid of dependency 'typing_inspect' giving problems with Python 3.6 by declaring our 'SCCD' types (for int, float, function, array, ...) instead of using Python's built-in types.

Joeri Exelmans 5 years ago
parent
commit
9ed4bbd9c6

+ 1 - 2
README.md

@@ -9,8 +9,7 @@
   * [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
   * [termcolor](https://pypi.org/project/termcolor/) for colored terminal output
-  * [dataclasses](https://pypi.org/project/dataclasses/) (not needed if using Python >= 3.7)
-  * [typing-inspect](https://github.com/ilevkivskyi/typing_inspect) (not needed if using Python >= 3.8)
+  * [dataclasses](https://pypi.org/project/dataclasses/) (not needed for Python >= 3.7)
 
 ### Optional
 

+ 5 - 5
src/sccd/execution/builtin_scope.py

@@ -9,25 +9,25 @@ def _in_state(ctx: EvalContext, state_list: List[str]) -> bool:
 
   return StatechartState.in_state(ctx.current_state, state_list)
 
-builtin_scope.add_python_function("INSTATE", _in_state)
+builtin_scope.add_constant("INSTATE", _in_state, SCCDFunction([SCCDArray(SCCDString)], SCCDBool))
 
 def _log10(ctx: EvalContext, i: int) -> float:
   import math
   return math.log10(i)
 
-builtin_scope.add_python_function("log10", _log10)
+builtin_scope.add_constant("log10", _log10, SCCDFunction([SCCDInt], SCCDFloat))
 
 def _float_to_int(ctx: EvalContext, x: float) -> int:
   return int(x)
 
-builtin_scope.add_python_function("float_to_int", _float_to_int)
+builtin_scope.add_constant("float_to_int", _float_to_int, SCCDFunction([SCCDFloat], SCCDInt))
 
 def _log(ctx: EvalContext, s: str) -> None:
   print_debug(termcolor.colored("log: ",'blue')+s)
 
-builtin_scope.add_python_function("log", _log)
+builtin_scope.add_constant("log", _log, SCCDFunction([SCCDString]))
 
 def _int_to_str(ctx: EvalContext, i: int) -> str:
   return str(i)
 
-builtin_scope.add_python_function("int_to_str", _int_to_str)
+builtin_scope.add_constant("int_to_str", _int_to_str, SCCDFunction([SCCDInt], SCCDString))

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

@@ -119,10 +119,10 @@ class _ExpressionTransformer(Transformer):
 
   def param_decl(self, node):
     type = {
-      "int": int,
-      "str": str,
-      "float": float,
-      "Duration": Duration
+      "int": SCCDInt,
+      "str": SCCDString,
+      "float": SCCDFloat,
+      "dur": SCCDDuration
     }[node[1]]
     return ParamDecl(name=node[0].value, type=type)
 

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

@@ -36,7 +36,7 @@ params_decl: ( "(" param_decl ("," param_decl)* ")" )?
 
 ?param_decl: IDENTIFIER ":" TYPE
 
-TYPE: "int" | "str" | "Duration"
+TYPE: "int" | "str" | "dur" | "float"
 
 // Expression parsing
 

+ 1 - 1
src/sccd/parser/statechart_parser.py

@@ -202,7 +202,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
             try:
               after_expr = parse_expression(globals, after)
               after_type = after_expr.init_rvalue(scope)
-              if after_type != Duration:
+              if after_type != SCCDDuration:
                 msg = "Expression is '%s' type. Expected 'Duration' type." % str(after_type)
                 if after_type == int:
                   msg += "\n Hint: Did you forget a duration unit sufix? ('s', 'ms', ...)"

+ 38 - 43
src/sccd/syntax/expression.py

@@ -9,18 +9,12 @@ from sccd.syntax.scope import *
 class StaticTypeError(ModelError):
     pass
 
-# 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: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         pass
 
     # Evaluation should NOT have side effects.
@@ -35,7 +29,7 @@ class Expression(ABC):
 class LValue(Expression):
     # Initialize the LValue as an LValue. 
     @abstractmethod
-    def init_lvalue(self, scope: Scope, expected_type: type):
+    def init_lvalue(self, scope: Scope, expected_type: SCCDType):
         pass
 
     @abstractmethod
@@ -52,7 +46,7 @@ class Identifier(LValue):
     name: str
     variable: Optional[Variable] = None
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         # assert self.variable is None
         self.variable = scope.get(self.name)
         # print("init rvalue", self.name, "as", self.variable)
@@ -76,22 +70,23 @@ class FunctionCall(Expression):
 
     type: Optional[type] = None
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         function_type = self.function.init_rvalue(scope)
-        if not isinstance(function_type, Callable):
-            raise StaticTypeError("Function call: Expression '%s' is not callable" % self.function.render())
-        formal_types, return_type = get_args(function_type)
-        self.type = return_type
+        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
 
         # We always secretly pass an EvalContext object with every function call
         # Not visible to the user.
-        assert formal_types[0] == EvalContext
+        # assert formal_types[0] == EvalContext
 
         actual_types = [p.init_rvalue(scope) for p in self.params]
-        for i, (formal, actual) in enumerate(zip(formal_types[1:], actual_types)):
+        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 self.type
+        return return_type
 
     def eval(self, ctx: EvalContext):
         f = self.function.eval(ctx)
@@ -118,14 +113,14 @@ class FunctionDeclaration(Expression):
     body: 'Statement'
     scope: Optional[Scope] = None
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         self.scope = Scope("function", scope)
         # Reserve space for arguments on stack
         for p in self.params_decl:
             p.init_param(self.scope)
         ret = self.body.init_stmt(self.scope)
         return_type = ret.get_return_type()
-        return Callable[[EvalContext,*[p.type for p in self.params_decl]], return_type]
+        return SCCDFunction([p.type for p in self.params_decl], return_type)
 
     def eval(self, ctx: EvalContext):
         def FUNCTION(ctx: EvalContext, *params):
@@ -146,8 +141,8 @@ class FunctionDeclaration(Expression):
 class StringLiteral(Expression):
     string: str
 
-    def init_rvalue(self, scope: Scope) -> type:
-        return str
+    def init_rvalue(self, scope: Scope) -> SCCDType:
+        return SCCDString
 
     def eval(self, ctx: EvalContext):
         return self.string
@@ -160,8 +155,8 @@ class StringLiteral(Expression):
 class IntLiteral(Expression):
     i: int 
 
-    def init_rvalue(self, scope: Scope) -> type:
-        return int
+    def init_rvalue(self, scope: Scope) -> SCCDType:
+        return SCCDInt
 
     def eval(self, ctx: EvalContext):
         return self.i
@@ -173,8 +168,8 @@ class IntLiteral(Expression):
 class BoolLiteral(Expression):
     b: bool 
 
-    def init_rvalue(self, scope: Scope) -> type:
-        return bool
+    def init_rvalue(self, scope: Scope) -> SCCDType:
+        return SCCDBool
 
     def eval(self, ctx: EvalContext):
         return self.b
@@ -186,8 +181,8 @@ class BoolLiteral(Expression):
 class DurationLiteral(Expression):
     d: Duration
 
-    def init_rvalue(self, scope: Scope) -> type:
-        return Duration
+    def init_rvalue(self, scope: Scope) -> SCCDType:
+        return SCCDDuration
 
     def eval(self, ctx: EvalContext):
         return self.d
@@ -199,16 +194,16 @@ class DurationLiteral(Expression):
 class Array(Expression):
     elements: List[Any]
 
-    type: Optional[type] = None
+    element_type: Optional[SCCDType] = None
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         for e in self.elements:
             t = e.init_rvalue(scope)
-            if self.type and self.type != t:
-                raise StaticTypeError("Mixed element types in Array expression: %s and %s" % (str(self.type), str(t)))
-            self.type = t
+            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 List[self.type]
+        return SCCDArray(self.element_type)
 
     def eval(self, ctx: EvalContext):
         return [e.eval(ctx) for e in self.elements]
@@ -222,7 +217,7 @@ class Array(Expression):
 class Group(Expression):
     subexpr: Expression
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         return self.subexpr.init_rvalue(scope)
 
     def eval(self, ctx: EvalContext):
@@ -237,13 +232,13 @@ class BinaryExpression(Expression):
     operator: str # token name from the grammar.
     rhs: Expression
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         lhs_t = self.lhs.init_rvalue(scope)
         rhs_t = self.rhs.init_rvalue(scope)
 
         def comparison_type():
             same_type()
-            return bool
+            return SCCDBool
 
         def same_type():
             if lhs_t != rhs_t:
@@ -255,15 +250,15 @@ class BinaryExpression(Expression):
                 if lhs_t == Duration:
                     raise StaticTypeError("Cannot multiply 'Duration' and 'Duration'")
                 return lhs_t
-            key = lambda x: {int: 1, float: 2, Duration: 3}[x]
+            key = lambda x: {SCCDInt: 1, SCCDFloat: 2, SCCDDuration: 3}[x]
             [smallest_type, largest_type] = sorted([lhs_t, rhs_t], key=key)
-            if largest_type == Duration and smallest_type == float:
+            if largest_type == SCCDDuration and smallest_type == SCCDFloat:
                 raise StaticTypeError("Cannot multiply 'float' and 'Duration'")
             return largest_type
 
         return {
-            "and": bool,
-            "or":  bool,
+            "and": SCCDBool,
+            "or":  SCCDBool,
             "==":  comparison_type(),
             "!=":  comparison_type(),
             ">":   comparison_type(),
@@ -273,7 +268,7 @@ class BinaryExpression(Expression):
             "+":   same_type(),
             "-":   same_type(),
             "*":   mult_type(),
-            "/":   float,
+            "/":   SCCDFloat,
             "//":  same_type(),
             "%":   same_type(),
             "**":  same_type(),
@@ -307,10 +302,10 @@ class UnaryExpression(Expression):
     operator: str # token value from the grammar.
     expr: Expression
 
-    def init_rvalue(self, scope: Scope) -> type:
+    def init_rvalue(self, scope: Scope) -> SCCDType:
         expr_type = self.expr.init_rvalue(scope)
         return {
-            "not": bool,
+            "not": SCCDBool,
             "-":   expr_type,
         }[self.operator]
 

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

@@ -2,6 +2,7 @@ from abc import *
 from typing import *
 from dataclasses import *
 from inspect import signature
+from sccd.syntax.types import *
 import itertools
 
 # Superclass for all "user errors", errors in the model being loaded.
@@ -20,7 +21,7 @@ class EvalContext:
 @dataclass
 class Value(ABC):
   name: str
-  type: type
+  type: SCCDType
 
   @abstractmethod
   def is_read_only(self) -> bool:
@@ -169,7 +170,7 @@ class Scope:
 
   # Add name to scope if it does not exist yet, otherwise return existing Variable for name.
   # This is done when encountering an assignment statement in a block.
-  def put_variable_assignment(self, name: str, expected_type: type) -> Variable:
+  def put_variable_assignment(self, name: str, expected_type: SCCDType) -> Variable:
     found = self._internal_lookup(name)
     if found:
       scope, variable = found
@@ -190,13 +191,13 @@ class Scope:
       scope, variable = found
       raise ScopeError("Name '%s' already in use in scope '%s'" % (name, scope.name))
 
-  def add_constant(self, name: str, value) -> Constant:
+  def add_constant(self, name: str, value: Any, type: SCCDType) -> Constant:
     self._assert_name_available(name)
-    c = Constant(name=name, type=type(value), value=value)
+    c = Constant(name=name, type=type, value=value)
     self.named_values[name] = c
     return c
 
-  def add_variable(self, name: str, expected_type: type) -> Variable:
+  def add_variable(self, name: str, expected_type: SCCDType) -> Variable:
     self._assert_name_available(name)
     variable = Variable(scope=self, name=name, type=expected_type, offset=self.local_size())
     self.named_values[name] = variable
@@ -210,7 +211,7 @@ class Scope:
     self.variables.append(variable)
     return variable
 
-  def add_event_parameter(self, event_name: str, param_name: str, type: type, param_offset=int) -> EventParam:
+  def add_event_parameter(self, event_name: str, param_name: str, type: SCCDType, param_offset=int) -> EventParam:
     self._assert_name_available(param_name)
     param = EventParam(scope=self,
       name=param_name, type=type, offset=self.local_size(),
@@ -219,12 +220,12 @@ class Scope:
     self.variables.append(param)
     return param
 
-  def add_python_function(self, name: str, function: Callable) -> Constant:
-    sig = signature(function)
-    return_type = sig.return_annotation
-    param_types = [a.annotation for a in sig.parameters.values()]
-    function_type = Callable[param_types, return_type]
+  # def add_python_function(self, name: str, function: Callable) -> Constant:
+  #   sig = signature(function)
+  #   return_type = sig.return_annotation
+  #   param_types = [a.annotation for a in sig.parameters.values()]
+  #   function_type = Callable[param_types, return_type]
     
-    c = Constant(name=name, type=function_type, value=function)
-    self.named_values[name] = c
-    return c
+  #   c = Constant(name=name, type=function_type, value=function)
+  #   self.named_values[name] = c
+  #   return c

+ 1 - 6
src/sccd/syntax/statechart.py

@@ -63,12 +63,7 @@ class SemanticConfiguration:
 
   @classmethod
   def get_fields(cls) -> Iterator[Tuple[str, SemanticAspect]]:
-    # Python < 3.8:
-    import sys
-    if sys.version_info.minor < 8:
-      from typing_inspect import get_args
-
-    return ((f.name, get_args(f.type)[0]) for  f in fields(cls))
+    return ((f.name, type(f.default)) for  f in fields(cls))
 
   # Whether multiple options are set for any aspect.
   def has_multiple_variants(self) -> bool:

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

@@ -21,7 +21,7 @@ class ReturnBehavior:
         NEVER = auto()
 
     when: When
-    type: Optional[type] = None
+    type: Optional[SCCDType] = None
 
     def __post_init__(self):
         assert (self.when == ReturnBehavior.When.NEVER) == (self.type is None)

+ 42 - 0
src/sccd/syntax/types.py

@@ -0,0 +1,42 @@
+from abc import *
+from dataclasses import *
+from typing import *
+
+class SCCDType(ABC):
+    @abstractmethod
+    def __str__(self):
+        pass
+
+@dataclass(frozen=True)
+class SCCDSimpleType(SCCDType):
+    name: str
+
+    def __str__(self):
+        return self.name
+
+@dataclass(frozen=True)
+class SCCDFunction(SCCDType):
+    param_types: List[SCCDType]
+    return_type: Optional[SCCDType] = None
+
+    def __str__(self):
+        if self.params:
+            s = "func(" + ",".join(str(p) for p in self.params) + ")"
+        else:
+            s = "func"
+        if self.ret:
+            s += " -> " + str(self.ret)
+        return s
+
+@dataclass(frozen=True)
+class SCCDArray(SCCDType):
+    element_type: SCCDType
+
+    def __str__(self):
+        return "[" + str(element_type) + "]"
+
+SCCDBool = SCCDSimpleType("bool")
+SCCDInt = SCCDSimpleType("int")
+SCCDFloat = SCCDSimpleType("float")
+SCCDDuration = SCCDSimpleType("dur")
+SCCDString = SCCDSimpleType("str")