Skip to content

Commit 7dec412

Browse files
committed
Route C generation through the prepared parser model
1 parent 084c70e commit 7dec412

4 files changed

Lines changed: 448 additions & 825 deletions

File tree

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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_invalid_trailer_fails_before_output(self):
117+
generator = self.make_generator("""
118+
@trailer '%(missing)s'
119+
start: NAME ENDMARKER
120+
""")
121+
with self.assertRaisesRegex(KeyError, "missing"):
122+
generator.generate("example.gram")
123+
self.assertEqual(generator.file.getvalue(), "")
124+
125+
def test_empty_keyword_tables(self):
126+
parser = self.make_generator("start: NAME ENDMARKER\n").prepare("example.gram")
127+
source = self.emit_parser(parser)
128+
129+
self.assertEqual(parser.keyword_groups, ())
130+
self.assertEqual(parser.soft_keywords, ())
131+
self.assertIn("static const int n_keyword_lists = 0;", source)
132+
self.assertIn(
133+
"static KeywordToken *reserved_keywords[] = {\n"
134+
" (KeywordToken[]) {{NULL, -1}},\n"
135+
"};",
136+
source,
137+
)
138+
self.assertIn("static char *soft_keywords[] = {\n NULL,\n};", source)
139+
140+
def test_lowering_rejects_undiscovered_items(self):
141+
for replacement in ("missing", "(NAME NUMBER)", None):
142+
with self.subTest(replacement=replacement):
143+
generator = self.make_generator("start: NAME ENDMARKER\n")
144+
generator.collect_rules()
145+
lowerer = generator.callmakervisitor.make_lowerer()
146+
inventory = tuple(generator.all_rules)
147+
counter = generator.counter
148+
rule = generator.rules["start"]
149+
items = rule.rhs.alts[0].items
150+
if replacement is None:
151+
items[0] = NamedItem(None, items[0].item)
152+
else:
153+
grammar = parse_string(f"start: {replacement}\n", GrammarParser)
154+
items[0].item = grammar.rules["start"].rhs.alts[0].items[0].item
155+
with self.assertRaisesRegex(RuntimeError, "not discovered"):
156+
lowerer.prepare_rule(rule)
157+
self.assertEqual(tuple(generator.all_rules), inventory)
158+
self.assertEqual(generator.counter, counter)
159+
160+
def test_helper_resolution_does_not_depend_on_display_settings(self):
161+
source = """
162+
start: NAME (a=NAME { a }) NAME* NAME+ ','.NAME+ ENDMARKER
163+
"""
164+
for simple in (True, False):
165+
with self.subTest(simple=simple), mock.patch.object(
166+
grammar_module, "SIMPLE_STR", simple
167+
):
168+
generator = self.make_generator(source)
169+
expected = generator.prepare("example.gram").rules
170+
with mock.patch.object(grammar_module, "SIMPLE_STR", not simple):
171+
actual = generator.prepare("example.gram").rules
172+
self.assertEqual(len(actual), len(expected))
173+
for old, new in zip(expected, actual):
174+
self.assertEqual(old.signature, new.signature)
175+
self.assertEqual(
176+
[alt.calls for alt in old.alternatives],
177+
[alt.calls for alt in new.alternatives],
178+
)
179+
180+
def test_invalid_rule_gating_uses_references(self):
181+
cases = (
182+
("invalid_example", True),
183+
("value=invalid_example", True),
184+
("[invalid_example]", True),
185+
("invalid_example?", True),
186+
("invalid_example*", True),
187+
("invalid_example+", True),
188+
("invalid_example.NAME+", True),
189+
("[invalid_example.NAME+]", True),
190+
("[invalid_example.(NAME NAME)+]", False),
191+
("[[invalid_example.(NAME NAME)+]]", False),
192+
("[invalid_example.(NAME | NUMBER)+]", False),
193+
("&invalid_example", False),
194+
("[invalid_example | NAME]", False),
195+
("invalid_name=NAME", False),
196+
)
197+
for item, requires_invalid_rules in cases:
198+
for simple in (True, False):
199+
with self.subTest(item=item, simple=simple), mock.patch.object(
200+
grammar_module, "SIMPLE_STR", simple
201+
):
202+
generator = self.make_generator(f"""
203+
start: {item} {{ _PyPegen_dummy_name(p) }}
204+
invalid_example: NAME
205+
""")
206+
start = generator.prepare("example.gram").rules[0]
207+
self.assertEqual(
208+
start.alternatives[0].requires_invalid_rules,
209+
requires_invalid_rules,
210+
)
211+
212+
def test_lowering_preserves_legacy_named_call_types(self):
213+
generator = self.make_generator("""
214+
start: Mixed LPAR ENDMARKER
215+
Mixed[expr_ty]: NAME
216+
""")
217+
start, mixed = generator.prepare("example.gram").rules
218+
self.assertEqual(mixed.signature.return_type, "expr_ty")
219+
for call, name in zip(start.alternatives[0].calls, ("Mixed", "LPAR")):
220+
with self.subTest(name=name):
221+
self.assertEqual(call.function, f"{name}_rule")
222+
self.assertIsNone(call.return_type)
223+
224+
def test_lowering_snapshots_symbols_and_tokens(self):
225+
grammar = parse_string("""
226+
start: 'pass' '+' atom ENDMARKER
227+
atom[expr_ty]: NAME
228+
""", GrammarParser)
229+
exact_tokens = dict(EXACT_TOKENS)
230+
non_exact_tokens = set(NON_EXACT_TOKENS)
231+
generator = CParserGenerator(
232+
grammar, ALL_TOKENS, exact_tokens, non_exact_tokens, io.StringIO()
233+
)
234+
generator.collect_rules()
235+
lowerer = generator.callmakervisitor.make_lowerer()
236+
start = generator.rules["start"]
237+
atom = generator.rules["atom"]
238+
expected = lowerer.prepare_rule(start)
239+
240+
atom.type = "stmt_ty"
241+
generator.all_rules.clear()
242+
generator.tokens.clear()
243+
generator.keywords.clear()
244+
exact_tokens.clear()
245+
non_exact_tokens.clear()
246+
247+
self.assertEqual(lowerer.prepare_rule(atom).signature.return_type, "expr_ty")
248+
self.assertEqual(lowerer.prepare_rule(start), expected)
249+
250+
251+
if __name__ == "__main__":
252+
unittest.main()

0 commit comments

Comments
 (0)