Use pairwise TypeCombinator::intersect folding for conditional expression holders to avoid exponential union distribution#5482
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
Performance benchmarksAll measurements use
¹ Measured with the Summary of all fixesPairwise
Avoid 2^N
|
…sion Process keys incrementally instead of generating all power-set variants upfront via `getAllArrays()`. For each key, fork partial results for optional keys and bail early when the count exceeds `CALCULATE_SCALARS_LIMIT`. This avoids allocating 2^N `ConstantArrayType` objects for N optional keys. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…Types()` Process keys incrementally instead of generating all power-set variants upfront via `getAllArrays()`. For each key, fork partial `ConstantArrayTypeBuilder` instances for optional keys and finite value variants, bailing early when the count exceeds `CALCULATE_SCALARS_LIMIT`. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…)` expansion Estimate the total power-set variant count before calling `getAllArrays()`. When a `ConstantArrayType` has more than ~14 optional keys (16384+ variants), return the type as-is instead of expanding. Also apply pairwise `TypeCombinator::intersect` folding in the combination loop. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
259ee72 to
4d74d2a
Compare
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