Skip to content

Commit 7eab3d2

Browse files
ondrejmirtesclaude
andcommitted
Batch unionWith in specifyTypesForFlattenedBooleanOr falsey path to avoid O(N²)
The falsey path called `unionWith` N times in a loop. Each call did `TypeCombinator::union()` on the incrementally growing `sureNotTypes`, making each step O(K) for K accumulated types — O(N²) total. Collect per-expression types into arrays first, then build the final `TypeCombinator::union()`/`intersect()` once at the end in O(N). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df2bf27 commit 7eab3d2

File tree

2 files changed

+66
-4
lines changed

2 files changed

+66
-4
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,13 +2020,36 @@ private function specifyTypesForFlattenedBooleanOr(
20202020
$arms = array_reverse($arms);
20212021

20222022
if ($context->false() || $context->falsey()) {
2023-
// Falsey: all arms are false → union all SpecifiedTypes
2024-
$result = new SpecifiedTypes([], []);
2023+
// Falsey: all arms are false → union all SpecifiedTypes.
2024+
// Collect per-expression types first, then build unions once
2025+
// to avoid O(N²) from incremental TypeCombinator::union() growth.
2026+
/** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2027+
$sureTypesPerExpr = [];
2028+
/** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2029+
$sureNotTypesPerExpr = [];
2030+
20252031
foreach ($arms as $arm) {
20262032
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2027-
$result = $result->unionWith($armTypes);
2033+
foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
2034+
$sureTypesPerExpr[$exprString][0] = $exprNode;
2035+
$sureTypesPerExpr[$exprString][1][] = $type;
2036+
}
2037+
foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
2038+
$sureNotTypesPerExpr[$exprString][0] = $exprNode;
2039+
$sureNotTypesPerExpr[$exprString][1][] = $type;
2040+
}
20282041
}
2029-
return $result->setRootExpr($expr);
2042+
2043+
$sureTypes = [];
2044+
foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
2045+
$sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)];
2046+
}
2047+
$sureNotTypes = [];
2048+
foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
2049+
$sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2050+
}
2051+
2052+
return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
20302053
}
20312054

20322055
// Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace BenchOrChainFalseyBlowup;
4+
5+
/**
6+
* Regression test for O(N²) in specifyTypesForFlattenedBooleanOr falsey path.
7+
* Each unionWith call incrementally grew the sureNotTypes union,
8+
* causing O(N²) TypeCombinator::union() work. The fix batches all
9+
* per-expression types and builds unions once at the end.
10+
*
11+
* Slow with BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH raised to 128.
12+
*/
13+
function test(string $x): void
14+
{
15+
if (
16+
$x === 'v001' || $x === 'v002' || $x === 'v003' || $x === 'v004' || $x === 'v005' ||
17+
$x === 'v006' || $x === 'v007' || $x === 'v008' || $x === 'v009' || $x === 'v010' ||
18+
$x === 'v011' || $x === 'v012' || $x === 'v013' || $x === 'v014' || $x === 'v015' ||
19+
$x === 'v016' || $x === 'v017' || $x === 'v018' || $x === 'v019' || $x === 'v020' ||
20+
$x === 'v021' || $x === 'v022' || $x === 'v023' || $x === 'v024' || $x === 'v025' ||
21+
$x === 'v026' || $x === 'v027' || $x === 'v028' || $x === 'v029' || $x === 'v030' ||
22+
$x === 'v031' || $x === 'v032' || $x === 'v033' || $x === 'v034' || $x === 'v035' ||
23+
$x === 'v036' || $x === 'v037' || $x === 'v038' || $x === 'v039' || $x === 'v040' ||
24+
$x === 'v041' || $x === 'v042' || $x === 'v043' || $x === 'v044' || $x === 'v045' ||
25+
$x === 'v046' || $x === 'v047' || $x === 'v048' || $x === 'v049' || $x === 'v050' ||
26+
$x === 'v051' || $x === 'v052' || $x === 'v053' || $x === 'v054' || $x === 'v055' ||
27+
$x === 'v056' || $x === 'v057' || $x === 'v058' || $x === 'v059' || $x === 'v060' ||
28+
$x === 'v061' || $x === 'v062' || $x === 'v063' || $x === 'v064' || $x === 'v065' ||
29+
$x === 'v066' || $x === 'v067' || $x === 'v068' || $x === 'v069' || $x === 'v070' ||
30+
$x === 'v071' || $x === 'v072' || $x === 'v073' || $x === 'v074' || $x === 'v075' ||
31+
$x === 'v076' || $x === 'v077' || $x === 'v078' || $x === 'v079' || $x === 'v080' ||
32+
$x === 'v081' || $x === 'v082' || $x === 'v083' || $x === 'v084' || $x === 'v085' ||
33+
$x === 'v086' || $x === 'v087' || $x === 'v088' || $x === 'v089' || $x === 'v090' ||
34+
$x === 'v091' || $x === 'v092' || $x === 'v093' || $x === 'v094' || $x === 'v095' ||
35+
$x === 'v096' || $x === 'v097' || $x === 'v098' || $x === 'v099' || $x === 'v100'
36+
) {
37+
echo $x;
38+
}
39+
}

0 commit comments

Comments
 (0)