Subject: Add support for Python 3.14
Author: Alexander Sulfrian <alexander@sulfrian.net>
Forwarded: https://github.com/we-like-parsers/pegen/pull/112

This adjusts the grammar and tests to support the changes introduced
with Python 3.14.

Index: python-pegen/data/python.gram
===================================================================
--- python-pegen.orig/data/python.gram
+++ python-pegen/data/python.gram
@@ -1047,13 +1047,29 @@ try_stmt[ast.Try]:
 
 except_block[ast.ExceptHandler]:
     | invalid_except_stmt_indent
+    | 'except' e=expressions ':' b=block {
+        PY_VERSION >= (3, 14);
+        ast.ExceptHandler(type=e, name=None, body=b, LOCATIONS) }
+    | 'except' e=expression 'as' t=NAME ':' b=block {
+        PY_VERSION >= (3, 14);
+        ast.ExceptHandler(type=e, name=t.string, body=b, LOCATIONS) }
     | 'except' e=expression t=['as' z=NAME { z.string }] ':' b=block {
+        PY_VERSION < (3, 14);
         ast.ExceptHandler(type=e, name=t, body=b, LOCATIONS) }
     | 'except' ':' b=block { ast.ExceptHandler(type=None, name=None, body=b, LOCATIONS) }
     | invalid_except_stmt
 except_star_block[ast.ExceptHandler]:
     | invalid_except_star_stmt_indent
+    | 'except' '*' e=expressions ':' b=block {
+        PY_VERSION >= (3, 14);
+        ast.ExceptHandler(type=e, name=None, body=b, LOCATIONS)
+     }
+    | 'except' '*' e=expression 'as' t=NAME ':' b=block {
+        PY_VERSION >= (3, 14);
+        ast.ExceptHandler(type=e, name=t.string, body=b, LOCATIONS)
+     }
     | 'except' '*' e=expression t=['as' z=NAME { z.string }] ':' b=block {
+        PY_VERSION < (3, 14);
         ast.ExceptHandler(type=e, name=t, body=b, LOCATIONS)
      }
     | invalid_except_stmt
@@ -2256,7 +2272,12 @@ invalid_try_stmt[NoReturn]:
         )
      }
 invalid_except_stmt[None]:
+    | 'except' '*'? a=expression ',' expressions 'as' NAME ':' {
+        PY_VERSION >= (3, 14);
+        self.raise_syntax_error_starting_from("multiple exception types must be parenthesized when using 'as'", a)
+     }
     | 'except' '*'? a=expression ',' expressions ['as' NAME ] ':' {
+        PY_VERSION < (3, 14);
         self.raise_syntax_error_starting_from("multiple exception types must be parenthesized", a)
      }
     | a='except' '*'? expression ['as' NAME ] NEWLINE { self.raise_syntax_error("expected ':'") }
@@ -2315,8 +2336,11 @@ invalid_as_pattern[NoReturn]:
     | or_pattern 'as' a="_" {
         self.raise_syntax_error_known_location("cannot use '_' as a target", a)
      }
-    | or_pattern 'as' !NAME a=expression {
-        self.raise_syntax_error_known_location("invalid pattern target", a)
+    | or_pattern 'as' a=expression {
+        self.raise_syntax_error_known_location(
+            f"cannot use {self.get_expr_name(a)} as pattern target", a)
+        if self.py_version >= (3, 14)
+        else self.raise_syntax_error_known_location("invalid pattern target", a)
      }
 invalid_class_pattern[NoReturn]:
     | name_or_attr '(' a=invalid_class_argument_pattern  {
Index: python-pegen/tests/python_parser/test_syntax_error_handling.py
===================================================================
--- python-pegen.orig/tests/python_parser/test_syntax_error_handling.py
+++ python-pegen/tests/python_parser/test_syntax_error_handling.py
@@ -976,26 +976,32 @@ def test_invalid_try_stmt(
                 sys.version_info < (3, 11), reason="Syntax unsupported before 3.11+"
             ),
         ),
-        (
+        pytest.param(
             "try:\n\tpass\nexcept ValueError, IndexError:",
             SyntaxError,
             "multiple exception types must be parenthesized",
             (3, 8),
             (3, 30),
+            marks=pytest.mark.skipif(
+                sys.version_info >= (3, 14), reason="PEP 758 allows unparenthesized except and except* blocks"
+            ),
         ),
-        (
+        pytest.param(
             "try:\n\tpass\nexcept ValueError, IndexError,:",
             SyntaxError,
             "multiple exception types must be parenthesized",
             (3, 8),
             (3, 31),
+            marks=pytest.mark.skipif(
+                sys.version_info >= (3, 14), reason="PEP 758 allows unparenthesized except and except* blocks"
+            ),
         ),
         (
             "try:\n\tpass\nexcept ValueError, IndexError, a=1:",
             SyntaxError,
             "invalid syntax",
-            (3, 18),
-            (3, 19),
+            (3, 33) if sys.version_info >= (3, 14) else (3, 18),
+            (3, 34) if sys.version_info >= (3, 14) else (3, 19),
         ),
         (
             "try:\n\tpass\nexcept Exception\npass",
@@ -1156,7 +1162,7 @@ def test_invalid_case_stmt(
         (
             "match a:\n\tcase 1 as 1+1:\n\t\tpass",
             SyntaxError,
-            "invalid pattern target",
+            "cannot use expression as pattern target" if sys.version_info >= (3, 14) else "invalid pattern target",
             (2, 12),
             (2, 15),
         ),
Index: python-pegen/src/pegen/python_generator.py
===================================================================
--- python-pegen.orig/src/pegen/python_generator.py
+++ python-pegen/src/pegen/python_generator.py
@@ -397,6 +397,7 @@ class PythonParserGenerator(ParserGenera
         locations = False
         unreachable = False
         used = None
+        version_check = None
         if action:
             # Replace magic name in the action rule
             if "LOCATIONS" in action:
@@ -405,6 +406,11 @@ class PythonParserGenerator(ParserGenera
             if "UNREACHABLE" in action:
                 unreachable = True
                 action = action.replace("UNREACHABLE", self.unreachable_formatting)
+            if ";" in action:
+                parts = action.split(";", 1)
+                if parts[0].startswith("PY_VERSION"):
+                    action = parts[1].lstrip()
+                    version_check = parts[0].replace("PY_VERSION", "self.py_version")
 
             # Extract the names actually used in the action.
             used = self.usednamesvisitor.visit(ast.parse(action))
@@ -423,6 +429,11 @@ class PythonParserGenerator(ParserGenera
                 if has_invalid:
                     self.print("self.call_invalid_rules")
                     first = False
+                if version_check:
+                    if not first:
+                        self.print("and")
+                    self.print(f"({version_check})")
+                    first = False
                 for item in node.items:
                     if first:
                         first = False
