Skip to content

Commit e1fe1fb

Browse files
committed
gh-138970: Add general metadata system to the peg generator
1 parent 32e1e06 commit e1fe1fb

File tree

5 files changed

+215
-23
lines changed

5 files changed

+215
-23
lines changed

Lib/test/test_peg_generator/test_pegen.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,3 +1106,66 @@ def test_deep_nested_rule(self) -> None:
11061106
)
11071107

11081108
self.assertEqual(output, expected_output)
1109+
1110+
def test_rule_flags(self) -> None:
1111+
"""Test the new rule flags syntax that accepts arbitrary lists of flags."""
1112+
# Test grammar with various flag combinations
1113+
grammar_source = """
1114+
start: simple_rule
1115+
1116+
simple_rule (memo):
1117+
| "hello"
1118+
1119+
multi_flag_rule (memo, custom, test):
1120+
| "world"
1121+
1122+
single_custom_flag (custom):
1123+
| "test"
1124+
1125+
no_flags_rule:
1126+
| "plain"
1127+
"""
1128+
1129+
grammar: Grammar = parse_string(grammar_source, GrammarParser)
1130+
rules = grammar.rules
1131+
1132+
# Test memo-only rule
1133+
simple_rule = rules['simple_rule']
1134+
self.assertTrue(simple_rule.memo, "simple_rule should have memo=True")
1135+
self.assertEqual(simple_rule.flags, frozenset(['memo']),
1136+
f"simple_rule flags should be ['memo'], got {simple_rule.flags}")
1137+
1138+
# Test multi-flag rule
1139+
multi_flag_rule = rules['multi_flag_rule']
1140+
self.assertTrue(multi_flag_rule.memo, "multi_flag_rule should have memo=True")
1141+
self.assertEqual(multi_flag_rule.flags, frozenset({'memo', 'custom', 'test'}),
1142+
f"multi_flag_rule flags should contain memo, custom, test, got {multi_flag_rule.flags}")
1143+
1144+
# Test single custom flag rule
1145+
single_custom_rule = rules['single_custom_flag']
1146+
self.assertFalse(single_custom_rule.memo, "single_custom_flag should have memo=False")
1147+
self.assertEqual(single_custom_rule.flags, frozenset(['custom']),
1148+
f"single_custom_flag flags should be ['custom'], got {single_custom_rule.flags}")
1149+
1150+
# Test no flags rule
1151+
no_flags_rule = rules['no_flags_rule']
1152+
self.assertFalse(no_flags_rule.memo, "no_flags_rule should have memo=False")
1153+
self.assertEqual(no_flags_rule.flags, [],
1154+
f"no_flags_rule flags should be [], got {no_flags_rule.flags}")
1155+
1156+
def test_memo(self) -> None:
1157+
"""Test that the old (memo) syntax works with the flag system"""
1158+
grammar_source = """
1159+
start: memo_rule
1160+
1161+
memo_rule (memo):
1162+
| "test"
1163+
"""
1164+
1165+
grammar: Grammar = parse_string(grammar_source, GrammarParser)
1166+
rules = grammar.rules
1167+
1168+
memo_rule = rules['memo_rule']
1169+
self.assertTrue(memo_rule.memo, "memo_rule should have memo=True")
1170+
self.assertEqual(memo_rule.flags, frozenset(['memo']),
1171+
f"memo_rule flags should be ['memo'], got {memo_rule.flags}")

Tools/peg_generator/pegen/grammar.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,19 @@ def __iter__(self) -> Iterator[Rule]:
5858

5959

6060
class Rule:
61-
def __init__(self, name: str, type: str | None, rhs: Rhs, memo: object | None = None):
61+
def __init__(self, name: str, type: str | None, rhs: Rhs, memo: object | None = None, flags: list[str] | None = None):
6262
self.name = name
6363
self.type = type
6464
self.rhs = rhs
65-
self.memo = bool(memo)
65+
self.flags = flags or []
6666
self.left_recursive = False
6767
self.leader = False
6868

69+
@property
70+
def memo(self) -> bool:
71+
"""Check if this rule should be memoized by looking for 'memo' in flags."""
72+
return "memo" in self.flags
73+
6974
def is_loop(self) -> bool:
7075
return self.name.startswith("_loop")
7176

Tools/peg_generator/pegen/grammar_parser.py

Lines changed: 54 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/peg_generator/pegen/metagrammar.gram

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,21 @@ rules[RuleList]:
5050
| rule { [rule] }
5151

