Repository: edgedb/edgedb

Issue:
Parsing precedence of binary comparison operators
In 3.x and prior versions, we have the following behavior:

```
edgedb> select a < b < c;   
EdgeQLSyntaxError: Unexpected '<'

edgedb> select a <= b <= c;
Valid syntax
```

In 4.x I've refactored the parser a bit and unknowingly changed the behavior to:

```
edgedb> select a < b < c;   
EdgeQLSyntaxError: Unexpected '<'

edgedb> select a <= b <= c;
EdgeQLSyntaxError: Unexpected '<='
```

---

My opinion is that both queries should either be allowed or forbidden. @msullivan is of opinion that they should be both forbidden, since we might want to introduce Python-style comparison in the future (`a < b < c` would be equivalent to `a < b and b < c`).

Given that:
- such queries make sense semantically only when the last operand is a boolean and
- comparing booleans in user code is very unconventional and probably a bug
... I'll forbid both queries, even though this is backward incompatible.

This issue is just documenting the decision.


Discussion:
This change in behavior applies to all comparison operators: `<`, `<=`, `>`, `>=`, `=`, `!=`, `?=`, `?!=`

Resolving patch:
diff --git a/edb/edgeql/parser/grammar/expressions.py b/edb/edgeql/parser/grammar/expressions.py
--- a/edb/edgeql/parser/grammar/expressions.py
+++ b/edb/edgeql/parser/grammar/expressions.py
@@ -1445,34 +1445,42 @@ def reduce_Expr_INTERSECT_Expr(self, *kids):
 
 class CompareOp(Nonterm):
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_DISTINCTFROM(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_GREATEREQ(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_LESSEQ(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_NOTDISTINCTFROM(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_NOTEQ(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_LANGBRACKET(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_RANGBRACKET(self, *_):
         pass
 
     @parsing.inline(0)
+    @parsing.precedence(precedence.P_COMPARE_OP)
     def reduce_EQUALS(self, *_):
         pass
 
diff --git a/edb/edgeql/parser/grammar/precedence.py b/edb/edgeql/parser/grammar/precedence.py
--- a/edb/edgeql/parser/grammar/precedence.py
+++ b/edb/edgeql/parser/grammar/precedence.py
@@ -54,15 +54,6 @@ class P_NOT(Precedence, assoc='right', tokens=('NOT',)):
     pass
 
 
-class P_EQUALS(Precedence, assoc='right', tokens=('EQUALS',)):
-    pass
-
-
-class P_ANGBRACKET(Precedence, assoc='nonassoc',
-                   tokens=('LANGBRACKET', 'RANGBRACKET')):
-    pass
-
-
 class P_LIKE_ILIKE(Precedence, assoc='nonassoc', tokens=('LIKE', 'ILIKE')):
     pass
 
@@ -77,9 +68,16 @@ class P_IDENT(Precedence, assoc='nonassoc', tokens=('IDENT', 'PARTITION')):
 
 class P_COMPARE_OP(
     Precedence,
-    assoc='left',
+    assoc='nonassoc',
     tokens=(
-        'DISTINCTFROM', 'GREATEREQ', 'LESSEQ', 'NOTDISTINCTFROM', 'NOTEQ'
+        'DISTINCTFROM',
+        'GREATEREQ',
+        'LESSEQ',
+        'NOTDISTINCTFROM',
+        'NOTEQ',
+        'LANGBRACKET',
+        'RANGBRACKET',
+        'EQUALS',
     )
 ):
     pass


Test patch:
diff --git a/tests/test_edgeql_syntax.py b/tests/test_edgeql_syntax.py
--- a/tests/test_edgeql_syntax.py
+++ b/tests/test_edgeql_syntax.py
@@ -830,6 +830,49 @@ def test_edgeql_syntax_ops_27(self):
             (IF (x = 'c') THEN 100 ELSE
             0)));
         """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"Unexpected '<'", line=2, col=22)
+    def test_edgeql_syntax_ops_28(self):
+        """
+        SELECT a < b < c;
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"Unexpected '>'", line=2, col=22)
+    def test_edgeql_syntax_ops_29(self):
+        """
+        SELECT a < b > c;
+        """
+
+    def test_edgeql_syntax_ops_30(self):
+        """
+        SELECT (a < b) > c;
+% OK %
+        SELECT ((a < b) > c);
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"Unexpected '>='", line=2, col=23)
+    def test_edgeql_syntax_ops_31(self):
+        """
+        SELECT a <= b >= c;
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"Unexpected '!='", line=2, col=23)
+    def test_edgeql_syntax_ops_32(self):
+        """
+        SELECT a != b != c;
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"Unexpected '='", line=2, col=22)
+    def test_edgeql_syntax_ops_33(self):
+        """
+        SELECT a = b = c;
+        """
+
     def test_edgeql_syntax_required_01(self):
         """
         SELECT REQUIRED (User.groups.description);
@@ -3888,14 +3931,12 @@ def test_edgeql_syntax_introspect_04(self):
         """
 
     @tb.must_fail(errors.EdgeQLSyntaxError,
-                  r"Missing '\('",
-                  line=2, col=39)
+                  r"Unexpected '>'",
+                  line=2, col=38)
     def test_edgeql_syntax_introspect_05(self):
         """
         SELECT INTROSPECT tuple<int64>;
         """
-        # XXX: error recovery quality regression
-        #      parsed as `select ((INTROSPECT tuple < int64) > {})`
 
     # DDL
     #