Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ named_expression[expr_ty]:
| invalid_named_expression
| expression !':='

disjunction[expr_ty] (memo):
disjunction[expr_ty] (memo, fastpath=_PyPegen_atom_fast_path):
| a=conjunction b=('or' c=conjunction { c })+ { _PyAST_BoolOp(
Or,
CHECK(asdl_expr_seq*, _PyPegen_seq_insert_in_front(p, a, b)),
Expand All @@ -775,7 +775,7 @@ conjunction[expr_ty] (memo):
EXTRA) }
| inversion

inversion[expr_ty] (memo):
inversion[expr_ty] (memo, fastpath=_PyPegen_atom_fast_path):
| 'not' a=inversion { _PyAST_UnaryOp(Not, a, EXTRA) }
| comparison

Expand Down
45 changes: 45 additions & 0 deletions InternalDocs/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,51 @@ in the generated C parse code that allows to measure how much each rule uses
memoization (check the [`Parser/pegen.c`](../Parser/pegen.c)
file for more information) but it needs to be manually activated.

Fast-path hooks
---------------

Some rules are entered so often that even the bookkeeping of trying their
alternatives is expensive. Consider parsing the argument ``a`` in a call
like ``f(a, b)``: the argument is a full expression, so the parser descends
the whole operator precedence chain

```
expression -> disjunction -> conjunction -> inversion -> comparison
-> bitwise_or -> bitwise_xor -> bitwise_and -> shift_expr -> sum
-> term -> factor -> power -> await_primary -> primary -> atom
```

before it can produce the ``Name`` node for ``a``: around fourteen rule
invocations, each paying its own C-stack check and memoization lookups, to
consume a single token. But since the following token is ``,``, which
cannot continue any binary or postfix expression, that outcome is already
known after peeking at two tokens.

For cases like this a rule can declare a hand-written C hook with the
``fastpath`` flag, next to where ``memo`` goes:

```
disjunction[expr_ty] (memo, fastpath=_PyPegen_atom_fast_path):
```

The generator calls the hook on rule entry, before any alternative is tried:

```c
if (_PyPegen_atom_fast_path(p, &_res)) {
p->level--;
return _res;
}
```

The hook receives the parser and a pointer to the rule's result variable and
returns 1 if it handled the parse (storing its result, which can be ``NULL``
to signal failure with ``p->error_indicator`` set) or 0 to fall through to
the rule's normal alternatives. The generator knows nothing about what the
hook does: all parsing logic lives in the hook itself, next to the other
token helpers in [`Parser/pegen.c`](../Parser/pegen.c). A hook must behave
exactly like the rule it accelerates, including which tokens it fills,
because error reporting depends on the number of tokens read (``p->fill``).

Automatic variables
-------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Speed up parsing of simple expressions by recognizing single-token atoms
without descending the full precedence rule chain.
8 changes: 8 additions & 0 deletions Parser/parser.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions Parser/pegen.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,52 @@ _resize_tokens_array(Parser *p) {
return 0;
}

// Fast path attached to the expression precedence chain's entry rules via
// the (fastpath=function) rule flag in the grammar. A bare NAME/NUMBER
// atom followed by a token that cannot start or continue a binary/postfix
// expression is a complete expression, so it is built directly instead of
// descending the whole chain. Returns 1 if it produced a result (stored
// in *result), 0 to fall through to the rule's alternatives. The second
// token is only examined when the first is NAME/NUMBER: the chain fills
// it too in that case, so error reporting (which keys off p->fill)
// observes an identical token fill state.
int
_PyPegen_atom_fast_path(Parser *p, void *result)
{
if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) {
p->error_indicator = 1;
*(void **)result = NULL;
return 1;
}
Token *t = p->tokens[p->mark];
if (t->type != NAME && t->type != NUMBER) {
return 0;
}
if (p->mark + 1 == p->fill && _PyPegen_fill_token(p) < 0) {
p->error_indicator = 1;
*(void **)result = NULL;
return 1;
}
switch (p->tokens[p->mark + 1]->type) {
case COMMA:
case RPAR:
case RSQB:
case RBRACE:
case COLON:
case NEWLINE:
case SEMI:
case EQUAL:
case ENDMARKER:
break;
default:
return 0;
}
expr_ty res = (t->type == NAME)
? _PyPegen_name_token(p) : _PyPegen_number_token(p);
*(void **)result = res;
return 1;
}

int
_PyPegen_fill_token(Parser *p)
{
Expand Down
1 change: 1 addition & 0 deletions Parser/pegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ expr_ty _PyPegen_soft_keyword_token(Parser *p);
expr_ty _PyPegen_fstring_middle_token(Parser* p);
Token *_PyPegen_get_last_nonnwhitespace_token(Parser *);
int _PyPegen_fill_token(Parser *p);
int _PyPegen_atom_fast_path(Parser *p, void *result);
expr_ty _PyPegen_name_token(Parser *p);
expr_ty _PyPegen_number_token(Parser *p);
void *_PyPegen_string_token(Parser *p);
Expand Down
34 changes: 34 additions & 0 deletions Tools/peg_generator/pegen/c_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,13 +598,47 @@ def _set_up_rule_memoization(self, node: Rule, result_type: str) -> None:
def _should_memoize(self, node: Rule) -> bool:
return "memo" in node.flags and not node.left_recursive

def _rule_fast_path(self, node: Rule) -> str | None:
"""Return the C fast-path hook declared by a (fastpath=<function>) rule flag.

The hook is called on rule entry and returns 1 if it handled the
parse (storing its result), 0 to fall through to the rule's
alternatives. For example, given

disjunction[expr_ty] (memo, fastpath=_PyPegen_atom_fast_path):

the disjunction rule body starts with

if (_PyPegen_atom_fast_path(p, &_res)) {
p->level--;
return _res;
}

See "Fast-path hooks" in InternalDocs/parser.md.
"""
for flag in node.flags:
name, _, func = flag.partition("=")
if name == "fastpath":
if not func:
raise ValueError(
f"rule {node.name!r}: fastpath flag needs a function"
)
return func
return None

def _handle_default_rule_body(self, node: Rule, rhs: Rhs, result_type: str) -> None:
memoize = self._should_memoize(node)

with self.indent():
self.add_level()
self._check_for_errors()
self.print(f"{result_type} _res = NULL;")
fastpath = self._rule_fast_path(node)
if fastpath:
self.print(f"if ({fastpath}(p, &_res)) {{")
with self.indent():
self.add_return("_res")
self.print("}")
if memoize:
self.print(f"if (_PyPegen_is_memoized(p, {node.name}_type, &_res)) {{")
with self.indent():
Expand Down
11 changes: 10 additions & 1 deletion Tools/peg_generator/pegen/grammar_parser.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Tools/peg_generator/pegen/metagrammar.gram
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ flags[frozenset[str]]:
| '(' a=','.flag+ ')' { frozenset(a) }

flag[str]:
| a=NAME '=' b=NAME { a.string + "=" + b.string }
| NAME { name.string }

alts[Rhs]:
Expand Down
Loading