Skip to content
Merged
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: 4 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3530,7 +3530,8 @@ public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals =
$ourExpressionTypes = $this->expressionTypes;
$theirExpressionTypes = $otherScope->expressionTypes;

$mergedExpressionTypes = ScopeOps::mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes);
$differingExpressionKeys = [];
$mergedExpressionTypes = ScopeOps::mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes, $differingExpressionKeys);
$conditionalExpressions = ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $otherScope->conditionalExpressions);
if ($preserveVacuousConditionals) {
$conditionalExpressions = $this->preserveVacuousConditionalExpressions(
Expand All @@ -3549,12 +3550,14 @@ public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals =
$ourExpressionTypes,
$theirExpressionTypes,
$mergedExpressionTypes,
$differingExpressionKeys,
);
$conditionalExpressions = ScopeOps::createConditionalExpressions(
$conditionalExpressions,
$theirExpressionTypes,
$ourExpressionTypes,
$mergedExpressionTypes,
$differingExpressionKeys,
);

[$mergedExpressionTypes, $mergedNativeTypes] = ScopeOps::finishMerge(
Expand Down
3 changes: 1 addition & 2 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1629,7 +1629,7 @@ public function processStmtNode(
}
}
$keyLoopTypes[] = $scopeWithIterableValueType->getType($keyVarExpr);
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($keyVarExpr);
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($keyVarExpr);
} else {
// No key variable: the narrowed value var is the array element type directly.
$dimFetchType = $scopeWithIterableValueType->getVariableType($stmt->valueVar->name);
Expand Down Expand Up @@ -1944,7 +1944,6 @@ public function processStmtNode(
foreach ($stmt->cond as $condExpr) {
$condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
$initScope = $condResult->getScope();
$condResultScope = $condResult->getScope();

// only the last condition expression is relevant whether the loop continues
// see https://www.php.net/manual/en/control-structures.for.php
Expand Down
33 changes: 28 additions & 5 deletions src/Analyser/ScopeOps.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use function array_filter;
use function array_key_exists;
use function array_key_first;
use function array_keys;
use function array_slice;
use function count;
use function get_class;
Expand Down Expand Up @@ -195,9 +196,10 @@ public static function scopeWith(
*
* @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
* @param array<string, ExpressionTypeHolder> $theirVariableTypeHolders
* @param array<string, true> $differingKeys
* @return array<string, ExpressionTypeHolder>
*/
public static function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array
public static function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders, array &$differingKeys = []): array
{
$intersectedVariableTypeHolders = [];
$globalVariableCallback = static fn (Node $node) => $node instanceof Variable && is_string($node->name) && in_array($node->name, Scope::SUPERGLOBAL_VARIABLES, true);
Expand All @@ -209,8 +211,10 @@ public static function mergeVariableHolders(array $ourVariableTypeHolders, array
continue;
}

$differingKeys[$exprString] = true;
$intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]);
} else {
$differingKeys[$exprString] = true;
$expr = $variableTypeHolder->getExpr();

$containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME);
Expand All @@ -231,6 +235,7 @@ public static function mergeVariableHolders(array $ourVariableTypeHolders, array
continue;
}

$differingKeys[$exprString] = true;
$expr = $variableTypeHolder->getExpr();

$containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME);
Expand Down Expand Up @@ -358,13 +363,15 @@ public static function intersectConditionalExpressions(array $ourConditionalExpr
* @param array<string, ExpressionTypeHolder> $ourExpressionTypes
* @param array<string, ExpressionTypeHolder> $theirExpressionTypes
* @param array<string, ExpressionTypeHolder> $mergedExpressionTypes
* @param array<string, true> $differingKeys
* @return array<string, ConditionalExpressionHolder[]>
*/
public static function createConditionalExpressions(
array $conditionalExpressions,
array $ourExpressionTypes,
array $theirExpressionTypes,
array $mergedExpressionTypes,
array $differingKeys,
): array
{
$newVariableTypes = $ourExpressionTypes;
Expand All @@ -375,7 +382,11 @@ public static function createConditionalExpressions(
// branch — but it remains a valid conditional *target*, so only exclude
// it from guard selection instead of dropping it entirely.
$guardsToExclude = [];
foreach ($theirExpressionTypes as $exprString => $holder) {
foreach (array_keys($differingKeys) as $exprString) {
if (!array_key_exists($exprString, $theirExpressionTypes)) {
continue;
}
$holder = $theirExpressionTypes[$exprString];
if (!array_key_exists($exprString, $mergedExpressionTypes)) {
continue;
}
Expand All @@ -396,7 +407,11 @@ public static function createConditionalExpressions(
}

$typeGuards = [];
foreach ($newVariableTypes as $exprString => $holder) {
foreach (array_keys($differingKeys) as $exprString) {
if (!array_key_exists($exprString, $newVariableTypes)) {
continue;
}
$holder = $newVariableTypes[$exprString];
if ($holder->getExpr() instanceof VirtualNode) {
continue;
}
Expand Down Expand Up @@ -436,7 +451,11 @@ public static function createConditionalExpressions(
$guardIsSuperTypeOfTheirExprCache = [];
$theirExprIsSuperTypeOfGuardCache = [];

foreach ($newVariableTypes as $exprString => $holder) {
foreach (array_keys($differingKeys) as $exprString) {
if (!array_key_exists($exprString, $newVariableTypes)) {
continue;
}
$holder = $newVariableTypes[$exprString];
if ($holder->getExpr() instanceof VirtualNode) {
continue;
}
Expand Down Expand Up @@ -497,7 +516,11 @@ public static function createConditionalExpressions(
}
}

foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) {
foreach (array_keys($differingKeys) as $exprString) {
if (!array_key_exists($exprString, $mergedExpressionTypes)) {
continue;
}
$mergedExprTypeHolder = $mergedExpressionTypes[$exprString];
if (array_key_exists($exprString, $ourExpressionTypes)) {
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class TurboExtensionEnabler
* version is the short SHA of the last commit touching turbo-ext/src/,
* enforced by the phar.yml turbo-version job.
*/
public const EXPECTED_EXTENSION_VERSION = 'e9ce4dc';
public const EXPECTED_EXTENSION_VERSION = '86312b7';

private static bool $typeCombinatorCacheEnabled = false;

Expand Down
112 changes: 82 additions & 30 deletions turbo-ext/src/ScopeOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,15 @@ class ScopeOps
return zv::Val::adopt(out);
}

/* Mirrors ScopeOps::mergeVariableHolders(). */
static zv::Val mergeVariableHolders(zv::TableRef ours, zv::TableRef theirs)
/*
* Mirrors ScopeOps::mergeVariableHolders(). differing (nullable) receives
* a true marker for every key that is not one shared holder on both sides
* — the twin's &$differingKeys out-parameter.
*/
static zv::Val mergeVariableHolders(zv::TableRef ours, zv::TableRef theirs, HashTable *differing)
{
zv::Arr merged = zv::Arr::create(ours.size());
if (UNEXPECTED(!mergeVariableHoldersInto(merged, ours, theirs))) {
if (UNEXPECTED(!mergeVariableHoldersInto(merged, ours, theirs, differing))) {
return zv::Val();
}
return zv::Val(std::move(merged));
Expand Down Expand Up @@ -394,7 +398,7 @@ class ScopeOps

/* mergedNative += filter(mergeVariableHolders(oursRemaining, theirsRemaining)) */
{
zv::Val remainingMerged = mergeVariableHolders(zv::TableRef(oursNativeRemaining.table()), zv::TableRef(theirsNativeRemaining.table()));
zv::Val remainingMerged = mergeVariableHolders(zv::TableRef(oursNativeRemaining.table()), zv::TableRef(theirsNativeRemaining.table()), NULL);
if (UNEXPECTED(remainingMerged.isUndef())) {
return zv::Val();
}
Expand Down Expand Up @@ -461,7 +465,7 @@ class ScopeOps
* per-guard caches; the input array is only duplicated once the first
* conditional is actually appended.
*/
static zv::Val createConditionalExpressions(zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged)
static zv::Val createConditionalExpressions(zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged, zv::TableRef differingKeys)
{
zend_class_entry *virtualNodeCe = pt_class(PT_CLASS_VIRTUAL_NODE);
if (UNEXPECTED(virtualNodeCe == NULL)) {
Expand All @@ -472,16 +476,22 @@ class ScopeOps
zv::ScratchTable typeGuards(8);

/* guardsToExclude: subtype-absorbed their-branch variables are poor
* guards but stay valid conditional targets */
for (auto entry : theirs) {
zend_string *key = entry.stringKeyOrNull();
zend_ulong idx = entry.indexKey();
* guards but stay valid conditional targets. Only the merge's differing
* keys can qualify — iterate those (in their insertion order, like the
* twin) instead of the whole holder maps. */
for (auto diffEntry : differingKeys) {
zend_string *key = diffEntry.stringKeyOrNull();
zend_ulong idx = diffEntry.indexKey();

zval *theirSlot = pt_ht_find(theirs.table(), key, idx);
if (theirSlot == NULL) {
continue;
}
zval *mergedSlot = pt_ht_find(merged.table(), key, idx);
if (mergedSlot == NULL) {
continue;
}
zv::Ref holder = entry.value().deref();
zv::Ref holder = zv::Ref(theirSlot).deref();
if (UNEXPECTED(!pt_check_holder(holder.raw()))) {
return zv::Val();
}
Expand Down Expand Up @@ -522,11 +532,15 @@ class ScopeOps
}

/* typeGuards */
for (auto entry : ours) {
zend_string *key = entry.stringKeyOrNull();
zend_ulong idx = entry.indexKey();
zv::Ref holder = entry.value().deref();
for (auto diffEntry : differingKeys) {
zend_string *key = diffEntry.stringKeyOrNull();
zend_ulong idx = diffEntry.indexKey();

zval *ourSlot = pt_ht_find(ours.table(), key, idx);
if (ourSlot == NULL) {
continue;
}
zv::Ref holder = zv::Ref(ourSlot).deref();
if (UNEXPECTED(!pt_check_holder(holder.raw()))) {
return zv::Val();
}
Expand Down Expand Up @@ -585,10 +599,15 @@ class ScopeOps
zv::Arr result; /* stays UNDEF until the first append duplicates the input */

/* main loop: pair non-merged expressions with guards */
for (auto entry : ours) {
zend_string *key = entry.stringKeyOrNull();
zend_ulong idx = entry.indexKey();
zv::Ref holder = entry.value().deref();
for (auto diffEntry : differingKeys) {
zend_string *key = diffEntry.stringKeyOrNull();
zend_ulong idx = diffEntry.indexKey();

zval *ourSlot = pt_ht_find(ours.table(), key, idx);
if (ourSlot == NULL) {
continue;
}
zv::Ref holder = zv::Ref(ourSlot).deref();

if (instanceof_function(holderExpr(holder)->ce, virtualNodeCe)) {
continue;
Expand Down Expand Up @@ -711,14 +730,18 @@ class ScopeOps
}

/* their-only expressions: record certainty-No conditionals per guard */
for (auto entry : merged) {
zend_string *key = entry.stringKeyOrNull();
zend_ulong idx = entry.indexKey();
for (auto diffEntry : differingKeys) {
zend_string *key = diffEntry.stringKeyOrNull();
zend_ulong idx = diffEntry.indexKey();

zval *mergedSlot = pt_ht_find(merged.table(), key, idx);
if (mergedSlot == NULL) {
continue;
}
if (pt_ht_exists(ours.table(), key, idx)) {
continue;
}
zv::Ref mergedHolder = entry.value().deref();
zv::Ref mergedHolder = zv::Ref(mergedSlot).deref();
if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) {
return zv::Val();
}
Expand Down Expand Up @@ -1381,8 +1404,19 @@ class ScopeOps
return zv::Val::adopt(created);
}

/* $differing[$key] = true (marker insert, overwrites) */
static void markDiffering(HashTable *differing, zend_string *skey, zend_ulong idx)
{
if (differing == NULL) {
return;
}
zval trueZv;
ZVAL_TRUE(&trueZv);
pt_ht_update(differing, skey, idx, &trueZv);
}

/* The two loops of mergeVariableHolders(), filling a caller-owned table. */
static bool mergeVariableHoldersInto(zv::Arr &merged, zv::TableRef ours, zv::TableRef theirs)
static bool mergeVariableHoldersInto(zv::Arr &merged, zv::TableRef ours, zv::TableRef theirs, HashTable *differing)
{
for (auto entry : ours) {
zend_string *key = entry.stringKeyOrNull();
Expand All @@ -1402,13 +1436,15 @@ class ScopeOps
if (holder.asObject() == theirHolder.asObject()) {
tableAddNewCopy(merged.table(), key, idx, holder);
} else {
markDiffering(differing, key, idx);
zval andHolder;
if (UNEXPECTED(!pt_holder_and(holder.raw(), theirHolder.raw(), &andHolder))) {
return false;
}
tableAddNew(merged.table(), key, idx, zv::Val::adopt(andHolder));
}
} else {
markDiffering(differing, key, idx);
bool containsSuperGlobal = pt_expr_contains_superglobal(holderExpr(holder));
if (UNEXPECTED(EG(exception))) {
return false;
Expand All @@ -1427,6 +1463,7 @@ class ScopeOps
if (pt_ht_exists(merged.table(), key, idx)) {
continue;
}
markDiffering(differing, key, idx);
zv::Ref holder = entry.value().deref();
if (UNEXPECTED(!pt_check_holder(holder.raw()))) {
return false;
Expand Down Expand Up @@ -2060,13 +2097,27 @@ void pt_register_scope_ops()
{
reg::Class cls("PHPStanTurbo\\ScopeOps");

cls.method("mergeVariableHolders", reg::PublicStatic, 2, { reg::arrayArg("ourVariableTypeHolders"), reg::arrayArg("theirVariableTypeHolders") }, [](INTERNAL_FUNCTION_PARAMETERS) {
cls.method("mergeVariableHolders", reg::PublicStatic, 2, { reg::arrayArg("ourVariableTypeHolders"), reg::arrayArg("theirVariableTypeHolders"), reg::any("differingKeys", true) }, [](INTERNAL_FUNCTION_PARAMETERS) {
HashTable *ours, *theirs;
ZEND_PARSE_PARAMETERS_START(2, 2)
zval *differing_zv = NULL;
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_ARRAY_HT(ours)
Z_PARAM_ARRAY_HT(theirs)
Z_PARAM_OPTIONAL
Z_PARAM_ZVAL(differing_zv)
ZEND_PARSE_PARAMETERS_END();
zv::Val result = ScopeOps::mergeVariableHolders(zv::TableRef(ours), zv::TableRef(theirs));
HashTable *differing = NULL;
if (differing_zv != NULL && Z_ISREF_P(differing_zv)) {
/* the twin declares `array &$differingKeys = []`; vivify like PHP
* would and write through the reference */
zval *inner = Z_REFVAL_P(differing_zv);
if (Z_TYPE_P(inner) != IS_ARRAY) {
convert_to_array(inner);
}
SEPARATE_ARRAY(inner);
differing = Z_ARRVAL_P(inner);
}
zv::Val result = ScopeOps::mergeVariableHolders(zv::TableRef(ours), zv::TableRef(theirs), differing);
if (UNEXPECTED(result.isUndef())) {
RETURN_THROWS();
}
Expand Down Expand Up @@ -2184,15 +2235,16 @@ void pt_register_scope_ops()
result.intoReturnValue(return_value);
});

cls.method("createConditionalExpressions", reg::PublicStatic, 4, { reg::arrayArg("conditionalExpressions"), reg::arrayArg("ourExpressionTypes"), reg::arrayArg("theirExpressionTypes"), reg::arrayArg("mergedExpressionTypes") }, [](INTERNAL_FUNCTION_PARAMETERS) {
HashTable *conditional, *ours, *theirs, *merged;
ZEND_PARSE_PARAMETERS_START(4, 4)
cls.method("createConditionalExpressions", reg::PublicStatic, 5, { reg::arrayArg("conditionalExpressions"), reg::arrayArg("ourExpressionTypes"), reg::arrayArg("theirExpressionTypes"), reg::arrayArg("mergedExpressionTypes"), reg::arrayArg("differingKeys") }, [](INTERNAL_FUNCTION_PARAMETERS) {
HashTable *conditional, *ours, *theirs, *merged, *differing_keys;
ZEND_PARSE_PARAMETERS_START(5, 5)
Z_PARAM_ARRAY_HT(conditional)
Z_PARAM_ARRAY_HT(ours)
Z_PARAM_ARRAY_HT(theirs)
Z_PARAM_ARRAY_HT(merged)
Z_PARAM_ARRAY_HT(differing_keys)
ZEND_PARSE_PARAMETERS_END();
zv::Val result = ScopeOps::createConditionalExpressions(zv::TableRef(conditional), zv::TableRef(ours), zv::TableRef(theirs), zv::TableRef(merged));
zv::Val result = ScopeOps::createConditionalExpressions(zv::TableRef(conditional), zv::TableRef(ours), zv::TableRef(theirs), zv::TableRef(merged), zv::TableRef(differing_keys));
if (UNEXPECTED(result.isUndef())) {
RETURN_THROWS();
}
Expand Down
Loading
Loading