Use pairwise TypeCombinator::intersect folding for conditional expression holders to avoid exponential union distribution#5482
Open
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
Conversation
…ession holders to avoid exponential union distribution - Change N-ary `TypeCombinator::intersect(...$allHolderTypes)` to pairwise folding in `MutatingScope::filterBySpecifiedTypes()` when processing matched conditional expressions - The N-ary call caused exponential blowup via the distributive law (A & (B|C) -> (A&B)|(A&C)) when multiple holders had large UnionTypes (e.g. 24^5 = ~8M combinations with 5 holders of 24 constant strings) - Pairwise folding produces the same result but reduces each step to at most N*M comparisons, where M shrinks after each intersection - Regression introduced in 52704a4 (Pass 2: Supertype match for conditional expressions) which allowed more holders to match - Add regression test and benchmark for phpstan/phpstan#14475
3bb7c25 to
7241b96
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.
Summary
PHPStan hangs when analysing code with wildcard constant types (
Foo::CATEGORY_*) in array shapes combined with repeated equality checks that narrow the type. This was a regression introduced in 2.1.47 by commit 52704a4 which added "Pass 2: Supertype match" for conditional expression resolution.Changes
src/Analyser/MutatingScope.php(line ~3269): replaced the N-aryTypeCombinator::intersect(...array_map(..., $expressions))call with a pairwise fold that intersects holder types two at a timetests/PHPStan/Analyser/data/bug-14475.phpand integration test methodtestBug14475inAnalyserIntegrationTest.phptests/bench/data/bug-14475.phpAnalogous cases probed
TypeCombinator::intersect(...)call sites insrc/Analyser/— no other N-ary intersect calls with dynamically-sized arrays of potential UnionTypes were foundTypeCombinator::intersect($resultType, ...$accessories)calls at lines 4130/4183 in MutatingScope.php are bounded byMAX_ACCESSORIES_LIMITand not affectedintersect(...)calls insrc/Type/TypeUtils.php,src/Reflection/Type/IntersectionType*.php, etc. are bounded by PHP syntax or reflection constraintsRoot cause
When
filterBySpecifiedTypes()processes matched conditional expressions, it collects all matchingConditionalExpressionHolderobjects for each expression string. The Pass 2 supertype matching (introduced in 52704a4) caused more holders to match than Pass 1's exact matching.For the reproduction case (
$input['category']with 25 possible constant values and 5 equality checks), 5 holders matched, each containing a UnionType of ~24 constant strings (each missing a different narrowed value).The original code called
TypeCombinator::intersect(union_24a, union_24b, union_24c, union_24d, union_24e). Due to the distributive law inintersect(A & (B|C)→(A&B) | (A&C)), this recursively expanded to 24^5 ≈ 8 million intersect operations, causing the hang.The fix changes this to pairwise folding:
intersect(intersect(intersect(intersect(union_24a, union_24b), union_24c), union_24d), union_24e). Each pairwise intersection reduces the union to its common members before proceeding, so the total work is roughly 2424 + 2324 + 2224 + 2124 ≈ 2,160 operations — a ~3,600x improvement.Intersection is associative, so pairwise folding produces the same result as N-ary intersection.
Test
testBug14475inAnalyserIntegrationTest— reproduces the exact code from the issue (array shape with wildcard constant types, repeated equality checks) and verifies analysis completes without errorstests/bench/data/bug-14475.php— same reproduction case for performance regression trackingFixes phpstan/phpstan#14475