Skip to content

Commit afe3d53

Browse files
committed
gh-153568: Reuse shared prefixes in generated parser rules
1 parent fd0970c commit afe3d53

6 files changed

Lines changed: 262 additions & 69 deletions

File tree

InternalDocs/parser.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,23 @@ in the generated C parse code that allows to measure how much each rule uses
563563
memoization (check the [`Parser/pegen.c`](../Parser/pegen.c)
564564
file for more information) but it needs to be manually activated.
565565

566+
The C generator also reuses memoized prefixes within consecutive alternatives.
567+
For example, in `prefix ':' NAME | prefix ':' NUMBER`, failure after the first
568+
`':'` normally requires another call to `prefix` and another memo lookup. The
569+
generated code can keep the result and ending position in local variables and
570+
reuse them when trying the next alternative.
571+
572+
This applies only when the shared first item is a memoized rule, including a
573+
left-recursion leader, that the generator can prove consumes input on success.
574+
The locals are reset on each rule-body invocation, including each seed-growing
575+
iteration. Alternative order, cuts, and suffix backtracking are preserved. When
576+
`call_invalid_rules` is enabled, the generated code uses the original rule calls.
577+
578+
The consumption analysis follows grammar items; it cannot inspect arbitrary C
579+
actions. As with memoization, actions must not invalidate cached results. In
580+
particular, suffix actions must not move the parser before their starting mark,
581+
rewrite buffered input, or replace memo entries for earlier positions.
582+
566583
Automatic variables
567584
-------------------
568585

Lib/test/test_peg_generator/test_c_parser.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,35 @@ def run_test(self, grammar_source, test_source):
145145
TEST_TEMPLATE.format(extension_path=self.tmp_path, test_source=test_source),
146146
)
147147

148+
def test_prefix_reuses_position(self) -> None:
149+
grammar_source = """
150+
start:
151+
| prefix ':' NAME NEWLINE? ENDMARKER
152+
| prefix ':' NUMBER NEWLINE? ENDMARKER
153+
| prefix '=' NUMBER NEWLINE? ENDMARKER
154+
prefix (memo): NAME NAME
155+
"""
156+
self.run_test(grammar_source, """
157+
self.check_input_strings_for_grammar(
158+
valid_cases=['one two : name', 'one two : 3', 'one two = 3'],
159+
invalid_cases=['one = 3', 'one two = name', 'one two :'],
160+
)
161+
""")
162+
163+
def test_prefix_respects_cut(self) -> None:
164+
grammar_source = """
165+
start:
166+
| prefix ':' ~ NAME NEWLINE? ENDMARKER
167+
| prefix ':' NUMBER NEWLINE? ENDMARKER
168+
prefix (memo): NAME NAME
169+
"""
170+
self.run_test(grammar_source, """
171+
self.check_input_strings_for_grammar(
172+
valid_cases=['one two : name'],
173+
invalid_cases=['one two : 3'],
174+
)
175+
""")
176+
148177
def test_c_parser(self) -> None:
149178
grammar_source = """
150179
start[mod_ty]: a[asdl_stmt_seq*]=stmt* $ { _PyAST_Module(a, NULL, p->arena) }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
3+
from test import test_tools
4+
5+
with test_tools.imports_under_tool("peg_generator"):
6+
from pegen.c_generator import consuming_rules
7+
from pegen.testutil import GrammarParser, parse_string
8+
9+
10+
class ConsumingRuleTests(unittest.TestCase):
11+
def test_predicates_cuts_and_nullable_repeats(self):
12+
grammar = parse_string("""
13+
start: NAME ENDMARKER
14+
positive: &NAME
15+
negative: !NAME
16+
cut: ~ { _PyPegen_dummy_name(p) }
17+
optional: [NAME]
18+
empty_repeat: NAME*
19+
nullable_repeat: optional+
20+
consuming_repeat: NAME+
21+
""", GrammarParser)
22+
self.assertEqual(consuming_rules(grammar.rules), {'start', 'consuming_repeat'})
23+
24+
def test_fixed_point_and_mixed_alternatives(self):
25+
grammar = parse_string("""
26+
start: expression ENDMARKER
27+
expression: expression '+' term | term
28+
term: atom
29+
atom: NAME | '(' expression ')'
30+
nullable: NAME | &NAME
31+
""", GrammarParser)
32+
self.assertEqual(consuming_rules(grammar.rules), {'start', 'expression', 'term', 'atom'})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up parsing by reusing memoized rule prefixes across consecutive grammar
2+
alternatives, avoiding repeated rule calls and cache lookups.

0 commit comments

Comments
 (0)