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
104 changes: 70 additions & 34 deletions src/Analyser/VariableLivenessResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Throwable;
use function array_diff_key;
use function array_fill_keys;
use function array_key_exists;
use function array_keys;
use function array_pop;
use function array_reverse;
Expand Down Expand Up @@ -89,6 +92,9 @@ final class VariableLivenessResolver
/** @var array<string, array<string, array<string, int>>> variable name => replaced slot => marker => id of the write leaving it */
private array $markersBySlot = [];

/** @var array<string, array<string, int>> variable name => marker => id of the write leaving it */
private array $markersByName = [];

/** @var array<int, true> */
private array $overwrittenIds = [];

Expand Down Expand Up @@ -264,9 +270,7 @@ private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContex
$this->observeOverwrite($item, $next);
}
}
foreach (array_keys($this->killedKeys[$id] ?? []) as $key) {
unset($next[$key]);
}
$next = self::withoutKeys($next, $this->killedKeys[$id] ?? []);
$next = $this->killOverwriteMarkers($flow->write, $next);
if (isset($this->overwriteMarkers[$id])) {
$next[$this->overwriteMarkers[$id]] = true;
Expand Down Expand Up @@ -532,6 +536,14 @@ private function compileAccesses(): void
$this->nameKeys[$name][$key] = true;
}
}
// the keys of every slot, shared by all writes of the whole variable
// instead of copied into each of them
$allKeys = [];
foreach ($keysBySlot as $keys) {
$allKeys += $keys;
}
$offsetKeys = array_diff_key($allKeys, $keysBySlot['container'] ?? []);
$allKills = array_fill_keys(array_keys($allKeys), true);
foreach ($accesses as $access) {
$write = $access->write;
if ($write === null) {
Expand All @@ -540,25 +552,21 @@ private function compileAccesses(): void
$id = $write->getId();
$offset = $write->getOffset();
if ($write->isOffsetWrite() && $offset !== null) {
$slot = self::offsetKey($offset);
$selectedKeys = [$slot => $keysBySlot[$slot] ?? []];
$keys = $keysBySlot[self::offsetKey($offset)] ?? [];
$kills = $write->replacesOffset();
} else {
$selectedKeys = $keysBySlot;
if ($write->isOffsetWrite()) {
unset($selectedKeys['container']);
}
$keys = $write->isOffsetWrite() ? $offsetKeys : $allKeys;
$kills = !$write->isOffsetWrite();
}
$kills = !$write->isOffsetWrite() || ($offset !== null && $write->replacesOffset());
foreach ($selectedKeys as $keys) {
foreach ($keys as $key => $targetId) {
$this->observedKeys[$id][$key] = $targetId;
if (!$kills) {
continue;
}

$this->killedKeys[$id][$key] = true;
}
if ($keys === []) {
continue;
}
$this->observedKeys[$id] = isset($this->observedKeys[$id]) ? $this->observedKeys[$id] + $keys : $keys;
if (!$kills) {
continue;
}
$killedKeys = $keys === $allKeys ? $allKills : array_fill_keys(array_keys($keys), true);
$this->killedKeys[$id] = isset($this->killedKeys[$id]) ? $this->killedKeys[$id] + $killedKeys : $killedKeys;
}
$this->compileOverwrites($name, $accesses);
}
Expand Down Expand Up @@ -597,6 +605,11 @@ private function compileOverwrites(string $name, array $accesses): void
}

$this->markersBySlot[$name] = $markersBySlot;
$markersOfName = [];
foreach ($markersBySlot as $markers) {
$markersOfName += $markers;
}
$this->markersByName[$name] = $markersOfName;
}

/** The slot a write replaces: '' for the whole variable, null when it keeps the old value. */
Expand All @@ -616,7 +629,19 @@ private static function replacedSlot(VariableWrite $write): ?string
/** @param array<string, true> $next */
private function observeWrite(int $id, array $next): void
{
foreach ($this->observedKeys[$id] ?? [] as $key => $targetId) {
$observedKeys = $this->observedKeys[$id] ?? [];
// walk the smaller of the two sets
if (count($observedKeys) > count($next)) {
$live = [];
foreach (array_keys($next) as $key) {
if (!array_key_exists($key, $observedKeys)) {
continue;
}
$live[$key] = $observedKeys[$key];
}
$observedKeys = $live;
}
foreach ($observedKeys as $key => $targetId) {
if (!isset($next[$key])) {
continue;
}
Expand Down Expand Up @@ -689,23 +714,34 @@ private function killOverwriteMarkers(VariableWrite $write, array $next): array
if ($slot === null) {
return $next;
}
foreach ($markersBySlot as $markerSlot => $markers) {
if ($slot !== '' && $markerSlot !== $slot) {
continue;
if ($slot === '') {
return self::withoutKeys($next, $this->markersByName[$write->getVariableName()]);
}

return self::withoutKeys($next, $markersBySlot[$slot] ?? []);
}

/**
* Walks the smaller of the two sets.
*
* @param array<string, true> $next
* @param array<string, mixed> $keys
* @return array<string, true>
*/
private static function withoutKeys(array $next, array $keys): array
{
if (count($keys) <= count($next)) {
foreach (array_keys($keys) as $key) {
unset($next[$key]);
}
// walk the smaller of the two sets
if (count($markers) <= count($next)) {
foreach (array_keys($markers) as $marker) {
unset($next[$marker]);
}

return $next;
}
foreach (array_keys($next) as $key) {
if (!array_key_exists($key, $keys)) {
continue;
}
foreach (array_keys($next) as $key) {
if (!isset($markers[$key])) {
continue;
}
unset($next[$key]);
}
unset($next[$key]);
}

return $next;
Expand Down
9 changes: 7 additions & 2 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class UnionType implements CompoundType

private ?TrinaryLogic $isCallable = null;

private ?bool $hasTemplateOrLateResolvableType = null;

/** @var array<int, Type> */
private array $possiblyLooselyEqualTypes = [];

Expand Down Expand Up @@ -1814,15 +1816,18 @@ private function notBenevolentPickFromTypes(callable $getValues): array

public function hasTemplateOrLateResolvableType(): bool
{
if ($this->hasTemplateOrLateResolvableType !== null) {
return $this->hasTemplateOrLateResolvableType;
}
foreach ($this->types as $type) {
if (!$type->hasTemplateOrLateResolvableType()) {
continue;
}

return true;
return $this->hasTemplateOrLateResolvableType = true;
}

return false;
return $this->hasTemplateOrLateResolvableType = false;
}

public function getPossiblyLooselyEqualType(PhpVersion $phpVersion): Type
Expand Down
22 changes: 16 additions & 6 deletions turbo-ext/src/UnionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* native BenevolentUnionType and the PHP TemplateUnionType extend it — their
* constructors call parent::__construct(), so the constructor is a proper
* method), implementing PHPStan\Type\CompoundType. State is the twin's
* ten private properties, declared typed property slots in the twin's
* declaration order — the eight memos ($sortedTypesCache, $cachedDescriptions,
* eleven private properties, declared typed property slots in the twin's
* declaration order — the nine memos ($sortedTypesCache, $cachedDescriptions,
* $finiteTypeSet, $finiteTypes, $isNull, $isCallable,
* $possiblyLooselyEqualTypes, $alwaysLooselyEqualTypes) first, the promoted
* $hasTemplateOrLateResolvableType, $possiblyLooselyEqualTypes,
* $alwaysLooselyEqualTypes) first, the promoted
* $types and $normalized after them — so the std object handlers do
* GC/clone and a PHP subclass's own properties follow them. The one trait
* the twin uses (NonGeneralizableTypeTrait) comes from the shared registrar
Expand Down Expand Up @@ -2145,18 +2146,26 @@ class UnionType
}

/* whether any member has one; -1 = pending exception */
/* memoized in $hasTemplateOrLateResolvableType */
int hasTemplateOrLateResolvableType() const
{
zval *memo = OBJ_PROP_NUM(self, slots::hasTemplateOrLateResolvableType);
if (Z_TYPE_P(memo) == IS_TRUE) return 1;
if (Z_TYPE_P(memo) == IS_FALSE) return 0;
zval *types = this->types();
if (UNEXPECTED(types == NULL)) return -1;
zv::Val typesCopy = zv::Val::copyOf(zv::Ref(types));
int result = 0;
for (zv::ArrayEntry entry : zv::ArrRef(typesCopy.raw())) {
int has = pt_type_call_is_true(entry.value().deref().asObject(), PT_LC("hastemplateorlateresolvabletype"), 0, NULL);
if (UNEXPECTED(has < 0)) return -1;
if (has == 0) continue;
return 1;
result = 1;
break;
}
return 0;
memo = OBJ_PROP_NUM(self, slots::hasTemplateOrLateResolvableType);
zv::Ref(memo).assign(zv::Val::boolean(result == 1));
return result;
}

/* the three protected methods: this class's own body when the object's
Expand Down Expand Up @@ -3031,14 +3040,15 @@ PT_MINIT_REGISTRATION(pt_register_union_type)
PT_RETURN_VAL(PT_THIS.memoizedLooselyEqualType(slots::alwaysLooselyEqualTypes, phpVersion, PT_LC("getalwayslooselyequaltype")));
});
cls.classConstantValue("EQUAL_UNION_CLASSES", pt_ut_equal_union_classes);
/* the slots PT_UT_PROP_*: the eight memo properties first, the promoted
/* the slots PT_UT_PROP_*: the nine memo properties first, the promoted
* $types and $normalized after them */
cls.privateTypedPropertyDefaultNull("sortedTypesCache", MAY_BE_ARRAY);
cls.privateTypedArrayPropertyDefaultEmpty("cachedDescriptions");
cls.privateTypedClassPropertyDefaultNull("finiteTypeSet", "PHPStan\\Type\\FiniteTypeSet", MAY_BE_FALSE);
cls.privateTypedPropertyDefaultNull("finiteTypes", MAY_BE_ARRAY);
cls.privateTypedClassPropertyDefaultNull("isNull", ptcls::trinaryLogic);
cls.privateTypedClassPropertyDefaultNull("isCallable", ptcls::trinaryLogic);
cls.privateTypedPropertyDefaultNull("hasTemplateOrLateResolvableType", MAY_BE_BOOL);
cls.privateTypedArrayPropertyDefaultEmpty("possiblyLooselyEqualTypes");
cls.privateTypedArrayPropertyDefaultEmpty("alwaysLooselyEqualTypes");
cls.privateTypedProperty("types", MAY_BE_ARRAY);
Expand Down
Loading
Loading