Repository: edgedb/edgedb

Issue:
confusing error message for malformed for loop
```
edgedb> WITH
.......   updated := (
.......     for s in array_unpack([1, 2, 3])
.......     union (SELECT s)
.......   )
....... select updated;
error: Unexpected token: <Token IDENT "array_unpack">
  ┌─ query:3:14
  │
3 │     for s in array_unpack([1, 2, 3])
  │              ^^^^^^^^^^^^ It appears that a ',' is missing in a tuple before 'array_unpack'
```

<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: package:1.0b2.dev5500+g2f67186c8-2021030200~focal.nightly
- OS Version: Linux 5.8.0-44-generic #50~20.04.1-Ubuntu SMP Wed Feb 10 21:07:30 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux



Discussion:

Resolving patch:
diff --git a/edb/edgeql/parser/parser.py b/edb/edgeql/parser/parser.py
--- a/edb/edgeql/parser/parser.py
+++ b/edb/edgeql/parser/parser.py
@@ -50,67 +50,7 @@ def get_exception(self, native_err, context, token=None):
                 # Look at the parsing stack and use tokens and
                 # non-terminals to infer the parser rule when the
                 # error occurred.
-                rule = ''
-                # The last valid token was a closing
-                # brace/parent/bracket, so we need to find a match for
-                # it before deciding what rule context we're in.
-                need_match = isinstance(ltok, (tokens.T_RBRACE,
-                                               tokens.T_RPAREN,
-                                               tokens.T_RBRACKET))
-                for i, (el, _) in enumerate(reversed(self.parser._stack)):
-                    if isinstance(el, tokens.Token):
-                        # We'll need the element right before "{", "[", or "(".
-                        prevel = self.parser._stack[-2 - i][0]
-
-                        if isinstance(el, tokens.T_LBRACE):
-                            if need_match and isinstance(ltok,
-                                                         tokens.T_RBRACE):
-                                # This is matched, while we're looking
-                                # for unmatched braces.
-                                need_match = False
-                                continue
-
-                            elif isinstance(prevel, gr_commondl.OptExtending):
-                                # This is some SDL/DDL
-                                rule = 'definition'
-                            elif (
-                                isinstance(prevel, gr_exprs.Expr) or
-                                (
-                                    isinstance(prevel, tokens.T_COLON) and
-                                    isinstance(self.parser._stack[-3 - i][0],
-                                               gr_exprs.ShapePointer)
-                                )
-                            ):
-                                # This is some kind of shape.
-                                rule = 'shape'
-                            break
-                        elif isinstance(el, tokens.T_LPAREN):
-                            if need_match and isinstance(ltok,
-                                                         tokens.T_RPAREN):
-                                # This is matched, while we're looking
-                                # for unmatched parentheses.
-                                need_match = False
-                                continue
-                            # This could be an argument list or a tuple.
-                            elif isinstance(prevel, gr_exprs.NodeName):
-                                rule = 'list of arguments'
-                            else:
-                                rule = 'tuple'
-                            break
-                        elif isinstance(el, tokens.T_LBRACKET):
-                            if need_match and isinstance(ltok,
-                                                         tokens.T_RBRACKET):
-                                # This is matched, while we're looking
-                                # for unmatched brackets.
-                                need_match = False
-                                continue
-                            # This is either an array literal or
-                            # array index.
-                            elif isinstance(prevel, gr_exprs.Expr):
-                                rule = 'array slice'
-                            else:
-                                rule = 'array'
-                            break
+                i, rule = self._get_rule()
 
                 if not token or token_kind == 'EOF':
                     msg = 'Unexpected end of line'
@@ -179,6 +119,9 @@ def get_exception(self, native_err, context, token=None):
                                f"instead")
                     else:
                         msg = f'Unexpected {token.text()!r}'
+                elif rule == 'for iterator':
+                    msg = (f"missing '{{' before {token.text()!r} "
+                           f"in a FOR query")
                 elif hasattr(token, 'val'):
                     msg = f'Unexpected {token.val!r}'
                 elif token_kind == 'NL':
@@ -189,6 +132,94 @@ def get_exception(self, native_err, context, token=None):
         return errors.EdgeQLSyntaxError(
             msg, hint=hint, context=context, token=token)
 
