Intersect an intersection's mapped members at once unless two are unions - #6663
Merged
Merged
Conversation
IntersectionType::traverse(), traverseSimultaneously() and the intersectTypes() helper (tryRemove() and friends) folded the mapped members back together two at a time. Every step re-reduces the growing intersection pairwise, so rebuilding n members costs O(n^3) pair checks instead of the O(n^2) of one TypeCombinator::intersect() over all of them. The fold exists to keep two or more unions from being distributed over each other at once (a79f4ca, b26d08c); with at most one union operand there is nothing to multiply, so those intersections go through the n-ary call. The isset() of 26 offsets in the bug-15061 bench narrows the native mixed subject to (non-empty-array & hasOffset(...) x25) | (ArrayAccess & ...), and since the falsey isset() narrowing removes hasOffset() of optional keys (77527df), every subject rebuilds that intersection: without the native extension the file's analysis time drops by 26%. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsHiTmsN24NMF7zvEhwbip
ondrejmirtes
force-pushed
the
intersection-type-n-ary-intersect
branch
from
October 3, 2026 09:13
c9781eb to
ac2a474
Compare
staabm
reviewed
Oct 3, 2026
| $result = TypeCombinator::intersect($result, $types[$i]); | ||
| } | ||
| return $result; | ||
| return self::intersectOperands($types); |
Contributor
There was a problem hiding this comment.
Could we instead pass all args into TypeCombinator::intersect() with a splat operator and do the optimization in the combinator instead?
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.
IntersectionType::traverse(),traverseSimultaneously()and theintersectTypes()helper (used bytryRemove()and friends) folded the mapped members back together two at a time. Each step re-reduces the growing intersection pairwise, so rebuilding n members costs O(n³) pair checks instead of the O(n²) of a singleTypeCombinator::intersect()over all of them.The fold exists so that two or more unions are not distributed over each other at once (a79f4ca, b26d08c). With at most one union operand there is nothing to multiply, so those intersections now go through the n-ary call. The native mirror in
turbo-ext/src/IntersectionType.cppgets the same change.Why now
tests/bench/data/bug-15061.phpgot ~3x slower on 2.3.x with 77527df (Narrow falsey isset() on an optional constant-array offset, found by bisecting). The 26-offsetisset()narrows the nativemixedsubject to(non-empty-array & hasOffset(...) ×25) | (ArrayAccess & …). Since that commit, the falsey narrowing removeshasOffset()of each optional key, and every removal rebuilds that 26-member intersection through the fold. 6888887 already won part of it back.Numbers
User CPU,
bin/phpstan analyse -l 8 <file> --debug, hyperfine, 12 runs, same session. Analysis cost is the file's time minus an empty file's:For reference, bug-15061 cost 0.44–0.47 s on 2.2.x. What's left of the gap is the extra falsey narrowing itself.
Self-analysis of
src/Type(no extension, single process, 4 ABBA pairs) is neutral: mean user CPU 21.68 s → 21.67 s, with identical errors.Verification
make testspasses with and without the extension loaded (22,485 tests, no expectation changes).turbo-extstrict build,tests/smoke.php,tests/signature-parity.phpandbin/side-by-side.phppass.generated/IntersectionType.his regenerated for the new private PHP method.🤖 Generated with Claude Code
https://claude.ai/code/session_01JsHiTmsN24NMF7zvEhwbip