Skip to content

Commit b7f81d4

Browse files
authored
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
1 parent 2a96282 commit b7f81d4

11 files changed

Lines changed: 1874 additions & 1105 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import io
2+
import unittest
3+
from unittest import mock
4+
5+
from test import test_tools
6+
7+
test_tools.skip_if_missing("peg_generator")
8+
with test_tools.imports_under_tool("peg_generator"):
9+
from pegen import grammar as grammar_module
10+
from pegen.c_generator import CParserGenerator
11+
from pegen.c_generator_file import CParserEmitter
12+
from pegen.grammar import NamedItem, RuleKind
13+
from pegen.grammar_parser import GeneratedParser as GrammarParser
14+
from pegen.testutil import ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, parse_string
15+
16+
17+
class TestCGenerator(unittest.TestCase):
18+
def make_generator(self, source):
19+
grammar = parse_string(source, GrammarParser)
20+
return CParserGenerator(
21+
grammar, ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, io.StringIO()
22+
)
23+
24+
def emit_parser(self, parser):
25+
output = io.StringIO()
26+
CParserEmitter(parser, output).emit()
27+
return output.getvalue()
28+
29+
def test_rule_types_distinguish_implicit_and_explicit_void_pointer(self):
30+
generator = self.make_generator("""
31+
start: &implicit implicit explicit
32+
implicit: NAME
33+
explicit[void*]: NAME
34+
""")
35+
generator.rules["explicit"].type = "void *"
36+
start, implicit, explicit = generator.prepare("example.gram").rules
37+
38+
self.assertIsNone(implicit.signature.return_type)
39+
self.assertEqual(explicit.signature.return_type, "void *")
40+
self.assertEqual(implicit.signature.c_return_type, "void *")
41+
self.assertEqual(explicit.signature.c_return_type, "void *")
42+
self.assertEqual(
43+
implicit.signature.declaration(), "static void *implicit_rule(Parser *p);"
44+
)
45+
self.assertEqual(
46+
explicit.signature.declaration(), "static void * explicit_rule(Parser *p);"
47+
)
48+
call = start.alternatives[0].calls[0]
49+
self.assertEqual(call.function, "_PyPegen_lookahead")
50+
generator = self.make_generator("start: &explicit\nexplicit[void*]: NAME\n")
51+
with self.assertRaisesRegex(RuntimeError, "return type is incompatible"):
52+
generator.prepare("example.gram")
53+
54+
def test_parser_plan_does_not_depend_on_compilation_state(self):
55+
generator = self.make_generator("""
56+
@header 'CUSTOM HEADER'
57+
@subheader 'CUSTOM SUBHEADER'
58+
@trailer 'CUSTOM TRAILER %(modulename)s %(mode)d'
59+
@modulename 'sample'
60+
@bytecode '1'
61+
start[mod_ty]: expr_without_invalid 'pass' "zsoft" "asoft" ('bb' | 'aa')* ENDMARKER
62+
expr_without_invalid[expr_ty] (memo): name=expr [NUMBER] { name }
63+
expr[expr_ty]: expr '+' NAME | NAME
64+
""")
65+
generator.debug = True
66+
parser = generator.prepare("some/path/example.gram")
67+
expected = self.emit_parser(parser)
68+
69+
self.assertEqual(generator.file.getvalue(), "")
70+
self.assertEqual(parser.source_name, "example.gram")
71+
self.assertEqual(parser.headers, ("CUSTOM HEADER", "CUSTOM SUBHEADER"))
72+
self.assertEqual(parser.trailer, "CUSTOM TRAILER sample 2")
73+
self.assertEqual(parser.soft_keywords, ("asoft", "zsoft"))
74+
self.assertEqual([word for word, _ in parser.keyword_groups[2]], ["bb", "aa"])
75+
self.assertTrue(any(rule.signature.kind is RuleKind.LOOP0 for rule in parser.rules))
76+
with self.assertRaises(AttributeError):
77+
parser.rules[0].alternatives[0].calls[0].assigned_variable = "changed"
78+
79+
generator.grammar.metas.clear()
80+
generator.grammar.metas["trailer"] = "%(missing)s"
81+
for rule in generator.all_rules.values():
82+
rule.name = "changed"
83+
rule.type = "changed_type"
84+
rule.flags = frozenset()
85+
rule.rhs.alts[0].action = "changed_action"
86+
rule.rhs.alts.clear()
87+
generator.rules.clear()
88+
generator.all_rules.clear()
89+
generator.keywords.clear()
90+
generator.soft_keywords.clear()
91+
generator.debug = False
92+
generator.skip_actions = True
93+
self.assertEqual(self.emit_parser(parser), expected)
94+
95+
def test_repeated_preparation_keeps_variable_names_local(self):
96+
source = """
97+
start: 'run' expr term bindings other ENDMARKER
98+
expr: expr '+' NAME | NAME
99+
term: term '*' NUMBER | NUMBER
100+
bindings: (name_var=NUMBER) name_var[expr_ty]=(NAME) [NUMBER] (NAME | NUMBER) { name_var_1 }
101+
other: name_var=NUMBER name_var=NAME { name_var_1 }
102+
"""
103+
generator = self.make_generator(source)
104+
parser = generator.prepare("example.gram")
105+
expected = self.emit_parser(parser)
106+
107+
self.assertEqual(self.emit_parser(parser), expected)
108+
self.assertEqual(generator.prepare("example.gram"), parser)
109+
other = self.make_generator(source).prepare("example.gram")
110+
self.assertEqual(other, parser)
111+
self.assertEqual(self.emit_parser(other), expected)
112+
self.assertEqual(expected.count("expr_ty name_var_1;"), 2)
113+
self.assertEqual(expected.count("_res = name_var_1;"), 2)
114+
self.assertNotIn("name_var_2", expected)
115+
116+
def test_prepared_prefixes_preserve_reuse_and_repeatability(self):
117+
generator = self.make_generator("""
118+
start: prefix ':' NAME | prefix ':' NUMBER | NAME | prefix '=' NAME
119+
prefix[expr_ty] (memo): NAME
120+
""")
121+
parser = generator.prepare("example.gram")
122+
start = parser.rules[0]
123+
prefix, = start.prefixes
124+
self.assertEqual(prefix.type, "expr_ty")
125+
for alt in start.alternatives[:2]:
126+
self.assertIn("!p->call_invalid_rules", alt.calls[0].expression())
127+
self.assertIn(prefix.result, alt.calls[0].expression())
128+
self.assertEqual(start.alternatives[3].calls[0].expression(), "prefix_rule(p)")
129+
self.assertEqual(generator.prepare("example.gram"), parser)
130+
expected = self.emit_parser(parser)
131+
generator.rules.clear()
132+
generator.all_rules.clear()
133+
self.assertEqual(self.emit_parser(parser), expected)
134+
135+
def test_nullable_prefix_is_not_reused(self):
136+
generator = self.make_generator("""
137+
start: prefix ':' NAME | prefix ':' NUMBER
138+
prefix (memo): NAME?
139+
""")
140+
start = generator.prepare("example.gram").rules[0]
141+
self.assertEqual(start.prefixes, ())
142+
for alt in start.alternatives:
143+
self.assertEqual(alt.calls[0].expression(), "prefix_rule(p)")
144+
145+
def test_invalid_trailer_fails_before_output(self):
146+
generator = self.make_generator("""
147+
@trailer '%(missing)s'
148+
start: NAME ENDMARKER
149+
""")
150+
with self.assertRaisesRegex(KeyError, "missing"):
151+
generator.generate("example.gram")
152+
self.assertEqual(generator.file.getvalue(), "")
153+
154+
def test_empty_keyword_tables(self):
155+
parser = self.make_generator("start: NAME ENDMARKER\n").prepare("example.gram")
156+
source = self.emit_parser(parser)
157+
158+
self.assertEqual(parser.keyword_groups, ())
159+
self.assertEqual(parser.soft_keywords, ())
160+
self.assertIn("static const int n_keyword_lists = 0;", source)
161+
self.assertIn(
162+
"static KeywordToken *reserved_keywords[] = {\n"
163+
" (KeywordToken[]) {{NULL, -1}},\n"
164+
"};",
165+
source,
166+
)
167+
self.assertIn("static char *soft_keywords[] = {\n NULL,\n};", source)
168+
169+
def test_lowering_rejects_undiscovered_items(self):
170+
for replacement in ("missing", "(NAME NUMBER)", None):
171+
with self.subTest(replacement=replacement):
172+
generator = self.make_generator("start: NAME ENDMARKER\n")
173+
generator.collect_rules()
174+
lowerer = generator.callmakervisitor.make_lowerer()
175+
inventory = tuple(generator.all_rules)
176+
counter = generator.counter
177+
rule = generator.rules["start"]
178+
items = rule.rhs.alts[0].items
179+
if replacement is None:
180+
items[0] = NamedItem(None, items[0].item)
181+
else:
182+
grammar = parse_string(f"start: {replacement}\n", GrammarParser)
183+
items[0].item = grammar.rules["start"].rhs.alts[0].items[0].item
184+
with self.assertRaisesRegex(RuntimeError, "not discovered"):
185+
lowerer.prepare_rule(rule)
186+
self.assertEqual(tuple(generator.all_rules), inventory)
187+
self.assertEqual(generator.counter, counter)
188+
189+
def test_helper_resolution_does_not_depend_on_display_settings(self):
190+
source = """
191+
start: NAME (a=NAME { a }) NAME* NAME+ ','.NAME+ ENDMARKER
192+
"""
193+
for simple in (True, False):
194+
with self.subTest(simple=simple), mock.patch.object(
195+
grammar_module, "SIMPLE_STR", simple
196+
):
197+
generator = self.make_generator(source)
198+
expected = generator.prepare("example.gram").rules
199+
with mock.patch.object(grammar_module, "SIMPLE_STR", not simple):
200+
actual = generator.prepare("example.gram").rules
201+
self.assertEqual(len(actual), len(expected))
202+
for old, new in zip(expected, actual):
203+
self.assertEqual(old.signature, new.signature)
204+
self.assertEqual(
205+
[alt.calls for alt in old.alternatives],
206+
[alt.calls for alt in new.alternatives],
207+
)
208+
209+
def test_invalid_rule_gating_uses_references(self):
210+
cases = (
211+
("invalid_example", True),
212+
("value=invalid_example", True),
213+
("[invalid_example]", True),
214+
("invalid_example?", True),
215+
("invalid_example*", True),
216+
("invalid_example+", True),
217+
("invalid_example.NAME+", True),
218+
("[invalid_example.NAME+]", True),
219+
("[invalid_example.(NAME NAME)+]", False),
220+
("[[invalid_example.(NAME NAME)+]]", False),
221+
("[invalid_example.(NAME | NUMBER)+]", False),
222+
("&invalid_example", False),
223+
("[invalid_example | NAME]", False),
224+
("invalid_name=NAME", False),
225+
)
226+
for item, requires_invalid_rules in cases:
227+
for simple in (True, False):
228+
with self.subTest(item=item, simple=simple), mock.patch.object(
229+
grammar_module, "SIMPLE_STR", simple
230+
):
231+
generator = self.make_generator(f"""
232+
start: {item} {{ _PyPegen_dummy_name(p) }}
233+
invalid_example: NAME
234+
""")
235+
start = generator.prepare("example.gram").rules[0]
236+
self.assertEqual(
237+
start.alternatives[0].requires_invalid_rules,
238+
requires_invalid_rules,
239+
)
240+
241+
def test_lowering_preserves_legacy_named_call_types(self):
242+
generator = self.make_generator("""
243+
start: Mixed LPAR ENDMARKER
244+
Mixed[expr_ty]: NAME
245+
""")
246+
start, mixed = generator.prepare("example.gram").rules
247+
self.assertEqual(mixed.signature.return_type, "expr_ty")
248+
for call, name in zip(start.alternatives[0].calls, ("Mixed", "LPAR")):
249+
with self.subTest(name=name):
250+
self.assertEqual(call.function, f"{name}_rule")
251+
self.assertIsNone(call.return_type)
252+
253+
def test_lowering_snapshots_symbols_and_tokens(self):
254+
grammar = parse_string("""
255+
start: 'pass' '+' atom ENDMARKER
256+
atom[expr_ty]: NAME
257+
""", GrammarParser)
258+
exact_tokens = dict(EXACT_TOKENS)
259+
non_exact_tokens = set(NON_EXACT_TOKENS)
260+
generator = CParserGenerator(
261+
grammar, ALL_TOKENS, exact_tokens, non_exact_tokens, io.StringIO()
262+
)
263+
generator.collect_rules()
264+
lowerer = generator.callmakervisitor.make_lowerer()
265+
start = generator.rules["start"]
266+
atom = generator.rules["atom"]
267+
expected = lowerer.prepare_rule(start)
268+
269+
atom.type = "stmt_ty"
270+
generator.all_rules.clear()
271+
generator.tokens.clear()
272+
generator.keywords.clear()
273+
exact_tokens.clear()
274+
non_exact_tokens.clear()
275+
276+
self.assertEqual(lowerer.prepare_rule(atom).signature.return_type, "expr_ty")
277+
self.assertEqual(lowerer.prepare_rule(start), expected)
278+
279+
280+
if __name__ == "__main__":
281+
unittest.main()

0 commit comments

Comments
 (0)