Skip to content

Commit efcf792

Browse files
committed
Visit only the pairs of constant arrays that can merge in reduceArrays()
reduceArrays() enumerated every pair of arrays sharing a key, then skipped the pairs whose single constant scalar values rule out a keys-superset. For n arrays with the same keys that is n^2/2 pairs, nearly all of them skipped - bug-12671 unions ~790 list{code, name, null} arrays. Among arrays with the same number of keys (two or more) and the same keys holding a single constant scalar, a pair is skipped when it differs in two or more of them, so the pairs that can get past the check are found through buckets of those values with one key left out. The pairs with other arrays, and with arrays that lost their single constant scalar values by a merge, are still all visited, and every pair is visited in the same order as before, so the merges and the result stay the same. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-d1e7-7395-b029-dd3c2510fac2
1 parent 610da13 commit efcf792

3 files changed

Lines changed: 495 additions & 134 deletions

File tree

‎src/Type/TypeCombinator.php‎

Lines changed: 223 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,23 @@
3333
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
3434
use function array_fill;
3535
use function array_filter;
36+
use function array_flip;
37+
use function array_intersect_key;
3638
use function array_key_exists;
3739
use function array_key_first;
3840
use function array_keys;
3941
use function array_merge;
42+
use function array_search;
4043
use function array_slice;
4144
use function array_splice;
4245
use function array_values;
4346
use function count;
4447
use function get_class;
4548
use function implode;
4649
use function in_array;
50+
use function ksort;
51+
use function serialize;
52+
use function sort;
4753
use function spl_object_id;
4854
use function sprintf;
4955
use function usort;
@@ -1722,6 +1728,29 @@ private static function haveDisjointConstantScalarValues(?array $a, ?array $b, b
17221728
return false;
17231729
}
17241730