5252
rule[Rule]:
53-
| rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT {
54-
Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt) }
55-
| rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT {
56-
Rule(rulename[0], rulename[1], more_alts, memo=opt) }
57-
| rulename memoflag? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, memo=opt) }
53+
| rulename flags? ":" alts NEWLINE INDENT more_alts DEDENT {
54+
Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), flags=opt) }
55+
| rulename flags? ":" NEWLINE INDENT more_alts DEDENT {
56+
Rule(rulename[0], rulename[1], more_alts, flags=opt) }
57+
| rulename flags? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, flags=opt) }
5858

5959
rulename[RuleName]:
6060
| NAME annotation { (name.string, annotation) }
6161
| NAME { (name.string, None) }
6262

63-
# In the future this may return something more complicated
64-
memoflag[str]:
65-
| '(' "memo" ')' { "memo" }
63+
flags[list]:
64+
| '(' a=','.flag+ ')' { frozenset(a) }
65+
66+
flag[str]:
67+
| NAME { name.string }
6668

6769
alts[Rhs]:
6870
| alt "|" alts { Rhs([alt] + alts.alts)}

Tools/peg_generator/test_flags.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import tempfile
5+
import os
6+
7+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pegen'))
8+
9+
from pegen.build import build_python_parser_and_generator
10+
11+
def test_new_flags_syntax():
12+
"""Test that the new flags syntax works correctly"""
13+
14+
# Test grammar with various flag combinations
15+
test_grammar = '''
16+
start[str]: simple_rule
17+
18+
simple_rule (memo):
19+
| "hello" { "memo_only" }
20+
21+
multi_flag_rule (memo, custom, test):
22+
| "world" { "multiple_flags" }
23+
24+
single_custom_flag (custom):
25+
| "test" { "single_custom" }
26+
27+
no_flags_rule:
28+
| "plain" { "no_flags" }
29+
'''
30+
31+
# Write test grammar to temporary file
32+
with tempfile.NamedTemporaryFile(mode='w', suffix='.gram', delete=False) as f:
33+
f.write(test_grammar)
34+
grammar_file = f.name
35+
36+
try:
37+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
38+
output_file = f.name
39+
40+
# Parse the grammar
41+
grammar, parser, tokenizer, gen = build_python_parser_and_generator(
42+
grammar_file, output_file, verbose_parser=True
43+
)
44+
45+
# Check that rules have correct flags
46+
rules = grammar.rules
47+
48+
# Test memo-only rule
49+
simple_rule = rules['simple_rule']
50+
assert simple_rule.memo == True, "simple_rule should have memo=True"
51+
assert simple_rule.flags == ['memo'], f"simple_rule flags should be ['memo'], got {simple_rule.flags}"
52+
53+
# Test multi-flag rule
54+
multi_flag_rule = rules['multi_flag_rule']
55+
assert multi_flag_rule.memo == True, "multi_flag_rule should have memo=True"
56+
assert set(multi_flag_rule.flags) == {'memo', 'custom', 'test'}, f"multi_flag_rule flags should contain memo, custom, test, got {multi_flag_rule.flags}"
57+
58+
# Test single custom flag rule
59+
single_custom_rule = rules['single_custom_flag']
60+
assert single_custom_rule.memo == False, "single_custom_flag should have memo=False"
61+
assert single_custom_rule.flags == ['custom'], f"single_custom_flag flags should be ['custom'], got {single_custom_rule.flags}"
62+
63+
# Test no flags rule
64+
no_flags_rule = rules['no_flags_rule']
65+
assert no_flags_rule.memo == False, "no_flags_rule should have memo=False"
66+
assert no_flags_rule.flags == [], f"no_flags_rule flags should be [], got {no_flags_rule.flags}"
67+
68+
print("✅ All tests passed!")
69+
print(f"✅ simple_rule: memo={simple_rule.memo}, flags={simple_rule.flags}")
70+
print(f"✅ multi_flag_rule: memo={multi_flag_rule.memo}, flags={multi_flag_rule.flags}")
71+
print(f"✅ single_custom_flag: memo={single_custom_rule.memo}, flags={single_custom_rule.flags}")
72+
print(f"✅ no_flags_rule: memo={no_flags_rule.memo}, flags={no_flags_rule.flags}")
73+
74+
finally:
75+
# Clean up
76+
os.unlink(grammar_file)
77+
if os.path.exists(output_file):
78+
os.unlink(output_file)
79+
80+
if __name__ == '__main__':
81+
test_new_flags_syntax()

0 commit comments

Comments
 (0)