Skip to content

Shadow the parser visitors, ExprPrinter and ClassStatementsGatherer natively - #6511

Merged
ondrejmirtes merged 4 commits into
2.3.xfrom
turbo-native-parser-visitors
Sep 22, 2026
Merged

ondrejmirtes merged 4 commits into
2.3.xfrom
turbo-native-parser-visitors

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Shadows twenty parser visitors, ExprPrinter and ClassStatementsGatherer natively. Apart from the #[ShadowedByTurboExtension] / #[ReferencedByTurboExtension] attributes, the PHP twins are unchanged.

Parser visitors

The node traverser calls every registered visitor once per node. Visitor enterNode()/leaveNode() bodies accounted for 18.1M of the 19.26M native→PHP crossings under PhpParser\NodeTraverser in a self-analysis run, which is 46.5% of all crossings. Most of these bodies only type-check the node and return null.

  • Twenty visitors in src/Parser/ are declared under their real names at activation, like any other shadowed class, so a direct $visitor->enterNode($node) still works.
  • Each one also registers a pt_native_visitor entry. The native traverser's per-traverse visitor plan resolves it by class entry and calls the C++ function pointer instead of going through the engine. A visitor without an entry takes the engine path as before.
  • Only visitors whose enterNode()/leaveNode() always return null and whose beforeTraverse() only resets state qualify, so the fast path doesn't have to duplicate the traverser's return-value protocol. GotoLabelVisitor, AnonymousClassVisitor, PipeTransformerVisitor, StandaloneThrowExprVisitor and UseAliasVisitor stay in PHP.
  • tests/parser-visitors.php traverses crafted snippets and real source files four ways (PHP and native visitors, each under the PHP and the native traverser) and requires byte-identical serialized ASTs.

ExprPrinter

The Variable fast path, the read of the node's attribute cache and the cache write happen in C++. Only a genuine cache miss crosses into PHP, straight to Printer::prettyPrintExpr(). The node-key path in support.cpp used to take both fast paths and then call the twin's printExpr(), which re-took them; it now calls the miss half directly, which removes that PHP frame for every expression key the analyser prints.

ClassStatementsGatherer

ClassLikeHandler hands a gatherer to every class body walk as its node callback. The native gatherer forwards each node to the wrapped callback and collects natively. It queries the scope through new MutatingScope direct entries (isInExpressionAssign, isInTrait, isInAnonymousFunction, getFunction) and the existing ScopeContext and ClassReflection ones, so the ~2.7M scope and reflection calls a self-analysis makes from here stay native. The collected value objects are still the PHP classes, built through the class map (16 new keys).

tests/class-statements-gatherer.php replays the node stream of a real walk into both gatherers and compares everything they collect. Self-analysis A/B, 8 interleaved pairs: −1.76% user time, with byte-identical output.

🤖 Generated with Claude Code

https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN

@ondrejmirtes
ondrejmirtes force-pushed the turbo-native-parser-visitors branch from 9598016 to 562f4bb Compare September 22, 2026 09:15
ondrejmirtes and others added 4 commits September 22, 2026 11:24
The node traverser calls every registered visitor once per node, which made
visitor enterNode()/leaveNode() bodies 18.1M of the run's 19.26M native->PHP
crossings under PhpParser\NodeTraverser — 46.5% of all crossings, spread
over twenty classes that mostly type-check the node and return null.

Each ported visitor is declared under its real name at activation like any
other shadowed class, so a direct $visitor->enterNode($node) still works, and
additionally registers a pt_native_visitor entry: NodeTraverser's per-traverse
visitor plan resolves it by class entry and calls the C++ function pointer
instead of crossing into the engine. The entry is only an optimisation — a
NULL hook falls back to the engine call, and the PHP twin stays the reference
implementation.

Only visitors whose enterNode()/leaveNode() always return null and whose
beforeTraverse() only resets state qualify, so the traverser's return-value
protocol does not have to be duplicated in the fast path. GotoLabelVisitor,
AnonymousClassVisitor, PipeTransformerVisitor, StandaloneThrowExprVisitor and
UseAliasVisitor stay in PHP.

ParserVisitors.h holds what the bodies share: NodeProp, a monomorphic inline
cache of a subnode's property offset resolved against the pt_class() entry it
is declared on, plus php-parser's toLowerString() comparison and
CallLike::isFirstClassCallable()/getArgs().

tests/parser-visitors.php traverses crafted snippets and real source files
four ways — PHP and native visitors under the PHP and native traversers — and
requires byte-identical serialized ASTs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
PHPStan\Node\Printer\ExprPrinter is declared natively: the Variable fast
path, the node's attribute cache read and the cache write happen in C++,
and only a genuine cache miss crosses into PHP, straight to
Printer::prettyPrintExpr(). The pretty printer itself (PrettyPrinterAbstract
and its Standard subclass) stays PHP.

The node-key path in support.cpp already took the two fast paths itself and
then called the twin's printExpr(), which re-took them before printing;
pt_node_printed_expr() now calls the miss half directly, absorbing that PHP
frame for every expression key printed in the analyser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
ClassLikeHandler hands a gatherer to every class body walk as its node
callback. The native one forwards each node to the wrapped callback and
gathers natively: it asks the scope through new MutatingScope direct
entries (isInExpressionAssign, isInTrait, isInAnonymousFunction and
getFunction, identified by their now named handlers) and the existing
ScopeContext and ClassReflection ones, so the ~2.7M scope and reflection
calls a self-analysis makes from here stay native. The collected value
objects stay the PHP classes, built through the class map (16 new keys).

tests/class-statements-gatherer.php replays the node stream of a real walk
over its fixture into both gatherers and compares everything they collect.

Self-analysis A/B: -1.76% user time (8 interleaved pairs, t=-2.58),
byte-identical output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
@ondrejmirtes
ondrejmirtes force-pushed the turbo-native-parser-visitors branch from 562f4bb to 4c761e1 Compare September 22, 2026 09:24
@ondrejmirtes
ondrejmirtes merged commit 4c761e1 into 2.3.x Sep 22, 2026
1 check passed
@ondrejmirtes
ondrejmirtes deleted the turbo-native-parser-visitors branch September 22, 2026 09:24
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