Repository navigation
Conversation
A closure's body is walked on a scope with an empty function-call stack, and what the stack contributes to the closure's type when the caller passes no parameter types - the innermost parameter's callable type - is already in the key as the callable parameters. Keying on the stack as well made the same closure, typed the same way, miss the cache when it was asked from inside the call it is passed to and again from outside it, as an argument's own walk and the call's return type do. The contextFree flag takes the stack's remaining job: it tells apart a closure typed with no context at all from one typed by its caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
ClosureTypeResolvercaches a closure's type per closure node, under a key built from the scope's free-variable slice (MutatingScope::getClosureScopeCacheKey()) and the parameter types it is walked with (closureContextCacheKey()). The scope part also included the function-call stack: its depth and every frame's parameter type.That key part dates from fd31156 (2024). There, the stack was the only part of the key that stood for how the closure's parameters were typed. Since then,
closureContextCacheKey()has keyed on the callable parameter types themselves, and the stack no longer adds anything:enterAnonymousFunctionWithoutReflection()andenterArrowFunctionWithoutReflection()create it with[]. So nothing inside the body reads the outer stack.buildParametersAndAcceptors(). When the caller passes no parameter types, it takes the innermost frame's parameter type as the callable the closure is passed to. What that yields is the callable parameters, which the key already has.So the stack only made the same closure, typed the same way, miss the cache when it was asked from different places: from inside the call it is passed to (its argument's walk) and from outside it (the call's return type, rules asking for the argument). Each miss is another walk of the body.
This change leaves the stack out of the key. One distinction the stack still made is kept explicitly: a
/freemarker for a closure typed with no context at all ($contextFree, where the inferred signature is used). Without it, such a closure could share a key with one its caller typed with the same parameter types.Effect
Closure body walks (
closureTypeBodyWalks,PHPSTAN_TEMPLATE_CLAMP_STATS), with output identical before and after:src/tests/PHPStan/Analyser/nsrt/Wall time of the self-analysis did not move measurably: those 290 walks are a small share of it, and the machine was shared, so run-to-run spread (up to ±20%) was larger than any effect.
#6996 (array_reduce() carry) walks each callback once per fixed-point pass, and its return type extension asks for the same passes as its parameter closure type extension, from outside the call. With this change those asks hit the cache: 11 → 7 body walks per
array_reduce()call, and 36 → 15 for a reduce nested in a reduce.On a file of 150 functions with four
array_reduce()closures each (user CPU, 4 alternating pairs, without Turbo), #6996 alone takes 14.53 s and #6996 with this change 10.80 s (−26%).Test
AnalyserIntegrationTest::testClosureInGenericCallIsWalkedOncePerTyping()analyses a closure and an arrow function passed to a generic function, and expects 2 body walks. It fails without the change (4 walks) and passes with it.Turbo
MutatingScopeandClosureTypeResolverare shadowed, so the same change is inturbo-ext/src/MutatingScope.cpp(getClosureScopeCacheKey()) andturbo-ext/src/ClosureTypeResolver.cpp(closureContextCacheKey()and its three callers), withgenerated/ClosureTypeResolver.hregenerated.make build-turbo,tests/smoke.php: ALL OK.tests/signature-parity.php: OK.bin/side-by-side.php: all classes paired.--error-format=rawoutput is byte-identical with the extension on and off, for the self-analysis ofsrc/and fornsrt/.🤖 Generated with Claude Code