Skip to content

Commit c116397

Browse files
ondrejmirtesclaude
andcommitted
Intersect an intersection's mapped members at once unless two are unions
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
1 parent 9f1ff68 commit c116397

3 files changed

Lines changed: 57 additions & 28 deletions

File tree

‎src/Type/IntersectionType.php‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,11 +1724,7 @@ public function traverse(callable $cb): Type
17241724
}
17251725

17261726
if ($changed) {
1727-
$result = $types[0];
1728-
for ($i = 1, $count = count($types); $i < $count; $i++) {
1729-
$result = TypeCombinator::intersect($result, $types[$i]);
1730-
}
1731-
return $result;
1727+
return self::intersectOperands($types);
17321728
}
17331729

17341730
return $this;
@@ -1765,11 +1761,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
17651761
return $this;
17661762
}
17671763

1768-
$result = $newTypes[0];
1769-
for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
1770-
$result = TypeCombinator::intersect($result, $newTypes[$i]);
1771-
}
1772-
return $result;
1764+
return self::intersectOperands($newTypes);
17731765
}
17741766

17751767
return $this;
@@ -1897,7 +1889,31 @@ private function intersectResults(
18971889
*/
18981890
private function intersectTypes(callable $getType): Type
18991891
{
1900-
$operands = array_map($getType, $this->types);
1892+
return self::intersectOperands(array_map($getType, $this->types));
1893+
}
1894+
1895+
/**
1896+
* The intersection of the members after they were mapped. All of them at once
1897+
* costs one pairwise reduction; folding them two at a time re-reduces the
1898+
* growing intersection at every step, which is cubic in the member count.
1899+
* Only two or more unions need the fold - distributing each over the others
1900+
* at once multiplies their sizes.
1901+
*
1902+
* @param list<Type> $operands
1903+
*/
1904+
private static function intersectOperands(array $operands): Type
1905+
{
1906+
$unionCount = 0;
1907+
foreach ($operands as $operand) {
1908+
if (!$operand instanceof UnionType) {
1909+
continue;
1910+
}
1911+
$unionCount++;
1912+
}
1913+
if ($unionCount < 2) {
1914+
return TypeCombinator::intersect(...$operands);
1915+
}
1916+
19011917
$result = $operands[0];
19021918
for ($i = 1, $count = count($operands); $i < $count; $i++) {
19031919
$result = TypeCombinator::intersect($result, $operands[$i]);

‎turbo-ext/src/IntersectionType.cpp‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,8 +2008,8 @@ class IntersectionType
20082008

20092009
zv::Val getReferencedTemplateTypes(zval *positionVariance) const { return concatOf(PT_LC("getreferencedtemplatetypes"), 1, positionVariance); }
20102010

2011-
/* TypeCombinator::intersect() folded over $cb of every member when any
2012-
* changed, $this otherwise; UNDEF = pending exception */
2011+
/* intersectOperands() over $cb of every member when any changed, $this
2012+
* otherwise; UNDEF = pending exception */
20132013
zv::Val traverse(zend_fcall_info *fci, zend_fcall_info_cache *fcc) const
20142014
{
20152015
zval *types = this->types();
@@ -2028,7 +2028,7 @@ class IntersectionType
20282028
}
20292029
newTypes.push(zv::Val::adopt(newType));
20302030
}
2031-
if (changed) return foldIntersect(newTypes.arrRef());
2031+
if (changed) return intersectOperands(newTypes.arrRef());
20322032
return thisValue();
20332033
}
20342034

@@ -2093,7 +2093,7 @@ class IntersectionType
20932093
newTypes.push(std::move(mapped));
20942094
}
20952095
if (!changed) return thisValue();
2096-
return foldIntersect(newTypes.arrRef());
2096+
return intersectOperands(newTypes.arrRef());
20972097
}
20982098

20992099
/* the TypeTraverser::map() callback of traverseSimultaneously(): the
@@ -2656,8 +2656,8 @@ class IntersectionType
26562656
}
26572657

26582658
/* intersectTypes(): $getType over every member (all of them first, as
2659-
* array_map does), folded with TypeCombinator::intersect(); UNDEF =
2660-
* pending exception */
2659+
* array_map does), intersected by intersectOperands(); UNDEF = pending
2660+
* exception */
26612661
template <typename F>
26622662
zv::Val intersectTypes(F getType) const
26632663
{
@@ -2674,12 +2674,20 @@ class IntersectionType
26742674
}
26752675
operands.push(std::move(operand));
26762676
}
2677-
return foldIntersect(operands.arrRef());
2677+
return intersectOperands(operands.arrRef());
26782678
}
26792679