+    def _get_rule(self):
+        ltok = self.parser._stack[-1][0]
+        # Look at the parsing stack and use tokens and non-terminals
+        # to infer the parser rule when the error occurred.
+        rule = ''
+
+        # Check if we're in the `FOR x IN <bad_token>` situation
+        if (len(self.parser._stack) >= 3 and
+                isinstance(self.parser._stack[-3][0], tokens.T_FOR) and
+                isinstance(self.parser._stack[-2][0], gr_exprs.Identifier) and
+                isinstance(self.parser._stack[-1][0], tokens.T_IN)):
+            return 2, 'for iterator'
+
+        # If the last valid token was a closing brace/parent/bracket,
+        # so we need to find a match for it before deciding what rule
+        # context we're in.
+        need_match = isinstance(ltok, (tokens.T_RBRACE,
+                                       tokens.T_RPAREN,
+                                       tokens.T_RBRACKET))
+        nextel = None
+        for i, (el, _) in enumerate(reversed(self.parser._stack)):
+            if isinstance(el, tokens.Token):
+                # We'll need the element right before "{", "[", or "(".
+                prevel = self.parser._stack[-2 - i][0]
+
+                if isinstance(el, tokens.T_LBRACE):
+                    if need_match and isinstance(ltok,
+                                                 tokens.T_RBRACE):
+                        # This is matched, while we're looking
+                        # for unmatched braces.
+                        need_match = False
+                        continue
+
+                    elif isinstance(prevel, gr_commondl.OptExtending):
+                        # This is some SDL/DDL
+                        rule = 'definition'
+                    elif (
+                        isinstance(prevel, gr_exprs.Expr) or
+                        (
+                            isinstance(prevel, tokens.T_COLON) and
+                            isinstance(self.parser._stack[-3 - i][0],
+                                       gr_exprs.ShapePointer)
+                        )
+                    ):
+                        # This is some kind of shape.
+                        rule = 'shape'
+                    break
+                elif isinstance(el, tokens.T_LPAREN):
+                    if need_match and isinstance(ltok,
+                                                 tokens.T_RPAREN):
+                        # This is matched, while we're looking
+                        # for unmatched parentheses.
+                        need_match = False
+                        continue
+                    elif isinstance(prevel, gr_exprs.NodeName):
+                        rule = 'list of arguments'
+                    elif isinstance(nextel, (tokens.T_FOR,
+                                             tokens.T_SELECT,
+                                             tokens.T_UPDATE,
+                                             tokens.T_DELETE,
+                                             tokens.T_INSERT,
+                                             tokens.T_FOR)):
+                        # A parenthesized subquery expression,
+                        # we should leave the error as is.
+                        break
+                    else:
+                        rule = 'tuple'
+                    break
+                elif isinstance(el, tokens.T_LBRACKET):
+                    if need_match and isinstance(ltok,
+                                                 tokens.T_RBRACKET):
+                        # This is matched, while we're looking
+                        # for unmatched brackets.
+                        need_match = False
+                        continue
+                    # This is either an array literal or
+                    # array index.
+                    elif isinstance(prevel, gr_exprs.Expr):
+                        rule = 'array slice'
+                    else:
+                        rule = 'array'
+                    break
+
+            # Also keep track of the element right after current.
+            nextel = el
+
+        return i, rule
+
     def get_lexer(self):
         return rust_lexer.EdgeQLLexer()
 


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
@@ -2589,6 +2589,15 @@ def test_edgeql_syntax_select_11(self):
         SELECT User.name OFFSET Foo.bar LIMIT (Foo.bar * 10);
         """
 
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r'Unexpected.+bar', hint=None, line=3, col=24)
+    def test_edgeql_syntax_select_12(self):
+        """
+        SELECT (
+            SELECT Foo bar
+        );
+        """
+
     def test_edgeql_syntax_group_01(self):
         """
         GROUP User
@@ -2837,6 +2846,15 @@ def test_edgeql_syntax_insert_21(self):
         } UNLESS CONFLICT ELSE (SELECT Foo);
         """
 
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r'Unexpected.+bar', hint=None, line=3, col=24)
+    def test_edgeql_syntax_insert_22(self):
+        """
+        SELECT (
+            INSERT Foo bar
+        );
+        """
+
     def test_edgeql_syntax_delete_01(self):
         """
         DELETE Foo;
@@ -2872,6 +2890,15 @@ def test_edgeql_syntax_delete_05(self):
         OFFSET 2 LIMIT 5;
         """
 
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r'Unexpected.+bar', hint=None, line=3, col=24)
+    def test_edgeql_syntax_delete_06(self):
+        """
+        SELECT (
+            DELETE Foo bar
+        );
+        """
+
     def test_edgeql_syntax_update_01(self):
         """
         UPDATE Foo SET {bar := 42};
@@ -2912,6 +2939,20 @@ def test_edgeql_syntax_update_07(self):
         };
         """
 
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r'Unexpected.+bad', hint=None, line=4, col=25)
+    def test_edgeql_syntax_update_08(self):
+        """
+        WITH x := (
+            UPDATE Foo
+            FILTER .bar bad = 24
+            SET {
+                bar := 42,
+            };
+        )
+        SELECT x
+        """
+
     def test_edgeql_syntax_insertfor_01(self):
         """
         FOR name IN {'a', 'b', 'c'}
@@ -2971,6 +3012,35 @@ def test_edgeql_syntax_selectfor_01(self):
         );
         """
 
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"missing '{' before.+array_unpack",
+                  line=2, col=26)
+    def test_edgeql_syntax_selectfor_02(self):
+        """
+        SELECT (FOR s IN array_unpack([1, 2, 3]) UNION s);
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r"missing '{' before.+array_unpack",
+                  line=3, col=22)
+    def test_edgeql_syntax_selectfor_03(self):
+        """
+        WITH x := (
+            FOR s IN array_unpack([1, 2, 3]) UNION s
+        )
+        SELECT x;
+        """
+
+    @tb.must_fail(errors.EdgeQLSyntaxError,
+                  r'Unexpected.+bad', hint=None, line=3, col=56)
+    def test_edgeql_syntax_selectfor_04(self):
+        """
+        WITH x := (
+            FOR s IN {array_unpack([1, 2, 3])} UNION s bad
+        )
+        SELECT x;
+        """
+
     def test_edgeql_syntax_deletefor_01(self):
         """
         FOR x IN {(('Alice', 'White') UNION ('Bob', 'Green'))}