Build closure and arrow function types from the single rule body walk - #6169
Open
ondrejmirtes wants to merge 5 commits into
Open
Build closure and arrow function types from the single rule body walk#6169ondrejmirtes wants to merge 5 commits into
ondrejmirtes wants to merge 5 commits into
Conversation
…sult processClosureNode() already sees every return, yield and execution end of the closure body on the scope they occur on. Record the statement-scope pairs and carry them - together with the impure points pre-merged in getClosureType() order - on ProcessClosureResult, so a closure type can later be built from the single walk instead of walking the body again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
buildClosureTypeForClosure()/buildClosureTypeForArrowFunction() construct a ClosureType from the return/yield statements and execution ends a prior body walk already gathered (reading their types off the gathered scopes, natively via the promoted flavour when asked), without walking the body again. getClosureType() - the walking resolver - now shares their whole tail: parameter building (defaults priced through InitializerExprTypeResolver), the never/void execution-end derivation, the Generator return shape, and the final assembly with its cache write. array_map() callbacks and immediately invoked closures keep re-walking (their parameters are typed from the call site, so a foreign walk's gathered scopes would answer wrongly). No caller feeds the builders yet; cache keys and cache participation are unchanged (arrows still price fresh on every ask). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
ClosureHandler::processExpr() walks the closure body for the rules; the lazy getClosureType() pricing then walked it again. The handler now seeds the per-node type cache from the gathered walk data, so the pricing asks answer without a second walk. The cache key becomes flavour- and parameter-aware (closureContextCacheKey), and the native-flavour slot is seeded with the same build: a closure's native type equals its phpdoc type, which until now only held when the promoted scope's holder hash happened to collide with the plain one - the pin is now deliberate and collision-independent. array_map() callbacks and immediately invoked closures seed through a getClosureType() walk with their call-site parameter types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
processArrowFunctionNode() now returns ProcessArrowFunctionResult: the walked arrow scope plus the throw points, impure points (merged in getClosureType() order) and invalidate expressions its type needs, gathered during the single body walk. ArrowFunctionHandler seeds the per-node type cache with both flavours built from the walk - unlike a closure, an arrow function's native return type genuinely differs from its phpdoc one, so the native slot gets its own build reading the walked body's native types. Arrow functions now participate in the cache read: the flavour- and parameter-aware key answers each ask from its own seeded slot. Both seeding paths (this one and the closure one) are gated on the storage having no parked fibers: a parked fiber may still append to the walk's gathered data - the invalidate expressions of a write like $this->prop[] = ... arrive only when the fiber flushes - so a seeded entry could be incomplete. When anything is parked, the lazy ask keeps re-walking with the fibers flushed, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
processArgs() walks closure and arrow function arguments itself; the reads that follow - the preferred ClosureType invalidate-expression read for closures and the invalidation read for arrow functions - then priced the argument through another body walk. Seeding the type cache from the walk just performed makes those reads cache hits. The reads themselves are untouched: when a parked fiber may still complete the gathered data, the seed is skipped and they keep re-walking with the fibers flushed, preserving the completeness contract documented at the closure read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
Member
Author
|
Benchmark: commit-toggled ABBA, same tree, no-turbo,
Directionally a win with a visibly tighter spread on the PR side, but the pairing is washed out by machine noise at this n — I can rerun a longer series on a quiet machine if you want a firmer number. The structural claim (pricing asks hit the seeded cache instead of re-walking) is independent of the benchmark. |
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.
A closure or arrow function passed through the analyser had its body walked several times: once by
processClosureNode()/processArrowFunctionNode()for the rules, and again by everygetClosureType()pricing ask — the lazy handlerresolveType(), the invalidate-expression reads inprocessArgs(), and the native-flavour ask each re-walked unless a whole-scope cache key happened to collide. This PR makes the pricing asks answer from the rule walk:ProcessClosureResultcarries the gathered return/yield statement-scope pairs, execution ends, and impure points (pre-merged ingetClosureType()order); the newProcessArrowFunctionResultcarries the walked arrow scope and the type-relevant points.ClosureTypeResolvergainsbuildClosureTypeForClosure()/buildClosureTypeForArrowFunction(), which construct the sameClosureTypefrom the gathered data — reading the return/yield types off the gathered scopes, natively via the promoted flavour when asked — andgetClosureType()now shares their whole tail (parameter building with defaults priced throughInitializerExprTypeResolver, the never/void derivation, Generator shape, assembly + cache write).array_map()callbacks and immediately invoked closures keep walking ingetClosureType()because their parameters are typed from the call site.processArgs()argument branches seed the per-node type cache from the walk they just performed. The cache key becomes parameter-aware, and flavour-separated for arrow functions (whose native return type genuinely differs); a closure's native type equals its phpdoc type, so its native-flavour slot is seeded with the same build — making that equality deliberate where it previously depended on a scope-hash collision.ClosureTypefor exactly this reason). All seeding is gated on the storage having no parked fibers, and the two protected reads inprocessArgs()are textually untouched — they become cache hits in the common case and keep re-walking flushed when anything is parked.Zero expectation churn: full test suite green in both turbo modes (17844 tests), self-analysis and CS clean. No turbo-shadowed class is touched, so no extension bump.
Follow-up (separate PR): shallow closure/arrow scope entry (
enterAnonymousFunction()currently re-walks the body to build the reflection) with the refined-type swap forClosureReturnStatementsNode/InArrowFunctionNode, and the free-variable-slice cache key that makes seed hits robust against unrelated scope changes.🤖 Generated with Claude Code
https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7