2680-
/* $result = $operands[0]; TypeCombinator::intersect($result, $operands[$i]) for the rest */
2681-
static zv::Val foldIntersect(zv::ArrRef operands)
2680+
/* intersectOperands(): TypeCombinator::intersect(...$operands) unless two
2681+
* or more of them are unions, then $result = $operands[0];
2682+
* TypeCombinator::intersect($result, $operands[$i]) for the rest */
2683+
static zv::Val intersectOperands(zv::ArrRef operands)
26822684
{
2685+
uint32_t unionCount = 0;
2686+
for (zv::ArrayEntry entry : operands) {
2687+
if (entry.value().deref().instanceOf(pt_ce_union_type)) unionCount++;
2688+
}
2689+
if (unionCount < 2) return combinatorIntersect(operands.table());
2690+
26832691
zv::Val result;
26842692
for (zv::ArrayEntry entry : operands) {
26852693
if (result.isUndef()) {

‎turbo-ext/src/generated/IntersectionType.h‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,12 @@ inline constexpr char strings[] =
277277
"intersectResults\0" /* 3555 */
278278
"getType\0" /* 3572 */
279279
"intersectTypes\0" /* 3580 */
280-
"intersectTypesPreserveTemplateType\0" /* 3595 */
281-
"PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 3630 */
282-
"toPhpDocNode\0" /* 3669 */
283-
"hasTemplateOrLateResolvableType"; /* 3682 */
280+
"operands\0" /* 3595 */
281+
"intersectOperands\0" /* 3604 */
282+
"intersectTypesPreserveTemplateType\0" /* 3622 */
283+
"PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 3657 */
284+
"toPhpDocNode\0" /* 3696 */
285+
"hasTemplateOrLateResolvableType"; /* 3709 */
284286
inline constexpr reg::PackedArg args[] = {
285287
reg::packed(0, MAY_BE_ARRAY), /* __construct $types */
286288
reg::packed(18, MAY_BE_ARRAY), /* getTypes return */
@@ -528,9 +530,11 @@ inline constexpr reg::PackedArg args[] = {
528530
reg::packed(18, 0, 542), /* intersectResults return */
529531
reg::packed(3572, MAY_BE_CALLABLE), /* intersectTypes $getType */
530532
reg::packed(18, 0, 56), /* intersectTypes return */
533+
reg::packed(3595, MAY_BE_ARRAY), /* intersectOperands $operands */
534+
reg::packed(18, 0, 56), /* intersectOperands return */
531535
reg::packed(3572, MAY_BE_CALLABLE), /* intersectTypesPreserveTemplateType $getType */
532536
reg::packed(18, 0, 56), /* intersectTypesPreserveTemplateType return */
533-
reg::packed(18, 0, 3630), /* toPhpDocNode return */
537+
reg::packed(18, 0, 3657), /* toPhpDocNode return */
534538
reg::packed(18, MAY_BE_BOOL), /* hasTemplateOrLateResolvableType return */
535539
};
536540
using Sig = reg::Sig<strings, args>;
@@ -683,9 +687,10 @@ inline constexpr sigtab::Sig indexFiniteTypes = { { 3484 /* indexFiniteTypes */,
683687
inline constexpr sigtab::Sig hasFiniteType = { { 3524 /* hasFiniteType */, 3, 237, 3, 240, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
684688
inline constexpr sigtab::Sig intersectResults = { { 3555 /* intersectResults */, 1, 241, 2, 243, ZEND_ACC_PRIVATE } };
685689
inline constexpr sigtab::Sig intersectTypes = { { 3580 /* intersectTypes */, 1, 244, 1, 245, ZEND_ACC_PRIVATE } };
686-
inline constexpr sigtab::Sig intersectTypesPreserveTemplateType = { { 3595 /* intersectTypesPreserveTemplateType */, 1, 246, 1, 247, ZEND_ACC_PRIVATE } };
687-
inline constexpr sigtab::Sig toPhpDocNode = { { 3669 /* toPhpDocNode */, 0, 248, 0, 248, ZEND_ACC_PUBLIC } };
688-
inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 3682 /* hasTemplateOrLateResolvableType */, 0, 249, 0, 249, ZEND_ACC_PUBLIC } };
690+
inline constexpr sigtab::Sig intersectOperands = { { 3604 /* intersectOperands */, 1, 246, 1, 247, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
691+
inline constexpr sigtab::Sig intersectTypesPreserveTemplateType = { { 3622 /* intersectTypesPreserveTemplateType */, 1, 248, 1, 249, ZEND_ACC_PRIVATE } };
692+
inline constexpr sigtab::Sig toPhpDocNode = { { 3696 /* toPhpDocNode */, 0, 250, 0, 250, ZEND_ACC_PUBLIC } };
693+
inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 3709 /* hasTemplateOrLateResolvableType */, 0, 251, 0, 251, ZEND_ACC_PUBLIC } };
689694
} // namespace sig
690695

691696
} // namespace ptdecl::IntersectionType

0 commit comments

Comments
 (0)