Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Analyser/StmtHandler/DoWhileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public function processStmt(
$previousPass = $passRecord;
$previousPassStorage = $storage;
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null && !$bodyIsReplayable) {
// a nested loop walks only its first iteration in a pass - its unconverged
// types can make the back edge look dead, so a walk converging it decides
$backEdgeScope = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())->filterOutLoopExitPoints()->getLoopBackEdgeScope();
}
$finalScope = $backEdgeScope === null ? $finalScope : $backEdgeScope->mergeWith($finalScope);
foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
Expand Down
6 changes: 6 additions & 0 deletions src/Analyser/StmtHandler/ForHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public function processStmt(
// entry state differs from the previous pass's (StatementsHandler::processLoopPassStmtNodes())
$previousPass = null;
$previousPassStorage = null;
$bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts);
do {
$prevScope = $bodyScope;
$storage = $originalStorage->duplicate();
Expand Down Expand Up @@ -231,6 +232,11 @@ public function processStmt(
$previousPassStorage = $storage;
$passFlows[] = $bodyScopeResult->getVariableFlow();
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null && !$bodyIsReplayable) {
// a nested loop walks only its first iteration in a pass - its unconverged
// types can make the back edge look dead, so a walk converging it decides
$backEdgeScope = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())->filterOutLoopExitPoints()->getLoopBackEdgeScope();
}
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ static function () use ($condResult, $emptyArrayType): Type {
$previousPass = $passRecord;
$previousPassStorage = $storage;
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null && !$bodyIsReplayable) {
// a nested loop walks only its first iteration in a pass - its unconverged
// types can make the back edge look dead, so a walk converging it decides
$backEdgeScope = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())->filterOutLoopExitPoints()->getLoopBackEdgeScope();
}
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
Expand Down Expand Up @@ -868,6 +873,11 @@ private function tryProcessUnrolledConstantArrayForeach(
$iterBodyScope = $this->enterForeach($nodeScopeResolver, $iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback());
$iterBodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$backEdgeScope = $iterBodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null && !$nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts)) {
// a nested loop walks only its first iteration in a pass - its unconverged
// types can make the back edge look dead, so a walk converging it decides
$backEdgeScope = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())->filterOutLoopExitPoints()->getLoopBackEdgeScope();
}
foreach ($iterBodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$endScope = $endScope->mergeWith($breakExitPoint->getScope());
}
Expand Down
5 changes: 5 additions & 0 deletions src/Analyser/StmtHandler/WhileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public function processStmt(
$previousPass = $passRecord;
$previousPassStorage = $storage;
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null && !$bodyIsReplayable) {
// a nested loop walks only its first iteration in a pass - its unconverged
// types can make the back edge look dead, so a walk converging it decides
$backEdgeScope = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())->filterOutLoopExitPoints()->getLoopBackEdgeScope();
}
} finally {
$scope->popExpressionResultStorage();
}
Expand Down
180 changes: 180 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15420.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php declare(strict_types = 1);

namespace Bug15420;

use function PHPStan\Testing\assertType;

class Test {
/** @param string[] $cards */
public function f(array $cards): void
{
for ($i = 0; $i < 10; $i++) {
$seen = [];
$later = [];
foreach ($cards as $card) {
if (array_key_exists($card, $seen)) {
$later[] = $card;
continue;
}
$seen[$card] = true;
}
assertType('int<0, 9>', $i);
if ($later !== []) {
$cards = $later;
} else {
break;
}
}
}
}

/** @param string[] $cards */
function whileLoop(array $cards): void
{
$i = 0;
while ($i < 10) {
$seen = [];
$later = [];
foreach ($cards as $card) {
if (array_key_exists($card, $seen)) {
$later[] = $card;
}
$seen[$card] = true;
}
if ($later === []) {
break;
}
assertType('int<0, 9>', $i);
$i++;
}
}

/** @param string[] $cards */
function doWhileLoop(array $cards): void
{
$i = 0;
do {
$seen = [];
$later = [];
foreach ($cards as $card) {
if (array_key_exists($card, $seen)) {
$later[] = $card;
}
$seen[$card] = true;
}
if ($later === []) {
break;
}
assertType('int<0, max>', $i);
$i++;
} while ($i < 10);
}

/**
* @param string[] $cards
* @param int[] $rounds
*/
function foreachLoop(array $cards, array $rounds): void
{
$i = 0;
foreach ($rounds as $round) {
$seen = [];
$later = [];
foreach ($cards as $card) {
if (array_key_exists($card, $seen)) {
$later[] = $card;
}
$seen[$card] = true;
}
if ($later === []) {
break;
}
assertType('int<0, max>', $i);
$i++;
}
}