1731+
/**
1732+
* The arrays after $i sharing a key with it, by the first key they share
1733+
* in $arraysToProcessPerKey's order, then by their indexes.
1734+
*
1735+
* @param array<int, list<int|string>> $keysByRank
1736+
* @param array<int|string, list<int>> $arraysToProcessPerKey
1737+
* @return list<int>
1738+
*/
1739+
private static function getReduceArraysPartners(int $i, array $keysByRank, array $arraysToProcessPerKey): array
1740+
{
1741+
$partners = [];
1742+
foreach ($keysByRank[$i] as $key) {
1743+
foreach ($arraysToProcessPerKey[$key] as $j) {
1744+
if ($j <= $i || isset($partners[$j])) {
1745+
continue;
1746+
}
1747+
$partners[$j] = $j;
1748+
}
1749+
}
1750+
1751+
return array_values($partners);
1752+
}
1753+
17251754
/**
17261755
* @param list<Type> $constantArrays
17271756
* @return list<Type>
@@ -1777,93 +1806,217 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged
17771806
}
17781807
}
17791808

1780-
$eligibleCombinations = [];
1809+
// The loop below visits the pairs of arrays sharing a key: by the first
1810+
// key they share in $arraysToProcessPerKey's order, then by their
1811+
// indexes. It skips the pairs whose single constant scalar values rule out
1812+
// a keys-superset (haveDisjointConstantScalarValues()). For arrays with the
1813+
// same number of keys (two or more) and the same keys holding a single
1814+
// constant scalar, those are the pairs differing in two or more of them, so
1815+
// only the pairs sharing a bucket of the values with one key left out are
1816+
// visited - enumerating all pairs is quadratic in the number of arrays,
1817+
// which dominated unions of many same-shaped arrays (bug-12671).
1818+
$keyRanks = array_flip(array_keys($arraysToProcessPerKey));
1819+
$keySets = [];
1820+
$keysByRank = [];
1821+
foreach ($arraysToProcess as $i => $arrayToProcess) {
1822+
$keys = [];
1823+
foreach ($arrayToProcess->getKeyTypes() as $keyType) {
1824+
$keys[$keyRanks[$keyType->getValue()]] = $keyType->getValue();
1825+
}
1826+
ksort($keys);
1827+
$keysByRank[$i] = array_values($keys);
1828+
$keySets[$i] = array_flip($keys);
1829+
}
1830+
1831+
$bucketGroups = [];
1832+
$bucketPartners = [];
1833+
$arraysPerKeyAndGroup = [];
1834+
if ($preserveTaggedUnions) {
1835+
$buckets = [];
1836+
foreach ($constantScalarValues as $i => $values) {
1837+
if ($values === null || $values === [] || count($keysByRank[$i]) < 2) {
1838+
continue;
1839+
}
17811840

1782-
foreach ($arraysToProcessPerKey as $arrays) {
1783-
for ($i = 0, $arraysCount = count($arrays); $i < $arraysCount - 1; $i++) {
1784-
for ($j = $i + 1; $j < $arraysCount; $j++) {
1785-
$eligibleCombinations[$arrays[$i]][$arrays[$j]] ??= 0;
1786-
$eligibleCombinations[$arrays[$i]][$arrays[$j]]++;
1841+
$sortedValues = [];
1842+
foreach ($values as $key => $value) {
1843+
// -0.0 === 0.0
1844+
$sortedValues[$keyRanks[$key]] = $value === 0.0 ? 0.0 : $value;
1845+
}
1846+
ksort($sortedValues);
1847+
$group = count($keysByRank[$i]) . ':' . implode(',', array_keys($sortedValues));
1848+
$bucketGroups[$i] = $group;
1849+
$sortedValues = array_values($sortedValues);
1850+
foreach (array_keys($sortedValues) as $leftOut) {
1851+
$projection = $sortedValues;
1852+
unset($projection[$leftOut]);
1853+
$buckets[$group . "\0" . $leftOut . "\0" . serialize($projection)][] = $i;
1854+
}
1855+
}
1856+
foreach ($buckets as $bucket) {
1857+
for ($a = 0, $bucketCount = count($bucket); $a < $bucketCount - 1; $a++) {
1858+
for ($b = $a + 1; $b < $bucketCount; $b++) {
1859+
$bucketPartners[$bucket[$a]][$bucket[$b]] = true;
1860+
}
1861+
}
1862+
}
1863+
foreach ($keysByRank as $i => $keys) {
1864+
foreach ($keys as $key) {
1865+
$arraysPerKeyAndGroup[$key][$bucketGroups[$i] ?? ''][] = $i;
17871866
}
17881867
}
17891868
}
17901869

1791-
foreach ($eligibleCombinations as $i => $other) {
1792-
if (!array_key_exists($i, $arraysToProcess)) {
1793-
continue;
1870+
$visitOrder = [];
1871+
foreach ($keysByRank as $i => $keys) {
1872+
foreach ($keys as $key) {
1873+
$arrays = $arraysToProcessPerKey[$key];
1874+
if ($arrays[count($arrays) - 1] !== $i) {
1875+
$visitOrder[$keyRanks[$key]][] = $i;
1876+
break;
1877+
}
17941878
}
1879+
}
1880+
ksort($visitOrder);
17951881

1796-
foreach ($other as $j => $overlappingKeysCount) {
1797-
if (!array_key_exists($j, $arraysToProcess)) {
1882+
$nullifiedConstantScalarValues = [];
1883+
foreach ($visitOrder as $arraysOfRank) {
1884+
foreach ($arraysOfRank as $i) {
1885+
if (!array_key_exists($i, $arraysToProcess)) {
17981886
continue;
17991887
}
18001888

1801-
// Merge two single-key arrays sharing the same key when their value
1802-
// types union into a single type (not a UnionType). This is lossless
1803-
// and prevents exponential union growth when narrowing nested
1804-
// ArrayDimFetch expressions on a ConstantArrayType parent (see
1805-
// phpstan/phpstan#14462).
1806-
if (
1807-
$preserveTaggedUnions
1808-
&& $overlappingKeysCount === 1
1809-
&& count($arraysToProcess[$i]->getKeyTypes()) === 1
1810-
&& count($arraysToProcess[$j]->getKeyTypes()) === 1
1811-
) {
1812-
$iValueType = $arraysToProcess[$i]->getValueTypes()[0];
1813-
$jValueType = $arraysToProcess[$j]->getValueTypes()[0];
1814-
$unionValueType = self::union($iValueType, $jValueType);
1815-
if (!$unionValueType instanceof UnionType) {
1889+
$allPartnersVisited = !isset($bucketGroups[$i]) || $constantScalarValues[$i] === null;
1890+
if ($allPartnersVisited) {
1891+
$partners = self::getReduceArraysPartners($i, $keysByRank, $arraysToProcessPerKey);
1892+
} else {
1893+
// the bucket partners, the arrays not in $i's group and the
1894+
// arrays that lost their single constant scalar values
1895+
$partners = $bucketPartners[$i] ?? [];
1896+
foreach ($keysByRank[$i] as $key) {
1897+
foreach ($arraysPerKeyAndGroup[$key] as $group => $arrays) {
1898+
if ($group === $bucketGroups[$i]) {
1899+
continue;
1900+
}
1901+
foreach ($arrays as $j) {
1902+
if ($j <= $i) {
1903+
continue;
1904+
}
1905+
$partners[$j] = true;
1906+
}
1907+
}
1908+
}
1909+
foreach (array_keys($nullifiedConstantScalarValues) as $j) {
1910+
if ($j <= $i || array_intersect_key($keySets[$i], $keySets[$j]) === []) {
1911+
continue;
1912+
}
1913+
$partners[$j] = true;
1914+
}
1915+
1916+
$partnersByRank = [];
1917+
foreach (array_keys($partners) as $j) {
1918+
foreach ($keysByRank[$i] as $key) {
1919+
if (isset($keySets[$j][$key])) {
1920+
$partnersByRank[$keyRanks[$key]][] = $j;
1921+
break;
1922+
}
1923+
}
1924+
}
1925+
ksort($partnersByRank);
1926+
$partners = [];
1927+
foreach ($partnersByRank as $partnersOfRank) {
1928+
sort($partnersOfRank);
1929+
foreach ($partnersOfRank as $j) {
1930+
$partners[] = $j;
1931+
}
1932+
}
1933+
}
1934+
1935+
for ($p = 0; $p < count($partners); $p++) {
1936+
$j = $partners[$p];
1937+
if (!array_key_exists($j, $arraysToProcess)) {
1938+
continue;
1939+
}
1940+
1941+
$overlappingKeysCount = count(array_intersect_key($keySets[$i], $keySets[$j]));
1942+
1943+
// Merge two single-key arrays sharing the same key when their value
1944+
// types union into a single type (not a UnionType). This is lossless
1945+
// and prevents exponential union growth when narrowing nested
1946+
// ArrayDimFetch expressions on a ConstantArrayType parent (see
1947+
// phpstan/phpstan#14462).
1948+
if (
1949+
$preserveTaggedUnions
1950+
&& $overlappingKeysCount === 1
1951+
&& count($arraysToProcess[$i]->getKeyTypes()) === 1
1952+
&& count($arraysToProcess[$j]->getKeyTypes()) === 1
1953+
) {
1954+
$iValueType = $arraysToProcess[$i]->getValueTypes()[0];
1955+
$jValueType = $arraysToProcess[$j]->getValueTypes()[0];
1956+
$unionValueType = self::union($iValueType, $jValueType);
1957+
if (!$unionValueType instanceof UnionType) {
1958+
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1959+
$constantScalarValues[$j] = null;
1960+
$nullifiedConstantScalarValues[$j] = true;
1961+
unset($arraysToProcess[$i]);
1962+
continue 2;
1963+
}
1964+
}
1965+
1966+
if (
1967+
$preserveTaggedUnions
1968+
&& self::haveDisjointConstantScalarValues(
1969+
$constantScalarValues[$i] ?? null,
1970+
$constantScalarValues[$j] ?? null,
1971+
count($arraysToProcess[$i]->getKeyTypes()) === count($arraysToProcess[$j]->getKeyTypes())
1972+
&& count($arraysToProcess[$i]->getKeyTypes()) >= 2,
1973+
)
1974+
) {
1975+
// neither array is a keys-superset of the other
1976+
continue;
1977+
}
1978+
1979+
if (
1980+
$preserveTaggedUnions
1981+
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1982+
&& $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])
1983+
) {
18161984
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
18171985
$constantScalarValues[$j] = null;
1986+
$nullifiedConstantScalarValues[$j] = true;
18181987
unset($arraysToProcess[$i]);
18191988
continue 2;
18201989
}
1821-
}
18221990

1823-
if (
1824-
$preserveTaggedUnions
1825-
&& self::haveDisjointConstantScalarValues(
1826-
$constantScalarValues[$i] ?? null,
1827-
$constantScalarValues[$j] ?? null,
1828-
count($arraysToProcess[$i]->getKeyTypes()) === count($arraysToProcess[$j]->getKeyTypes())
1829-
&& count($arraysToProcess[$i]->getKeyTypes()) >= 2,
1830-
)
1831-
) {
1832-
// neither array is a keys-superset of the other
1833-
continue;
1834-
}
1835-
1836-
if (
1837-
$preserveTaggedUnions
1838-
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1839-
&& $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])
1840-
) {
1841-
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1842-
$constantScalarValues[$j] = null;
1843-
unset($arraysToProcess[$i]);
1844-
continue 2;
1845-
}
1846-
1847-
if (
1848-
$preserveTaggedUnions
1849-
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1850-
&& $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])
1851-
) {
1852-
$arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1853-
$constantScalarValues[$i] = null;
1854-
unset($arraysToProcess[$j]);
1855-
continue 1;
1856-
}
1991+
if (
1992+
$preserveTaggedUnions
1993+
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1994+
&& $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])
1995+
) {
1996+
$arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1997+
$constantScalarValues[$i] = null;
1998+
$nullifiedConstantScalarValues[$i] = true;
1999+
unset($arraysToProcess[$j]);
2000+
if (!$allPartnersVisited) {
2001+
// from now on visited with every array after $j
2002+
$allPartners = self::getReduceArraysPartners($i, $keysByRank, $arraysToProcessPerKey);
2003+
$partners = array_slice($allPartners, (int) array_search($j, $allPartners, true) + 1);
2004+
$p = -1;
2005+
$allPartnersVisited = true;
2006+
}
2007+
continue 1;
2008+
}
18572009

1858-
if (
1859-
!$preserveTaggedUnions
1860-
// both arrays have same keys
1861-
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1862-
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1863-
) {
1864-
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1865-
unset($arraysToProcess[$i]);
1866-
continue 2;
2010+
if (
2011+
!$preserveTaggedUnions
2012+
// both arrays have same keys
2013+
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
2014+
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
2015+
) {
2016+
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
2017+
unset($arraysToProcess[$i]);
2018+
continue 2;
2019+
}
18672020
}
18682021
}
18692022
}

0 commit comments

Comments
 (0)