Skip to content

Commit f3cc7e4

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. The rules are marked in the grammar with the new @fastpath meta, which attaches a hand-written entry hook to a rule; the generator knows nothing about tokens or expressions.
1 parent 106eb53 commit f3cc7e4

6 files changed

Lines changed: 83 additions & 0 deletions

File tree

Grammar/python.gram

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

3+
@fastpath_disjunction _PyPegen_atom_fast_path
4+
@fastpath_inversion _PyPegen_atom_fast_path
5+
36
@trailer '''
47
void *
58
_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: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Parser/pegen.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,52 @@ _resize_tokens_array(Parser *p) {
241241
return 0;
242242
}
243243

244+
// Fast path attached to the expression precedence chain's entry rules via
245+
// the @fastpath meta in the grammar. A bare NAME/NUMBER atom followed by a
246+
// token that cannot start or continue a binary/postfix expression is a
247+
// complete expression, so it is built directly instead of descending the
248+
// whole chain. Returns 1 if it produced a result (stored in *result), 0 to
249+
// fall through to the rule's alternatives. The second token is only
250+
// examined when the first is NAME/NUMBER: the precedence chain fills it
251+
// too in that case, so error reporting (which keys off p->fill) observes
252+
// an identical token fill state.
253+
int
254+
_PyPegen_atom_fast_path(Parser *p, void *result)
255+
{
256+
if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) {
257+
p->error_indicator = 1;
258+
*(void **)result = NULL;
259+
return 1;
260+
}
261+
Token *t = p->tokens[p->mark];
262+
if (t->type != NAME && t->type != NUMBER) {
263+
return 0;
264+
}
265+
if (p->mark + 1 == p->fill && _PyPegen_fill_token(p) < 0) {
266+
p->error_indicator = 1;
267+
*(void **)result = NULL;
268+
return 1;
269+
}
270+
switch (p->tokens[p->mark + 1]->type) {
271+
case COMMA:
272+
case RPAR:
273+
case RSQB:
274+
case RBRACE:
275+
case COLON:
276+
case NEWLINE:
277+
case SEMI:
278+
case EQUAL:
279+
case ENDMARKER:
280+
break;
281+
default:
282+
return 0;
283+
}
284+
expr_ty res = (t->type == NAME)
285+
? _PyPegen_name_token(p) : _PyPegen_number_token(p);
286+
*(void **)result = res;
287+
return 1;
288+
}
289+
244290
int
245291
_PyPegen_fill_token(Parser *p)
246292
{

Parser/pegen.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ 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+
int _PyPegen_atom_fast_path(Parser *p, void *result);
162163
expr_ty _PyPegen_name_token(Parser *p);
163164
expr_ty _PyPegen_number_token(Parser *p);
164165
void *_PyPegen_string_token(Parser *p);

Tools/peg_generator/pegen/c_generator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,13 +598,36 @@ 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 _rule_fast_paths(self) -> dict:
602+
# A "@fastpath_<rule> <function>" meta attaches a hand-written C
603+
# hook to a rule, one meta per rule. The hook is called on rule
604+
# entry and returns 1 if it handled the parse (storing its result),
605+
# 0 to fall through to the rule's alternatives.
606+
hooks = {}
607+
for key, func in self.grammar.metas.items():
608+
if not key.startswith("fastpath_"):
609+
continue
610+
rulename = key[len("fastpath_"):]
611+
if rulename not in self.rules:
612+
raise ValueError(f"@{key} names unknown rule {rulename!r}")
613+
if not func:
614+
raise ValueError(f"@{key} is missing its hook function")
615+
hooks[rulename] = func
616+
return hooks
617+
601618
def _handle_default_rule_body(self, node: Rule, rhs: Rhs, result_type: str) -> None:
602619
memoize = self._should_memoize(node)
603620

604621
with self.indent():
605622
self.add_level()
606623
self._check_for_errors()
607624
self.print(f"{result_type} _res = NULL;")
625+
fastpath = self._rule_fast_paths().get(node.name)
626+
if fastpath:
627+
self.print(f"if ({fastpath}(p, &_res)) {{")
628+
with self.indent():
629+
self.add_return("_res")
630+
self.print("}")
608631
if memoize:
609632
self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, &_res)) {{")
610633
with self.indent():

0 commit comments

Comments
 (0)