https://github.com/python/cpython/commit/b7f81d4fab8f2d7f096642433967ce0e713802dd
commit: b7f81d4fab8f2d7f096642433967ce0e713802dd
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-24T16:27:08Z
summary:

Refactor pegen C generation into explicit compilation phases (#157501)

* Move pegen grammar analysis into its own module

* Record pegen helper rule kinds explicitly

* Introduce immutable C parser data and separate lowering

* Add C emitters with local rule and output state

* Route C generation through the prepared parser model

* Fix pegen import formatting for lint

* Preserve and test shared-prefix preparation after rebase

files:
A Lib/test/test_peg_generator/test_c_generator.py
A Tools/peg_generator/pegen/c_generator_calls.py
A Tools/peg_generator/pegen/c_generator_file.py
A Tools/peg_generator/pegen/c_generator_model.py
A Tools/peg_generator/pegen/c_generator_rules.py
A Tools/peg_generator/pegen/grammar_analysis.py
M Lib/test/test_peg_generator/test_c_parser.py
M Lib/test/test_peg_generator/test_pegen.py
M Tools/peg_generator/pegen/c_generator.py
M Tools/peg_generator/pegen/grammar.py
M Tools/peg_generator/pegen/parser_generator.py

diff --git a/Lib/test/test_peg_generator/test_c_generator.py 
b/Lib/test/test_peg_generator/test_c_generator.py
new file mode 100644
index 00000000000000..5b48738c2620a9
--- /dev/null
+++ b/Lib/test/test_peg_generator/test_c_generator.py
@@ -0,0 +1,281 @@
+import io
+import unittest
+from unittest import mock
+
+from test import test_tools
+
+test_tools.skip_if_missing("peg_generator")
+with test_tools.imports_under_tool("peg_generator"):
+    from pegen import grammar as grammar_module
+    from pegen.c_generator import CParserGenerator
+    from pegen.c_generator_file import CParserEmitter
+    from pegen.grammar import NamedItem, RuleKind
+    from pegen.grammar_parser import GeneratedParser as GrammarParser
+    from pegen.testutil import ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, 
parse_string
+
+
+class TestCGenerator(unittest.TestCase):
+    def make_generator(self, source):
+        grammar = parse_string(source, GrammarParser)
+        return CParserGenerator(
+            grammar, ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, io.StringIO()
+        )
+
+    def emit_parser(self, parser):
+        output = io.StringIO()
+        CParserEmitter(parser, output).emit()
+        return output.getvalue()
+
+    def test_rule_types_distinguish_implicit_and_explicit_void_pointer(self):
+        generator = self.make_generator("""
+            start: &implicit implicit explicit
+            implicit: NAME
+            explicit[void*]: NAME
+        """)
+        generator.rules["explicit"].type = "void *"
+        start, implicit, explicit = generator.prepare("example.gram").rules
+
+        self.assertIsNone(implicit.signature.return_type)
+        self.assertEqual(explicit.signature.return_type, "void *")
+        self.assertEqual(implicit.signature.c_return_type, "void *")
+        self.assertEqual(explicit.signature.c_return_type, "void *")
+        self.assertEqual(
+            implicit.signature.declaration(), "static void 
*implicit_rule(Parser *p);"
+        )
+        self.assertEqual(
+            explicit.signature.declaration(), "static void * 
explicit_rule(Parser *p);"
+        )
+        call = start.alternatives[0].calls[0]
+        self.assertEqual(call.function, "_PyPegen_lookahead")
+        generator = self.make_generator("start: &explicit\nexplicit[void*]: 
NAME\n")
+        with self.assertRaisesRegex(RuntimeError, "return type is 
incompatible"):
+            generator.prepare("example.gram")
+
+    def test_parser_plan_does_not_depend_on_compilation_state(self):
+        generator = self.make_generator("""
+            @header 'CUSTOM HEADER'
+            @subheader 'CUSTOM SUBHEADER'
+            @trailer 'CUSTOM TRAILER %(modulename)s %(mode)d'
+            @modulename 'sample'
+            @bytecode '1'
+            start[mod_ty]: expr_without_invalid 'pass' "zsoft" "asoft" ('bb' | 
'aa')* ENDMARKER
+            expr_without_invalid[expr_ty] (memo): name=expr [NUMBER] { name }
+            expr[expr_ty]: expr '+' NAME | NAME
+        """)
+        generator.debug = True
+        parser = generator.prepare("some/path/example.gram")
+        expected = self.emit_parser(parser)
+
+        self.assertEqual(generator.file.getvalue(), "")
+        self.assertEqual(parser.source_name, "example.gram")
+        self.assertEqual(parser.headers, ("CUSTOM HEADER", "CUSTOM SUBHEADER"))
+        self.assertEqual(parser.trailer, "CUSTOM TRAILER sample 2")
+        self.assertEqual(parser.soft_keywords, ("asoft", "zsoft"))
+        self.assertEqual([word for word, _ in parser.keyword_groups[2]], 
["bb", "aa"])
+        self.assertTrue(any(rule.signature.kind is RuleKind.LOOP0 for rule in 
parser.rules))
+        with self.assertRaises(AttributeError):
+            parser.rules[0].alternatives[0].calls[0].assigned_variable = 
"changed"
+
+        generator.grammar.metas.clear()
+        generator.grammar.metas["trailer"] = "%(missing)s"
+        for rule in generator.all_rules.values():
+            rule.name = "changed"
+            rule.type = "changed_type"
+            rule.flags = frozenset()
+            rule.rhs.alts[0].action = "changed_action"
+            rule.rhs.alts.clear()
+        generator.rules.clear()
+        generator.all_rules.clear()
+        generator.keywords.clear()
+        generator.soft_keywords.clear()
+        generator.debug = False
+        generator.skip_actions = True
+        self.assertEqual(self.emit_parser(parser), expected)
+
+    def test_repeated_preparation_keeps_variable_names_local(self):
+        source = """
+            start: 'run' expr term bindings other ENDMARKER
+            expr: expr '+' NAME | NAME
+            term: term '*' NUMBER | NUMBER
+            bindings: (name_var=NUMBER) name_var[expr_ty]=(NAME) [NUMBER] 
(NAME | NUMBER) { name_var_1 }
+            other: name_var=NUMBER name_var=NAME { name_var_1 }
+        """
+        generator = self.make_generator(source)
+        parser = generator.prepare("example.gram")
+        expected = self.emit_parser(parser)
+
+        self.assertEqual(self.emit_parser(parser), expected)
+        self.assertEqual(generator.prepare("example.gram"), parser)
+        other = self.make_generator(source).prepare("example.gram")
+        self.assertEqual(other, parser)
+        self.assertEqual(self.emit_parser(other), expected)
+        self.assertEqual(expected.count("expr_ty name_var_1;"), 2)
+        self.assertEqual(expected.count("_res = name_var_1;"), 2)
+        self.assertNotIn("name_var_2", expected)
+
+    def test_prepared_prefixes_preserve_reuse_and_repeatability(self):
+        generator = self.make_generator("""
+            start: prefix ':' NAME | prefix ':' NUMBER | NAME | prefix '=' NAME
+            prefix[expr_ty] (memo): NAME
+        """)
+        parser = generator.prepare("example.gram")
+        start = parser.rules[0]
+        prefix, = start.prefixes
+        self.assertEqual(prefix.type, "expr_ty")
+        for alt in start.alternatives[:2]:
+            self.assertIn("!p->call_invalid_rules", alt.calls[0].expression())
+            self.assertIn(prefix.result, alt.calls[0].expression())
+        self.assertEqual(start.alternatives[3].calls[0].expression(), 
"prefix_rule(p)")
+        self.assertEqual(generator.prepare("example.gram"), parser)
+        expected = self.emit_parser(parser)
+        generator.rules.clear()
+        generator.all_rules.clear()
+        self.assertEqual(self.emit_parser(parser), expected)
+
+    def test_nullable_prefix_is_not_reused(self):
+        generator = self.make_generator("""
+            start: prefix ':' NAME | prefix ':' NUMBER
+            prefix (memo): NAME?
+        """)
+        start = generator.prepare("example.gram").rules[0]
+        self.assertEqual(start.prefixes, ())
+        for alt in start.alternatives:
+            self.assertEqual(alt.calls[0].expression(), "prefix_rule(p)")
+
+    def test_invalid_trailer_fails_before_output(self):
+        generator = self.make_generator("""
+            @trailer '%(missing)s'
+            start: NAME ENDMARKER
+        """)
+        with self.assertRaisesRegex(KeyError, "missing"):
+            generator.generate("example.gram")
+        self.assertEqual(generator.file.getvalue(), "")
+
+    def test_empty_keyword_tables(self):
+        parser = self.make_generator("start: NAME 
ENDMARKER\n").prepare("example.gram")
+        source = self.emit_parser(parser)
+
+        self.assertEqual(parser.keyword_groups, ())
+        self.assertEqual(parser.soft_keywords, ())
+        self.assertIn("static const int n_keyword_lists = 0;", source)
+        self.assertIn(
+            "static KeywordToken *reserved_keywords[] = {\n"
+            "    (KeywordToken[]) {{NULL, -1}},\n"
+            "};",
+            source,
+        )
+        self.assertIn("static char *soft_keywords[] = {\n    NULL,\n};", 
source)
+
+    def test_lowering_rejects_undiscovered_items(self):
+        for replacement in ("missing", "(NAME NUMBER)", None):
+            with self.subTest(replacement=replacement):
+                generator = self.make_generator("start: NAME ENDMARKER\n")
+                generator.collect_rules()
+                lowerer = generator.callmakervisitor.make_lowerer()
+                inventory = tuple(generator.all_rules)
+                counter = generator.counter
+                rule = generator.rules["start"]
+                items = rule.rhs.alts[0].items
+                if replacement is None:
+                    items[0] = NamedItem(None, items[0].item)
+                else:
+                    grammar = parse_string(f"start: {replacement}\n", 
GrammarParser)
+                    items[0].item = 
grammar.rules["start"].rhs.alts[0].items[0].item
+                with self.assertRaisesRegex(RuntimeError, "not discovered"):
+                    lowerer.prepare_rule(rule)
+                self.assertEqual(tuple(generator.all_rules), inventory)
+                self.assertEqual(generator.counter, counter)
+
+    def test_helper_resolution_does_not_depend_on_display_settings(self):
+        source = """
+            start: NAME (a=NAME { a }) NAME* NAME+ ','.NAME+ ENDMARKER
+        """
+        for simple in (True, False):
+            with self.subTest(simple=simple), mock.patch.object(
+                grammar_module, "SIMPLE_STR", simple
+            ):
+                generator = self.make_generator(source)
+                expected = generator.prepare("example.gram").rules
+                with mock.patch.object(grammar_module, "SIMPLE_STR", not 
simple):
+                    actual = generator.prepare("example.gram").rules
+                self.assertEqual(len(actual), len(expected))
+                for old, new in zip(expected, actual):
+                    self.assertEqual(old.signature, new.signature)
+                    self.assertEqual(
+                        [alt.calls for alt in old.alternatives],
+                        [alt.calls for alt in new.alternatives],
+                    )
+
+    def test_invalid_rule_gating_uses_references(self):
+        cases = (
+            ("invalid_example", True),
+            ("value=invalid_example", True),
+            ("[invalid_example]", True),
+            ("invalid_example?", True),
+            ("invalid_example*", True),
+            ("invalid_example+", True),
+            ("invalid_example.NAME+", True),
+            ("[invalid_example.NAME+]", True),
+            ("[invalid_example.(NAME NAME)+]", False),
+            ("[[invalid_example.(NAME NAME)+]]", False),
+            ("[invalid_example.(NAME | NUMBER)+]", False),
+            ("&invalid_example", False),
+            ("[invalid_example | NAME]", False),
+            ("invalid_name=NAME", False),
+        )
+        for item, requires_invalid_rules in cases:
+            for simple in (True, False):
+                with self.subTest(item=item, simple=simple), mock.patch.object(
+                    grammar_module, "SIMPLE_STR", simple
+                ):
+                    generator = self.make_generator(f"""
+                        start: {item} {{ _PyPegen_dummy_name(p) }}
+                        invalid_example: NAME
+                    """)
+                    start = generator.prepare("example.gram").rules[0]
+                    self.assertEqual(
+                        start.alternatives[0].requires_invalid_rules,
+                        requires_invalid_rules,
+                    )
+
+    def test_lowering_preserves_legacy_named_call_types(self):
+        generator = self.make_generator("""
+            start: Mixed LPAR ENDMARKER
+            Mixed[expr_ty]: NAME
+        """)
+        start, mixed = generator.prepare("example.gram").rules
+        self.assertEqual(mixed.signature.return_type, "expr_ty")
+        for call, name in zip(start.alternatives[0].calls, ("Mixed", "LPAR")):
+            with self.subTest(name=name):
+                self.assertEqual(call.function, f"{name}_rule")
+                self.assertIsNone(call.return_type)
+
+    def test_lowering_snapshots_symbols_and_tokens(self):
+        grammar = parse_string("""
+            start: 'pass' '+' atom ENDMARKER
+            atom[expr_ty]: NAME
+        """, GrammarParser)
+        exact_tokens = dict(EXACT_TOKENS)
+        non_exact_tokens = set(NON_EXACT_TOKENS)
+        generator = CParserGenerator(
+            grammar, ALL_TOKENS, exact_tokens, non_exact_tokens, io.StringIO()
+        )
+        generator.collect_rules()
+        lowerer = generator.callmakervisitor.make_lowerer()
+        start = generator.rules["start"]
+        atom = generator.rules["atom"]
+        expected = lowerer.prepare_rule(start)
+
+        atom.type = "stmt_ty"
+        generator.all_rules.clear()
+        generator.tokens.clear()
+        generator.keywords.clear()
+        exact_tokens.clear()
+        non_exact_tokens.clear()
+
+        self.assertEqual(lowerer.prepare_rule(atom).signature.return_type, 
"expr_ty")
+        self.assertEqual(lowerer.prepare_rule(start), expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Lib/test/test_peg_generator/test_c_parser.py 
b/Lib/test/test_peg_generator/test_c_parser.py
index cd0b907667e4a2..c430f648bc1563 100644
--- a/Lib/test/test_peg_generator/test_c_parser.py
+++ b/Lib/test/test_peg_generator/test_c_parser.py
@@ -239,6 +239,21 @@ def test_negative_lookahead(self) -> None:
         """
         self.run_test(grammar_source, test_source)
 
+    def test_optional_gather_with_invalid_separator(self) -> None:
+        grammar_source = """
+        start: 'prefix' guard_without_invalid NAME NEWLINE ENDMARKER
+        guard_without_invalid:
+            | [invalid_separator.(NAME NAME)+] { _PyPegen_dummy_name(p) }
+        invalid_separator: '+'
+        """
+        test_source = """
+        self.check_input_strings_for_grammar(
+            valid_cases=["prefix hello", "prefix a b hello", "prefix a b + c d 
hello"],
+            invalid_cases=["prefix", "prefix a b"],
+        )
+        """
+        self.run_test(grammar_source, test_source)
+
     def test_cut(self) -> None:
         grammar_source = """
         start: X ~ Y Z | X Q S
@@ -404,6 +419,132 @@ def test_same_name_different_types(self) -> None:
         """
         self.run_test(grammar_source, test_source)
 
+    def test_alternative_variable_bindings(self) -> None:
+        grammar_source = """
+        start[mod_ty]: a=stmt NEWLINE ENDMARKER {
+            _PyAST_Module((asdl_stmt_seq *)_PyPegen_singleton_seq(p, a), NULL, 
p->arena) }
+        stmt[stmt_ty]:
+            | &NAME NAME name_var[expr_ty]=NAME NUMBER? {
+                _PyAST_Expr(name_var_1, EXTRA) }
+            | &NUMBER name_var=NUMBER name_var[expr_ty]=NAME {
+                _PyAST_Expr(name_var_1, EXTRA) }
+        """
+        test_source = """
+        for source in ("first second", "first second 42", "42 second"):
+            actual = parse.parse_string(source, mode=1)
+            self.assertEqual(len(actual.body), 1)
+            self.assertIsInstance(actual.body[0], ast.Expr)
+            self.assertIsInstance(actual.body[0].value, ast.Name)
+            self.assertEqual(actual.body[0].value.id, "second")
+        """
+        self.run_test(grammar_source, test_source)
+
+    def test_rule_cleanup(self) -> None:
+        grammar_source = """
+        @subheader '''
+        #define CHECK_INVALID(expected) \\
+            (assert(p->call_invalid_rules == (expected)), 
_PyPegen_dummy_name(p))
+        '''
+        start: enable (checked_without_invalid '+' | checked_without_invalid 
after | after) NEWLINE ENDMARKER
+        enable: 'enable' { (p->call_invalid_rules = 1, _PyPegen_dummy_name(p)) 
}
+        checked_without_invalid (memo): "value" ~ NAME { CHECK_INVALID(0) }
+        after: NAME { CHECK_INVALID(1) }
+        """
+        test_source = """
+        self.check_input_strings_for_grammar([
+            "enable value name +",  # Successful rule return.
+            "enable value name tail",  # Memoized return after backtracking.
+            "enable fallback",  # Failed rule return.
+            "enable value",  # Early return through a cut.
+        ])
+        """
+        self.run_test(grammar_source, test_source)
+
+    def test_left_recursive_rule_cleanup(self) -> None:
+        grammar_source = """
+        @subheader '''
+        #define CHECK_INVALID(expected) \\
+            (assert(p->call_invalid_rules == (expected)), 
_PyPegen_dummy_name(p))
+        '''
+        start: enable (expr_without_invalid after | after) NEWLINE ENDMARKER
+        enable: 'enable' { (p->call_invalid_rules = 1, _PyPegen_dummy_name(p)) 
}
+        expr_without_invalid:
+            | expr_without_invalid '+' NAME { CHECK_INVALID(0) }
+            | NAME { CHECK_INVALID(0) }
+        after: NAME { CHECK_INVALID(1) } | NUMBER { CHECK_INVALID(1) }
+        """
+        test_source = """
+        self.check_input_strings_for_grammar([
+            "enable name tail",
+            "enable name + other + last tail",
+            "enable fallback",  # Backtrack past a successful recursive rule.
+            "enable 42",  # The recursive rule has no successful alternative.
+        ])
+        """
+        self.run_test(grammar_source, test_source)
+
+    def test_nested_rule_cleanup(self) -> None:
+        grammar_source = """
+        @subheader '''
+        #define CHECK_INVALID(expected) \\
+            (assert(p->call_invalid_rules == (expected)), 
_PyPegen_dummy_name(p))
+        '''
+        start: enable outer_without_invalid after NEWLINE ENDMARKER
+        enable: 'enable' { (p->call_invalid_rules = 1, _PyPegen_dummy_name(p)) 
}
+        outer_without_invalid:
+            | inner_without_invalid '+' { CHECK_INVALID(0) }
+            | inner_without_invalid inside { CHECK_INVALID(0) }
+            | inside { CHECK_INVALID(0) }
+        inner_without_invalid (memo): 'value' NAME { CHECK_INVALID(0) }
+        inside: NAME { CHECK_INVALID(0) }
+        after: NAME { CHECK_INVALID(1) }
+        """
+        test_source = """
+        self.check_input_strings_for_grammar([
+            "enable value name + tail",  # Restore the enclosing disabled 
state.
+            "enable value name middle tail",  # Restore it on a memoized 
return.
+            "enable fallback tail",  # Restore it when the inner rule fails.
+        ])
+        """
+        self.run_test(grammar_source, test_source)
+
+    def test_repetition_result_order(self) -> None:
+        grammar_source = """
+        start[mod_ty]: a=statements NEWLINE ENDMARKER {
+            _PyAST_Module(a, NULL, p->arena) }
+        statements[asdl_stmt_seq*]:
+            | 'repeat0' a=stmt* { (asdl_stmt_seq*)a }
+            | 'repeat1' a=stmt+ { (asdl_stmt_seq*)a }
+            | 'gather' a=','.stmt+ { (asdl_stmt_seq*)a }
+        stmt[stmt_ty]: a=NAME { _PyAST_Expr(a, EXTRA) }
+        """
+        test_source = """
+        for mode, separator in (("repeat0", " "), ("repeat1", " "), ("gather", 
",")):
+            for count in (1, 2, 5, 17):
+                with self.subTest(mode=mode, count=count):
+                    names = ["name" + str(index) for index in range(count)]
+                    result = parse.parse_string(mode + " " + 
separator.join(names), mode=1)
+                    self.assertEqual([stmt.value.id for stmt in result.body], 
names)
+        result = parse.parse_string("repeat0", mode=1)
+        self.assertEqual(result.body, [])
+        """
+        self.run_test(grammar_source, test_source)
+
+    def test_repetition_action_errors(self) -> None:
+        grammar_source = """
+        start: ('repeat0' item* | 'repeat1' item+ | 'gather' ','.item+) 
NEWLINE ENDMARKER
+        item: NAME | 'fail' { PyTuple_New(-1) }
+        """
+        test_source = """
+        for mode, separator in (("repeat0", " "), ("repeat1", " "), ("gather", 
",")):
+            for items in (("fail",), ("first", "second", "fail")):
+                with self.subTest(mode=mode, items=items):
+                    with self.assertRaises(SystemError):
+                        parse.parse_string(mode + " " + separator.join(items), 
mode=0)
+            parse.parse_string(mode + " first", mode=0)
+        """
+        self.run_test(grammar_source, test_source)
+
     def test_with_stmt_with_paren(self) -> None:
         grammar_source = """
         start[mod_ty]: a=[statements] ENDMARKER { _PyAST_Module(a, NULL, 
p->arena) }
diff --git a/Lib/test/test_peg_generator/test_pegen.py 
b/Lib/test/test_peg_generator/test_pegen.py
index f39fcc2e0d8daf..64c875367cef27 100644
--- a/Lib/test/test_peg_generator/test_pegen.py
+++ b/Lib/test/test_peg_generator/test_pegen.py
@@ -12,7 +12,7 @@
 with test_tools.imports_under_tool("peg_generator"):
     from pegen.grammar_parser import GeneratedParser as GrammarParser
     from pegen.testutil import parse_string, generate_parser, make_parser
-    from pegen.grammar import GrammarVisitor, GrammarError, Grammar
+    from pegen.grammar import GrammarVisitor, GrammarError, Grammar, RuleKind
     from pegen.grammar_visualizer import ASTGrammarPrinter
     from pegen.parser import Parser
     from pegen.parser_generator import compute_nullables, 
compute_left_recursives
@@ -537,6 +537,29 @@ def test_advanced_left_recursive(self) -> None:
         self.assertTrue(rules["start"].left_recursive)
         self.assertFalse(rules["sign"].left_recursive)
 
+    def test_helper_rule_kinds_do_not_depend_on_names(self) -> None:
+        grammar = parse_string("""
+            start: NAME* NUMBER+ ','.NAME+
+        """, GrammarParser)
+        generator = PythonParserGenerator(grammar, io.StringIO())
+        generator.collect_rules()
+        helpers = [
+            rule for rule in generator.all_rules.values()
+            if rule is not grammar.rules["start"]
+        ]
+        self.assertCountEqual(
+            [rule.kind for rule in helpers],
+            [RuleKind.LOOP0, RuleKind.LOOP1, RuleKind.LOOP0, RuleKind.GATHER],
+        )
+        for rule in helpers:
+            is_loop, is_gather = rule.is_loop(), rule.is_gather()
+            rule.name = "renamed"
+            self.assertEqual(rule.is_loop(), is_loop)
+            self.assertEqual(rule.is_gather(), is_gather)
+        grammar.rules["start"].name = "_loop1_name_only"
+        self.assertFalse(grammar.rules["start"].is_loop())
+        self.assertFalse(grammar.rules["start"].is_gather())
+
     def test_mutually_left_recursive(self) -> None:
         grammar_source = """
         start: foo 'E'
diff --git a/Tools/peg_generator/pegen/c_generator.py 
b/Tools/peg_generator/pegen/c_generator.py
index 044366c3aac140..b2978d3a0baa75 100644
--- a/Tools/peg_generator/pegen/c_generator.py
+++ b/Tools/peg_generator/pegen/c_generator.py
@@ -1,32 +1,23 @@
-import ast
+"""Prepare a complete C parser description and emit it."""
+
 import os.path
-import re
-from collections.abc import Callable
-from dataclasses import dataclass, field
-from enum import Enum
-from typing import IO, Any
+from typing import IO
 
 from pegen import grammar
-from pegen.grammar import (
-    Alt,
-    Cut,
-    Forced,
-    Gather,
-    GrammarVisitor,
-    Group,
-    Leaf,
-    Lookahead,
-    NamedItem,
-    NameLeaf,
-    NegativeLookahead,
-    Opt,
-    PositiveLookahead,
-    Repeat0,
-    Repeat1,
-    Rhs,
-    Rule,
-    StringLeaf,
+from pegen.c_generator_calls import (
+    CCallMakerVisitor as CCallMakerVisitor,
+)
+from pegen.c_generator_calls import (
+    FunctionCall as FunctionCall,
+)
+from pegen.c_generator_calls import (
+    NodeTypes as NodeTypes,
+)
+from pegen.c_generator_calls import (
+    consuming_rules as consuming_rules,
 )
+from pegen.c_generator_file import CParserEmitter
+from pegen.c_generator_model import CParser, CRule, CRuleSignature
 from pegen.parser_generator import ParserGenerator
 
 EXTENSION_PREFIX = """\
@@ -66,345 +57,7 @@
 """
 
 
-class NodeTypes(Enum):
-    NAME_TOKEN = 0
-    NUMBER_TOKEN = 1
-    STRING_TOKEN = 2
-    GENERIC_TOKEN = 3
-    KEYWORD = 4
-    SOFT_KEYWORD = 5
-    CUT_OPERATOR = 6
-    F_STRING_CHUNK = 7
-
-
-BASE_NODETYPES = {
-    "NAME": NodeTypes.NAME_TOKEN,
-    "NUMBER": NodeTypes.NUMBER_TOKEN,
-    "STRING": NodeTypes.STRING_TOKEN,
-    "SOFT_KEYWORD": NodeTypes.SOFT_KEYWORD,
-}
-
-
-@dataclass
-class FunctionCall:
-    function: str
-    arguments: list[Any] = field(default_factory=list)
-    assigned_variable: str | None = None
-    assigned_variable_type: str | None = None
-    return_type: str | None = None
-    nodetype: NodeTypes | None = None
-    force_true: bool = False
-    comment: str | None = None
-
-    def __str__(self) -> str:
-        parts = []
-        parts.append(self.function)
-        if self.arguments:
-            parts.append(f"({', '.join(map(str, self.arguments))})")
-        if self.force_true:
-            parts.append(", !p->error_indicator")
-        if self.assigned_variable:
-            if self.assigned_variable_type:
-                parts = [
-                    "(",
-                    self.assigned_variable,
-                    " = ",
-                    "(",
-                    self.assigned_variable_type,
-                    ")",
-                    *parts,
-                    ")",
-                ]
-            else:
-                parts = ["(", self.assigned_variable, " = ", *parts, ")"]
-        if self.comment:
-            parts.append(f"  // {self.comment}")
-        return "".join(parts)
-
-
-class CCallMakerVisitor(GrammarVisitor):
-    def __init__(
-        self,
-        parser_generator: ParserGenerator,
-        exact_tokens: dict[str, int],
-        non_exact_tokens: set[str],
-    ):
-        self.gen = parser_generator
-        self.exact_tokens = exact_tokens
-        self.non_exact_tokens = non_exact_tokens
-        self.cache: dict[str, str] = {}
-        self.cleanup_statements: list[str] = []
-
-    def keyword_helper(self, keyword: str) -> FunctionCall:
-        return FunctionCall(
-            assigned_variable="_keyword",
-            function="_PyPegen_expect_token",
-            arguments=["p", self.gen.keywords[keyword]],
-            return_type="Token *",
-            nodetype=NodeTypes.KEYWORD,
-            comment=f"token='{keyword}'",
-        )
-
-    def soft_keyword_helper(self, value: str) -> FunctionCall:
-        return FunctionCall(
-            assigned_variable="_keyword",
-            function="_PyPegen_expect_soft_keyword",
-            arguments=["p", value],
-            return_type="expr_ty",
-            nodetype=NodeTypes.SOFT_KEYWORD,
-            comment=f"soft_keyword='{value}'",
-        )
-
-    def visit_NameLeaf(self, node: NameLeaf) -> FunctionCall:
-        name = node.value
-        if name in self.non_exact_tokens:
-            if name in BASE_NODETYPES:
-                return FunctionCall(
-                    assigned_variable=f"{name.lower()}_var",
-                    function=f"_PyPegen_{name.lower()}_token",
-                    arguments=["p"],
-                    nodetype=BASE_NODETYPES[name],
-                    return_type="expr_ty",
-                    comment=name,
-                )
-            return FunctionCall(
-                assigned_variable=f"{name.lower()}_var",
-                function="_PyPegen_expect_token",
-                arguments=["p", name],
-                nodetype=NodeTypes.GENERIC_TOKEN,
-                return_type="Token *",
-                comment=f"token='{name}'",
-            )
-
-        type = None
-        rule = self.gen.all_rules.get(name.lower())
-        if rule is not None:
-            type = "asdl_seq *" if rule.is_loop() or rule.is_gather() else 
rule.type
-
-        return FunctionCall(
-            assigned_variable=f"{name}_var",
-            function=f"{name}_rule",
-            arguments=["p"],
-            return_type=type,
-            comment=f"{node}",
-        )
-
-    def visit_StringLeaf(self, node: StringLeaf) -> FunctionCall:
-        val = ast.literal_eval(node.value)
-        if re.match(r"[a-zA-Z_]\w*\Z", val):  # This is a keyword
-            if node.value.endswith("'"):
-                return self.keyword_helper(val)
-            else:
-                return self.soft_keyword_helper(node.value)
-        else:
-            assert val in self.exact_tokens, f"{node.value} is not a known 
literal"
-            type = self.exact_tokens[val]
-            return FunctionCall(
-                assigned_variable="_literal",
-                function="_PyPegen_expect_token",
-                arguments=["p", type],
-                nodetype=NodeTypes.GENERIC_TOKEN,
-                return_type="Token *",
-                comment=f"token='{val}'",
-            )
-
-    def visit_NamedItem(self, node: NamedItem) -> FunctionCall:
-        call = self.generate_call(node.item)
-        if node.name:
-            call.assigned_variable = node.name
-        if node.type:
-            call.assigned_variable_type = node.type
-        return call
-
-    def assert_no_undefined_behavior(
-        self, call: FunctionCall, wrapper: str, expected_rtype: str | None,
-    ) -> None:
-        if call.return_type != expected_rtype:
-            raise RuntimeError(
-                f"{call.function} return type is incompatible with {wrapper}: "
-                f"expect: {expected_rtype}, actual: {call.return_type}"
-            )
-
-    def lookahead_call_helper(self, node: Lookahead, positive: int) -> 
FunctionCall:
-        call = self.generate_call(node.node)
-        comment = None
-        if call.nodetype is NodeTypes.NAME_TOKEN:
-            function = "_PyPegen_lookahead_for_expr"
-            self.assert_no_undefined_behavior(call, function, "expr_ty")
-        elif call.nodetype is NodeTypes.STRING_TOKEN:
-            # _PyPegen_string_token() returns 'void *' instead of 'Token *';
-            # in addition, the overall function call would return 'expr_ty'.
-            assert call.function == "_PyPegen_string_token"
-            function = "_PyPegen_lookahead"
-            self.assert_no_undefined_behavior(call, function, "expr_ty")
-        elif call.nodetype == NodeTypes.SOFT_KEYWORD:
-            function = "_PyPegen_lookahead_with_string"
-            self.assert_no_undefined_behavior(call, function, "expr_ty")
-        elif call.nodetype in {NodeTypes.GENERIC_TOKEN, NodeTypes.KEYWORD}:
-            function = "_PyPegen_lookahead_with_int"
-            self.assert_no_undefined_behavior(call, function, "Token *")
-            comment = f"token={node.node}"
-        elif call.return_type == "expr_ty":
-            function = "_PyPegen_lookahead_for_expr"
-        elif call.return_type == "stmt_ty":
-            function = "_PyPegen_lookahead_for_stmt"
-        else:
-            function = "_PyPegen_lookahead"
-            self.assert_no_undefined_behavior(call, function, None)
-        return FunctionCall(
-            function=function,
-            arguments=[positive, call.function, *call.arguments],
-            return_type="int",
-            comment=comment,
-        )
-
-    def visit_PositiveLookahead(self, node: PositiveLookahead) -> FunctionCall:
-        return self.lookahead_call_helper(node, 1)
-
-    def visit_NegativeLookahead(self, node: NegativeLookahead) -> FunctionCall:
-        return self.lookahead_call_helper(node, 0)
-
-    def visit_Forced(self, node: Forced) -> FunctionCall:
-        call = self.generate_call(node.node)
-        if isinstance(node.node, Leaf):
-            assert isinstance(node.node, Leaf)
-            val = ast.literal_eval(node.node.value)
-            assert val in self.exact_tokens, f"{node.node.value} is not a 
known literal"
-            type = self.exact_tokens[val]
-            return FunctionCall(
-                assigned_variable="_literal",
-                function="_PyPegen_expect_forced_token",
-                arguments=["p", type, f'"{val}"'],
-                nodetype=NodeTypes.GENERIC_TOKEN,
-                return_type="Token *",
-                comment=f"forced_token='{val}'",
-            )
-        if isinstance(node.node, Group):
-            call = self.visit(node.node.rhs)
-            call.assigned_variable = None
-            call.comment = None
-            return FunctionCall(
-                assigned_variable="_literal",
-                function="_PyPegen_expect_forced_result",
-                arguments=["p", str(call), f'"{node.node.rhs!s}"'],
-                return_type="void *",
-                comment=f"forced_token=({node.node.rhs!s})",
-            )
-        else:
-            raise NotImplementedError(f"Forced tokens don't work with 
{node.node} nodes")
-
-    def visit_Opt(self, node: Opt) -> FunctionCall:
-        call = self.generate_call(node.node)
-        return FunctionCall(
-            assigned_variable="_opt_var",
-            function=call.function,
-            arguments=call.arguments,
-            force_true=True,
-            comment=f"{node}",
-        )
-
-    def _generate_artificial_rule_call(
-        self,
-        node: Any,
-        prefix: str,
-        rule_generation_func: Callable[[], str],
-        return_type: str | None = None,
-    ) -> FunctionCall:
-        node_str = f"{node}"
-        key = f"{prefix}_{node_str}"
-        if key in self.cache:
-            name = self.cache[key]
-        else:
-            name = rule_generation_func()
-            self.cache[key] = name
-
-        return FunctionCall(
-            assigned_variable=f"{name}_var",
-            function=f"{name}_rule",
-            arguments=["p"],
-            return_type=return_type,
-            comment=node_str,
-        )
-
-    def visit_Rhs(self, node: Rhs) -> FunctionCall:
-        if node.can_be_inlined:
-            return self.generate_call(node.alts[0].items[0])
-
-        return self._generate_artificial_rule_call(
-            node,
-            "rhs",
-            lambda: self.gen.artificial_rule_from_rhs(node),
-        )
-
-    def visit_Repeat0(self, node: Repeat0) -> FunctionCall:
-        return self._generate_artificial_rule_call(
-            node,
-            "repeat0",
-            lambda: self.gen.artificial_rule_from_repeat(node.node, 
is_repeat1=False),
-            "asdl_seq *",
-        )
-
-    def visit_Repeat1(self, node: Repeat1) -> FunctionCall:
-        return self._generate_artificial_rule_call(
-            node,
-            "repeat1",
-            lambda: self.gen.artificial_rule_from_repeat(node.node, 
is_repeat1=True),
-            "asdl_seq *",
-        )
-
-    def visit_Gather(self, node: Gather) -> FunctionCall:
-        return self._generate_artificial_rule_call(
-            node,
-            "gather",
-            lambda: self.gen.artificial_rule_from_gather(node),
-            "asdl_seq *",
-        )
-
-    def visit_Group(self, node: Group) -> FunctionCall:
-        return self.generate_call(node.rhs)
-
-    def visit_Cut(self, node: Cut) -> FunctionCall:
-        return FunctionCall(
-            assigned_variable="_cut_var",
-            return_type="int",
-            function="1",
-            nodetype=NodeTypes.CUT_OPERATOR,
-        )
-
-    def generate_call(self, node: Any) -> FunctionCall:
-        return super().visit(node)
-
-
-def consuming_rules(rules: dict[str, Rule]) -> set[str]:
-    """Conservatively prove which rules consume a token whenever they 
succeed."""
-    consuming: set[str] = set()
-
-    def consumes(node: Any) -> bool:
-        if isinstance(node, NamedItem):
-            return consumes(node.item)
-        if isinstance(node, NameLeaf):
-            return node.value not in rules or node.value in consuming
-        if isinstance(node, StringLeaf):
-            return True
-        if isinstance(node, Group):
-            return consumes(node.rhs)
-        if isinstance(node, Rhs):
-            return bool(node.alts) and all(any(consumes(i) for i in alt.items) 
for alt in node.alts)
-        if isinstance(node, (Forced, Repeat1, Gather)):
-            return consumes(node.node)
-        # Predicates, cuts, optional items, and zero-or-more items can succeed
-        # without consuming. Actions are assumed not to rewrite parser marks.
-        return False
-
-    while True:
-        added = {name for name, rule in rules.items()
-                 if name not in consuming and consumes(rule.rhs)}
-        if not added:
-            return consuming
-        consuming.update(added)
-
-
-class CParserGenerator(ParserGenerator, GrammarVisitor):
+class CParserGenerator(ParserGenerator):
     def __init__(
         self,
         grammar: grammar.Grammar,
@@ -419,551 +72,63 @@ def __init__(
         self.callmakervisitor: CCallMakerVisitor = CCallMakerVisitor(
             self, exact_tokens, non_exact_tokens
         )
-        self._varname_counter = 0
+        self._collected = False
         self.debug = debug
         self.skip_actions = skip_actions
-        self.cleanup_statements: list[str] = []
-        self.consuming = consuming_rules(self.rules)
-        self.prefix_calls: dict[int, tuple[str, str, str]] = {}
-
-    def add_level(self) -> None:
-        self.print("if (p->level++ == MAXSTACK || _PyPegen_stack_exhausted(p)) 
{")
-        with self.indent():
-            self.print("_Pypegen_stack_overflow(p);")
-        self.print("}")
-
-    def remove_level(self) -> None:
-        self.print("p->level--;")
-
-    def add_return(self, ret_val: str) -> None:
-        for stmt in self.cleanup_statements:
-            self.print(stmt)
-        self.remove_level()
-        self.print(f"return {ret_val};")
-
-    def unique_varname(self, name: str = "tmpvar") -> str:
-        new_var = name + "_" + str(self._varname_counter)
-        self._varname_counter += 1
-        return new_var
-
-    def call_with_errorcheck_return(self, call_text: str, returnval: str) -> 
None:
-        error_var = self.unique_varname()
-        self.print(f"int {error_var} = {call_text};")
-        self.print(f"if ({error_var}) {{")
-        with self.indent():
-            self.add_return(returnval)
-        self.print("}")
-
-    def call_with_errorcheck_goto(self, call_text: str, goto_target: str) -> 
None:
-        error_var = self.unique_varname()
-        self.print(f"int {error_var} = {call_text};")
-        self.print(f"if ({error_var}) {{")
-        with self.indent():
-            self.print(f"goto {goto_target};")
-        self.print("}")
-
-    def out_of_memory_return(
-        self,
-        expr: str,
-        cleanup_code: str | None = None,
-    ) -> None:
-        self.print(f"if ({expr}) {{")
-        with self.indent():
-            if cleanup_code is not None:
-                self.print(cleanup_code)
-            self.print("p->error_indicator = 1;")
-            self.print("PyErr_NoMemory();")
-            self.add_return("NULL")
-        self.print("}")
-
-    def out_of_memory_goto(self, expr: str, goto_target: str) -> None:
-        self.print(f"if ({expr}) {{")
-        with self.indent():
-            self.print("PyErr_NoMemory();")
-            self.print(f"goto {goto_target};")
-        self.print("}")
 
     def generate(self, filename: str) -> None:
+        parser = self.prepare(filename)
+        CParserEmitter(parser, self.file).emit()
+
+    def prepare(self, filename: str) -> CParser:
         self.collect_rules()
-        basename = os.path.basename(filename)
-        self.print(f"// @generated by pegen from {basename}")
-        header = self.grammar.metas.get("header", EXTENSION_PREFIX)
-        if header:
-            self.print(header.rstrip("\n"))
-        subheader = self.grammar.metas.get("subheader", "")
-        if subheader:
-            self.print(subheader)
-        self._setup_keywords()
-        self._setup_soft_keywords()
-        for i, (rulename, rule) in enumerate(self.all_rules.items(), 1000):
-            comment = "  // Left-recursive" if rule.left_recursive else ""
-            self.print(f"#define {rulename}_type {i}{comment}")
-        self.print()
-        for rulename, rule in self.all_rules.items():
-            if rule.is_loop() or rule.is_gather():
-                type = "asdl_seq *"
-            elif rule.type:
-                type = rule.type + " "
-            else:
-                type = "void *"
-            self.print(f"static {type}{rulename}_rule(Parser *p);")
-        self.print()
-        for rulename, rule in list(self.all_rules.items()):
-            self.print()
-            if rule.left_recursive:
-                self.print("// Left-recursive")
-            self.visit(rule)
+        lowerer = self.callmakervisitor.make_lowerer()
+        rules = tuple(
+            lowerer.prepare_rule(rule, skip_actions=self.skip_actions)
+            for rule in self.all_rules.values()
+        )
+        headers = []
+        if header := self.grammar.metas.get("header", EXTENSION_PREFIX):
+            headers.append(header.rstrip("\n"))
+        if subheader := self.grammar.metas.get("subheader", ""):
+            headers.append(subheader)
+        return CParser(
+            source_name=os.path.basename(filename),
+            headers=tuple(headers),
+            keyword_groups=self._prepare_keywords(),
+            soft_keywords=tuple(sorted(self.soft_keywords)),
+            rules=rules,
+            trailer=self._prepare_trailer(rules),
+            debug=self.debug,
+        )
+
+    def collect_rules(self) -> None:
+        # Keyword generation also uses this entry point without emitting C.
+        if not self._collected:
+            super().collect_rules()
+            self._collected = True
+
+    def _prepare_keywords(self) -> tuple[tuple[tuple[str, int], ...], ...]:
+        if not self.keywords:
+            return ()
+        groups: list[list[tuple[str, int]]] = [
+            [] for _ in range(max(map(len, self.keywords)) + 1)
+        ]
+        for keyword, token_type in self.keywords.items():
+            groups[len(keyword)].append((keyword, token_type))
+        return tuple(tuple(group) for group in groups)
+
+    def _prepare_trailer(self, rules: tuple[CRule, ...]) -> str | None:
         if self.skip_actions:
             mode = 0
         else:
-            mode = int(self.rules["start"].type == "mod_ty") if "start" in 
self.rules else 1
-            if mode == 1 and self.grammar.metas.get("bytecode"):
-                mode += 1
+            start = next((rule.signature for rule in rules if 
rule.signature.name == "start"), None)
+            match start:
+                case None | CRuleSignature(return_type="mod_ty"):
+                    mode = 2 if self.grammar.metas.get("bytecode") else 1
+                case _:
+                    mode = 0
         modulename = self.grammar.metas.get("modulename", "parse")
-        trailer = self.grammar.metas.get("trailer", EXTENSION_SUFFIX)
-        if trailer:
-            self.print(trailer.rstrip("\n") % dict(mode=mode, 
modulename=modulename))
-
-    def _group_keywords_by_length(self) -> dict[int, list[tuple[str, int]]]:
-        groups: dict[int, list[tuple[str, int]]] = {}
-        for keyword_str, keyword_type in self.keywords.items():
-            length = len(keyword_str)
-            if length in groups:
-                groups[length].append((keyword_str, keyword_type))
-            else:
-                groups[length] = [(keyword_str, keyword_type)]
-        return groups
-
-    def _setup_keywords(self) -> None:
-        n_keyword_lists = (
-            len(max(self.keywords.keys(), key=len)) + 1 if len(self.keywords) 
> 0 else 0
-        )
-        self.print(f"static const int n_keyword_lists = {n_keyword_lists};")
-        groups = self._group_keywords_by_length()
-        self.print("static KeywordToken *reserved_keywords[] = {")
-        with self.indent():
-            num_groups = max(groups) + 1 if groups else 1
-            for keywords_length in range(num_groups):
-                if keywords_length not in groups.keys():
-                    self.print("(KeywordToken[]) {{NULL, -1}},")
-                else:
-                    self.print("(KeywordToken[]) {")
-                    with self.indent():
-                        for keyword_str, keyword_type in 
groups[keywords_length]:
-                            self.print(f'{{"{keyword_str}", {keyword_type}}},')
-                        self.print("{NULL, -1},")
-                    self.print("},")
-        self.print("};")
-
-    def _setup_soft_keywords(self) -> None:
-        soft_keywords = sorted(self.soft_keywords)
-        self.print("static char *soft_keywords[] = {")
-        with self.indent():
-            for keyword in soft_keywords:
-                self.print(f'"{keyword}",')
-            self.print("NULL,")
-        self.print("};")
-
-    def _set_up_token_start_metadata_extraction(self) -> None:
-        self.print("if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) {")
-        with self.indent():
-            self.print("p->error_indicator = 1;")
-            self.add_return("NULL")
-        self.print("}")
-        self.print("int _start_lineno = p->tokens[_mark]->lineno;")
-        self.print("UNUSED(_start_lineno); // Only used by EXTRA macro")
-        self.print("int _start_col_offset = p->tokens[_mark]->col_offset;")
-        self.print("UNUSED(_start_col_offset); // Only used by EXTRA macro")
-
-    def _set_up_token_end_metadata_extraction(self) -> None:
-        self.print("Token *_token = 
_PyPegen_get_last_nonnwhitespace_token(p);")
-        self.print("if (_token == NULL) {")
-        with self.indent():
-            self.add_return("NULL")
-        self.print("}")
-        self.print("int _end_lineno = _token->end_lineno;")
-        self.print("UNUSED(_end_lineno); // Only used by EXTRA macro")
-        self.print("int _end_col_offset = _token->end_col_offset;")
-        self.print("UNUSED(_end_col_offset); // Only used by EXTRA macro")
-
-    def _check_for_errors(self) -> None:
-        self.print("if (p->error_indicator) {")
-        with self.indent():
-            self.add_return("NULL")
-        self.print("}")
-
-    def _set_up_rule_memoization(self, node: Rule, result_type: str) -> None:
-        self.print("{")
-        with self.indent():
-            self.add_level()
-            self.print(f"{result_type} _res = NULL;")
-            self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, &_res)) 
{{")
-            with self.indent():
-                self.add_return("_res")
-            self.print("}")
-            self.print("int _mark = p->mark;")
-            self.print("int _resmark = p->mark;")
-            self.print(f"Memo *_memo = _PyPegen_insert_memo_direct(p, _mark, 
{node.name}_type);")
-            self.print("if (_memo == NULL) {")
-            with self.indent():
-                self.add_return("NULL")
-            self.print("}")
-            self.print("while (1) {")
-            with self.indent():
-                self.print("_memo->node = _res;")
-                self.print("_memo->mark = p->mark;")
-                self.print("p->mark = _mark;")
-                self.print(f"void *_raw = {node.name}_raw(p);")
-                self.print("if (p->error_indicator) {")
-                with self.indent():
-                    self.add_return("NULL")
-                self.print("}")
-                self.print("if (_raw == NULL || p->mark <= _resmark)")
-                with self.indent():
-                    self.print("break;")
-                self.print("_resmark = p->mark;")
-                self.print("_res = _raw;")
-            self.print("}")
-            self.print("p->mark = _resmark;")
-            self.add_return("_res")
-        self.print("}")
-        self.print(f"static {result_type}")
-        self.print(f"{node.name}_raw(Parser *p)")
-
-    def _should_memoize(self, node: Rule) -> bool:
-        return "memo" in node.flags and not node.left_recursive
-
-    def _handle_default_rule_body(self, node: Rule, rhs: Rhs, result_type: 
str) -> None:
-        memoize = self._should_memoize(node)
-
-        with self.indent():
-            self.add_level()
-            self._check_for_errors()
-            self.print(f"{result_type} _res = NULL;")
-            if memoize:
-                self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, 
&_res)) {{")
-                with self.indent():
-                    self.add_return("_res")
-                self.print("}")
-            self.print("int _mark = p->mark;")
-            self.prepare_prefix_calls(rhs)
-            if any(alt.action and "EXTRA" in alt.action for alt in rhs.alts):
-                self._set_up_token_start_metadata_extraction()
-            self.visit(
-                rhs,
-                is_loop=False,
-                is_gather=node.is_gather(),
-                rulename=node.name,
-            )
-            if self.debug:
-                self.print(f'D(fprintf(stderr, "Fail at %d: {node.name}\\n", 
p->mark));')
-            self.print("_res = NULL;")
-        self.print("  done:")
-        with self.indent():
-            if memoize:
-                self.print(f"_PyPegen_insert_memo(p, _mark, {node.name}_type, 
_res);")
-            self.add_return("_res")
-
-    def _handle_loop_rule_body(self, node: Rule, rhs: Rhs) -> None:
-        memoize = self._should_memoize(node)
-        is_repeat1 = node.name.startswith("_loop1")
-
-        with self.indent():
-            self.add_level()
-            self._check_for_errors()
-            self.print("void *_res = NULL;")
-            if memoize:
-                self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, 
&_res)) {{")
-                with self.indent():
-                    self.add_return("_res")
-                self.print("}")
-            self.print("int _mark = p->mark;")
-            if memoize:
-                self.print("int _start_mark = p->mark;")
-            self.print("void **_children = PyMem_Malloc(sizeof(void *));")
-            self.out_of_memory_return("!_children")
-            self.print("Py_ssize_t _children_capacity = 1;")
-            self.print("Py_ssize_t _n = 0;")
-            if any(alt.action and "EXTRA" in alt.action for alt in rhs.alts):
-                self._set_up_token_start_metadata_extraction()
-            self.visit(
-                rhs,
-                is_loop=True,
-                is_gather=node.is_gather(),
-                rulename=node.name,
-            )
-            if is_repeat1:
-                self.print("if (_n == 0 || p->error_indicator) {")
-                with self.indent():
-                    self.print("PyMem_Free(_children);")
-                    self.add_return("NULL")
-                self.print("}")
-            self.print("asdl_seq *_seq = 
(asdl_seq*)_Py_asdl_generic_seq_new(_n, p->arena);")
-            self.out_of_memory_return("!_seq", 
cleanup_code="PyMem_Free(_children);")
-            self.print("for (Py_ssize_t i = 0; i < _n; i++) 
asdl_seq_SET_UNTYPED(_seq, i, _children[i]);")
-            self.print("PyMem_Free(_children);")
-            if memoize and node.name:
-                self.print(f"_PyPegen_insert_memo(p, _start_mark, 
{node.name}_type, _seq);")
-            self.add_return("_seq")
-
-    def prepare_prefix_calls(self, rhs: Rhs) -> None:
-        # Reuse a memoized, consuming prefix only within a consecutive group.
-        # Suffix parsing starts after the prefix and cannot revisit its start
-        # through ordinary grammar backtracking. Diagnostic calls are 
unchanged.
-        def candidate(alt: Alt) -> Rule | None:
-            if not alt.items or not isinstance(alt.items[0].item, NameLeaf):
-                return None
-            rule = self.rules.get(alt.items[0].item.value)
-            if rule is None or rule.name not in self.consuming:
-                return None
-            if self._should_memoize(rule) or (rule.left_recursive and 
rule.leader):
-                return rule
-            return None
-
-        i = 0
-        while i < len(rhs.alts):
-            rule = candidate(rhs.alts[i])
-            j = i + 1
-            while rule is not None and j < len(rhs.alts) and 
candidate(rhs.alts[j]) is rule:
-                j += 1
-            if rule is not None and j - i > 1:
-                name = self.unique_varname("_prefix")
-                result, end, valid = name + "_result", name + "_end", name + 
"_valid"
-                self.print(f"{rule.type or 'void *'} {result} = NULL;")
-                self.print(f"int {end} = 0, {valid} = 0;")
-                for alt in rhs.alts[i:j]:
-                    self.prefix_calls[id(alt.items[0])] = result, end, valid
-            i = j
-
-    def visit_Rule(self, node: Rule) -> None:
-        self.prefix_calls = {}
-        is_loop = node.is_loop()
-        is_gather = node.is_gather()
-        rhs = node.flatten()
-        if is_loop or is_gather:
-            result_type = "asdl_seq *"
-        elif node.type:
-            result_type = node.type
-        else:
-            result_type = "void *"
-
-        for line in str(node).splitlines():
-            self.print(f"// {line}")
-        if node.left_recursive and node.leader:
-            self.print(f"static {result_type} {node.name}_raw(Parser *);")
-
-        self.print(f"static {result_type}")
-        self.print(f"{node.name}_rule(Parser *p)")
-
-        if node.left_recursive and node.leader:
-            self._set_up_rule_memoization(node, result_type)
-
-        self.print("{")
-
-        if node.name.endswith("without_invalid"):
-            with self.indent():
-                self.print("int _prev_call_invalid = p->call_invalid_rules;")
-                self.print("p->call_invalid_rules = 0;")
-                self.cleanup_statements.append("p->call_invalid_rules = 
_prev_call_invalid;")
-
-        if is_loop:
-            self._handle_loop_rule_body(node, rhs)
-        else:
-            self._handle_default_rule_body(node, rhs, result_type)
-
-        if node.name.endswith("without_invalid"):
-            self.cleanup_statements.pop()
-
-        self.print("}")
-
-    def visit_NamedItem(self, node: NamedItem) -> None:
-        call = self.callmakervisitor.generate_call(node)
-        if id(node) in self.prefix_calls:
-            result, end, valid = self.prefix_calls[id(node)]
-            original = f"{call.function}({', '.join(map(str, 
call.arguments))})"
-            call.function = (
-                f"((!p->call_invalid_rules && {valid}) ? "
-                f"(p->mark = {end}, {result}) : "
-                f"({result} = {original}, {end} = p->mark, {valid} = 1, 
{result}))"
-            )
-            call.arguments = []
-        if call.assigned_variable:
-            call.assigned_variable = self.dedupe(call.assigned_variable)
-        self.print(call)
-
-    def visit_Rhs(
-        self, node: Rhs, is_loop: bool, is_gather: bool, rulename: str | None
-    ) -> None:
-        if is_loop:
-            assert len(node.alts) == 1
-        for alt in node.alts:
-            self.visit(alt, is_loop=is_loop, is_gather=is_gather, 
rulename=rulename)
-
-    def join_conditions(self, keyword: str, node: Any) -> None:
-        self.print(f"{keyword} (")
-        with self.indent():
-            first = True
-            for item in node.items:
-                if first:
-                    first = False
-                else:
-                    self.print("&&")
-                self.visit(item)
-        self.print(")")
-
-    def emit_action(self, node: Alt, cleanup_code: str | None = None) -> None:
-        self.print(f"_res = {node.action};")
-
-        self.print("if ((_res == NULL || p->error_indicator) && 
PyErr_Occurred()) {")
-        with self.indent():
-            self.print("p->error_indicator = 1;")
-            if cleanup_code:
-                self.print(cleanup_code)
-            self.add_return("NULL")
-        self.print("}")
-
-        if self.debug:
-            self.print(
-                f'D(fprintf(stderr, "Hit with action [%d-%d]: %s\\n", _mark, 
p->mark, "{node}"));'
-            )
-
-    def emit_default_action(self, is_gather: bool, node: Alt) -> None:
-        if len(self.local_variable_names) > 1:
-            if is_gather:
-                assert len(self.local_variable_names) == 2
-                self.print(
-                    f"_res = _PyPegen_seq_insert_in_front(p, "
-                    f"{self.local_variable_names[0]}, 
{self.local_variable_names[1]});"
-                )
-            else:
-                if self.debug:
-                    self.print(
-                        f'D(fprintf(stderr, "Hit without action [%d:%d]: 
%s\\n", _mark, p->mark, "{node}"));'
-                    )
-                self.print(
-                    f"_res = _PyPegen_dummy_name(p, {', 
'.join(self.local_variable_names)});"
-                )
-        else:
-            if self.debug:
-                self.print(
-                    f'D(fprintf(stderr, "Hit with default action [%d:%d]: 
%s\\n", _mark, p->mark, "{node}"));'
-                )
-            self.print(f"_res = {self.local_variable_names[0]};")
-
-    def emit_dummy_action(self) -> None:
-        self.print("_res = _PyPegen_dummy_name(p);")
-
-    def handle_alt_normal(self, node: Alt, is_gather: bool, rulename: str | 
None) -> None:
-        self.join_conditions(keyword="if", node=node)
-        self.print("{")
-        # We have parsed successfully all the conditions for the option.
-        with self.indent():
-            node_str = str(node).replace('"', '\\"')
-            self.print(
-                f'D(fprintf(stderr, "%*c+ {rulename}[%d-%d]: %s 
succeeded!\\n", p->level, \' \', _mark, p->mark, "{node_str}"));'
-            )
-            # Prepare to emit the rule action and do so
-            if node.action and "EXTRA" in node.action:
-                self._set_up_token_end_metadata_extraction()
-            if self.skip_actions:
-                self.emit_dummy_action()
-            elif node.action:
-                self.emit_action(node)
-            else:
-                self.emit_default_action(is_gather, node)
-
-            # As the current option has parsed correctly, do not continue with 
the rest.
-            self.print("goto done;")
-        self.print("}")
-
-    def handle_alt_loop(self, node: Alt, is_gather: bool, rulename: str | 
None) -> None:
-        # Condition of the main body of the alternative
-        self.join_conditions(keyword="while", node=node)
-        self.print("{")
-        # We have parsed successfully one item!
-        with self.indent():
-            # Prepare to emit the rule action and do so
-            if node.action and "EXTRA" in node.action:
-                self._set_up_token_end_metadata_extraction()
-            if self.skip_actions:
-                self.emit_dummy_action()
-            elif node.action:
-                self.emit_action(node, cleanup_code="PyMem_Free(_children);")
-            else:
-                self.emit_default_action(is_gather, node)
-
-            # Add the result of rule to the temporary buffer of children. This 
buffer
-            # will populate later an asdl_seq with all elements to return.
-            self.print("if (_n == _children_capacity) {")
-            with self.indent():
-                self.print("_children_capacity *= 2;")
-                self.print(
-                    "void **_new_children = PyMem_Realloc(_children, 
_children_capacity*sizeof(void *));"
-                )
-                self.out_of_memory_return("!_new_children", 
cleanup_code="PyMem_Free(_children);")
-                self.print("_children = _new_children;")
-            self.print("}")
-            self.print("_children[_n++] = _res;")
-            self.print("_mark = p->mark;")
-        self.print("}")
-
-    def visit_Alt(
-        self, node: Alt, is_loop: bool, is_gather: bool, rulename: str | None
-    ) -> None:
-        if len(node.items) == 1 and str(node.items[0]).startswith("invalid_"):
-            self.print(f"if (p->call_invalid_rules) {{ // {node}")
-        else:
-            self.print(f"{{ // {node}")
-        with self.indent():
-            self._check_for_errors()
-            node_str = str(node).replace('"', '\\"')
-            self.print(
-                f'D(fprintf(stderr, "%*c> {rulename}[%d-%d]: %s\\n", p->level, 
\' \', _mark, p->mark, "{node_str}"));'
-            )
-            # Prepare variable declarations for the alternative
-            vars = self.collect_vars(node)
-            for v, var_type in sorted(item for item in vars.items() if item[0] 
is not None):
-                if not var_type:
-                    var_type = "void *"
-                else:
-                    var_type += " "
-                if v == "_cut_var":
-                    v += " = 0"  # cut_var must be initialized
-                self.print(f"{var_type}{v};")
-                if v and v.startswith("_opt_var"):
-                    self.print(f"UNUSED({v}); // Silence compiler warnings")
-
-            with self.local_variable_context():
-                if is_loop:
-                    self.handle_alt_loop(node, is_gather, rulename)
-                else:
-                    self.handle_alt_normal(node, is_gather, rulename)
-
-            self.print("p->mark = _mark;")
-            node_str = str(node).replace('"', '\\"')
-            self.print(
-                f"D(fprintf(stderr, \"%*c%s {rulename}[%d-%d]: %s 
failed!\\n\", p->level, ' ',\n"
-                f'                  p->error_indicator ? "ERROR!" : "-", 
_mark, p->mark, "{node_str}"));'
-            )
-            if "_cut_var" in vars:
-                self.print("if (_cut_var) {")
-                with self.indent():
-                    self.add_return("NULL")
-                self.print("}")
-        self.print("}")
-
-    def collect_vars(self, node: Alt) -> dict[str | None, str | None]:
-        types = {}
-        with self.local_variable_context():
-            for item in node.items:
-                name, type = self.add_var(item)
-                types[name] = type
-        return types
-
-    def add_var(self, node: NamedItem) -> tuple[str | None, str | None]:
-        call = self.callmakervisitor.generate_call(node.item)
-        name = node.name if node.name else call.assigned_variable
-        if name is not None:
-            name = self.dedupe(name)
-        return_type = call.return_type if node.type is None else node.type
-        return name, return_type
+        if trailer := self.grammar.metas.get("trailer", EXTENSION_SUFFIX):
+            return trailer.rstrip("\n") % dict(mode=mode, 
modulename=modulename)
+        return None
diff --git a/Tools/peg_generator/pegen/c_generator_calls.py 
b/Tools/peg_generator/pegen/c_generator_calls.py
new file mode 100644
index 00000000000000..dc3c0cca130412
--- /dev/null
+++ b/Tools/peg_generator/pegen/c_generator_calls.py
@@ -0,0 +1,553 @@
+"""Discover calls and helper rules, then prepare rules from resolved calls."""
+
+import ast
+import re
+from collections.abc import Mapping
+from dataclasses import replace
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any
+
+from pegen.c_generator_model import (
+    CAction,
+    CAlternative,
+    CBindingKind,
+    CPrefix,
+    CRule,
+    CRuleSignature,
+    CVariable,
+)
+from pegen.c_generator_model import (
+    FunctionCall as FunctionCall,
+)
+from pegen.c_generator_model import (
+    NodeTypes as NodeTypes,
+)
+from pegen.grammar import (
+    Alt,
+    Cut,
+    Forced,
+    Gather,
+    GrammarVisitor,
+    Group,
+    Item,
+    Leaf,
+    Lookahead,
+    NamedItem,
+    NameLeaf,
+    NegativeLookahead,
+    Opt,
+    PositiveLookahead,
+    Repeat0,
+    Repeat1,
+    Rhs,
+    Rule,
+    RuleKind,
+    StringLeaf,
+)
+
+if TYPE_CHECKING:
+    from pegen.parser_generator import ParserGenerator
+
+
+BASE_NODETYPES = {
+    "NAME": NodeTypes.NAME_TOKEN,
+    "NUMBER": NodeTypes.NUMBER_TOKEN,
+    "STRING": NodeTypes.STRING_TOKEN,
+    "SOFT_KEYWORD": NodeTypes.SOFT_KEYWORD,
+}
+
+_HelperNode = Rhs | Repeat0 | Repeat1 | Gather
+
+
+def rule_signature(rule: Rule) -> CRuleSignature:
+    return_type = rule.type if rule.kind is RuleKind.NORMAL else "asdl_seq *"
+    return CRuleSignature(rule.name, rule.kind, return_type)
+
+
+def bind_call(node: NamedItem, call: FunctionCall) -> FunctionCall:
+    if not node.name and not node.type:
+        return call
+    return replace(
+        call,
+        assigned_variable=node.name or call.assigned_variable,
+        assigned_variable_type=node.type or call.assigned_variable_type,
+        binding_kind=CBindingKind.NORMAL if node.name else call.binding_kind,
+    )
+
+
+def consuming_rules(rules: dict[str, Rule]) -> set[str]:
+    """Conservatively prove which rules consume a token whenever they 
succeed."""
+    consuming: set[str] = set()
+
+    def consumes(node: Any) -> bool:
+        if isinstance(node, NamedItem):
+            return consumes(node.item)
+        if isinstance(node, NameLeaf):
+            return node.value not in rules or node.value in consuming
+        if isinstance(node, StringLeaf):
+            return True
+        if isinstance(node, Group):
+            return consumes(node.rhs)
+        if isinstance(node, Rhs):
+            return bool(node.alts) and all(any(consumes(i) for i in alt.items) 
for alt in node.alts)
+        if isinstance(node, (Forced, Repeat1, Gather)):
+            return consumes(node.node)
+        # Predicates, cuts, optional items, and zero-or-more items can succeed
+        # without consuming. Actions are assumed not to rewrite parser marks.
+        return False
+
+    while True:
+        added = {name for name, rule in rules.items()
+                 if name not in consuming and consumes(rule.rhs)}
+        if not added:
+            return consuming
+        consuming.update(added)
+
+
+class CCallMakerVisitor(GrammarVisitor):
+    def __init__(
+        self,
+        parser_generator: "ParserGenerator",
+        exact_tokens: dict[str, int],
+        non_exact_tokens: set[str],
+    ):
+        self._registry = parser_generator
+        self._keywords = parser_generator.keywords
+        self._exact_tokens = exact_tokens
+        self._non_exact_tokens = non_exact_tokens
+        self._helper_cache: dict[tuple[type, str], str] = {}
+        self._calls: dict[NamedItem, tuple[Item, FunctionCall]] = {}
+
+    def visit(self, node: Any, *args: Any, **kwargs: Any) -> FunctionCall:
+        match node:
+            case NamedItem(item=item):
+                call = self.visit(item)
+                self._calls[node] = (item, call)
+                return bind_call(node, call)
+            case NameLeaf():
+                return self._name_call(node)
+            case StringLeaf():
+                return self._string_call(node)
+            case PositiveLookahead():
+                return self._lookahead_call(node, 1)
+            case NegativeLookahead():
+                return self._lookahead_call(node, 0)
+            case Forced():
+                return self._forced_call(node)
+            case Opt():
+                return self._optional_call(node)
+            case Rhs(can_be_inlined=True):
+                return self.visit(node.alts[0].items[0])
+            case Rhs() | Repeat0() | Repeat1() | Gather():
+                return self._helper_call(node)
+            case Group(rhs=rhs):
+                return self.visit(rhs)
+            case Cut():
+                return FunctionCall(
+                    assigned_variable="_cut_var",
+                    return_type="int",
+                    function="1",
+                    nodetype=NodeTypes.CUT_OPERATOR,
+                    binding_kind=CBindingKind.CUT,
+                )
+            case _:
+                return self.generic_visit(node, *args, **kwargs)
+
+    def _keyword_call(self, keyword: str) -> FunctionCall:
+        return FunctionCall(
+            assigned_variable="_keyword",
+            function="_PyPegen_expect_token",
+            arguments=("p", self._keywords[keyword]),
+            return_type="Token *",
+            nodetype=NodeTypes.KEYWORD,
+            comment=f"token='{keyword}'",
+        )
+
+    def _soft_keyword_call(self, value: str) -> FunctionCall:
+        return FunctionCall(
+            assigned_variable="_keyword",
+            function="_PyPegen_expect_soft_keyword",
+            arguments=("p", value),
+            return_type="expr_ty",
+            nodetype=NodeTypes.SOFT_KEYWORD,
+            comment=f"soft_keyword='{value}'",
+        )
+
+    def _name_call(self, node: NameLeaf) -> FunctionCall:
+        name = node.value
+        if name in self._non_exact_tokens:
+            if name in BASE_NODETYPES:
+                return FunctionCall(
+                    assigned_variable=f"{name.lower()}_var",
+                    function=f"_PyPegen_{name.lower()}_token",
+                    arguments=("p",),
+                    nodetype=BASE_NODETYPES[name],
+                    return_type="expr_ty",
+                    comment=name,
+                )
+            return FunctionCall(
+                assigned_variable=f"{name.lower()}_var",
+                function="_PyPegen_expect_token",
+                arguments=("p", name),
+                nodetype=NodeTypes.GENERIC_TOKEN,
+                return_type="Token *",
+                comment=f"token='{name}'",
+            )
+
+        type = None
+        if (signature := self._lookup_rule(name)) is not None:
+            type = signature.return_type
+
+        return FunctionCall(
+            assigned_variable=f"{name}_var",
+            function=f"{name}_rule",
+            arguments=("p",),
+            return_type=type,
+            comment=f"{node}",
+        )
+
+    def _string_call(self, node: StringLeaf) -> FunctionCall:
+        val = ast.literal_eval(node.value)
+        if re.match(r"[a-zA-Z_]\w*\Z", val):  # This is a keyword
+            if node.value.endswith("'"):
+                return self._keyword_call(val)
+            else:
+                return self._soft_keyword_call(node.value)
+        else:
+            assert val in self._exact_tokens, f"{node.value} is not a known 
literal"
+            type = self._exact_tokens[val]
+            return FunctionCall(
+                assigned_variable="_literal",
+                function="_PyPegen_expect_token",
+                arguments=("p", type),
+                nodetype=NodeTypes.GENERIC_TOKEN,
+                return_type="Token *",
+                comment=f"token='{val}'",
+            )
+
+    def _assert_compatible_return_type(
+        self, call: FunctionCall, wrapper: str, expected_rtype: str | None,
+    ) -> None:
+        if call.return_type != expected_rtype:
+            raise RuntimeError(
+                f"{call.function} return type is incompatible with {wrapper}: "
+                f"expect: {expected_rtype}, actual: {call.return_type}"
+            )
+
+    def _lookahead_call(self, node: Lookahead, positive: int) -> FunctionCall:
+        call = self.visit(node.node)
+        comment = None
+        match call:
+            case FunctionCall(nodetype=NodeTypes.NAME_TOKEN):
+                function = "_PyPegen_lookahead_for_expr"
+                self._assert_compatible_return_type(call, function, "expr_ty")
+            case FunctionCall(nodetype=NodeTypes.STRING_TOKEN):
+                # _PyPegen_string_token() returns 'void *' instead of 'Token 
*';
+                # in addition, the overall function call would return 
'expr_ty'.
+                assert call.function == "_PyPegen_string_token"
+                function = "_PyPegen_lookahead"
+                self._assert_compatible_return_type(call, function, "expr_ty")
+            case FunctionCall(nodetype=NodeTypes.SOFT_KEYWORD):
+                function = "_PyPegen_lookahead_with_string"
+                self._assert_compatible_return_type(call, function, "expr_ty")
+            case FunctionCall(nodetype=NodeTypes.GENERIC_TOKEN | 
NodeTypes.KEYWORD):
+                function = "_PyPegen_lookahead_with_int"
+                self._assert_compatible_return_type(call, function, "Token *")
+                comment = f"token={node.node}"
+            case FunctionCall(return_type="expr_ty"):
+                function = "_PyPegen_lookahead_for_expr"
+            case FunctionCall(return_type="stmt_ty"):
+                function = "_PyPegen_lookahead_for_stmt"
+            case _:
+                function = "_PyPegen_lookahead"
+                self._assert_compatible_return_type(call, function, None)
+        return FunctionCall(
+            function=function,
+            arguments=(positive, call.function, *call.arguments),
+            return_type="int",
+            comment=comment,
+        )
+
+    def _forced_call(self, node: Forced) -> FunctionCall:
+        call = self.visit(node.node)
+        match node.node:
+            case Leaf(value=value):
+                val = ast.literal_eval(value)
+                assert val in self._exact_tokens, f"{value} is not a known 
literal"
+                return FunctionCall(
+                    assigned_variable="_literal",
+                    function="_PyPegen_expect_forced_token",
+                    arguments=("p", self._exact_tokens[val], f'"{val}"'),
+                    nodetype=NodeTypes.GENERIC_TOKEN,
+                    return_type="Token *",
+                    comment=f"forced_token='{val}'",
+                )
+            case Group(rhs=rhs):
+                return FunctionCall(
+                    assigned_variable="_literal",
+                    function="_PyPegen_expect_forced_result",
+                    arguments=("p", call.expression(), f'"{rhs!s}"'),
+                    return_type="void *",
+                    comment=f"forced_token=({rhs!s})",
+                )
+            case _:
+                raise NotImplementedError(f"Forced tokens don't work with 
{node.node} nodes")
+
+    def _optional_call(self, node: Opt) -> FunctionCall:
+        call = self.visit(node.node)
+        return FunctionCall(
+            assigned_variable="_opt_var",
+            function=call.function,
+            arguments=call.arguments,
+            force_true=True,
+            comment=f"{node}",
+            binding_kind=CBindingKind.OPTIONAL,
+        )
+
+    def _helper_call(
+        self,
+        node: _HelperNode,
+    ) -> FunctionCall:
+        node_str = f"{node}"
+        signature = self._resolve_artificial_rule(node)
+        name = signature.name
+        return FunctionCall(
+            assigned_variable=f"{name}_var",
+            function=f"{name}_rule",
+            arguments=("p",),
+            return_type=signature.return_type,
+            comment=node_str,
+        )
+
+    def _lookup_rule(self, name: str) -> CRuleSignature | None:
+        if (rule := self._registry.all_rules.get(name.lower())) is not None:
+            return rule_signature(rule)
+        return None
+
+    def _resolve_artificial_rule(self, node: _HelperNode) -> CRuleSignature:
+        # Preserve helper reuse and numbering from the fixed-point traversal.
+        key = (type(node), str(node))
+        if (name := self._helper_cache.get(key)) is None:
+            match node:
+                case Rhs():
+                    name = self._registry.artificial_rule_from_rhs(node)
+                case Repeat0(node=child):
+                    name = self._registry.artificial_rule_from_repeat(child, 
is_repeat1=False)
+                case Repeat1(node=child):
+                    name = self._registry.artificial_rule_from_repeat(child, 
is_repeat1=True)
+                case Gather():
+                    name = self._registry.artificial_rule_from_gather(node)
+            self._helper_cache[key] = name
+        return rule_signature(self._registry.all_rules[name])
+
+    def make_lowerer(self) -> "CCallLowerer":
+        return CCallLowerer(
+            calls=self._calls,
+            rules=self._registry.all_rules,
+            original_rules=self._registry.rules,
+            signatures={
+                name: rule_signature(rule) for name, rule in 
self._registry.all_rules.items()
+            },
+        )
+
+
+class CCallLowerer:
+    """Resolve bindings, actions and control flow without registering rules.
+
+    Discovery and lowering operate on the same, unchanged grammar.
+    """
+
+    def __init__(
+        self,
+        *,
+        calls: Mapping[NamedItem, tuple[Item, FunctionCall]],
+        rules: Mapping[str, Rule],
+        original_rules: dict[str, Rule],
+        signatures: Mapping[str, CRuleSignature],
+    ):
+        self._calls = MappingProxyType(dict(calls))
+        self._signatures = MappingProxyType(dict(signatures))
+        self._prefixes: dict[str, tuple[CPrefix, ...]] = {}
+        self._prefix_calls: dict[NamedItem, CPrefix] = {}
+        consuming = consuming_rules(original_rules)
+        counter = 0
+
+        def candidate(alt: Alt) -> Rule | None:
+            if not alt.items or not isinstance(alt.items[0].item, NameLeaf):
+                return None
+            rule = original_rules.get(alt.items[0].item.value)
+            if rule is None or rule.name not in consuming:
+                return None
+            if ("memo" in rule.flags and not rule.left_recursive) or (
+                rule.left_recursive and rule.leader
+            ):
+                return rule
+            return None
+
+        for rule in rules.values():
+            if rule.kind in {RuleKind.LOOP0, RuleKind.LOOP1}:
+                continue
+            # Reuse a consuming prefix only within a consecutive group.
+            # Diagnostic calls still invoke the original rule.
+            prefixes = []
+            alts = rule.flatten().alts
+            i = 0
+            while i < len(alts):
+                prefix_rule = candidate(alts[i])
+                j = i + 1
+                while prefix_rule is not None and j < len(alts) and 
candidate(alts[j]) is prefix_rule:
+                    j += 1
+                if prefix_rule is not None and j - i > 1:
+                    prefix = CPrefix(f"_prefix_{counter}", prefix_rule.type or 
"void *")
+                    counter += 1
+                    prefixes.append(prefix)
+                    for alt in alts[i:j]:
+                        self._prefix_calls[alt.items[0]] = prefix
+                i = j
+            self._prefixes[rule.name] = tuple(prefixes)
+
+    def prepare_rule(self, rule: Rule, *, skip_actions: bool = False) -> CRule:
+        if (signature := self._signatures.get(rule.name)) is None:
+            raise RuntimeError(f"Rule {rule.name!r} was not discovered")
+        rhs = rule.flatten()
+        if signature.kind in {RuleKind.LOOP0, RuleKind.LOOP1}:
+            assert len(rhs.alts) == 1
+        return CRule(
+            signature=signature,
+            text=str(rule),
+            alternatives=tuple(
+                self.prepare_alt(alt, kind=signature.kind, 
skip_actions=skip_actions)
+                for alt in rhs.alts
+            ),
+            left_recursive=rule.left_recursive,
+            leader=rule.leader,
+            memoize="memo" in rule.flags and not rule.left_recursive,
+            disable_invalid_rules=rule.name.endswith("without_invalid"),
+            prefixes=self._prefixes.get(rule.name, ()),
+        )
+
+    def prepare_alt(
+        self,
+        node: Alt,
+        *,
+        kind: RuleKind = RuleKind.NORMAL,
+        skip_actions: bool = False,
+    ) -> CAlternative:
+        calls: list[FunctionCall] = []
+        variables: dict[str, CVariable] = {}
+        cut_variable = None
+        for item in node.items:
+            recorded = self._calls.get(item)
+            if recorded is None or recorded[0] is not item.item:
+                raise RuntimeError(f"Item {item} was not discovered")
+            call = bind_call(item, recorded[1])
+            if (prefix := self._prefix_calls.get(item)) is not None:
+                result, end, valid = prefix.result, prefix.end, prefix.valid
+                original = call.expression()
+                call = replace(
+                    call,
+                    function=(
+                        f"((!p->call_invalid_rules && {valid}) ? "
+                        f"(p->mark = {end}, {result}) : "
+                        f"({result} = {original}, {end} = p->mark, {valid} = 
1, {result}))"
+                    ),
+                    arguments=(),
+                )
+            if original_name := call.assigned_variable:
+                name = original_name
+                counter = 0
+                while name in variables:
+                    counter += 1
+                    name = f"{original_name}_{counter}"
+                if name != original_name:
+                    call = replace(call, assigned_variable=name)
+                initializer = (
+                    "0" if call.binding_kind is CBindingKind.CUT and 
cut_variable is None
+                    else None
+                )
+                if initializer is not None:
+                    cut_variable = name
+                variables[name] = CVariable(
+                    name=name,
+                    type=call.return_type if item.type is None else item.type,
+                    initializer=initializer,
+                    unused=call.binding_kind is CBindingKind.OPTIONAL,
+                )
+            calls.append(call)
+        return CAlternative(
+            text=str(node),
+            action=self._prepare_action(node, list(variables), kind, 
skip_actions),
+            calls=tuple(calls),
+            variables=tuple(variables.values()),
+            cut_variable=cut_variable,
+            requires_invalid_rules=self._requires_invalid_rules(node),
+            uses_locations=bool(node.action and "EXTRA" in node.action),
+        )
+
+    def _requires_invalid_rules(self, node: Alt) -> bool:
+        match node.items:
+            case [NamedItem(item=item)]:
+                pass
+            case _:
+                return False
+        # Preserve the source convention for bare, optional and repeated
+        # invalid references, including an invalid gather separator.
+        while True:
+            match item:
+                case Rhs(alts=[Alt(items=[NamedItem(item=child)])]):
+                    item = child
+                case Gather(separator=separator):
+                    item = separator
+                case Opt(node=child) | Repeat0(node=child) | 
Repeat1(node=child):
+                    # A compound optional may match empty, so an invalid
+                    # reference inside it must not gate the whole alternative.
+                    if self._is_compound(child):
+                        return False
+                    item = child
+                case NameLeaf(value=name):
+                    return name.startswith("invalid_")
+                case _:
+                    return False
+
+    def _is_compound(self, item: Item) -> bool:
+        match item:
+            case Rhs(alts=alts):
+                return len(alts) > 1 or any(
+                    len(alt.items) > 1 or any(self._is_compound(part.item) for 
part in alt.items)
+                    for alt in alts
+                )
+            case Group(rhs=rhs):
+                return self._is_compound(rhs)
+            case Gather(separator=separator, node=child):
+                return self._is_compound(separator) or self._is_compound(child)
+            case (
+                Opt(node=child) | Repeat0(node=child) | Repeat1(node=child)
+                | Forced(node=child) | Lookahead(node=child)
+            ):
+                return self._is_compound(child)
+            case StringLeaf(value=value):
+                return " " in value
+            case _:
+                return False
+
+    @staticmethod
+    def _prepare_action(
+        node: Alt, names: list[str], kind: RuleKind, skip_actions: bool,
+    ) -> CAction:
+        if skip_actions:
+            return CAction("_PyPegen_dummy_name(p)")
+        if action := node.action:
+            return CAction(
+                action, checked=True, debug_message="Hit with action [%d-%d]: 
%s",
+            )
+        match names:
+            case [first, rest] if kind is RuleKind.GATHER:
+                return CAction(f"_PyPegen_seq_insert_in_front(p, {first}, 
{rest})")
+            case [_, _, *_]:
+                assert kind is not RuleKind.GATHER
+                return CAction(
+                    f"_PyPegen_dummy_name(p, {', '.join(names)})",
+                    debug_message="Hit without action [%d:%d]: %s",
+                )
+            case _:
+                return CAction(names[0], debug_message="Hit with default 
action [%d:%d]: %s")
diff --git a/Tools/peg_generator/pegen/c_generator_file.py 
b/Tools/peg_generator/pegen/c_generator_file.py
new file mode 100644
index 00000000000000..4ce9345d5f5c82
--- /dev/null
+++ b/Tools/peg_generator/pegen/c_generator_file.py
@@ -0,0 +1,82 @@
+"""Emit a complete C parser from an immutable plan and local output state."""
+
+from collections.abc import Iterator
+from contextlib import contextmanager
+from typing import IO
+
+from pegen.c_generator_model import CParser
+from pegen.c_generator_rules import CRuleEmitter
+
+
+class CParserEmitter:
+    def __init__(self, parser: CParser, file: IO[str] | None):
+        self._parser = parser
+        self._file = file
+        self._level = 0
+
+    def emit(self) -> None:
+        self._emit_headers()
+        self._emit_keywords()
+        self._emit_declarations()
+        self._emit_rules()
+        if self._parser.trailer is not None:
+            self.print(self._parser.trailer)
+
+    def _emit_headers(self) -> None:
+        self.print(f"// @generated by pegen from {self._parser.source_name}")
+        for header in self._parser.headers:
+            self.print(header)
+
+    def _emit_keywords(self) -> None:
+        groups = self._parser.keyword_groups
+        self.print(f"static const int n_keyword_lists = {len(groups)};")
+        self.print("static KeywordToken *reserved_keywords[] = {")
+        with self.indent():
+            for group in groups or ((),):
+                if not group:
+                    self.print("(KeywordToken[]) {{NULL, -1}},")
+                else:
+                    self.print("(KeywordToken[]) {")
+                    with self.indent():
+                        for keyword, token_type in group:
+                            self.print(f'{{"{keyword}", {token_type}}},')
+                        self.print("{NULL, -1},")
+                    self.print("},")
+        self.print("};")
+        self.print("static char *soft_keywords[] = {")
+        with self.indent():
+            for keyword in self._parser.soft_keywords:
+                self.print(f'"{keyword}",')
+            self.print("NULL,")
+        self.print("};")
+
+    def _emit_declarations(self) -> None:
+        for rule_id, rule in enumerate(self._parser.rules, 1000):
+            comment = "  // Left-recursive" if rule.left_recursive else ""
+            self.print(f"#define {rule.signature.name}_type 
{rule_id}{comment}")
+        self.print()
+        for rule in self._parser.rules:
+            self.print(rule.signature.declaration())
+        self.print()
+
+    def _emit_rules(self) -> None:
+        for rule in self._parser.rules:
+            self.print()
+            if rule.left_recursive:
+                self.print("// Left-recursive")
+            CRuleEmitter(self, rule, debug=self._parser.debug).emit()
+
+    def print(self, *args: object) -> None:
+        if not args:
+            print(file=self._file)
+        else:
+            print("    " * self._level, end="", file=self._file)
+            print(*args, file=self._file)
+
+    @contextmanager
+    def indent(self) -> Iterator[None]:
+        self._level += 1
+        try:
+            yield
+        finally:
+            self._level -= 1
diff --git a/Tools/peg_generator/pegen/c_generator_model.py 
b/Tools/peg_generator/pegen/c_generator_model.py
new file mode 100644
index 00000000000000..7b25977d70efb5
--- /dev/null
+++ b/Tools/peg_generator/pegen/c_generator_model.py
@@ -0,0 +1,142 @@
+"""Immutable C parser descriptions shared by lowering and emission."""
+
+from dataclasses import dataclass
+from enum import Enum, auto
+
+from pegen.grammar import RuleKind
+
+
+class NodeTypes(Enum):
+    NAME_TOKEN = 0
+    NUMBER_TOKEN = 1
+    STRING_TOKEN = 2
+    GENERIC_TOKEN = 3
+    KEYWORD = 4
+    SOFT_KEYWORD = 5
+    CUT_OPERATOR = 6
+    F_STRING_CHUNK = 7
+
+
+class CBindingKind(Enum):
+    NORMAL = auto()
+    OPTIONAL = auto()
+    CUT = auto()
+
+
+@dataclass(frozen=True, slots=True)
+class FunctionCall:
+    function: str
+    arguments: tuple[str | int, ...] = ()
+    assigned_variable: str | None = None
+    assigned_variable_type: str | None = None
+    return_type: str | None = None
+    nodetype: NodeTypes | None = None
+    force_true: bool = False
+    comment: str | None = None
+    binding_kind: CBindingKind = CBindingKind.NORMAL
+
+    def expression(self) -> str:
+        """Render the invocation without its alternative-local binding or 
comment."""
+        expression = self.function
+        if arguments := self.arguments:
+            expression += f"({', '.join(map(str, arguments))})"
+        if self.force_true:
+            expression += ", !p->error_indicator"
+        return expression
+
+    def __str__(self) -> str:
+        expression = self.expression()
+        if variable := self.assigned_variable:
+            cast = f"({self.assigned_variable_type})" if 
self.assigned_variable_type else ""
+            expression = f"({variable} = {cast}{expression})"
+        if comment := self.comment:
+            expression += f"  // {comment}"
+        return expression
+
+
+@dataclass(frozen=True, slots=True)
+class CRuleSignature:
+    name: str
+    kind: RuleKind
+    return_type: str | None
+
+    @property
+    def c_return_type(self) -> str:
+        return self.return_type or "void *"
+
+    def declaration(self) -> str:
+        separator = " " if self.kind is RuleKind.NORMAL and self.return_type 
else ""
+        return f"static {self.c_return_type}{separator}{self.name}_rule(Parser 
*p);"
+
+
+@dataclass(frozen=True, slots=True)
+class CVariable:
+    name: str
+    type: str | None
+    initializer: str | None = None
+    unused: bool = False
+
+
+@dataclass(frozen=True, slots=True)
+class CAction:
+    expression: str
+    checked: bool = False
+    debug_message: str | None = None
+
+
+@dataclass(frozen=True, slots=True)
+class CAlternative:
+    text: str
+    action: CAction
+    calls: tuple[FunctionCall, ...]
+    variables: tuple[CVariable, ...]
+    cut_variable: str | None
+    requires_invalid_rules: bool
+    uses_locations: bool
+
+
+@dataclass(frozen=True, slots=True)
+class CPrefix:
+    name: str
+    type: str
+
+    @property
+    def result(self) -> str:
+        return self.name + "_result"
+
+    @property
+    def end(self) -> str:
+        return self.name + "_end"
+
+    @property
+    def valid(self) -> str:
+        return self.name + "_valid"
+
+
+@dataclass(frozen=True, slots=True)
+class CRule:
+    signature: CRuleSignature
+    text: str
+    alternatives: tuple[CAlternative, ...]
+    left_recursive: bool
+    leader: bool
+    memoize: bool
+    disable_invalid_rules: bool
+    prefixes: tuple[CPrefix, ...] = ()
+
+    @property
+    def uses_locations(self) -> bool:
+        return any(alt.uses_locations for alt in self.alternatives)
+
+
+@dataclass(frozen=True, slots=True)
+class CParser:
+    """Complete file-emission input, independent of compilation state."""
+
+    source_name: str
+    headers: tuple[str, ...]
+    keyword_groups: tuple[tuple[tuple[str, int], ...], ...]
+    soft_keywords: tuple[str, ...]
+    rules: tuple[CRule, ...]
+    trailer: str | None
+    debug: bool
diff --git a/Tools/peg_generator/pegen/c_generator_rules.py 
b/Tools/peg_generator/pegen/c_generator_rules.py
new file mode 100644
index 00000000000000..6dd598beb1b39c
--- /dev/null
+++ b/Tools/peg_generator/pegen/c_generator_rules.py
@@ -0,0 +1,363 @@
+"""Render prepared C rules with rule-local output and cleanup state."""
+
+from collections.abc import Iterator
+from contextlib import AbstractContextManager, contextmanager
+from dataclasses import dataclass
+from typing import Protocol
+
+from pegen.c_generator_model import CAlternative, CRule
+from pegen.grammar import RuleKind
+
+
+class CWriter(Protocol):
+    def print(self, *args: object) -> None:
+        ...
+
+    def indent(self) -> AbstractContextManager[None]:
+        ...
+
+
+@dataclass(frozen=True, slots=True)
+class _CReturnEmitter:
+    writer: CWriter
+    cleanups: tuple[str, ...] = ()
+
+    def with_cleanup(self, cleanup: str) -> "_CReturnEmitter":
+        return _CReturnEmitter(self.writer, (cleanup, *self.cleanups))
+
+    def emit(self, value: str) -> None:
+        for cleanup in self.cleanups:
+            self.writer.print(cleanup)
+        self.writer.print("p->level--;")
+        self.writer.print(f"return {value};")
+
+    def check_memory(self, expr: str) -> None:
+        self.writer.print(f"if ({expr}) {{")
+        with self.writer.indent():
+            self.no_memory()
+        self.writer.print("}")
+
+    def no_memory(self) -> None:
+        self.writer.print("p->error_indicator = 1;")
+        self.writer.print("PyErr_NoMemory();")
+        self.emit("NULL")
+
+
+class _LoopBuffer:
+    """Temporary repetition storage and the exits that release it."""
+
+    _release = "PyMem_Free(_children);"
+
+    def __init__(self, writer: CWriter, returns: _CReturnEmitter):
+        self._print = writer.print
+        self._indent = writer.indent
+        self._returns = returns
+        self.error_returns = returns.with_cleanup(self._release)
+
+    def initialize(self) -> None:
+        self._print("void **_children = PyMem_Malloc(sizeof(void *));")
+        self._returns.check_memory("!_children")
+        self._print("Py_ssize_t _children_capacity = 1;")
+        self._print("Py_ssize_t _n = 0;")
+
+    def append(self, value: str) -> None:
+        self._print("if (_n == _children_capacity) {")
+        with self._indent():
+            self._print("_children_capacity *= 2;")
+            self._print(
+                "void **_new_children = PyMem_Realloc(_children, 
_children_capacity*sizeof(void *));"
+            )
+            self._check_memory("!_new_children")
+            self._print("_children = _new_children;")
+        self._print("}")
+        self._print(f"_children[_n++] = {value};")
+
+    def finish(self, *, require_one: bool) -> str:
+        if require_one:
+            self._print("if (_n == 0 || p->error_indicator) {")
+            with self._indent():
+                self.error_returns.emit("NULL")
+            self._print("}")
+        self._print("asdl_seq *_seq = (asdl_seq*)_Py_asdl_generic_seq_new(_n, 
p->arena);")
+        self._check_memory("!_seq")
+        self._print("for (Py_ssize_t i = 0; i < _n; i++) 
asdl_seq_SET_UNTYPED(_seq, i, _children[i]);")
+        self._print(self._release)
+        return "_seq"
+
+    def _check_memory(self, expr: str) -> None:
+        self._print(f"if ({expr}) {{")
+        with self._indent():
+            self._print(self._release)
+            self._returns.no_memory()
+        self._print("}")
+
+
+class CRuleEmitter:
+    def __init__(self, writer: CWriter, rule: CRule, *, debug: bool = False):
+        self._writer = writer
+        self._print = writer.print
+        self._indent = writer.indent
+        self._rule = rule
+        self._debug = debug
+        self._returns = _CReturnEmitter(writer)
+
+    def emit(self) -> None:
+        rule = self._rule
+        signature = rule.signature
+        result_type = signature.c_return_type
+        for line in rule.text.splitlines():
+            self._print(f"// {line}")
+        if rule.left_recursive and rule.leader:
+            self._print(f"static {result_type} {signature.name}_raw(Parser 
*);")
+        self._print(f"static {result_type}")
+        self._print(f"{signature.name}_rule(Parser *p)")
+        if rule.left_recursive and rule.leader:
+            self._emit_left_recursive_wrapper()
+        self._print("{")
+        with self._invalid_rule_context():
+            match signature.kind:
+                case RuleKind.LOOP0 | RuleKind.LOOP1:
+                    self._emit_loop_body()
+                case RuleKind.NORMAL | RuleKind.GATHER:
+                    self._emit_rule_body()
+        self._print("}")
+
+    @contextmanager
+    def _invalid_rule_context(self) -> Iterator[None]:
+        if not self._rule.disable_invalid_rules:
+            yield
+            return
+        with self._indent():
+            self._print("int _prev_call_invalid = p->call_invalid_rules;")
+            self._print("p->call_invalid_rules = 0;")
+        previous_returns = self._returns
+        self._returns = previous_returns.with_cleanup(
+            "p->call_invalid_rules = _prev_call_invalid;"
+        )
+        try:
+            yield
+        finally:
+            self._returns = previous_returns
+
+    def _emit_left_recursive_wrapper(self) -> None:
+        signature = self._rule.signature
+        result_type = signature.c_return_type
+        self._print("{")
+        with self._indent():
+            self._emit_recursion_check()
+            self._print(f"{result_type} _res = NULL;")
+            self._print(f"if (_PyPegen_is_memoized(p, {signature.name}_type, 
&_res)) {{")
+            with self._indent():
+                self._returns.emit("_res")
+            self._print("}")
+            self._print("int _mark = p->mark;")
+            self._print("int _resmark = p->mark;")
+            self._print(f"Memo *_memo = _PyPegen_insert_memo_direct(p, _mark, 
{signature.name}_type);")
+            self._print("if (_memo == NULL) {")
+            with self._indent():
+                self._returns.emit("NULL")
+            self._print("}")
+            self._print("while (1) {")
+            with self._indent():
+                self._print("_memo->node = _res;")
+                self._print("_memo->mark = p->mark;")
+                self._print("p->mark = _mark;")
+                self._print(f"void *_raw = {signature.name}_raw(p);")
+                self._print("if (p->error_indicator) {")
+                with self._indent():
+                    self._returns.emit("NULL")
+                self._print("}")
+                self._print("if (_raw == NULL || p->mark <= _resmark)")
+                with self._indent():
+                    self._print("break;")
+                self._print("_resmark = p->mark;")
+                self._print("_res = _raw;")
+            self._print("}")
+            self._print("p->mark = _resmark;")
+            self._returns.emit("_res")
+        self._print("}")
+        self._print(f"static {result_type}")
+        self._print(f"{signature.name}_raw(Parser *p)")
+
+    def _emit_rule_body(self) -> None:
+        signature = self._rule.signature
+        memoize = self._rule.memoize
+        result_type = signature.c_return_type
+
+        with self._indent():
+            self._emit_recursion_check()
+            self._emit_error_check()
+            self._print(f"{result_type} _res = NULL;")
+            if memoize:
+                self._print(f"if (_PyPegen_is_memoized(p, 
{signature.name}_type, &_res)) {{")
+                with self._indent():
+                    self._returns.emit("_res")
+                self._print("}")
+            self._print("int _mark = p->mark;")
+            for prefix in self._rule.prefixes:
+                self._print(f"{prefix.type} {prefix.result} = NULL;")
+                self._print(f"int {prefix.end} = 0, {prefix.valid} = 0;")
+            if self._rule.uses_locations:
+                self._emit_token_start_metadata()
+            for alt in self._rule.alternatives:
+                with self._alternative(alt):
+                    self._emit_normal_alt(alt)
+            if self._debug:
+                self._print(f'D(fprintf(stderr, "Fail at %d: 
{signature.name}\\n", p->mark));')
+            self._print("_res = NULL;")
+        self._print("  done:")
+        with self._indent():
+            if memoize:
+                self._print(f"_PyPegen_insert_memo(p, _mark, 
{signature.name}_type, _res);")
+            self._returns.emit("_res")
+
+    def _emit_loop_body(self) -> None:
+        rule = self._rule
+        signature = rule.signature
+        buffer = _LoopBuffer(self._writer, self._returns)
+        with self._indent():
+            self._emit_recursion_check()
+            self._emit_error_check()
+            self._print("void *_res = NULL;")
+            if rule.memoize:
+                self._print(f"if (_PyPegen_is_memoized(p, 
{signature.name}_type, &_res)) {{")
+                with self._indent():
+                    self._returns.emit("_res")
+                self._print("}")
+            self._print("int _mark = p->mark;")
+            if rule.memoize:
+                self._print("int _start_mark = p->mark;")
+            buffer.initialize()
+            if rule.uses_locations:
+                self._emit_token_start_metadata()
+            alt, = rule.alternatives
+            with self._alternative(alt):
+                self._emit_loop_alt(alt, buffer)
+            result = buffer.finish(require_one=signature.kind is 
RuleKind.LOOP1)
+            if rule.memoize:
+                self._print(f"_PyPegen_insert_memo(p, _start_mark, 
{signature.name}_type, {result});")
+            self._returns.emit(result)
+
+    @contextmanager
+    def _alternative(self, alt: CAlternative) -> Iterator[None]:
+        rulename = self._rule.signature.name
+        if alt.requires_invalid_rules:
+            self._print(f"if (p->call_invalid_rules) {{ // {alt.text}")
+        else:
+            self._print(f"{{ // {alt.text}")
+        with self._indent():
+            self._emit_error_check()
+            node_str = alt.text.replace('"', '\\"')
+            self._print(
+                f'D(fprintf(stderr, "%*c> {rulename}[%d-%d]: %s\\n", p->level, 
\' \', _mark, p->mark, "{node_str}"));'
+            )
+            for variable in sorted(alt.variables, key=lambda var: var.name):
+                ctype = variable.type + " " if variable.type else "void *"
+                initializer = (
+                    f" = {variable.initializer}" if variable.initializer is 
not None else ""
+                )
+                self._print(f"{ctype}{variable.name}{initializer};")
+                if variable.unused:
+                    self._print(f"UNUSED({variable.name}); // Silence compiler 
warnings")
+
+            yield
+
+            self._print("p->mark = _mark;")
+            self._print(
+                f"D(fprintf(stderr, \"%*c%s {rulename}[%d-%d]: %s 
failed!\\n\", p->level, ' ',\n"
+                f'                  p->error_indicator ? "ERROR!" : "-", 
_mark, p->mark, "{node_str}"));'
+            )
+            if alt.cut_variable is not None:
+                self._print(f"if ({alt.cut_variable}) {{")
+                with self._indent():
+                    self._returns.emit("NULL")
+                self._print("}")
+        self._print("}")
+
+    def _emit_conditions(self, keyword: str, alt: CAlternative) -> None:
+        self._print(f"{keyword} (")
+        with self._indent():
+            for index, call in enumerate(alt.calls):
+                if index:
+                    self._print("&&")
+                self._print(call)
+        self._print(")")
+
+    def _emit_normal_alt(self, alt: CAlternative) -> None:
+        rulename = self._rule.signature.name
+        self._emit_conditions(keyword="if", alt=alt)
+        self._print("{")
+        with self._indent():
+            node_str = alt.text.replace('"', '\\"')
+            self._print(
+                f'D(fprintf(stderr, "%*c+ {rulename}[%d-%d]: %s 
succeeded!\\n", p->level, \' \', _mark, p->mark, "{node_str}"));'
+            )
+            self._emit_alt_action(alt, self._returns)
+
+            self._print("goto done;")
+        self._print("}")
+
+    def _emit_loop_alt(self, alt: CAlternative, buffer: _LoopBuffer) -> None:
+        self._emit_conditions(keyword="while", alt=alt)
+        self._print("{")
+        with self._indent():
+            self._emit_alt_action(alt, buffer.error_returns)
+            buffer.append("_res")
+            self._print("_mark = p->mark;")
+        self._print("}")
+
+    def _emit_alt_action(self, alt: CAlternative, error_returns: 
_CReturnEmitter) -> None:
+        # Location failures and explicit-action failures have distinct cleanup
+        # paths in the generated parser. Keep their return contexts separate.
+        if alt.uses_locations:
+            self._emit_token_end_metadata()
+        if not alt.action.checked:
+            self._emit_action_debug(alt)
+        self._print(f"_res = {alt.action.expression};")
+        if alt.action.checked:
+            self._print("if ((_res == NULL || p->error_indicator) && 
PyErr_Occurred()) {")
+            with self._indent():
+                self._print("p->error_indicator = 1;")
+                error_returns.emit("NULL")
+            self._print("}")
+            self._emit_action_debug(alt)
+
+    def _emit_action_debug(self, alt: CAlternative) -> None:
+        if self._debug and alt.action.debug_message:
+            self._print(
+                f'D(fprintf(stderr, "{alt.action.debug_message}\\n", _mark, 
p->mark, "{alt.text}"));'
+            )
+
+    def _emit_token_start_metadata(self) -> None:
+        self._print("if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) {")
+        with self._indent():
+            self._print("p->error_indicator = 1;")
+            self._returns.emit("NULL")
+        self._print("}")
+        self._print("int _start_lineno = p->tokens[_mark]->lineno;")
+        self._print("UNUSED(_start_lineno); // Only used by EXTRA macro")
+        self._print("int _start_col_offset = p->tokens[_mark]->col_offset;")
+        self._print("UNUSED(_start_col_offset); // Only used by EXTRA macro")
+
+    def _emit_token_end_metadata(self) -> None:
+        self._print("Token *_token = 
_PyPegen_get_last_nonnwhitespace_token(p);")
+        self._print("if (_token == NULL) {")
+        with self._indent():
+            self._returns.emit("NULL")
+        self._print("}")
+        self._print("int _end_lineno = _token->end_lineno;")
+        self._print("UNUSED(_end_lineno); // Only used by EXTRA macro")
+        self._print("int _end_col_offset = _token->end_col_offset;")
+        self._print("UNUSED(_end_col_offset); // Only used by EXTRA macro")
+
+    def _emit_error_check(self) -> None:
+        self._print("if (p->error_indicator) {")
+        with self._indent():
+            self._returns.emit("NULL")
+        self._print("}")
+
+    def _emit_recursion_check(self) -> None:
+        self._print("if (p->level++ == MAXSTACK || 
_PyPegen_stack_exhausted(p)) {")
+        with self._indent():
+            self._print("_Pypegen_stack_overflow(p);")
+        self._print("}")
diff --git a/Tools/peg_generator/pegen/grammar.py 
b/Tools/peg_generator/pegen/grammar.py
index d3c2eca6615a9f..f81e64aa583778 100644
--- a/Tools/peg_generator/pegen/grammar.py
+++ b/Tools/peg_generator/pegen/grammar.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from collections.abc import Iterable, Iterator, Set
+from enum import Enum, auto
 from typing import Any
 
 
@@ -57,20 +58,36 @@ def __iter__(self) -> Iterator[Rule]:
 SIMPLE_STR = True
 
 
+class RuleKind(Enum):
+    NORMAL = auto()
+    LOOP0 = auto()
+    LOOP1 = auto()
+    GATHER = auto()
+
+
 class Rule:
-    def __init__(self, name: str, type: str | None, rhs: Rhs, flags: 
frozenset[str] | None = None):
+    def __init__(
+        self,
+        name: str,
+        type: str | None,
+        rhs: Rhs,
+        flags: frozenset[str] | None = None,
+        *,
+        kind: RuleKind = RuleKind.NORMAL,
+    ):
         self.name = name
         self.type = type
         self.rhs = rhs
         self.flags = flags or frozenset()
+        self.kind = kind
         self.left_recursive = False
         self.leader = False
 
     def is_loop(self) -> bool:
-        return self.name.startswith("_loop")
+        return self.kind in (RuleKind.LOOP0, RuleKind.LOOP1)
 
     def is_gather(self) -> bool:
-        return self.name.startswith("_gather")
+        return self.kind is RuleKind.GATHER
 
     def __str__(self) -> str:
         if SIMPLE_STR or self.type is None:
@@ -84,22 +101,18 @@ def __str__(self) -> str:
         return "\n".join(lines)
 
     def __repr__(self) -> str:
-        return f"Rule({self.name!r}, {self.type!r}, {self.rhs!r})"
+        kind = f", kind=RuleKind.{self.kind.name}" if self.kind is not 
RuleKind.NORMAL else ""
+        return f"Rule({self.name!r}, {self.type!r}, {self.rhs!r}{kind})"
 
     def __iter__(self) -> Iterator[Rhs]:
         yield self.rhs
 
     def flatten(self) -> Rhs:
         # If it's a single parenthesized group, flatten it.
-        rhs = self.rhs
-        if (
-            not self.is_loop()
-            and len(rhs.alts) == 1
-            and len(rhs.alts[0].items) == 1
-            and isinstance(rhs.alts[0].items[0].item, Group)
-        ):
-            rhs = rhs.alts[0].items[0].item.rhs
-        return rhs
+        match self.rhs:
+            case Rhs(alts=[Alt(items=[NamedItem(item=Group(rhs=rhs))])]) if 
not self.is_loop():
+                return rhs
+        return self.rhs
 
 
 class Leaf:
@@ -147,12 +160,11 @@ def __iter__(self) -> Iterator[list[Alt]]:
 
     @property
     def can_be_inlined(self) -> bool:
-        if len(self.alts) != 1 or len(self.alts[0].items) != 1:
-            return False
-        # If the alternative has an action we cannot inline
-        if getattr(self.alts[0], "action", None) is not None:
-            return False
-        return True
+        match self.alts:
+            case [Alt(items=[_], action=None)]:
+                return True
+            case _:
+                return False
 
 
 class Alt:
diff --git a/Tools/peg_generator/pegen/grammar_analysis.py 
b/Tools/peg_generator/pegen/grammar_analysis.py
new file mode 100644
index 00000000000000..5b716cf209e000
--- /dev/null
+++ b/Tools/peg_generator/pegen/grammar_analysis.py
@@ -0,0 +1,155 @@
+"""Nullable and left-recursion analysis for source grammar rules."""
+
+from collections.abc import Iterable, Set
+from typing import Any
+
+from pegen import sccutils
+from pegen.grammar import (
+    Alt,
+    Cut,
+    Forced,
+    Gather,
+    GrammarVisitor,
+    Group,
+    NamedItem,
+    NameLeaf,
+    Opt,
+    Repeat0,
+    Repeat1,
+    Rhs,
+    Rule,
+    StringLeaf,
+)
+
+
+class NullableVisitor(GrammarVisitor):
+    def __init__(self, rules: dict[str, Rule]) -> None:
+        self.rules = rules
+        self.visited: set[Any] = set()
+        self.nullables: set[Rule | NamedItem] = set()
+
+    def visit(self, node: Any, *args: Any, **kwargs: Any) -> bool | None:
+        match node:
+            case Rule(rhs=rhs):
+                if node in self.visited:
+                    return False
+                self.visited.add(node)
+                if self.visit(rhs):
+                    self.nullables.add(node)
+                return node in self.nullables
+            case NamedItem(item=item):
+                if self.visit(item):
+                    self.nullables.add(node)
+                return node in self.nullables
+            case Rhs(alts=alts):
+                return any(self.visit(alt) for alt in alts)
+            case Alt(items=items):
+                return all(self.visit(item) for item in items)
+            case Forced() | Opt() | Repeat0():
+                return True
+            case Repeat1() | Gather() | Cut():
+                return False
+            case Group(rhs=rhs):
+                return self.visit(rhs)
+            case NameLeaf(value=name):
+                if (rule := self.rules.get(name)) is not None:
+                    return self.visit(rule)
+                # Token or unknown; never empty.
+                return False
+            case StringLeaf(value=value):
+                # The string token '' is considered empty.
+                return not value
+            case _:
+                return self.generic_visit(node, *args, **kwargs)
+
+
+def compute_nullables(rules: dict[str, Rule]) -> set[Any]:
+    """Compute which rules in a grammar are nullable.
+
+    Thanks to TatSu (tatsu/leftrec.py) for inspiration.
+    """
+    nullable_visitor = NullableVisitor(rules)
+    for rule in rules.values():
+        nullable_visitor.visit(rule)
+    return nullable_visitor.nullables
+
+
+class InitialNamesVisitor(GrammarVisitor):
+    def __init__(self, rules: dict[str, Rule]) -> None:
+        self.rules = rules
+        self.nullables = compute_nullables(rules)
+
+    def generic_visit(self, node: Iterable[Any], *args: Any, **kwargs: Any) -> 
set[Any]:
+        names: set[str] = set()
+        for value in node:
+            if isinstance(value, list):
+                for item in value:
+                    names |= self.visit(item, *args, **kwargs)
+            else:
+                names |= self.visit(value, *args, **kwargs)
+        return names
+
+    def visit(self, node: Any, *args: Any, **kwargs: Any) -> set[Any]:
+        match node:
+            case Alt(items=items):
+                names: set[str] = set()
+                for item in items:
+                    names |= self.visit(item)
+                    if item not in self.nullables:
+                        break
+                return names
+            case Forced() | Cut() | StringLeaf():
+                return set()
+            case NameLeaf(value=name):
+                return {name}
+            case _:
+                return self.generic_visit(node, *args, **kwargs)
+
+
+def compute_left_recursives(
+    rules: dict[str, Rule]
+) -> tuple[dict[str, Set[str]], list[Set[str]]]:
+    graph = make_first_graph(rules)
+    sccs = list(sccutils.strongly_connected_components(graph.keys(), graph))
+    for scc in sccs:
+        if len(scc) > 1:
+            for name in scc:
+                rules[name].left_recursive = True
+            # Try to find a leader such that all cycles go through it.
+            leaders = set(scc)
+            for start in scc:
+                for cycle in sccutils.find_cycles_in_scc(graph, scc, start):
+                    # print("Cycle:", " -> ".join(cycle))
+                    leaders -= scc - set(cycle)
+                    if not leaders:
+                        raise ValueError(
+                            f"SCC {scc} has no leadership candidate (no 
element is included in all cycles)"
+                        )
+            # print("Leaders:", leaders)
+            leader = min(leaders)  # Pick an arbitrary leader from the 
candidates.
+            rules[leader].leader = True
+        else:
+            name = min(scc)  # The only element.
+            if name in graph[name]:
+                rules[name].left_recursive = True
+                rules[name].leader = True
+    return graph, sccs
+
+
+def make_first_graph(rules: dict[str, Rule]) -> dict[str, Set[str]]:
+    """Compute the graph of left-invocations.
+
+    There's an edge from A to B if A may invoke B at its initial
+    position.
+
+    Note that this requires the nullable flags to have been computed.
+    """
+    initial_name_visitor = InitialNamesVisitor(rules)
+    graph: dict[str, Set[str]] = {}
+    vertices: set[str] = set()
+    for rulename, rhs in rules.items():
+        graph[rulename] = names = initial_name_visitor.visit(rhs)
+        vertices |= names
+    for vertex in vertices:
+        graph.setdefault(vertex, set())
+    return graph
diff --git a/Tools/peg_generator/pegen/parser_generator.py 
b/Tools/peg_generator/pegen/parser_generator.py
index 81314b0cc073f9..f7783f1e7810b0 100644
--- a/Tools/peg_generator/pegen/parser_generator.py
+++ b/Tools/peg_generator/pegen/parser_generator.py
@@ -3,30 +3,38 @@
 import re
 import sys
 from abc import abstractmethod
-from collections.abc import Iterable, Iterator, Set
-from typing import IO, Any
+from collections.abc import Iterator
+from typing import IO
 
-from pegen import sccutils
 from pegen.grammar import (
     Alt,
-    Cut,
-    Forced,
     Gather,
     Grammar,
     GrammarError,
     GrammarVisitor,
-    Group,
-    Lookahead,
     NamedItem,
     NameLeaf,
-    Opt,
     Plain,
-    Repeat0,
-    Repeat1,
     Rhs,
     Rule,
+    RuleKind,
     StringLeaf,
 )
+from pegen.grammar_analysis import (
+    InitialNamesVisitor as InitialNamesVisitor,
+)
+from pegen.grammar_analysis import (
+    NullableVisitor as NullableVisitor,
+)
+from pegen.grammar_analysis import (
+    compute_left_recursives as compute_left_recursives,
+)
+from pegen.grammar_analysis import (
+    compute_nullables as compute_nullables,
+)
+from pegen.grammar_analysis import (
+    make_first_graph as make_first_graph,
+)
 
 
 class RuleCollectorVisitor(GrammarVisitor):
@@ -93,12 +101,7 @@ def __init__(self, grammar: Grammar, tokens: set[str], 
file: IO[str] | None):
         self.keywords: dict[str, int] = {}
         self.soft_keywords: set[str] = set()
         self.rules = grammar.rules
-        self.validate_rule_names()
-        if "trailer" not in grammar.metas and "start" not in self.rules:
-            raise GrammarError("Grammar without a trailer must have a 'start' 
rule")
-        checker = RuleCheckingVisitor(self.rules, self.tokens)
-        for rule in self.rules.values():
-            checker.visit(rule)
+        self._validate_grammar()
         self.file = file
         self.level = 0
         self.first_graph, self.first_sccs = compute_left_recursives(self.rules)
@@ -107,6 +110,14 @@ def __init__(self, grammar: Grammar, tokens: set[str], 
file: IO[str] | None):
         self.all_rules: dict[str, Rule] = self.rules.copy()  # Rules + 
temporal rules
         self._local_variable_stack: list[list[str]] = []
 
+    def _validate_grammar(self) -> None:
+        self.validate_rule_names()
+        if "trailer" not in self.grammar.metas and "start" not in self.rules:
+            raise GrammarError("Grammar without a trailer must have a 'start' 
rule")
+        checker = RuleCheckingVisitor(self.rules, self.tokens)
+        for rule in self.rules.values():
+            checker.visit(rule)
+
     def validate_rule_names(self) -> None:
         for rule in self.rules:
             if rule.startswith("_"):
@@ -175,10 +186,14 @@ def artificial_rule_from_repeat(self, node: Plain, 
is_repeat1: bool) -> str:
         self.counter += 1
         if is_repeat1:
             prefix = "_loop1_"
+            kind = RuleKind.LOOP1
         else:
             prefix = "_loop0_"
+            kind = RuleKind.LOOP0
         name = f"{prefix}{self.counter}"
-        self.all_rules[name] = Rule(name, None, Rhs([Alt([NamedItem(None, 
node)])]))
+        self.all_rules[name] = Rule(
+            name, None, Rhs([Alt([NamedItem(None, node)])]), kind=kind
+        )
         return name
 
     def artificial_rule_from_gather(self, node: Gather) -> str:
@@ -192,6 +207,7 @@ def artificial_rule_from_gather(self, node: Gather) -> str:
             extra_function_name,
             None,
             Rhs([extra_function_alt]),
+            kind=RuleKind.LOOP0,
         )
         self.counter += 1
         name = f"_gather_{self.counter}"
@@ -202,6 +218,7 @@ def artificial_rule_from_gather(self, node: Gather) -> str:
             name,
             None,
             Rhs([alt]),
+            kind=RuleKind.GATHER,
         )
         return name
 
@@ -213,168 +230,3 @@ def dedupe(self, name: str) -> str:
             name = f"{origname}_{counter}"
         self.local_variable_names.append(name)
         return name
-
-
-class NullableVisitor(GrammarVisitor):
-    def __init__(self, rules: dict[str, Rule]) -> None:
-        self.rules = rules
-        self.visited: set[Any] = set()
-        self.nullables: set[Rule | NamedItem] = set()
-
-    def visit_Rule(self, rule: Rule) -> bool:
-        if rule in self.visited:
-            return False
-        self.visited.add(rule)
-        if self.visit(rule.rhs):
-            self.nullables.add(rule)
-        return rule in self.nullables
-
-    def visit_Rhs(self, rhs: Rhs) -> bool:
-        for alt in rhs.alts:
-            if self.visit(alt):
-                return True
-        return False
-
-    def visit_Alt(self, alt: Alt) -> bool:
-        for item in alt.items:
-            if not self.visit(item):
-                return False
-        return True
-
-    def visit_Forced(self, force: Forced) -> bool:
-        return True
-
-    def visit_LookAhead(self, lookahead: Lookahead) -> bool:
-        return True
-
-    def visit_Opt(self, opt: Opt) -> bool:
-        return True
-
-    def visit_Repeat0(self, repeat: Repeat0) -> bool:
-        return True
-
-    def visit_Repeat1(self, repeat: Repeat1) -> bool:
-        return False
-
-    def visit_Gather(self, gather: Gather) -> bool:
-        return False
-
-    def visit_Cut(self, cut: Cut) -> bool:
-        return False
-
-    def visit_Group(self, group: Group) -> bool:
-        return self.visit(group.rhs)
-
-    def visit_NamedItem(self, item: NamedItem) -> bool:
-        if self.visit(item.item):
-            self.nullables.add(item)
-        return item in self.nullables
-
-    def visit_NameLeaf(self, node: NameLeaf) -> bool:
-        if node.value in self.rules:
-            return self.visit(self.rules[node.value])
-        # Token or unknown; never empty.
-        return False
-
-    def visit_StringLeaf(self, node: StringLeaf) -> bool:
-        # The string token '' is considered empty.
-        return not node.value
-
-
-def compute_nullables(rules: dict[str, Rule]) -> set[Any]:
-    """Compute which rules in a grammar are nullable.
-
-    Thanks to TatSu (tatsu/leftrec.py) for inspiration.
-    """
-    nullable_visitor = NullableVisitor(rules)
-    for rule in rules.values():
-        nullable_visitor.visit(rule)
-    return nullable_visitor.nullables
-
-
-class InitialNamesVisitor(GrammarVisitor):
-    def __init__(self, rules: dict[str, Rule]) -> None:
-        self.rules = rules
-        self.nullables = compute_nullables(rules)
-
-    def generic_visit(self, node: Iterable[Any], *args: Any, **kwargs: Any) -> 
set[Any]:
-        names: set[str] = set()
-        for value in node:
-            if isinstance(value, list):
-                for item in value:
-                    names |= self.visit(item, *args, **kwargs)
-            else:
-                names |= self.visit(value, *args, **kwargs)
-        return names
-
-    def visit_Alt(self, alt: Alt) -> set[Any]:
-        names: set[str] = set()
-        for item in alt.items:
-            names |= self.visit(item)
-            if item not in self.nullables:
-                break
-        return names
-
-    def visit_Forced(self, force: Forced) -> set[Any]:
-        return set()
-
-    def visit_LookAhead(self, lookahead: Lookahead) -> set[Any]:
-        return set()
-
-    def visit_Cut(self, cut: Cut) -> set[Any]:
-        return set()
-
-    def visit_NameLeaf(self, node: NameLeaf) -> set[Any]:
-        return {node.value}
-
-    def visit_StringLeaf(self, node: StringLeaf) -> set[Any]:
-        return set()
-
-
-def compute_left_recursives(
-    rules: dict[str, Rule]
-) -> tuple[dict[str, Set[str]], list[Set[str]]]:
-    graph = make_first_graph(rules)
-    sccs = list(sccutils.strongly_connected_components(graph.keys(), graph))
-    for scc in sccs:
-        if len(scc) > 1:
-            for name in scc:
-                rules[name].left_recursive = True
-            # Try to find a leader such that all cycles go through it.
-            leaders = set(scc)
-            for start in scc:
-                for cycle in sccutils.find_cycles_in_scc(graph, scc, start):
-                    # print("Cycle:", " -> ".join(cycle))
-                    leaders -= scc - set(cycle)
-                    if not leaders:
-                        raise ValueError(
-                            f"SCC {scc} has no leadership candidate (no 
element is included in all cycles)"
-                        )
-            # print("Leaders:", leaders)
-            leader = min(leaders)  # Pick an arbitrary leader from the 
candidates.
-            rules[leader].leader = True
-        else:
-            name = min(scc)  # The only element.
-            if name in graph[name]:
-                rules[name].left_recursive = True
-                rules[name].leader = True
-    return graph, sccs
-
-
-def make_first_graph(rules: dict[str, Rule]) -> dict[str, Set[str]]:
-    """Compute the graph of left-invocations.
-
-    There's an edge from A to B if A may invoke B at its initial
-    position.
-
-    Note that this requires the nullable flags to have been computed.
-    """
-    initial_name_visitor = InitialNamesVisitor(rules)
-    graph = {}
-    vertices: set[str] = set()
-    for rulename, rhs in rules.items():
-        graph[rulename] = names = initial_name_visitor.visit(rhs)
-        vertices |= names
-    for vertex in vertices:
-        graph.setdefault(vertex, set())
-    return graph

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to