Skip to content

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
phpstan-bot:create-pull-request/patch-vj244vy
Open

Use pairwise TypeCombinator::intersect folding for conditional expression holders to avoid exponential union distribution#5482
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-vj244vy

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

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

  • Changed src/Analyser/MutatingScope.php (line ~3269): replaced the N-ary TypeCombinator::intersect(...array_map(..., $expressions)) call with a pairwise fold that intersects holder types two at a time
  • Added regression test tests/PHPStan/Analyser/data/bug-14475.php and integration test method testBug14475 in AnalyserIntegrationTest.php
  • Added benchmark test tests/bench/data/bug-14475.php

Analogous cases probed

  • Searched all TypeCombinator::intersect(...) call sites in src/Analyser/ — no other N-ary intersect calls with dynamically-sized arrays of potential UnionTypes were found
  • TypeCombinator::intersect($resultType, ...$accessories) calls at lines 4130/4183 in MutatingScope.php are bounded by MAX_ACCESSORIES_LIMIT and not affected
  • Other intersect(...) calls in src/Type/TypeUtils.php, src/Reflection/Type/IntersectionType*.php, etc. are bounded by PHP syntax or reflection constraints

Root cause

When filterBySpecifiedTypes() processes matched conditional expressions, it collects all matching ConditionalExpressionHolder objects 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 in intersect (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

  • Integration test: testBug14475 in AnalyserIntegrationTest — reproduces the exact code from the issue (array shape with wildcard constant types, repeated equality checks) and verifies analysis completes without errors
  • Benchmark test: tests/bench/data/bug-14475.php — same reproduction case for performance regression tracking

Fixes phpstan/phpstan#14475

phpstan-bot and others added 2 commits April 16, 2026 09:34
…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
@ondrejmirtes ondrejmirtes force-pushed the create-pull-request/patch-vj244vy branch from 3bb7c25 to 7241b96 Compare April 16, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants