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
23 changes: 16 additions & 7 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
use function abs;
use function array_diff_key;
use function array_filter;
use function array_intersect_key;
use function array_key_exists;
use function array_keys;
use function array_last;
Expand Down Expand Up @@ -1515,17 +1516,25 @@ private static function getVariablesOfExpressionKey(string $key): ?array
public function withRecordedStatementDelta(self $recordedEntry, self $recordedExit): self
{
$conditionalExpressions = $this->conditionalExpressions;
foreach ($recordedExit->conditionalExpressions as $key => $holders) {
if (isset($recordedEntry->conditionalExpressions[$key]) && $recordedEntry->conditionalExpressions[$key] === $holders) {
foreach (array_keys($recordedExit->conditionalExpressions + $recordedEntry->conditionalExpressions) as $key) {
$entryHolders = $recordedEntry->conditionalExpressions[$key] ?? [];
$exitHolders = $recordedExit->conditionalExpressions[$key] ?? [];
if ($entryHolders === $exitHolders) {
continue;
}
$conditionalExpressions[$key] = $holders;
}
foreach (array_keys($recordedEntry->conditionalExpressions) as $key) {
if (isset($recordedExit->conditionalExpressions[$key])) {
$currentHolders = $conditionalExpressions[$key] ?? [];
if ($currentHolders !== $entryHolders) {
// the statement changed the holders of an expression whose holders
// differ here (e.g. it assigned the guard of one): the recorded
// exit's holders describe the recorded state, so keep only this
// scope's holders that the recorded walk kept
$exitHolders = array_intersect_key($currentHolders, $entryHolders, $exitHolders);
}
if ($exitHolders === []) {
unset($conditionalExpressions[$key]);
continue;
}
unset($conditionalExpressions[$key]);
$conditionalExpressions[$key] = $exitHolders;
}

return $this->duplicateWith(
Expand Down
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/Bug15736Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

/**
* Without bleeding edge, which builds the loop's array shapes differently
* and does not reach the replayed statement this covers.
*/
class Bug15736Test extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-15736.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

}
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/data/bug-15736.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace Bug15736;

use stdClass;
use function PHPStan\Testing\assertType;

/** @return array{value: int}|false */
function fetchRow(object $result)
{
return rand(0, 1) === 1 ? ['value' => 1] : false;
}

function query(): object
{
return new stdClass();
}

function doFoo(object $rows): void
{
$values = [];
$header = 0;
$cols = 0;
$rowCount = 0;
$items = query();
while ($row = fetchRow($rows)) {
$header++;
if ($header === 1) {
while ($item = fetchRow($items)) {
$cols++;
}
}

$col = 0;
$items = query();
while ($item = fetchRow($items)) {
$values[$col][$rowCount] = $item['value'];
$col++;
}
$rowCount++;
}

assertType('array<int<0, max>, non-empty-array<int<0, max>, int>>', $values);
for ($col = 0; $col < $cols; $col++) {
assertType('array<int<0, max>, non-empty-array<int<0, max>, int>>', $values);
for ($r = 0; $r < $rowCount; $r++) {
echo $values[$col][$r];
}
}
}
50 changes: 41 additions & 9 deletions turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2996,6 +2996,42 @@ class MutatingScope
return true;
}

/* withRecordedStatementDelta()'s step for one conditional expression
* key; a missing or null holder list (NULL here) is the twin's `?? []` */
static void applyRecordedConditionalDelta(zv::Arr &conditionalExpressions, const zv::ArrayEntry &key, zval *entryHolders, zval *exitHolders)
{
zval empty;
ZVAL_EMPTY_ARRAY(&empty);
if (entryHolders == NULL || Z_TYPE_P(entryHolders) == IS_NULL) entryHolders = &empty;
if (exitHolders == NULL || Z_TYPE_P(exitHolders) == IS_NULL) exitHolders = &empty;
if (zend_is_identical(entryHolders, exitHolders)) return;

zval *currentHolders = findByEntryKey(conditionalExpressions.table(), key);
if (currentHolders == NULL || Z_TYPE_P(currentHolders) == IS_NULL) currentHolders = &empty;
zval kept;
if (!zend_is_identical(currentHolders, entryHolders)) {
/* array_intersect_key($currentHolders, $entryHolders, $exitHolders) */
zv::Arr intersected = zv::Arr::create(0);
for (auto holder : zv::TableRef(Z_ARRVAL_P(currentHolders))) {
if (findByEntryKey(Z_ARRVAL_P(entryHolders), holder) == NULL || findByEntryKey(Z_ARRVAL_P(exitHolders), holder) == NULL) continue;
zval copy;
ZVAL_COPY(&copy, holder.value().raw());
updateByEntryKey(intersected.table(), holder, &copy);
}
kept = intersected.take();
} else {
ZVAL_COPY(&kept, exitHolders);
}

conditionalExpressions.separate();
if (zend_hash_num_elements(Z_ARRVAL(kept)) == 0) {
zval_ptr_dtor(&kept);
deleteByEntryKey(conditionalExpressions.table(), key);
return;
}
updateByEntryKey(conditionalExpressions.table(), key, &kept);
}

/* This scope after a statement whose recorded walk stands. */
zv::Val withRecordedStatementDelta(zend_object *recordedEntryObject, zend_object *recordedExitObject)
{
Expand All @@ -3009,18 +3045,14 @@ class MutatingScope
zv::Arr conditionalExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()));
HashTable *entryConditionals = Z_ARRVAL_P(recordedEntry.slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw());
HashTable *exitConditionals = Z_ARRVAL_P(recordedExit.slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw());
/* the twin's `$recordedExit + $recordedEntry` key walk: the exit's
* keys, then the keys only the entry has */
for (auto entry : zv::TableRef(exitConditionals)) {
zval *entryHolders = findByEntryKey(entryConditionals, entry);
if (entryHolders != NULL && Z_TYPE_P(entryHolders) != IS_NULL && zend_is_identical(entryHolders, entry.value().raw())) continue;
conditionalExpressions.separate();
zval copy;
ZVAL_COPY(&copy, entry.value().raw());
updateByEntryKey(conditionalExpressions.table(), entry, &copy);
applyRecordedConditionalDelta(conditionalExpressions, entry, findByEntryKey(entryConditionals, entry), entry.value().raw());
}
for (auto entry : zv::TableRef(entryConditionals)) {
if (issetByEntryKey(exitConditionals, entry)) continue;
conditionalExpressions.separate();
deleteByEntryKey(conditionalExpressions.table(), entry);
if (findByEntryKey(exitConditionals, entry) != NULL) continue;
applyRecordedConditionalDelta(conditionalExpressions, entry, entry.value().raw(), NULL);
}

static const struct { uint32_t slot; const char *name; } holderTables[] = {
Expand Down
Loading