/**
* @param string[] $cards
* @param array{a: int, b: int, ...<string, int>} $rounds
*/
function unsealedConstantArrayForeachLoop(array $cards, array $rounds): void
{
$i = 0;
foreach ($rounds as $round) {
$seen = [];
$later = [];
foreach ($cards as $card) {
if (array_key_exists($card, $seen)) {
$later[] = $card;
}
$seen[$card] = true;
}
if ($later === []) {
break;
}
assertType('int<0, max>', $i);
$i++;
}
}

/** @param string[] $cards */
function innerWhile(array $cards): void
{
for ($i = 0; $i < 10; $i++) {
$seen = false;
$later = false;
$j = 0;
while ($j < count($cards)) {
if ($seen) {
$later = true;
}
$seen = true;
$j++;
}
if (!$later) {
break;
}
assertType('int<0, 9>', $i);
}
}

/** @param string[] $cards */
function innerFor(array $cards): void
{
for ($i = 0; $i < 10; $i++) {
$seen = false;
$later = false;
for ($j = 0; $j < count($cards); $j++) {
if ($seen) {
$later = true;
}
$seen = true;
}
if (!$later) {
break;
}
assertType('int<0, 9>', $i);
}
}

/** @param string[] $cards */
function innerDoWhile(array $cards): void
{
for ($i = 0; $i < 10; $i++) {
$seen = false;
$later = false;
$j = 0;
do {
if ($seen) {
$later = true;
}
$seen = true;
$j++;
} while ($j < count($cards));
if (!$later) {
break;
}
assertType('int<0, 9>', $i);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ public function testBug15243(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15243.php'], []);
}

public function testBug15420(): void
{
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15420.php'], []);
}

public function testShiftLeftUnboundedRange(): void
{
$this->analyse([__DIR__ . '/../../Analyser/nsrt/shift-left-unbounded-range.php'], []);
Expand Down
4 changes: 4 additions & 0 deletions turbo-ext/src/DoWhileHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class DoWhileHandler
if (UNEXPECTED(bodyScopeResult.isUndef())) return;
zv::Val backEdgeScope = pt_internal_statement_result_loop_back_edge_scope(bodyScopeResult.raw());
if (UNEXPECTED(backEdgeScope.isUndef())) return;
if (backEdgeScope.isNull() && !bodyIsReplayable) {
backEdgeScope = ptlh::convergedPassBackEdgeScope(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), context);
if (UNEXPECTED(backEdgeScope.isUndef())) return;
}
if (!backEdgeScope.isNull()) {
if (UNEXPECTED(!ptlh::otherMergeWith(backEdgeScope.raw(), finalScope))) return;
}
Expand Down
10 changes: 10 additions & 0 deletions turbo-ext/src/ForHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ class ForHandler
// entry state differs from the previous pass's (StatementsHandler::processLoopPassStmtNodes())
zv::Val previousPass = zv::Val::null();
zv::Val previousPassStorage = zv::Val::null();
bool bodyIsReplayable;
{
zval *bodyStmts = ptsh::readNodeProperty(pt_fh_stmts_site, stmt, PT_LC("stmts"));
if (UNEXPECTED(bodyStmts == NULL)) return zv::Val();
if (UNEXPECTED(!pt_node_scope_resolver_is_replayable_convergence_body(nodeScopeResolver, stmt, bodyStmts, bodyIsReplayable))) return zv::Val();
}
do {
zv::Val prevScope = zv::Val::copyOf(bodyScope.ref());
zv::Val storage = pt_expression_result_storage_duplicate(originalStorage);
Expand Down Expand Up @@ -279,6 +285,10 @@ class ForHandler
}
zv::Val backEdgeScope = pt_internal_statement_result_loop_back_edge_scope(bodyScopeResult.raw());
if (UNEXPECTED(backEdgeScope.isUndef())) return;
if (backEdgeScope.isNull() && !bodyIsReplayable) {
backEdgeScope = ptlh::convergedPassBackEdgeScope(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), context);
if (UNEXPECTED(backEdgeScope.isUndef())) return;
}
if (backEdgeScope.isNull()) {
bodyScope = std::move(prevScope);
brokeOut = true;
Expand Down
12 changes: 12 additions & 0 deletions turbo-ext/src/ForeachHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,10 @@ class ForeachHandler
if (UNEXPECTED(bodyScopeResult.isUndef())) return;
zv::Val backEdgeScope = pt_internal_statement_result_loop_back_edge_scope(bodyScopeResult.raw());
if (UNEXPECTED(backEdgeScope.isUndef())) return;
if (backEdgeScope.isNull() && !bodyIsReplayable) {
backEdgeScope = ptlh::convergedPassBackEdgeScope(nodeScopeResolver, stmt, parts.stmts.raw(), bodyScope.raw(), storage.raw(), context);
if (UNEXPECTED(backEdgeScope.isUndef())) return;
}
if (backEdgeScope.isNull()) {
bodyScope = std::move(prevScope);
brokeOut = true;
Expand Down Expand Up @@ -1724,6 +1728,14 @@ class ForeachHandler
if (UNEXPECTED(iterBodyScopeResult.isUndef())) return false;
zv::Val backEdgeScope = pt_internal_statement_result_loop_back_edge_scope(iterBodyScopeResult.raw());
if (UNEXPECTED(backEdgeScope.isUndef())) return false;
if (backEdgeScope.isNull()) {
bool bodyIsReplayable;
if (UNEXPECTED(!pt_node_scope_resolver_is_replayable_convergence_body(nodeScopeResolver, stmt, parts.stmts.raw(), bodyIsReplayable))) return false;
if (!bodyIsReplayable) {
backEdgeScope = ptlh::convergedPassBackEdgeScope(nodeScopeResolver, stmt, parts.stmts.raw(), iterBodyScope.raw(), iterStorage.raw(), context);
if (UNEXPECTED(backEdgeScope.isUndef())) return false;
}
}
{
zv::Val breaks = ptlh::breakExitPoints(iterBodyScopeResult.raw());
if (UNEXPECTED(breaks.isUndef())) return false;
Expand Down
21 changes: 21 additions & 0 deletions turbo-ext/src/LoopHandlerCalls.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,27 @@ inline zv::Val newPassNodeCallback(bool replayable)
return replayable ? newRecordingNodeCallback() : newNoopNodeCallback();
}

/* a nested loop walks only its first iteration in a pass - its unconverged
* types can make the back edge look dead, so a walk converging it decides:
* $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $entryScope,
* $storage->duplicate(), new NoopNodeCallback(), $context->withoutTemplateArgumentResolution())
* ->filterOutLoopExitPoints()->getLoopBackEdgeScope()
* (UNDEF = pending exception) */
inline zv::Val convergedPassBackEdgeScope(zval *nodeScopeResolver, zval *stmt, zval *stmts, zval *entryScope, zval *storage, zval *context)
{
zv::Val storageCopy = pt_expression_result_storage_duplicate(storage);
if (UNEXPECTED(storageCopy.isUndef())) return zv::Val();
zv::Val noop = newNoopNodeCallback();
if (UNEXPECTED(noop.isUndef())) return zv::Val();
zv::Val walkContext = pt_statement_context_without_template_argument_resolution(context);
if (UNEXPECTED(walkContext.isUndef())) return zv::Val();
zv::Val walked = pt_node_scope_resolver_process_stmt_nodes_internal(nodeScopeResolver, stmt, stmts, entryScope, storageCopy.raw(), noop.raw(), walkContext.raw());
if (UNEXPECTED(walked.isUndef())) return zv::Val();
zv::Val result = pt_internal_statement_result_filter_out_loop_exit_points(walked.raw());
if (UNEXPECTED(result.isUndef())) return zv::Val();
return pt_internal_statement_result_loop_back_edge_scope(result.raw());
}

/* $into = array_merge($into, $more): the array itself when $into is empty and
* $more a list without holes (what array_merge() returns then), appended
* otherwise; false = pending exception */
Expand Down
4 changes: 4 additions & 0 deletions turbo-ext/src/WhileHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ class WhileHandler
bodyScopeResult = pt_internal_statement_result_filter_out_loop_exit_points(walked.raw());
if (UNEXPECTED(bodyScopeResult.isUndef())) return;
backEdgeScope = pt_internal_statement_result_loop_back_edge_scope(bodyScopeResult.raw());
if (UNEXPECTED(backEdgeScope.isUndef())) return;
if (backEdgeScope.isNull() && !bodyIsReplayable) {
backEdgeScope = ptlh::convergedPassBackEdgeScope(nodeScopeResolver, stmt, stmtsHold.raw(), bodyScope.raw(), storage.raw(), context);
}
}();
pt_finally([&]() { (void) pt_mutating_scope_pop_expression_result_storage(Z_OBJ_P(scope.raw())); });
if (UNEXPECTED(EG(exception))) return zv::Val();
Expand Down
Loading