Skip to content

Skip constant-array guards when creating conditional expressions for subtype-absorbed targets#6002

Merged
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-g2xf5hu
Jul 4, 2026
Merged

Skip constant-array guards when creating conditional expressions for subtype-absorbed targets#6002
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-g2xf5hu

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

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 (nested foreach over 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 (two isSuperTypeOf() 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 the ConditionalExpressionHolder creation and the expensive isSuperTypeOf() 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. the foreach value $changesets or self::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:

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 NodeScopeResolverTest case 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).

  • Local in-process bench (median of 7 steady-state iterations, same setup as RegressionBench): bug-10538.php went 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.
  • Confirmed no inference change: a subtype-absorbed target guarded by a constant-array equality (if ($arr === ['a' => 1]) { $value = 5; } if ($arr === ['a' => 1]) { … }) already inferred the widened int|string both 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 repeated is_array/boolean-flag guard) and bug-11281.php remain green.
  • All 1671 NodeScopeResolverTest inference tests and the full tests/PHPStan/Analyser suite (2852 tests) pass unchanged; make phpstan is clean.

Fixes phpstan/phpstan#14921

…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.
@ondrejmirtes ondrejmirtes merged commit 6807620 into phpstan:2.2.x Jul 4, 2026
667 of 671 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-g2xf5hu branch July 4, 2026 19:31
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.

Performance regression: keeping subtype-absorbed conditional targets (phpstan-src#5876) adds ~15% on constant-array loop code (bench bug-10538.php)

2 participants