|
33 | 33 | use PHPStan\Type\Generic\UnresolvedTemplateArgumentType; |
34 | 34 | use function array_fill; |
35 | 35 | use function array_filter; |
| 36 | +use function array_flip; |
| 37 | +use function array_intersect_key; |
36 | 38 | use function array_key_exists; |
37 | 39 | use function array_key_first; |
38 | 40 | use function array_keys; |
39 | 41 | use function array_merge; |
| 42 | +use function array_search; |
40 | 43 | use function array_slice; |
41 | 44 | use function array_splice; |
42 | 45 | use function array_values; |
43 | 46 | use function count; |
44 | 47 | use function get_class; |
45 | 48 | use function implode; |
46 | 49 | use function in_array; |
| 50 | +use function ksort; |
| 51 | +use function serialize; |
| 52 | +use function sort; |
47 | 53 | use function spl_object_id; |
48 | 54 | use function sprintf; |
49 | 55 | use function usort; |
@@ -1722,6 +1728,29 @@ private static function haveDisjointConstantScalarValues(?array $a, ?array $b, b |
1722 | 1728 | return false; |
1723 | 1729 | } |
1724 | 1730 |
|
| 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 | + |
1725 | 1754 | /** |
1726 | 1755 | * @param list<Type> $constantArrays |
1727 | 1756 | * @return list<Type> |
@@ -1777,93 +1806,217 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged |
1777 | 1806 | } |
1778 | 1807 | } |
1779 | 1808 |
|
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 | + } |
1781 | 1840 |
|
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; |
1787 | 1866 | } |
1788 | 1867 | } |
1789 | 1868 | } |
1790 | 1869 |
|
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 | + } |
1794 | 1878 | } |
| 1879 | + } |
| 1880 | + ksort($visitOrder); |
1795 | 1881 |
|
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)) { |
1798 | 1886 | continue; |
1799 | 1887 | } |
1800 | 1888 |
|
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 | + ) { |
1816 | 1984 | $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); |
1817 | 1985 | $constantScalarValues[$j] = null; |
| 1986 | + $nullifiedConstantScalarValues[$j] = true; |
1818 | 1987 | unset($arraysToProcess[$i]); |
1819 | 1988 | continue 2; |
1820 | 1989 | } |
1821 | | - } |
1822 | 1990 |
|
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 | + } |
1857 | 2009 |
|
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 | + } |
1867 | 2020 | } |
1868 | 2021 | } |
1869 | 2022 | } |
|
0 commit comments