closes #69185: syntax error with unary operators stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 02 Aug 2011 17:05:41 +0200
branchstable
changeset 655 4f1ca9594a30
parent 654 7decc7f6c23f
child 656 b3c48a8f87a8
closes #69185: syntax error with unary operators
ChangeLog
base.py
nodes.py
parser.g
parser.py
stcheck.py
test/unittest_parser.py
--- a/ChangeLog	Tue Aug 02 17:05:40 2011 +0200
+++ b/ChangeLog	Tue Aug 02 17:05:41 2011 +0200
@@ -3,7 +3,7 @@
 
 	--
 
-    * add some missing operators (#72295):
+    * #72295: add some missing operators:
 
       * % (modulo),
       * ^ (power),
@@ -13,10 +13,13 @@
       * << (bitwise left shift),
       * >> (bitwise right shift)
 
-    * new #72052: optional 'optcomparisons' key in variable stinfo, containing
+    * #72052: new optional 'optcomparisons' key in variable stinfo, containing
       HAVING comparison nodes where it's used and optional (eg outer
       join)
 
+    * #69185: fix syntax error with unary operators by introducing
+      `UnaryExpression` node
+
     * fix Comparison.as_string to considerer its optional attribute
 
 2011-07-27  --  0.29.1
--- a/base.py	Tue Aug 02 17:05:40 2011 +0200
+++ b/base.py	Tue Aug 02 17:05:41 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -22,6 +22,7 @@
 
 __docformat__ = "restructuredtext en"
 
+
 class BaseNode(object):
     __slots__ = ('parent',)
 
@@ -196,4 +197,3 @@
         stmt is the root node, which should be use to get new variables.
         """
         return self.__class__(*self.initargs(stmt))
-
--- a/nodes.py	Tue Aug 02 17:05:40 2011 +0200
+++ b/nodes.py	Tue Aug 02 17:05:41 2011 +0200
@@ -76,6 +76,31 @@
             yield vref
 
 
+class OperatorExpressionMixin(object):
+
+    def initargs(self, stmt):
+        """return list of arguments to give to __init__ to clone this node"""
+        return (self.operator,)
+
+    def is_equivalent(self, other):
+        if not Node.is_equivalent(self, other):
+            return False
+        return self.operator == other.operator
+
+    def get_description(self, mainindex, tr):
+        """if there is a variable in the math expr used as rhs of a relation,
+        return the name of this relation, else return the type of the math
+        expression
+        """
+        try:
+            return tr(self.get_type())
+        except CoercionError:
+            for vref in self.iget_nodes(VariableRef):
+                vtype = vref.get_description(mainindex, tr)
+                if vtype != 'Any':
+                    return tr(vtype)
+
+
 class HSMixin(object):
     """mixin class for classes which may be the lhs or rhs of an expression"""
     __slots__ = ()
@@ -541,23 +566,14 @@
         return '%s %s' % (self.operator, ', '.join(repr(c) for c in self.children))
 
 
-class MathExpression(HSMixin, BinaryNode):
-    """Operators plus, minus, multiply, divide."""
+class MathExpression(OperatorExpressionMixin, HSMixin, BinaryNode):
+    """Mathematical Operators"""
     __slots__ = ('operator',)
 
     def __init__(self, operator, lhs=None, rhs=None):
         BinaryNode.__init__(self, lhs, rhs)
         self.operator = operator.encode()
 
-    def initargs(self, stmt):
-        """return list of arguments to give to __init__ to clone this node"""
-        return (self.operator,)
-
-    def is_equivalent(self, other):
-        if not Node.is_equivalent(self, other):
-            return False
-        return self.operator == other.operator
-
     def as_string(self, encoding=None, kwargs=None):
         """return the tree as an encoded rql string"""
         return '(%s %s %s)' % (self.children[0].as_string(encoding, kwargs),
@@ -592,18 +608,31 @@
                 return 'Float'
             raise CoercionError(key)
 
-    def get_description(self, mainindex, tr):
-        """if there is a variable in the math expr used as rhs of a relation,
-        return the name of this relation, else return the type of the math
-        expression
+
+class UnaryExpression(OperatorExpressionMixin, Node):
+    """Unary Operators"""
+    __slots__ = ('operator',)
+
+    def __init__(self, operator, child=None):
+        Node.__init__(self)
+        self.operator = operator.encode()
+        if child is not None:
+            self.append(child)
+
+    def as_string(self, encoding=None, kwargs=None):
+        """return the tree as an encoded rql string"""
+        return '%s%s' % (self.operator.encode(),
+                         self.children[0].as_string(encoding, kwargs))
+
+    def __repr__(self):
+        return '%s%r' % (self.operator, self.children[0])
+
+    def get_type(self, solution=None, kwargs=None):
+        """return the type of object returned by this expression if known
+
+        solution is an optional variable/etype mapping
         """
-        try:
-            return tr(self.get_type())
-        except CoercionError:
-            for vref in self.iget_nodes(VariableRef):
-                vtype = vref.get_description(mainindex, tr)
-                if vtype != 'Any':
-                    return tr(vtype)
+        return self.children[0].get_type(solution, kwargs)
 
 
 class Function(HSMixin, Node):
