diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 8ce937dc..3f7e2e82 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -1241,6 +1241,43 @@ class SsaAnalysisTest extends TestCase $this->assertArrayNotHasKey('n', $locals); } + public function testLoopVarOptimizerRejectsCounterReusedAsForeachKey(): void + { + $locals = $this->optimizeLoopVarsForCode(' + foreach (["name" => 1] as $i => $value) { + echo $value; + } + for ($i = 0; $i < 10; $i++) { + echo $i; + } + '); + + $this->assertArrayNotHasKey('i', $locals); + } + + public function testLoopVarOptimizerRejectsCounterReusedAsForeachValueOrDestructuringTarget(): void + { + $valueLocals = $this->optimizeLoopVarsForCode(' + foreach (["value"] as $i) { + echo $i; + } + for ($i = 0; $i < 10; $i++) { + echo $i; + } + '); + $listLocals = $this->optimizeLoopVarsForCode(' + foreach ([["value"]] as [$i]) { + echo $i; + } + for ($i = 0; $i < 10; $i++) { + echo $i; + } + '); + + $this->assertArrayNotHasKey('i', $valueLocals); + $this->assertArrayNotHasKey('i', $listLocals); + } + // ======================================================================== // SsaTypeOptimizer: detectSsaDefType // ======================================================================== diff --git a/src/Optimizer/LoopVarOptimizer.php b/src/Optimizer/LoopVarOptimizer.php index 601d4a39..b0b1349c 100644 --- a/src/Optimizer/LoopVarOptimizer.php +++ b/src/Optimizer/LoopVarOptimizer.php @@ -758,6 +758,17 @@ trait LoopVarOptimizer } } + if ($expr instanceof Stmt\Foreach_) { + // Foreach key/value variables are assigned implicitly on every + // iteration. A name that is also used by a range-proven `for` + // counter cannot share a function-scoped php::Int slot with an + // arbitrary key or value (including list destructuring targets). + if (($expr->keyVar instanceof Node && $this->exprUsesVar($expr->keyVar, $varName)) + || $this->exprUsesVar($expr->valueVar, $varName)) { + return true; + } + } + foreach ($expr->getSubNodeNames() as $name) { $value = $expr->$name; if ($value instanceof Node) { diff --git a/tests/compiler/optimizations/loop-counter-reused-by-foreach.phpt b/tests/compiler/optimizations/loop-counter-reused-by-foreach.phpt new file mode 100644 index 00000000..7af0ff9e --- /dev/null +++ b/tests/compiler/optimizations/loop-counter-reused-by-foreach.phpt @@ -0,0 +1,43 @@ +--TEST-- +Loop counter narrowing rejects variables previously assigned by foreach +--FILE-- + 10, 20 => 30] as $i => $value) { + var_dump($i, $value); + } + + for ($i = 0; $i < 2; $i++) { + var_dump($i); + } + + foreach (['text'] as $i) { + var_dump($i); + } + + for ($i = 0; $i < 1; $i++) { + var_dump($i); + } + + foreach ([['nested']] as [$i]) { + var_dump($i); + } + + for ($i = 0; $i < 1; $i++) { + var_dump($i); + } +} +?> +--EXPECT-- +string(4) "name" +int(10) +int(20) +int(30) +int(0) +int(1) +string(4) "text" +int(0) +string(6) "nested" +int(0)