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
127 changes: 86 additions & 41 deletions src/Analyser/VariableLivenessResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ final class VariableLivenessResolver
/** @var array<int, string> */
private array $overwriteMarkers = [];

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

/** @var array<int, true> */
private array $overwrittenIds = [];
Expand Down Expand Up @@ -258,15 +258,16 @@ private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContex
$id = $flow->write->getId();
if ($flow->kind !== VariableFlow::DISCARD) {
$this->observeWrite($id, $next);
$this->observeOverwrite($id, $next);
$this->observeOverwrite($flow->write, $next);
foreach ($this->literalItems[$id] ?? [] as $item) {
$this->observeWrite($item->getId(), $next);
$this->observeOverwrite($item->getId(), $next);
$this->observeOverwrite($item, $next);
}
}
foreach (array_keys($this->killedKeys[$id] ?? []) as $key) {
unset($next[$key]);
}
$next = $this->killOverwriteMarkers($flow->write, $next);
if (isset($this->overwriteMarkers[$id])) {
$next[$this->overwriteMarkers[$id]] = true;
}
Expand Down Expand Up @@ -569,6 +570,10 @@ private function compileAccesses(): void
* write finding another write's marker after it reaches that replacement
* on some path, with no replacement in between.
*
* Only the markers are compiled, by slot: which of them a write observes
* or kills is answered against the live set when the write is reached, so
* the work stays linear in the variable's writes.
*
* @param list<VariableAccessFlow> $accesses
*/
private function compileOverwrites(string $name, array $accesses): void
Expand All @@ -590,38 +595,8 @@ private function compileOverwrites(string $name, array $accesses): void
if ($markersBySlot === []) {
return;
}
foreach ($accesses as $access) {
$write = $access->write;
if ($write === null) {
continue;
}
$id = $write->getId();
$offset = $write->getOffset();
$observed = $markersBySlot[''] ?? [];
if ($write->isOffsetWrite() && $offset !== null) {
$observed += $markersBySlot[self::offsetKey($offset)] ?? [];
}
foreach ($observed as $marker => $writerId) {
// a loop running the same write again replaces nothing it
// wrote - nor the items of the array literal it assigns
if ($writerId === $id || $writerId === $write->getParentId()) {
continue;
}
$this->overwriteKeys[$id][$marker] = true;
}
$slot = self::replacedSlot($write);
if ($slot === null) {
continue;
}
foreach ($markersBySlot as $markerSlot => $markers) {
if ($slot !== '' && $markerSlot !== $slot) {
continue;
}
foreach (array_keys($markers) as $marker) {
$this->killedKeys[$id][$marker] = true;
}
}
}

$this->markersBySlot[$name] = $markersBySlot;
}

/** The slot a write replaces: '' for the whole variable, null when it keeps the old value. */
Expand Down Expand Up @@ -655,15 +630,85 @@ private function observeWrite(int $id, array $next): void
}

