Skip to content

Commit f5d126f

Browse files
committed
gh-153568: Skip the expression precedence chain for single-token atoms
A bare name or number followed by a token that cannot extend an expression is now built directly, avoiding a dozen rule invocations per atom.
1 parent 106eb53 commit f5d126f

5 files changed

Lines changed: 105 additions & 0 deletions

File tree

Grammar/python.gram

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# PEG grammar for Python
22

3+
@atom_fastpath 'disjunction inversion'
4+
35
@trailer '''
46
void *
57
_PyPegen_parse(Parser *p)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up parsing of simple expressions by recognizing single-token atoms
2+
without descending the full precedence rule chain.

Parser/parser.c

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

Parser/pegen.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,27 @@ expr_ty _PyPegen_soft_keyword_token(Parser *p);
159159
expr_ty _PyPegen_fstring_middle_token(Parser* p);
160160
Token *_PyPegen_get_last_nonnwhitespace_token(Parser *);
161161
int _PyPegen_fill_token(Parser *p);
162+
163+
// Token types that cannot start or continue a binary/postfix expression:
164+
// a single-token atom followed by one of these is a complete expression.
165+
static inline int
166+
_PyPegen_atom_follow_token(int type)
167+
{
168+
switch (type) {
169+
case COMMA:
170+
case RPAR:
171+
case RSQB:
172+
case RBRACE:
173+
case COLON:
174+
case NEWLINE:
175+
case SEMI:
176+
case EQUAL:
177+
case ENDMARKER:
178+
return 1;
179+
default:
180+
return 0;
181+
}
182+
}
162183
expr_ty _PyPegen_name_token(Parser *p);
163184
expr_ty _PyPegen_number_token(Parser *p);
164185
void *_PyPegen_string_token(Parser *p);

Tools/peg_generator/pegen/c_generator.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,12 +598,54 @@ def _set_up_rule_memoization(self, node: Rule, result_type: str) -> None:
598598
def _should_memoize(self, node: Rule) -> bool:
599599
return "memo" in node.flags and not node.left_recursive
600600

601+
def _atom_fast_path_rules(self) -> frozenset:
602+
# Rules marked with the @atom_fastpath meta in the grammar: pure
603+
# entry points into the expression precedence chain, for which a
604+
# bare NAME/NUMBER atom followed by a token that cannot start or
605+
# continue a binary/postfix expression parses to exactly that atom,
606+
# so a two-token fast path is emitted that builds it directly.
607+
meta = self.grammar.metas.get("atom_fastpath")
608+
rules = frozenset(meta.split()) if meta else frozenset()
609+
for rulename in rules:
610+
if rulename not in self.rules:
611+
raise ValueError(
612+
f"@atom_fastpath names unknown rule {rulename!r}"
613+
)
614+
return rules
615+
616+
def _emit_atom_fast_path(self) -> None:
617+
# Only look one token ahead when the current token is NAME/NUMBER:
618+
# the tower fills it too in that case, so error reporting (which
619+
# keys off p->fill) is unperturbed.
620+
self.print("if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) {")
621+
with self.indent():
622+
self.print("p->error_indicator = 1;")
623+
self.add_return("NULL")
624+
self.print("}")
625+
self.print("Token *_fast_tok = p->tokens[p->mark];")
626+
self.print("if (_fast_tok->type == NAME || _fast_tok->type == NUMBER) {")
627+
with self.indent():
628+
self.print("if (p->mark + 1 == p->fill && _PyPegen_fill_token(p) < 0) {")
629+
with self.indent():
630+
self.print("p->error_indicator = 1;")
631+
self.add_return("NULL")
632+
self.print("}")
633+
self.print("if (_PyPegen_atom_follow_token(p->tokens[p->mark + 1]->type)) {")
634+
with self.indent():
635+
self.print("expr_ty _fast_res = (_fast_tok->type == NAME)")
636+
self.print(" ? _PyPegen_name_token(p) : _PyPegen_number_token(p);")
637+
self.add_return("_fast_res")
638+
self.print("}")
639+
self.print("}")
640+
601641
def _handle_default_rule_body(self, node: Rule, rhs: Rhs, result_type: str) -> None:
602642
memoize = self._should_memoize(node)
603643

604644
with self.indent():
605645
self.add_level()
606646
self._check_for_errors()
647+
if node.name in self._atom_fast_path_rules():
648+
self._emit_atom_fast_path()
607649
self.print(f"{result_type} _res = NULL;")
608650
if memoize:
609651
self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, &_res)) {{")

0 commit comments

Comments
 (0)