Short-circuit ScopeOps scans over conditional expressions#6063
Merged
Conversation
Straight-line code with many distinct tracked sub-expressions made every statement scan the whole accumulated scope state, turning analysis quadratic with a heavy constant (6058 in the phpstan/phpstan repro): - matchConditionalExpressions() now returns immediately when nothing is specified: every holder has at least one condition (enforced by the ConditionalExpressionHolder constructor), so nothing can match, yet the per-statement call from processStmtNode() scanned every holder twice. - invalidateExpressionEntries() applies the compositional-key substring gate of shouldInvalidateExpression() directly on the stored table keys, skipping whole entries and holders without per-expression calls. The conditional-expressions map key doubles as the target's node key, so the target expression is no longer re-printed per entry per invalidation, and holder lists are rebuilt lazily so untouched entries keep sharing the original arrays. Same changes mirrored in the turbo extension's ScopeOps.cpp; analysis output is byte-identical with the extension on and off, and the full test suite passes in both modes. 800 sequential `$vN = $a[N];` assignments (tests/bench/data/bug-14972.php) drop from 4.8s to 1.4s (PHP) / 0.9s (turbo); the marginal per-statement cost is now linear under the extension and near-linear in PHP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh8bVRSqtiLRW7ifukHdW8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh8bVRSqtiLRW7ifukHdW8
ondrejmirtes
force-pushed
the
worktree-issue-14972-perf
branch
from
July 18, 2026 19:40
31d2d41 to
fc99f5d
Compare
|
Wow, that was fast, thanks! |
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.
Closes phpstan/phpstan#14972
Straight-line code with many distinct tracked sub-expressions made every statement scan the whole accumulated scope state, turning analysis quadratic with a heavy constant. Each plain
$vN = $a[N];assignment adds ~16ConditionalExpressionHolderentries (fromAssignHandler's falsey-scalar decomposition), and two per-statement operations then scanned all of it:ScopeOps::matchConditionalExpressions()— called viafilterBySpecifiedTypes()on every expression statement fromprocessStmtNode(), scanning every holder twice even with an empty specified-expressions set, which provably can never match anything (theConditionalExpressionHolderconstructor enforces at least one condition). Now returns immediately.ScopeOps::invalidateExpressionEntries()— called on everyassignVariable(), runningshouldInvalidateExpression()for every tracked expression, holder target, and holder condition, and re-printing each target expression vianodeKey(). Now applies the compositional-key substring gate directly on the stored table keys as a prefilter (the conditional-expressions map key doubles as the printed target key, and a holder's array key embeds all its condition keys verbatim), and rebuilds holder lists lazily so untouched entries keep sharing the original arrays.Both changes are mirrored in the turbo extension's
ScopeOps.cpp, with the follow-up expected-version bump.Numbers
tests/bench/data/bug-14972.php(800 sequential distinct-key assignments, the issue's variant C generator):The marginal per-statement cost is now linear under the extension and near-linear in PHP (the remaining tail is the copy-on-write scope-map duplication the issue also identifies, which would need persistent maps).
The issue's variant A (concat-heavy statements) only improves from 5.3s to 4.6s — its dominant cost is a different channel, not the ScopeOps scans.
Verification
PHPSTAN_TURBO=0src/Typeself-analysismake phpstanclean; turbo strict build,smoke.php,signature-parity.php, andside-by-side.php --checkall pass🤖 Generated with Claude Code
https://claude.ai/code/session_01Gh8bVRSqtiLRW7ifukHdW8