Skip to content

Commit f46f011

Browse files
ondrejmirtesclaude
andcommitted
Cap cumulative unroll factor when nesting unrolled constant-array foreach
Closes phpstan/phpstan#14590 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6a03691 commit f46f011

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class NodeScopeResolver
189189
private const LOOP_SCOPE_ITERATIONS = 3;
190190
private const GENERALIZE_AFTER_ITERATION = 1;
191191
private const FOREACH_UNROLL_LIMIT = 16;
192+
private const FOREACH_UNROLL_NESTED_LIMIT = 16;
192193

193194
/** @var array<string, true> filePath(string) => bool(true) */
194195
private array $analysedFiles = [];
@@ -3896,6 +3897,9 @@ private function tryProcessUnrolledConstantArrayForeach(
38963897
if ($totalKeys === 0 || $totalKeys > self::FOREACH_UNROLL_LIMIT) {
38973898
return null;
38983899
}
3900+
if ($context->getForeachUnrollFactor() * $totalKeys > self::FOREACH_UNROLL_NESTED_LIMIT) {
3901+
return null;
3902+
}
38993903

39003904
$nativeIterateeType = $originalScope->getNativeType($stmt->expr);
39013905
$nativeConstantArrays = $nativeIterateeType->getConstantArrays();
@@ -3908,6 +3912,8 @@ private function tryProcessUnrolledConstantArrayForeach(
39083912
$allChainScopes = [];
39093913
$allBreakScopes = [];
39103914

3915+
$bodyContext = $context->enterUnrolledForeach($totalKeys);
3916+
39113917
foreach ($constantArrays as $arrayIndex => $constantArray) {
39123918
$keyTypes = $constantArray->getKeyTypes();
39133919
$valueTypes = $constantArray->getValueTypes();
@@ -3971,7 +3977,7 @@ private function tryProcessUnrolledConstantArrayForeach(
39713977
$iterScope,
39723978
$iterStorage,
39733979
new NoopNodeCallback(),
3974-
$context,
3980+
$bodyContext,
39753981
)->filterOutLoopExitPoints();
39763982

39773983
$iterEndScope = $bodyResult->getScope();

src/Analyser/StatementContext.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class StatementContext
1515

1616
private function __construct(
1717
private bool $isTopLevel,
18+
private int $foreachUnrollFactor = 1,
1819
)
1920
{
2021
}
@@ -40,13 +41,23 @@ public function isTopLevel(): bool
4041
return $this->isTopLevel;
4142
}
4243

44+
public function getForeachUnrollFactor(): int
45+
{
46+
return $this->foreachUnrollFactor;
47+
}
48+
4349
public function enterDeep(): self
4450
{
4551
if ($this->isTopLevel) {
46-
return self::createDeep();
52+
return new self(false, $this->foreachUnrollFactor);
4753
}
4854

4955
return $this;
5056
}
5157

58+
public function enterUnrolledForeach(int $totalKeys): self
59+
{
60+
return new self($this->isTopLevel, $this->foreachUnrollFactor * $totalKeys);
61+
}
62+
5263
}

tests/bench/data/bug-14590.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Bug14590;
4+
5+
function provider(): array {
6+
$cases = [];
7+
foreach ([1, 2] as $v0) {
8+
foreach ([1, 2] as $v1) {
9+
foreach ([1, 2] as $v2) {
10+
foreach ([1, 2] as $v3) {
11+
foreach ([1, 2] as $v4) {
12+
foreach ([1, 2] as $v5) {
13+
foreach ([1, 2] as $v6) {
14+
foreach ([1, 2] as $v7) {
15+
foreach ([1, 2] as $v8) {
16+
$cases[] = $v0;
17+
}
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
return $cases;
27+
}

0 commit comments

Comments
 (0)