diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 838df16e51..6f36f1ceec 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -2084,7 +2084,21 @@ public static function doIntersect(Type ...$types): Type $accessoryBaseTypes = null; break; } - $accessoryBaseTypes[] = $type->getDefaultBaseType(); + // Accessory types share their default base type: every string accessory + // returns `string`, hasOffset() and hasOffsetValue() both return + // `array|ArrayAccess`. Adding the same base type again narrows nothing - + // intersection is idempotent - but the intersect() below distributes + // `A & (B | C)` one union at a time, so n copies of `array|ArrayAccess` + // would cost 2^n recursive calls before the duplicates are recognized at + // the leaves. That is why isset() with many offsets used to grow + // exponentially: each offset contributes one hasOffset(). + $baseType = $type->getDefaultBaseType(); + foreach ($accessoryBaseTypes as $addedBaseType) { + if ($addedBaseType->equals($baseType)) { + continue 2; + } + } + $accessoryBaseTypes[] = $baseType; } if ($accessoryBaseTypes !== null) { // Accessory types never stand alone — supply the base type they refine. diff --git a/tests/bench/data/bug-15061.php b/tests/bench/data/bug-15061.php new file mode 100644 index 0000000000..86911e59e4 --- /dev/null +++ b/tests/bench/data/bug-15061.php @@ -0,0 +1,84 @@ + $entities */ + $entities = []; + + foreach ($entities as $entity) { + $ok = isset( + $entity['a'], + $entity['b'], + $entity['c'], + $entity['d'], + $entity['e'], + $entity['f'], + $entity['g'], + $entity['h'], + $entity['i'], + $entity['j'], + $entity['k'], + $entity['l'], + $entity['m'], + $entity['n'], + $entity['o'], + $entity['p'], + $entity['q'], + $entity['r'], + $entity['s'], + $entity['t'], + $entity['u'], + $entity['v'], + $entity['w'], + $entity['x'], + $entity['y'], + $entity['z'], + ); + + if (!$ok) { + continue; + } + } + } + +}