@@ -1065,5 +1094,5 @@
 
 
 build_visitor_stub((SubQuery, And, Or, Not, Exists, Relation,
-                    Comparison, MathExpression, Function, Constant,
-                    VariableRef, SortTerm, ColumnAlias, Variable))
+                    Comparison, MathExpression, UnaryExpression, Function,
+                    Constant, VariableRef, SortTerm, ColumnAlias, Variable))
--- a/parser.g	Tue Aug 02 17:05:40 2011 +0200
+++ b/parser.g	Tue Aug 02 17:05:41 2011 +0200
@@ -92,6 +92,7 @@
     token ADD_OP:      r'\+|-|\||#'
     token MUL_OP:      r'\*|/|%|&'
     token POW_OP:      r'\^|>>|<<'
+    token UNARY_OP:    r'-|~'
     token FUNCTION:    r'[A-Za-z_]+\s*(?=\()'
     token R_TYPE:      r'[a-z_][a-z0-9_]*'
     token E_TYPE:      r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*'
@@ -316,6 +317,9 @@
 rule expr_add<<S>>: expr_mul<<S>>          {{ node = expr_mul }}
                     ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
                     )*                     {{ return node }}
+                  | UNARY_OP expr_mul<<S>> {{ node = UnaryExpression( UNARY_OP, expr_mul ) }}
+                    ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
+                    )*                     {{ return node }}
 
 
 rule expr_mul<<S>>: expr_pow<<S>>          {{ node = expr_pow }}
--- a/parser.py	Tue Aug 02 17:05:40 2011 +0200
+++ b/parser.py	Tue Aug 02 17:05:41 2011 +0200
@@ -99,6 +99,7 @@
         ('ADD_OP', re.compile('\\+|-|\\||#')),
         ('MUL_OP', re.compile('\\*|/|%|&')),
         ('POW_OP', re.compile('\\^|>>|<<')),
+        ('UNARY_OP', re.compile('-|~')),
         ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
         ('R_TYPE', re.compile('[a-z_][a-z0-9_]*')),
         ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[0-9]*')),
@@ -500,7 +501,7 @@
 
     def exprs_not(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'exprs_not', [S])
-        _token = self._peek('NOT', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('NOT', 'r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'NOT':
             NOT = self._scan('NOT', context=_context)
             balanced_expr = self.balanced_expr(S, _context)
@@ -511,7 +512,7 @@
 
     def balanced_expr(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'balanced_expr', [S])
-        _token = self._peek('r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'r"\\("':
             self._scan('r"\\("', context=_context)
             logical_expr = self.logical_expr(S, _context)
@@ -578,7 +579,7 @@
 
     def expr(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr', [S])
-        _token = self._peek('CMP_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('CMP_OP', 'UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'CMP_OP':
             CMP_OP = self._scan('CMP_OP', context=_context)
             expr_add = self.expr_add(S, _context)
@@ -589,13 +590,24 @@
 
     def expr_add(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr_add', [S])
-        expr_mul = self.expr_mul(S, _context)
-        node = expr_mul
-        while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
-            ADD_OP = self._scan('ADD_OP', context=_context)
+        _token = self._peek('UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        if _token != 'UNARY_OP':
             expr_mul = self.expr_mul(S, _context)
-            node = MathExpression( ADD_OP, node, expr_mul )
-        return node
+            node = expr_mul
+            while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
+                ADD_OP = self._scan('ADD_OP', context=_context)
+                expr_mul = self.expr_mul(S, _context)
+                node = MathExpression( ADD_OP, node, expr_mul )
+            return node
+        else: # == 'UNARY_OP'
+            UNARY_OP = self._scan('UNARY_OP', context=_context)
+            expr_mul = self.expr_mul(S, _context)
+            node = UnaryExpression( UNARY_OP, expr_mul )
+            while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
+                ADD_OP = self._scan('ADD_OP', context=_context)
+                expr_mul = self.expr_mul(S, _context)
+                node = MathExpression( ADD_OP, node, expr_mul )
+            return node
 
     def expr_mul(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr_mul', [S])
@@ -643,7 +655,7 @@
         FUNCTION = self._scan('FUNCTION', context=_context)
         self._scan('r"\\("', context=_context)
         F = Function(FUNCTION)
-        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
             expr_add = self.expr_add(S, _context)
             while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
                 F.append(expr_add)
@@ -658,7 +670,7 @@
         self._scan("'IN'", context=_context)
         self._scan('r"\\("', context=_context)
         F = Function('IN')
-        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
             expr_add = self.expr_add(S, _context)
             while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
                 F.append(expr_add)
--- a/stcheck.py	Tue Aug 02 17:05:40 2011 +0200
+++ b/stcheck.py	Tue Aug 02 17:05:41 2011 +0200
@@ -408,6 +408,10 @@
         pass #assert len(mathexpr.children) == 2, len(mathexpr.children)
     def leave_mathexpression(self, node, state):
         pass
+    def visit_unaryexpression(self, unaryexpr, state):
+        pass #assert len(unaryexpr.children) == 2, len(unaryexpr.children)
+    def leave_unaryexpression(self, node, state):
+        pass
 
     def visit_function(self, function, state):
         try:
--- a/test/unittest_parser.py	Tue Aug 02 17:05:40 2011 +0200
+++ b/test/unittest_parser.py	Tue Aug 02 17:05:41 2011 +0200
@@ -82,6 +82,7 @@
     "INSERT Person X: X nom 'bidule', X ami Y WHERE Y nom 'chouette';",
     "SET X nom 'toto', X prenom 'original' WHERE X is Person, X nom 'bidule';",
     "SET X know Y WHERE X ami Y;",
+    "SET X value -Y WHERE X value Y;",
     "DELETE Person X WHERE X nom 'toto';",
     "DELETE X ami Y WHERE X is Person, X nom 'toto';",