Shadow the parser visitors, ExprPrinter and ClassStatementsGatherer natively - #6511
Merged
Merged
Conversation
ondrejmirtes
force-pushed
the
turbo-native-parser-visitors
branch
from
September 22, 2026 09:15
9598016 to
562f4bb
Compare
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
force-pushed
the
turbo-native-parser-visitors
branch
from
September 22, 2026 09:24
562f4bb to
4c761e1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Shadows twenty parser visitors,
ExprPrinterandClassStatementsGatherernatively. 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 underPhpParser\NodeTraverserin a self-analysis run, which is 46.5% of all crossings. Most of these bodies only type-check the node and return null.src/Parser/are declared under their real names at activation, like any other shadowed class, so a direct$visitor->enterNode($node)still works.pt_native_visitorentry. 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.enterNode()/leaveNode()always return null and whosebeforeTraverse()only resets state qualify, so the fast path doesn't have to duplicate the traverser's return-value protocol.GotoLabelVisitor,AnonymousClassVisitor,PipeTransformerVisitor,StandaloneThrowExprVisitorandUseAliasVisitorstay in PHP.tests/parser-visitors.phptraverses 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
Variablefast 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 toPrinter::prettyPrintExpr(). The node-key path insupport.cppused to take both fast paths and then call the twin'sprintExpr(), which re-took them; it now calls the miss half directly, which removes that PHP frame for every expression key the analyser prints.ClassStatementsGatherer
ClassLikeHandlerhands 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 newMutatingScopedirect entries (isInExpressionAssign,isInTrait,isInAnonymousFunction,getFunction) and the existingScopeContextandClassReflectionones, 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.phpreplays 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