Skip to content

Generate the parser from doc/grammars with phplrt - #325

Closed
ondrejmirtes wants to merge 4 commits into
2.3.xfrom
phplrt-generated-parser
Closed

ondrejmirtes wants to merge 4 commits into
2.3.xfrom
phplrt-generated-parser

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Proof of concept: the parser is generated from the grammars in doc/grammars with phplrt instead of being written by hand. Meant for the next major version.

How it works

  • tools/phplrt/build.php (make grammars-build, PHP 8.4) compiles doc/grammars/phpdoc.pp3 with phplrt 4.2.4 and passes the compiled grammar to tools/phplrt/ParserGenerator.php.
  • The generator writes src/Parser/Generated/GeneratedParser.php, a PHP 7.4 class with one method per rule. Each method reads its rule exactly the way phplrt's RecursiveDescentTracer would with the same prediction tables, including which tokens are reported as expected on failure. Small rules are inlined, and lexemes and token classes become plain isset() checks.
  • AstBuilder extends the generated class. Its reduce* methods build the AST eagerly while parsing, instead of recording a trace and reducing it afterwards. Every rule exists twice, as a building method and a syntax-only one. Predicates, rules whose reducer doesn't read its children, and error explanations use the syntax-only method.
  • ConstExprParser, TypeParser and PhpDocParser are thin wrappers with the same constructors and parse*() methods as before.
  • CI checks that the committed generated parser is up to date.

Compatibility

  • The AST is unchanged; all existing tests pass. About a dozen expected ParserException details for invalid input changed (the token where the parser stopped, or the expected token); see the test diff.
  • These TypeParser public helpers are removed: parseGeneric(), parseTemplateTagValue(), isHtml() and enrichWithAttributes(). slevomat/coding-standard and phpstan-src only call parse().
  • New dependency ondrejmirtes/phplrt-runtime. With the generated parser, the runtime is only used for Phplrt\Parser\Context, which the reducers receive. Dropping the dependency entirely would be a small change.

Performance

Minimum of 4 runs, PHP 8.4 CLI without OPcache, lexing excluded:

Case Hand-written (2.3.x) phplrt runtime interpreter This PR (generated)
5,371 real PHPDocs from vendor/ 67 ms ~245 ms 108 ms
same, with all attributes 89 ms – 138 ms
/** @var int */ 4.6 µs ~15 µs 6.4 µs
@param array<string, int|null> $foo + @return void 22.6 µs ~67 µs 33.2 µs
200 lines of text 221 µs ~1000 µs 248 µs
200 @param tags 3.4 ms ~8 ms 4.0 ms
200 @return with array shapes and callables 128 ms ~62 ms 57 ms

The generated parser is 2–4× faster than running the grammar through the phplrt runtime, but on typical PHPDocs it is still about 1.5× slower than the hand-written parser. The exception is pathological input, where it is about 2× faster. What remains is generic value building: a value stack, merging child values and a reducer call per kept rule. No single hotspot is left.

ConstExprParser, TypeParser and PhpDocParser are no longer written by hand.
tools/phplrt/build.php compiles doc/grammars/phpdoc.pp3 with phplrt and writes
every rule of the compiled grammar down as a method of
src/Parser/Generated/GeneratedParser.php, which reads exactly what the phplrt
runtime would read with the same tables and reports the same failures. The
AST is built by the reducers of AstBuilder, which extends the generated class.

The public API of the three parsers is unchanged. Some details of exceptions
thrown for invalid input differ, and a few public helper methods of
TypeParser are removed (see UPGRADING.md).

Amp-Thread-ID: https://ampcode.com/threads/T-01a17820-94d1-75c8-9206-2e802d9c0556
Lexer::tokenize() now classifies the tokens for the generated parser while
lexing, so the parsers no longer rebuild the stream on every read.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17820-94d1-75c8-9206-2e802d9c0556
The reducers are given the first and last token index as two ints computed
inline, instead of a phplrt Context filled by a call per reduction. Attributes
are skipped without a call when none are used, and a reading no longer saves
and restores the state of a previous one.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17820-94d1-75c8-9206-2e802d9c0556
The generator leaves out token checks the surrounding code already
guarantees, and builds children with array_slice() now that no reducer
pushes a list onto the value stack.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17820-94d1-75c8-9206-2e802d9c0556
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant