Skip constant-array guards when creating conditional expressions for subtype-absorbed targets#6002
Merged
ondrejmirtes merged 1 commit intoJul 4, 2026
Conversation
…subtype-absorbed targets - In MutatingScope::createConditionalExpressions(), a subtype-absorbed variable is kept as a conditional-expression *target* (since phpstan#5876) so it can be re-narrowed when its guard is re-asserted. Pairing such a target with a constant-array guard, however, never re-narrows anything: a constant array represents a unique literal value that is not re-asserted as a condition later. Those conditional expressions were pure cost — the downstream isSuperTypeOf() guard machinery (including the reverse check added in phpstan#5848) compared these potentially huge constant arrays on every branch merge during loop convergence. - Skip creating a conditional expression for a subtype-absorbed target when the guard's type is a constant array, avoiding both the holder creation and the expensive guard-selection comparisons. - Recovers the ~15% analysis-time regression on constant-array-heavy loop code (tests/bench/data/bug-10538.php) while preserving the phpstan#5876 inference improvement (bug-7948, bug-11281 remain green). - Verified the removed conditional expressions never produced a re-narrowing (a constant-array-guarded absorbed target already inferred the widened type both before and after the change), so behavior is unchanged — all 1671 NodeScopeResolver inference tests pass identically. - Probed broadening the skip to all targets (not just subtype-absorbed ones): it recovers even more time but breaks a legitimate re-narrowing that relies on a constant-array guard, so the skip is scoped to subtype-absorbed targets.
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-src#5876 ("Keep subtype-absorbed variables as conditional-expression targets when merging branches") added a ~15% analysis-time regression on constant-array-heavy loop code — the bench
tests/bench/data/bug-10538.php(nestedforeachover a ~100-entry constant array of string array shapes).Where the previous code dropped a subtype-absorbed variable entirely (
unset($newVariableTypes[$exprString])), #5876 keeps it as a conditional-expression target so it can be re-narrowed when its guard is re-asserted. The regression comes from these extra targets being paired with constant-array guards during branch merging in loop convergence: those pairings never re-narrow anything, but the guard-selection machinery (twoisSuperTypeOf()calls per pair, one of them added by phpstan-src#5848) still pays to compare the potentially huge constant arrays on every merge.This change keeps the #5876 inference improvement but stops creating those useless-but-expensive conditional expressions.
Changes
src/Analyser/MutatingScope.php,createConditionalExpressions(): when the current conditional-expression target is a subtype-absorbed variable (i.e. it is in$guardsToExclude, kept only for re-narrowing), skip guards whose type is a constant array ($guardHolder->getType()->isConstantArray()->yes()). This avoids both theConditionalExpressionHoldercreation and the expensiveisSuperTypeOf()guard comparisons for those pairs.Root cause
The subtype-absorbed target (e.g.
$stateId = 9) was paired with every type guard, including guards whose type is a large nested constant array (e.g. theforeachvalue$changesetsorself::CHANGESET[$stateCode]). A constant array is a unique literal value that is essentially never re-asserted as a condition, so the resulting conditional expression$stateId = 9 WHEN $changesets = array{…huge…}can never fire a re-narrowing. It is, however, disproportionately expensive:ConditionalExpressionHolder::getKey()serialises the guard type viadescribe(VerbosityLevel::precise()).isSuperTypeOf()comparisons per (target, guard) pair, and phpstan-src#5848 added the reverse comparison — both over the huge constant arrays.Restricting the skip to subtype-absorbed targets is deliberate: probing a broader skip (constant-array guards for all targets) recovers even more time but breaks a legitimate re-narrowing where a non-absorbed target relies on a constant-array guard (one
NodeScopeResolverTestcase fails). Subtype-absorbed targets are the ones newly kept by #5876, and for them the constant-array-guarded conditional expressions were verified to be dead (see Test).Test
This is a performance fix with no behavior change; the regression is covered by the existing bench file
tests/bench/data/bug-10538.php(bench.yml Test job).RegressionBench):bug-10538.phpwent from ~3777 ms (before) to ~3373 ms (after), matching the ~3344 ms measured with phpstan-src#5876 reverted — i.e. the full Keep subtype-absorbed variables as conditional-expression targets when merging branches #5876 step is recovered.if ($arr === ['a' => 1]) { $value = 5; } if ($arr === ['a' => 1]) { … }) already inferred the widenedint|stringboth before and after the change — the removed conditional expressions never re-narrowed.#5876's inference improvement is preserved:bug-7948.php(subtype absorption re-narrowed under a repeatedis_array/boolean-flag guard) andbug-11281.phpremain green.NodeScopeResolverTestinference tests and the fulltests/PHPStan/Analysersuite (2852 tests) pass unchanged;make phpstanis clean.Fixes phpstan/phpstan#14921