Skip to content

Commit 3f5f2de

Browse files
ondrejmirtesclaude
andcommitted
Add flattening optimization for deep BooleanAnd chains in truthy context
Unlike `BooleanOr` which already had `specifyTypesForFlattenedBooleanOr`, `BooleanAnd` chains had no flattening — each level recursed through `specifyTypesInCondition` and `filterByTruthyValue`, creating O(N²) scope operations. This was slow even at the original `BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4`. Add `specifyTypesForFlattenedBooleanAnd` that flattens the chain, processes each arm independently in the original scope, and batches the type union construction to avoid incremental O(N²) growth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7eab3d2 commit 3f5f2de

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 61 additions & 0 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);
@@ -2075,6 +2086,56 @@ private function specifyTypesForFlattenedBooleanOr(
20752086
return $result->setRootExpr($expr);
20762087
}
20772088

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+
20782139
/**
20792140
* @return array<string, ConditionalExpressionHolder[]>
20802141
*/
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)