/** @param array<string, true> $next */
private function observeOverwrite(int $id, array $next): void
private function observeOverwrite(VariableWrite $write, array $next): void
{
foreach (array_keys($this->overwriteKeys[$id] ?? []) as $key) {
if (!isset($next[$key])) {
$markersBySlot = $this->markersBySlot[$write->getVariableName()] ?? null;
if ($markersBySlot === null) {
return;
}
$id = $write->getId();
$parentId = $write->getParentId();
$observedSlots = [''];
$offset = $write->getOffset();
if ($write->isOffsetWrite() && $offset !== null) {
$observedSlots[] = self::offsetKey($offset);
}
foreach ($observedSlots as $slot) {
$markers = $markersBySlot[$slot] ?? [];
// walk the smaller of the two sets
if (count($markers) <= count($next)) {
foreach ($markers as $marker => $writerId) {
if (!isset($next[$marker])) {
continue;
}
// a loop running the same write again replaces nothing it
// wrote - nor the items of the array literal it assigns
if ($writerId === $id || $writerId === $parentId) {
continue;
}
$this->overwrittenIds[$id] = true;
return;
}
continue;
}
$this->overwrittenIds[$id] = true;
return;
foreach (array_keys($next) as $key) {
$writerId = $markers[$key] ?? null;
if ($writerId === null || $writerId === $id || $writerId === $parentId) {
continue;
}
$this->overwrittenIds[$id] = true;
return;
}
}
}

/**
* Drops the markers of the slots $write replaces - all of the variable's
* when it replaces the whole variable.
*
* @param array<string, true> $next
* @return array<string, true>
*/
private function killOverwriteMarkers(VariableWrite $write, array $next): array
{
$markersBySlot = $this->markersBySlot[$write->getVariableName()] ?? null;
if ($markersBySlot === null) {
return $next;
}
$slot = self::replacedSlot($write);
if ($slot === null) {
return $next;
}
foreach ($markersBySlot as $markerSlot => $markers) {
if ($slot !== '' && $markerSlot !== $slot) {
continue;
}
// walk the smaller of the two sets
if (count($markers) <= count($next)) {
foreach (array_keys($markers) as $marker) {
unset($next[$marker]);
}
continue;
}
foreach (array_keys($next) as $key) {
if (!isset($markers[$key])) {
continue;
}
unset($next[$key]);
}
}

return $next;
}

private function resolveDependencies(): void
Expand Down
64 changes: 48 additions & 16 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ public static function doRemove(Type $fromType, Type $typeToRemove): Type
return $fromType->subtract(count($remainingTypes) === 1 ? $remainingTypes[0] : new UnionType($remainingTypes));
}

// a value none of the union's keyed members stands for overlaps none
// of them - one lookup instead of a removal per member
$fromTypeKey = FiniteTypeSet::key($fromType);
if ($fromTypeKey !== null) {
$finiteTypeSet = $typeToRemove->getFiniteTypeSet();
if ($finiteTypeSet !== null && $finiteTypeSet->isComplete() && !$finiteTypeSet->has($fromTypeKey)) {
return $fromType;
}
}

foreach ($typeToRemove->getTypes() as $unionTypeToRemove) {
$fromType = self::remove($fromType, $unionTypeToRemove);
if ($fromType instanceof NeverType) {
Expand Down Expand Up @@ -598,11 +608,29 @@ public static function doUnion(Type ...$types): Type
);
$typesCount = count($types);

// $alreadyNormalized keeps the members alive, so their object ids stay
// unique while this lookup refers to them
$normalizedGroupsByType = [];
$normalizedTwice = [];
if ($typesCount > 1) {
foreach ($alreadyNormalized as $group => $normalizedTypes) {
foreach ($normalizedTypes as $normalizedType) {
$normalizedTypeId = spl_object_id($normalizedType);
if (isset($normalizedGroupsByType[$normalizedTypeId][$group])) {
$normalizedTwice[$normalizedTypeId] = true;
continue;
}
$normalizedGroupsByType[$normalizedTypeId][$group] = $group;
}
}
}

// transform A | A to A
// transform A | never to A
for ($i = 0; $i < $typesCount; $i++) {
$iGroups = $normalizedGroupsByType[spl_object_id($types[$i])] ?? null;
for ($j = $i + 1; $j < $typesCount; $j++) {
if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) {
if ($iGroups !== null && self::isAlreadyNormalized($iGroups, $normalizedGroupsByType, $normalizedTwice, $types[$i], $types[$j])) {
continue;
}
$compareResult = self::compareTypesInUnion($types[$i], $types[$j]);
Expand All @@ -613,6 +641,7 @@ public static function doUnion(Type ...$types): Type
[$a, $b] = $compareResult;
if ($a !== null) {
$types[$i] = $a;
$iGroups = $normalizedGroupsByType[spl_object_id($a)] ?? null;
array_splice($types, $j--, 1);
$typesCount--;
continue 1;
Expand Down Expand Up @@ -757,24 +786,27 @@ private static function unionWithMarkers(Type $rest, array $markers): Type
}

/**
* @param array<int, Type[]> $alreadyNormalized
* Whether $a and $b are members of one union that was flattened into this
* one - such members were already compared against each other.
*
* @param array<int, int> $aGroups the groups of $a in $normalizedGroupsByType
* @param array<int, array<int, int>> $normalizedGroupsByType member object id => (index of a flattened union holding it => the same index)
* @param array<int, true> $normalizedTwice member object ids a flattened union held twice
*/
private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool
private static function isAlreadyNormalized(array $aGroups, array $normalizedGroupsByType, array $normalizedTwice, Type $a, Type $b): bool
{
foreach ($alreadyNormalized as $normalizedTypes) {
foreach ($normalizedTypes as $i => $normalizedType) {
if ($normalizedType !== $a) {
continue;
}
if ($a === $b) {
return isset($normalizedTwice[spl_object_id($a)]);
}

foreach ($normalizedTypes as $j => $anotherNormalizedType) {
if ($i === $j) {
continue;
}
if ($anotherNormalizedType === $b) {
return true;
}
}
$bGroups = $normalizedGroupsByType[spl_object_id($b)] ?? null;
if ($bGroups === null) {
return false;
}

foreach ($aGroups as $group) {
if (isset($bGroups[$group])) {
return true;
}
}

Expand Down
Loading
Loading