Skip to content

Commit c51795c

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents fcd10a0 + 8019f65 commit c51795c

File tree

5 files changed

+198
-9
lines changed

5 files changed

+198
-9
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,17 @@ public function specifyTypesInCondition(
714714
if (!$scope instanceof MutatingScope) {
715715
throw new ShouldNotHappenException();
716716
}
717+
718+
// For deep BooleanAnd chains in truthy context, flatten and
719+
// process all arms at once to avoid O(N²) recursive
720+
// filterByTruthyValue calls.
721+
if (
722+
$context->true()
723+
&& BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH
724+
) {
725+
return $this->specifyTypesForFlattenedBooleanAnd($scope, $expr, $context);
726+
}
727+
717728
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
718729
$rightScope = $scope->filterByTruthyValue($expr->left);
719730
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
@@ -2020,13 +2031,36 @@ private function specifyTypesForFlattenedBooleanOr(
20202031
$arms = array_reverse($arms);
20212032

20222033
if ($context->false() || $context->falsey()) {
2023-
// Falsey: all arms are false → union all SpecifiedTypes
2024-
$result = new SpecifiedTypes([], []);
2034+
// Falsey: all arms are false → union all SpecifiedTypes.
2035+
// Collect per-expression types first, then build unions once
2036+
// to avoid O(N²) from incremental TypeCombinator::union() growth.
2037+
/** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2038+
$sureTypesPerExpr = [];
2039+
/** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2040+
$sureNotTypesPerExpr = [];
2041+
20252042
foreach ($arms as $arm) {
20262043
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2027-
$result = $result->unionWith($armTypes);
2044+
foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
2045+
$sureTypesPerExpr[$exprString][0] = $exprNode;
2046+
$sureTypesPerExpr[$exprString][1][] = $type;
2047+
}
2048+
foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
2049+
$sureNotTypesPerExpr[$exprString][0] = $exprNode;
2050+
$sureNotTypesPerExpr[$exprString][1][] = $type;
2051+
}
2052+
}
2053+
2054+
$sureTypes = [];
2055+
foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
2056+
$sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)];
20282057
}
2029-
return $result->setRootExpr($expr);
2058+
$sureNotTypes = [];
2059+
foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
2060+
$sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2061+
}
2062+
2063+
return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
20302064
}
20312065

20322066
// Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
@@ -2052,6 +2086,56 @@ private function specifyTypesForFlattenedBooleanOr(
20522086
return $result->setRootExpr($expr);
20532087
}
20542088

2089+
/**
2090+
* @param BooleanAnd|LogicalAnd $expr
2091+
*/
2092+
private function specifyTypesForFlattenedBooleanAnd(
2093+
MutatingScope $scope,
2094+
Expr $expr,
2095+
TypeSpecifierContext $context,
2096+
): SpecifiedTypes
2097+
{
2098+
$arms = [];
2099+
$current = $expr;
2100+
while ($current instanceof BooleanAnd || $current instanceof LogicalAnd) {
2101+
$arms[] = $current->right;
2102+
$current = $current->left;
2103+
}
2104+
$arms[] = $current;
2105+
$arms = array_reverse($arms);
2106+
2107+
// Truthy: all arms are true → union all SpecifiedTypes.
2108+
// Collect per-expression types first, then build unions once
2109+
// to avoid O(N²) from incremental growth.
2110+
/** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2111+
$sureTypesPerExpr = [];
2112+
/** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2113+
$sureNotTypesPerExpr = [];
2114+
2115+
foreach ($arms as $arm) {
2116+
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2117+
foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
2118+
$sureTypesPerExpr[$exprString][0] = $exprNode;
2119+
$sureTypesPerExpr[$exprString][1][] = $type;
2120+
}
2121+
foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
2122+
$sureNotTypesPerExpr[$exprString][0] = $exprNode;
2123+
$sureNotTypesPerExpr[$exprString][1][] = $type;
2124+
}
2125+
}
2126+
2127+
$sureTypes = [];
2128+
foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
2129+
$sureTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2130+
}
2131+
$sureNotTypes = [];
2132+
foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
2133+
$sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2134+
}
2135+
2136+
return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
2137+
}
2138+
20552139
/**
20562140
* @return array<string, ConditionalExpressionHolder[]>
20572141
*/

src/Type/Constant/ConstantArrayType.php

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ class ConstantArrayType implements Type
9494

9595
private ?Type $iterableValueType = null;
9696

97+
/** @var array<int|string, int>|null */
98+
private ?array $keyIndexMap = null;
99+
97100
/**
98101
* @api
99102
* @param list<ConstantIntegerType|ConstantStringType> $keyTypes
@@ -1805,15 +1808,17 @@ public function isKeysSupersetOf(self $otherArray): bool
18051808

18061809
$failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;
18071810

1808-
$keyTypes = $this->keyTypes;
1811+
$keyIndexMap = $this->getKeyIndexMap();
1812+
$otherKeyValues = [];
18091813

18101814
foreach ($otherArray->keyTypes as $j => $keyType) {
1811-
$i = self::findKeyIndex($keyType, $keyTypes);
1815+
$keyValue = $keyType->getValue();
1816+
$i = $keyIndexMap[$keyValue] ?? null;
18121817
if ($i === null) {
18131818
return false;
18141819
}
18151820

1816-
unset($keyTypes[$i]);
1821+
$otherKeyValues[$keyValue] = true;
18171822

18181823
$valueType = $this->valueTypes[$i];
18191824
$otherValueType = $otherArray->valueTypes[$j];
@@ -1828,7 +1833,10 @@ public function isKeysSupersetOf(self $otherArray): bool
18281833
}
18291834

18301835
$requiredKeyCount = 0;
1831-
foreach (array_keys($keyTypes) as $i) {
1836+
foreach ($this->keyTypes as $i => $keyType) {
1837+
if (isset($otherKeyValues[$keyType->getValue()])) {
1838+
continue;
1839+
}
18321840
if ($this->isOptionalKey($i)) {
18331841
continue;
18341842
}
@@ -1868,12 +1876,29 @@ public function mergeWith(self $otherArray): self
18681876
return $this->recreate($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList));
18691877
}
18701878

1879+
/**
1880+
* @return array<int|string, int>
1881+
*/
1882+
private function getKeyIndexMap(): array
1883+
{
1884+
if ($this->keyIndexMap !== null) {
1885+
return $this->keyIndexMap;
1886+
}
1887+
1888+
$map = [];
1889+
foreach ($this->keyTypes as $i => $keyType) {
1890+
$map[$keyType->getValue()] = $i;
1891+
}
1892+
1893+
return $this->keyIndexMap = $map;
1894+
}
1895+
18711896
/**
18721897
* @param ConstantIntegerType|ConstantStringType $otherKeyType
18731898
*/
18741899
private function getKeyIndex($otherKeyType): ?int
18751900
{
1876-
return self::findKeyIndex($otherKeyType, $this->keyTypes);
1901+
return $this->getKeyIndexMap()[$otherKeyType->getValue()] ?? null;
18771902
}
18781903

18791904
/**
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace BenchAndChainTruthyBlowup;
4+
5+
/**
6+
* Regression test for O(N²) in deep BooleanAnd chains.
7+
* Without the flattening optimization, each level recursed through
8+
* specifyTypesInCondition and filterByTruthyValue, creating O(N²) scope operations.
9+
* Slow at the original BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4.
10+
*/
11+
function test(string $x): void {
12+
if ($x !== "val_1" && $x !== "val_2" && $x !== "val_3" && $x !== "val_4" && $x !== "val_5" && $x !== "val_6" && $x !== "val_7" && $x !== "val_8" && $x !== "val_9" && $x !== "val_10" && $x !== "val_11" && $x !== "val_12" && $x !== "val_13" && $x !== "val_14" && $x !== "val_15" && $x !== "val_16" && $x !== "val_17" && $x !== "val_18" && $x !== "val_19" && $x !== "val_20" && $x !== "val_21" && $x !== "val_22" && $x !== "val_23" && $x !== "val_24" && $x !== "val_25" && $x !== "val_26" && $x !== "val_27" && $x !== "val_28" && $x !== "val_29" && $x !== "val_30" && $x !== "val_31" && $x !== "val_32" && $x !== "val_33" && $x !== "val_34" && $x !== "val_35" && $x !== "val_36" && $x !== "val_37" && $x !== "val_38" && $x !== "val_39" && $x !== "val_40" && $x !== "val_41" && $x !== "val_42" && $x !== "val_43" && $x !== "val_44" && $x !== "val_45" && $x !== "val_46" && $x !== "val_47" && $x !== "val_48" && $x !== "val_49" && $x !== "val_50" && $x !== "val_51" && $x !== "val_52" && $x !== "val_53" && $x !== "val_54" && $x !== "val_55" && $x !== "val_56" && $x !== "val_57" && $x !== "val_58" && $x !== "val_59" && $x !== "val_60" && $x !== "val_61" && $x !== "val_62" && $x !== "val_63" && $x !== "val_64" && $x !== "val_65" && $x !== "val_66" && $x !== "val_67" && $x !== "val_68" && $x !== "val_69" && $x !== "val_70" && $x !== "val_71" && $x !== "val_72" && $x !== "val_73" && $x !== "val_74" && $x !== "val_75" && $x !== "val_76" && $x !== "val_77" && $x !== "val_78" && $x !== "val_79" && $x !== "val_80" && $x !== "val_81" && $x !== "val_82" && $x !== "val_83" && $x !== "val_84" && $x !== "val_85" && $x !== "val_86" && $x !== "val_87" && $x !== "val_88" && $x !== "val_89" && $x !== "val_90" && $x !== "val_91" && $x !== "val_92" && $x !== "val_93" && $x !== "val_94" && $x !== "val_95" && $x !== "val_96" && $x !== "val_97" && $x !== "val_98" && $x !== "val_99" && $x !== "val_100") {
13+
echo $x;
14+
}
15+
}

0 commit comments

Comments
 (0)