diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a52b1c94f2..4072b78918 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -193,7 +193,7 @@ class NodeScopeResolver private const LOOP_SCOPE_ITERATIONS = 3; private const GENERALIZE_AFTER_ITERATION = 1; private const FOREACH_UNROLL_LIMIT = 16; - private const FOREACH_UNROLL_NESTED_LIMIT = 16; + private const FOREACH_UNROLL_NESTED_LIMIT = 8; /** @var array filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -1450,6 +1450,7 @@ public function processStmtNode( $originalStorage = $storage; $unrolledEndScope = null; + $unrolledTotalKeys = null; if ($context->isTopLevel()) { $storage = $originalStorage->duplicate(); @@ -1458,6 +1459,7 @@ public function processStmtNode( if ($unrolledResult !== null) { $bodyScope = $unrolledResult['bodyScope']; $unrolledEndScope = $unrolledResult['endScope']; + $unrolledTotalKeys = $unrolledResult['totalKeys']; } else { $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback); $count = 0; @@ -1486,7 +1488,8 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); $storage = $originalStorage; $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); - $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); + $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context; + $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); $scopesWithIterableValueType = []; @@ -4079,7 +4082,7 @@ public function processVarAnnotation(MutatingScope $scope, array $variableNames, } /** - * @return array{bodyScope: MutatingScope, endScope: MutatingScope}|null + * @return array{bodyScope: MutatingScope, endScope: MutatingScope, totalKeys: int}|null */ private function tryProcessUnrolledConstantArrayForeach( Foreach_ $stmt, @@ -4114,7 +4117,8 @@ private function tryProcessUnrolledConstantArrayForeach( if ($totalKeys === 0 || $totalKeys > self::FOREACH_UNROLL_LIMIT) { return null; } - if ($context->getForeachUnrollFactor() * $totalKeys > self::FOREACH_UNROLL_NESTED_LIMIT) { + $foreachUnrollFactor = $context->getForeachUnrollFactor(); + if ($foreachUnrollFactor > 1 && $foreachUnrollFactor * $totalKeys > self::FOREACH_UNROLL_NESTED_LIMIT) { return null; } @@ -4242,7 +4246,7 @@ private function tryProcessUnrolledConstantArrayForeach( $endScope = $endScope->mergeWith($breakScope); } - return ['bodyScope' => $bodyScope, 'endScope' => $endScope]; + return ['bodyScope' => $bodyScope, 'endScope' => $endScope, 'totalKeys' => $totalKeys]; } private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee): ?InternalThrowPoint diff --git a/tests/bench/data/bug-14674.php b/tests/bench/data/bug-14674.php new file mode 100644 index 0000000000..8d42eb6295 --- /dev/null +++ b/tests/bench/data/bug-14674.php @@ -0,0 +1,83 @@ +assertTrue($closure()); + } + + /** + * @return iterable + */ + public static function performanceProvider(): iterable { + foreach(['0', '1'] as $level_1) { + $keys = [$level_1]; + + foreach(['0', '1'] as $level_2) { + $keys[] = $level_2; + + foreach(['0', '1'] as $level_3) { + $keys[] = $level_3; + + foreach(['0', '1'] as $level_4) { + $keys[] = $level_4; + + foreach(['0', '1'] as $level_5) { + $keys[] = $level_5; + + foreach(['0', '1'] as $level_6) { + $keys[] = $level_6; + + foreach(['0', '1'] as $level_7) { + $keys[] = $level_7; + + foreach(['0', '1'] as $level_8) { + $keys[] = $level_8; + + foreach(['0', '1'] as $level_9) { + $keys[] = $level_9; + + foreach(['0', '1'] as $level_10) { + $keys[] = $level_10; + + foreach(['0', '1'] as $level_11) { + $keys[] = $level_11; + + foreach(['0', '1'] as $level_12) { + $keys[] = $level_12; + + foreach(['0', '1'] as $level_13) { + $keys[] = $level_13; + + $case = [ + 'closure' => function () use ($level_1, $level_3, $level_5, $level_13) { + return $level_1 === '1' && $level_3 === '1' && $level_5 === '1' && $level_13 === '1'; + }, + ]; + + yield implode('-', $keys) => $case; + } + } + } + } + } + } + } + } + } + } + } + } + } + } + +}