diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index f42af28f3f2..3de0ccb807b 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -33,10 +33,13 @@ use PHPStan\Type\Generic\UnresolvedTemplateArgumentType; use function array_fill; use function array_filter; +use function array_flip; +use function array_intersect_key; use function array_key_exists; use function array_key_first; use function array_keys; use function array_merge; +use function array_search; use function array_slice; use function array_splice; use function array_values; @@ -44,6 +47,9 @@ use function get_class; use function implode; use function in_array; +use function ksort; +use function serialize; +use function sort; use function spl_object_id; use function sprintf; use function usort; @@ -1368,6 +1374,69 @@ private static function processArrayTypes(array $arrayTypes): array return $reducedArrayTypes; } + /** + * Merges the arrays like a chain of mergeWith() calls. They have the same + * keys in the same order, so the chain unions the values at each key. At a + * key where every array holds only constant scalars, one union of all the + * values is the same as the chain of unions, which was quadratic in the size + * of its growing result - the chain only merges the rest of the shape, with a + * placeholder at that key. + * + * @param non-empty-list $arrays + */ + private static function mergeConstantArraysWithSameKeys(array $arrays): ConstantArrayType + { + $constantScalarValueTypes = []; + if (!$arrays[0] instanceof TemplateType) { + foreach (array_keys($arrays[0]->getValueTypes()) as $i) { + $constantScalarValueTypes[$i] = []; + } + } + foreach ($arrays as $array) { + foreach ($array->getValueTypes() as $i => $valueType) { + if (!array_key_exists($i, $constantScalarValueTypes)) { + continue; + } + // the classes whose union is a plain concatenation of their + // members, deduplicated - not ones merely holding a constant scalar + $members = $valueType instanceof UnionType && !$valueType instanceof BenevolentUnionType && !$valueType instanceof TemplateType + ? $valueType->getTypes() + : [$valueType]; + foreach ($members as $member) { + if (!$member instanceof ConstantScalarType || $member instanceof TemplateType) { // @phpstan-ignore phpstanApi.instanceofType + unset($constantScalarValueTypes[$i]); + continue 2; + } + } + $constantScalarValueTypes[$i][] = $valueType; + } + } + + $placeholder = new NeverType(true); + $merged = null; + foreach ($arrays as $array) { + if ($constantScalarValueTypes !== []) { + $valueTypes = $array->getValueTypes(); + foreach (array_keys($constantScalarValueTypes) as $i) { + $valueTypes[$i] = $placeholder; + } + $array = new ConstantArrayType($array->getKeyTypes(), $valueTypes, $array->getNextAutoIndexes(), $array->getOptionalKeys(), $array->isList(), $array->getUnsealedTypes()); + } + $merged = $merged === null ? $array : $merged->mergeWith($array); + } + + if ($constantScalarValueTypes === []) { + return $merged; + } + + $valueTypes = $merged->getValueTypes(); + foreach ($constantScalarValueTypes as $i => $types) { + $valueTypes[$i] = self::union(...$types); + } + + return new ConstantArrayType($merged->getKeyTypes(), $valueTypes, $merged->getNextAutoIndexes(), $merged->getOptionalKeys(), $merged->isList(), $merged->getUnsealedTypes()); + } + /** * @param Type[] $types * @return Type[] @@ -1411,11 +1480,7 @@ private static function optimizeConstantArrays(array $types): array $collapsed[] = $group[0]; continue; } - $merged = $group[0]; - for ($i = 1, $count = count($group); $i < $count; $i++) { - $merged = $merged->mergeWith($group[$i]); - } - $collapsed[] = $merged; + $collapsed[] = self::mergeConstantArraysWithSameKeys($group); $anyMerged = true; } if ($anyMerged) { @@ -1631,6 +1696,29 @@ private static function haveDisjointConstantScalarValues(?array $a, ?array $b, b return false; } + /** + * The arrays after $i sharing a key with it, by the first key they share + * in $arraysToProcessPerKey's order, then by their indexes. + * + * @param array> $keysByRank + * @param array> $arraysToProcessPerKey + * @return list + */ + private static function getReduceArraysPartners(int $i, array $keysByRank, array $arraysToProcessPerKey): array + { + $partners = []; + foreach ($keysByRank[$i] as $key) { + foreach ($arraysToProcessPerKey[$key] as $j) { + if ($j <= $i || isset($partners[$j])) { + continue; + } + $partners[$j] = $j; + } + } + + return array_values($partners); + } + /** * @param list $constantArrays * @return list @@ -1686,93 +1774,217 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged } } - $eligibleCombinations = []; + // The loop below visits the pairs of arrays sharing a key: by the first + // key they share in $arraysToProcessPerKey's order, then by their + // indexes. It skips the pairs whose single constant scalar values rule out + // a keys-superset (haveDisjointConstantScalarValues()). For arrays with the + // same number of keys (two or more) and the same keys holding a single + // constant scalar, those are the pairs differing in two or more of them, so + // only the pairs sharing a bucket of the values with one key left out are + // visited - enumerating all pairs is quadratic in the number of arrays, + // which dominated unions of many same-shaped arrays (bug-12671). + $keyRanks = array_flip(array_keys($arraysToProcessPerKey)); + $keySets = []; + $keysByRank = []; + foreach ($arraysToProcess as $i => $arrayToProcess) { + $keys = []; + foreach ($arrayToProcess->getKeyTypes() as $keyType) { + $keys[$keyRanks[$keyType->getValue()]] = $keyType->getValue(); + } + ksort($keys); + $keysByRank[$i] = array_values($keys); + $keySets[$i] = array_flip($keys); + } + + $bucketGroups = []; + $bucketPartners = []; + $arraysPerKeyAndGroup = []; + if ($preserveTaggedUnions) { + $buckets = []; + foreach ($constantScalarValues as $i => $values) { + if ($values === null || $values === [] || count($keysByRank[$i]) < 2) { + continue; + } - foreach ($arraysToProcessPerKey as $arrays) { - for ($i = 0, $arraysCount = count($arrays); $i < $arraysCount - 1; $i++) { - for ($j = $i + 1; $j < $arraysCount; $j++) { - $eligibleCombinations[$arrays[$i]][$arrays[$j]] ??= 0; - $eligibleCombinations[$arrays[$i]][$arrays[$j]]++; + $sortedValues = []; + foreach ($values as $key => $value) { + // -0.0 === 0.0 + $sortedValues[$keyRanks[$key]] = $value === 0.0 ? 0.0 : $value; + } + ksort($sortedValues); + $group = count($keysByRank[$i]) . ':' . implode(',', array_keys($sortedValues)); + $bucketGroups[$i] = $group; + $sortedValues = array_values($sortedValues); + foreach (array_keys($sortedValues) as $leftOut) { + $projection = $sortedValues; + unset($projection[$leftOut]); + $buckets[$group . "\0" . $leftOut . "\0" . serialize($projection)][] = $i; + } + } + foreach ($buckets as $bucket) { + for ($a = 0, $bucketCount = count($bucket); $a < $bucketCount - 1; $a++) { + for ($b = $a + 1; $b < $bucketCount; $b++) { + $bucketPartners[$bucket[$a]][$bucket[$b]] = true; + } + } + } + foreach ($keysByRank as $i => $keys) { + foreach ($keys as $key) { + $arraysPerKeyAndGroup[$key][$bucketGroups[$i] ?? ''][] = $i; } } } - foreach ($eligibleCombinations as $i => $other) { - if (!array_key_exists($i, $arraysToProcess)) { - continue; + $visitOrder = []; + foreach ($keysByRank as $i => $keys) { + foreach ($keys as $key) { + $arrays = $arraysToProcessPerKey[$key]; + if ($arrays[count($arrays) - 1] !== $i) { + $visitOrder[$keyRanks[$key]][] = $i; + break; + } } + } + ksort($visitOrder); - foreach ($other as $j => $overlappingKeysCount) { - if (!array_key_exists($j, $arraysToProcess)) { + $nullifiedConstantScalarValues = []; + foreach ($visitOrder as $arraysOfRank) { + foreach ($arraysOfRank as $i) { + if (!array_key_exists($i, $arraysToProcess)) { continue; } - // Merge two single-key arrays sharing the same key when their value - // types union into a single type (not a UnionType). This is lossless - // and prevents exponential union growth when narrowing nested - // ArrayDimFetch expressions on a ConstantArrayType parent (see - // phpstan/phpstan#14462). - if ( - $preserveTaggedUnions - && $overlappingKeysCount === 1 - && count($arraysToProcess[$i]->getKeyTypes()) === 1 - && count($arraysToProcess[$j]->getKeyTypes()) === 1 - ) { - $iValueType = $arraysToProcess[$i]->getValueTypes()[0]; - $jValueType = $arraysToProcess[$j]->getValueTypes()[0]; - $unionValueType = self::union($iValueType, $jValueType); - if (!$unionValueType instanceof UnionType) { + $allPartnersVisited = !isset($bucketGroups[$i]) || $constantScalarValues[$i] === null; + if ($allPartnersVisited) { + $partners = self::getReduceArraysPartners($i, $keysByRank, $arraysToProcessPerKey); + } else { + // the bucket partners, the arrays not in $i's group and the + // arrays that lost their single constant scalar values + $partners = $bucketPartners[$i] ?? []; + foreach ($keysByRank[$i] as $key) { + foreach ($arraysPerKeyAndGroup[$key] as $group => $arrays) { + if ($group === $bucketGroups[$i]) { + continue; + } + foreach ($arrays as $j) { + if ($j <= $i) { + continue; + } + $partners[$j] = true; + } + } + } + foreach (array_keys($nullifiedConstantScalarValues) as $j) { + if ($j <= $i || array_intersect_key($keySets[$i], $keySets[$j]) === []) { + continue; + } + $partners[$j] = true; + } + + $partnersByRank = []; + foreach (array_keys($partners) as $j) { + foreach ($keysByRank[$i] as $key) { + if (isset($keySets[$j][$key])) { + $partnersByRank[$keyRanks[$key]][] = $j; + break; + } + } + } + ksort($partnersByRank); + $partners = []; + foreach ($partnersByRank as $partnersOfRank) { + sort($partnersOfRank); + foreach ($partnersOfRank as $j) { + $partners[] = $j; + } + } + } + + for ($p = 0; $p < count($partners); $p++) { + $j = $partners[$p]; + if (!array_key_exists($j, $arraysToProcess)) { + continue; + } + + $overlappingKeysCount = count(array_intersect_key($keySets[$i], $keySets[$j])); + + // Merge two single-key arrays sharing the same key when their value + // types union into a single type (not a UnionType). This is lossless + // and prevents exponential union growth when narrowing nested + // ArrayDimFetch expressions on a ConstantArrayType parent (see + // phpstan/phpstan#14462). + if ( + $preserveTaggedUnions + && $overlappingKeysCount === 1 + && count($arraysToProcess[$i]->getKeyTypes()) === 1 + && count($arraysToProcess[$j]->getKeyTypes()) === 1 + ) { + $iValueType = $arraysToProcess[$i]->getValueTypes()[0]; + $jValueType = $arraysToProcess[$j]->getValueTypes()[0]; + $unionValueType = self::union($iValueType, $jValueType); + if (!$unionValueType instanceof UnionType) { + $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + $constantScalarValues[$j] = null; + $nullifiedConstantScalarValues[$j] = true; + unset($arraysToProcess[$i]); + continue 2; + } + } + + if ( + $preserveTaggedUnions + && self::haveDisjointConstantScalarValues( + $constantScalarValues[$i] ?? null, + $constantScalarValues[$j] ?? null, + count($arraysToProcess[$i]->getKeyTypes()) === count($arraysToProcess[$j]->getKeyTypes()) + && count($arraysToProcess[$i]->getKeyTypes()) >= 2, + ) + ) { + // neither array is a keys-superset of the other + continue; + } + + if ( + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) + ) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); $constantScalarValues[$j] = null; + $nullifiedConstantScalarValues[$j] = true; unset($arraysToProcess[$i]); continue 2; } - } - - if ( - $preserveTaggedUnions - && self::haveDisjointConstantScalarValues( - $constantScalarValues[$i] ?? null, - $constantScalarValues[$j] ?? null, - count($arraysToProcess[$i]->getKeyTypes()) === count($arraysToProcess[$j]->getKeyTypes()) - && count($arraysToProcess[$i]->getKeyTypes()) >= 2, - ) - ) { - // neither array is a keys-superset of the other - continue; - } - - if ( - $preserveTaggedUnions - && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) - && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) - ) { - $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); - $constantScalarValues[$j] = null; - unset($arraysToProcess[$i]); - continue 2; - } - if ( - $preserveTaggedUnions - && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) - && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) - ) { - $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); - $constantScalarValues[$i] = null; - unset($arraysToProcess[$j]); - continue 1; - } + if ( + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) + ) { + $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); + $constantScalarValues[$i] = null; + $nullifiedConstantScalarValues[$i] = true; + unset($arraysToProcess[$j]); + if (!$allPartnersVisited) { + // from now on visited with every array after $j + $allPartners = self::getReduceArraysPartners($i, $keysByRank, $arraysToProcessPerKey); + $partners = array_slice($allPartners, (int) array_search($j, $allPartners, true) + 1); + $p = -1; + $allPartnersVisited = true; + } + continue 1; + } - if ( - !$preserveTaggedUnions - // both arrays have same keys - && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) - && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) - ) { - $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); - unset($arraysToProcess[$i]); - continue 2; + if ( + !$preserveTaggedUnions + // both arrays have same keys + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + ) { + $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + unset($arraysToProcess[$i]); + continue 2; + } } } } diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 0f5f69d4ce0..0c13591fd42 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -33,6 +33,9 @@ namespace sigs = ptdecl::TypeCombinator::sig; +#include +#include +#include #include zend_class_entry *pt_ce_type_combinator = nullptr; @@ -2458,6 +2461,135 @@ class TypeCombinator traversed.intoReturnValue(return_value); } + /* whether the union of $valueType with others like it is a plain + * concatenation of their members, deduplicated: constant scalars only, or + * a plain union of them; -1 = pending exception */ + static int holdsOnlyConstantScalars(zval *valueType) + { + int isTemplate = isInstanceMap(valueType, PT_CLASS_TEMPLATE_TYPE); + if (UNEXPECTED(isTemplate < 0)) return -1; + if (isInstance(valueType, pt_ce_union_type) && !isInstance(valueType, pt_ce_benevolent_union_type) && !isTemplate) { + zv::Val members = getTypes(valueType); + if (UNEXPECTED(members.isUndef())) return -1; + for (zv::ArrayEntry member : zv::ArrRef(members.raw())) { + int holds = holdsOnlyConstantScalar(member.value().raw()); + if (holds <= 0) return holds; + } + return 1; + } + return holdsOnlyConstantScalar(valueType); + } + + /* $member instanceof ConstantScalarType && !$member instanceof TemplateType */ + static int holdsOnlyConstantScalar(zval *member) + { + int isConstantScalar = isInstanceMap(member, PT_CLASS_CONSTANT_SCALAR_TYPE); + if (isConstantScalar <= 0) return isConstantScalar; + int isTemplate = isInstanceMap(member, PT_CLASS_TEMPLATE_TYPE); + if (UNEXPECTED(isTemplate < 0)) return -1; + return isTemplate ? 0 : 1; + } + + /* new ConstantArrayType($array's keys, $valueTypes, and the rest of + * $array's shape) */ + static zv::Val withValueTypes(zval *array, zval *valueTypes) + { + zv::Val keyTypes = call(array, PT_LC("getkeytypes")); + PT_FAIL_IF_UNDEF(keyTypes); + zv::Val nextAutoIndexes = call(array, PT_LC("getnextautoindexes")); + PT_FAIL_IF_UNDEF(nextAutoIndexes); + zv::Val optionalKeys = call(array, PT_LC("getoptionalkeys")); + PT_FAIL_IF_UNDEF(optionalKeys); + zv::Val isList = call(array, PT_LC("islist")); + PT_FAIL_IF_UNDEF(isList); + zv::Val unsealed = call(array, PT_LC("getunsealedtypes")); + PT_FAIL_IF_UNDEF(unsealed); + zval out; + if (UNEXPECTED(!pt_constant_array_type_new(&out, keyTypes.raw(), valueTypes, nextAutoIndexes.raw(), optionalKeys.raw(), isList.raw(), unsealed.raw()))) { + return zv::Val(); + } + return zv::Val::adopt(out); + } + + /* private static mergeConstantArraysWithSameKeys(non-empty-list $arrays): ConstantArrayType + * — a chain of mergeWith() calls; at a key where every array holds only + * constant scalars, one union of all the values stands for the chain of + * unions, and the chain merges the rest of the shape with a placeholder + * there */ + static zv::Val mergeConstantArraysWithSameKeys(zval *arrays) + { + uint32_t arraysCount = countOf(arrays); + zval *first = indexOf(arrays, 0); + zv::Val firstValueTypes = call(first, PT_LC("getvaluetypes")); + PT_FAIL_IF_UNDEF(firstValueTypes); + int firstIsTemplate = isInstanceMap(first, PT_CLASS_TEMPLATE_TYPE); + PT_FAIL_IF_NEG(firstIsTemplate); + size_t valuesCount = countOf(firstValueTypes.raw()); + std::vector constantScalar(valuesCount, firstIsTemplate ? 0 : 1); + std::vector constantScalarValueTypes(valuesCount); + for (uint32_t a = 0; a < arraysCount; a++) { + zv::Val valueTypes = call(indexOf(arrays, a), PT_LC("getvaluetypes")); + PT_FAIL_IF_UNDEF(valueTypes); + for (zv::ArrayEntry entry : zv::ArrRef(valueTypes.raw())) { + zend_ulong i = entry.indexKey(); + if (i >= valuesCount || !constantScalar[i]) continue; + int holds = holdsOnlyConstantScalars(entry.value().raw()); + PT_FAIL_IF_NEG(holds); + if (!holds) { + constantScalar[i] = 0; + constantScalarValueTypes[i].clear(); + continue; + } + constantScalarValueTypes[i].push_back(copy(entry.value().raw())); + } + } + bool anyConstantScalar = std::find(constantScalar.begin(), constantScalar.end(), 1) != constantScalar.end(); + + zv::Val placeholder = neverType(true); + PT_FAIL_IF_UNDEF(placeholder); + zv::Val merged; + for (uint32_t a = 0; a < arraysCount; a++) { + zval *array = indexOf(arrays, a); + zv::Val current = copy(array); + if (anyConstantScalar) { + zv::Val arrayValueTypes = call(array, PT_LC("getvaluetypes")); + PT_FAIL_IF_UNDEF(arrayValueTypes); + zv::Arr valueTypes = zv::Arr::adoptVal(std::move(arrayValueTypes)); + valueTypes.separate(); + for (size_t i = 0; i < valuesCount; i++) { + if (!constantScalar[i]) continue; + Z_ADDREF_P(placeholder.raw()); + zend_hash_index_update(valueTypes.table(), i, placeholder.raw()); + } + current = withValueTypes(array, valueTypes.raw()); + PT_FAIL_IF_UNDEF(current); + } + if (a == 0) { + merged = std::move(current); + continue; + } + zv::Val next = call(merged.raw(), PT_LC("mergewith"), 1, current.raw()); + PT_FAIL_IF_UNDEF(next); + merged = std::move(next); + } + + if (!anyConstantScalar) return merged; + + zv::Val mergedValueTypes = call(merged.raw(), PT_LC("getvaluetypes")); + PT_FAIL_IF_UNDEF(mergedValueTypes); + zv::Arr valueTypes = zv::Arr::adoptVal(std::move(mergedValueTypes)); + valueTypes.separate(); + for (size_t i = 0; i < valuesCount; i++) { + if (!constantScalar[i]) continue; + zv::Val unioned = unionOf(constantScalarValueTypes[i]); + PT_FAIL_IF_UNDEF(unioned); + zval v = unioned.take(); + zend_hash_index_update(valueTypes.table(), i, &v); + } + + return withValueTypes(merged.raw(), valueTypes.raw()); + } + /* private static optimizeConstantArrays(Type[] $types): Type[] */ static zv::Val optimizeConstantArrays(zval *typesArray) { @@ -2499,12 +2631,8 @@ class TypeCombinator collapsed.push(zv::Ref(indexOf(group, 0))); continue; } - zv::Val merged = copy(indexOf(group, 0)); - for (uint32_t i = 1; i < count; i++) { - zv::Val next = call(merged.raw(), PT_LC("mergewith"), 1, indexOf(group, i)); - PT_FAIL_IF_UNDEF(next); - merged = std::move(next); - } + zv::Val merged = mergeConstantArraysWithSameKeys(group); + PT_FAIL_IF_UNDEF(merged); collapsed.push(std::move(merged)); anyMerged = true; } @@ -2795,6 +2923,91 @@ class TypeCombinator zend_hash_index_update(constantScalarValues.table(), index, &null); } + /* $nullified[$index] = true, for the pairs visited from now on */ + static void nullify(std::vector &nullified, std::vector &nullifiedList, zend_ulong index) + { + if (nullified[index]) return; + nullified[index] = 1; + nullifiedList.push_back(index); + } + + /* the number of keys two arrays share, from their sorted key ranks */ + static size_t sharedKeysCount(const std::vector &a, const std::vector &b) + { + size_t count = 0; + auto ai = a.begin(); + auto bi = b.begin(); + while (ai != a.end() && bi != b.end()) { + if (*ai < *bi) { + ai++; + } else if (*bi < *ai) { + bi++; + } else { + count++; + ai++; + bi++; + } + } + return count; + } + + /* appends an encoding of a single constant scalar value - equal for + * identical (===) values - to a bucket key */ + static void encodeConstantScalarValue(std::string &out, zval *value) + { + switch (Z_TYPE_P(value)) { + case IS_NULL: + out += 'n'; + break; + case IS_FALSE: + out += 'f'; + break; + case IS_TRUE: + out += 't'; + break; + case IS_LONG: { + zend_long l = Z_LVAL_P(value); + out += 'l'; + out.append(reinterpret_cast(&l), sizeof(l)); + break; + } + case IS_DOUBLE: { + /* -0.0 === 0.0 */ + double d = Z_DVAL_P(value) == 0.0 ? 0.0 : Z_DVAL_P(value); + out += 'd'; + out.append(reinterpret_cast(&d), sizeof(d)); + break; + } + case IS_STRING: { + size_t length = Z_STRLEN_P(value); + out += 's'; + out.append(reinterpret_cast(&length), sizeof(length)); + out.append(Z_STRVAL_P(value), length); + break; + } + default: + out += '?'; + break; + } + } + + /* private static getReduceArraysPartners(int $i, ...): list — the + * arrays after $i sharing a key with it, by the first key they share, + * then by their indexes */ + static std::vector getReduceArraysPartners(zend_ulong i, const std::vector> &keysByRank, const std::vector> &arraysToProcessPerKey) + { + std::vector partners; + std::vector seen(keysByRank.size(), 0); + for (zend_ulong keyRank : keysByRank[i]) { + for (zend_ulong j : arraysToProcessPerKey[keyRank]) { + if (j <= i || seen[j]) continue; + seen[j] = 1; + partners.push_back(j); + } + } + return partners; + } + /* private static reduceArrays(list $constantArrays, bool $preserveTaggedUnions): list */ static zv::Val reduceArrays(zval *constantArrays, bool preserveTaggedUnions) { @@ -2837,25 +3050,35 @@ class TypeCombinator } } - zv::Arr arraysToProcessPerKey = zv::Arr::create(0); + /* the keys in $arraysToProcessPerKey's order (their rank), the arrays + * having each key, and each array's keys by rank */ + zv::Arr keyRanks = zv::Arr::create(0); + std::vector> arraysToProcessPerKey; + std::vector> keysByRank; for (zv::ArrayEntry entry : zv::ArrRef(arraysToProcess.raw())) { + zend_ulong i = entry.indexKey(); + if (keysByRank.size() <= i) keysByRank.resize(i + 1); zv::Val keyTypes = call(entry.value().raw(), PT_LC("getkeytypes")); PT_FAIL_IF_UNDEF(keyTypes); for (zv::ArrayEntry keyEntry : zv::ArrRef(keyTypes.raw())) { zv::Val keyValue = call(keyEntry.value().raw(), PT_LC("getvalue")); PT_FAIL_IF_UNDEF(keyValue); - zval *perKey = symtableFind(arraysToProcessPerKey.table(), keyValue.raw()); - if (perKey == NULL) { + zval *rank = symtableFind(keyRanks.table(), keyValue.raw()); + zend_ulong keyRank; + if (rank == NULL) { if (UNEXPECTED(EG(exception))) return zv::Val(); + keyRank = arraysToProcessPerKey.size(); zval fresh; - array_init(&fresh); - if (UNEXPECTED(!symtableSet(arraysToProcessPerKey.table(), keyValue.raw(), &fresh))) return zv::Val(); - perKey = symtableFind(arraysToProcessPerKey.table(), keyValue.raw()); + ZVAL_LONG(&fresh, (zend_long) keyRank); + if (UNEXPECTED(!symtableSet(keyRanks.table(), keyValue.raw(), &fresh))) return zv::Val(); + arraysToProcessPerKey.emplace_back(); + } else { + keyRank = (zend_ulong) Z_LVAL_P(rank); } - zval index; - ZVAL_LONG(&index, (zend_long) entry.indexKey()); - zend_hash_next_index_insert(Z_ARRVAL_P(perKey), &index); + arraysToProcessPerKey[keyRank].push_back(i); + keysByRank[i].push_back(keyRank); } + std::sort(keysByRank[i].begin(), keysByRank[i].end()); } /* the single constant scalar value at each key of the arrays without @@ -2871,40 +3094,131 @@ class TypeCombinator } } - zv::Arr eligibleCombinations = zv::Arr::create(0); - for (zv::ArrayEntry entry : zv::ArrRef(arraysToProcessPerKey.raw())) { - HashTable *arrays = Z_ARRVAL_P(entry.value().raw()); - zend_long arraysCount = (zend_long) zend_hash_num_elements(arrays); - for (zend_long i = 0; i < arraysCount - 1; i++) { - zend_ulong first = (zend_ulong) Z_LVAL_P(zend_hash_index_find(arrays, (zend_ulong) i)); - for (zend_long j = i + 1; j < arraysCount; j++) { - zend_ulong second = (zend_ulong) Z_LVAL_P(zend_hash_index_find(arrays, (zend_ulong) j)); - zval *other = zend_hash_index_find(eligibleCombinations.table(), first); - if (other == NULL) { - zval fresh; - array_init(&fresh); - other = zend_hash_index_update(eligibleCombinations.table(), first, &fresh); + /* The loop below visits the pairs of arrays sharing a key: by the + * first key they share in $arraysToProcessPerKey's order, then by + * their indexes. It skips the pairs whose single constant scalar + * values rule out a keys-superset; for arrays with the same number of + * keys (two or more) and the same keys holding a single constant + * scalar, those are the pairs differing in two or more of them, so + * only the pairs sharing a bucket of the values with one key left out + * are visited (see the twin) */ + size_t arraysCount = keysByRank.size(); + std::vector bucketGroups(arraysCount, -1); + std::vector> bucketPartners(arraysCount); + std::vector>> arraysPerKeyAndGroup(arraysToProcessPerKey.size()); + if (preserveTaggedUnions) { + std::unordered_map groupIds; + std::unordered_map> buckets; + for (zv::ArrayEntry entry : zv::ArrRef(arraysToProcess.raw())) { + zend_ulong i = entry.indexKey(); + zval *values = zend_hash_index_find(constantScalarValues.table(), i); + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(values)) == 0 || keysByRank[i].size() < 2) { + continue; + } + + std::vector> sortedValues; + for (zv::ArrayEntry valueEntry : zv::ArrRef(values)) { + zend_string *key = valueEntry.stringKeyOrNull(); + zval *rank = key != NULL ? zend_hash_find(keyRanks.table(), key) : zend_hash_index_find(keyRanks.table(), valueEntry.indexKey()); + sortedValues.emplace_back((zend_ulong) Z_LVAL_P(rank), valueEntry.value().raw()); + } + std::sort(sortedValues.begin(), sortedValues.end(), [](const std::pair &a, const std::pair &b) { + return a.first < b.first; + }); + std::string group = std::to_string(keysByRank[i].size()) + ":"; + for (auto &sortedValue : sortedValues) { + group += std::to_string(sortedValue.first) + ","; + } + auto groupId = groupIds.emplace(group, (int) groupIds.size()).first->second; + bucketGroups[i] = groupId; + for (size_t leftOut = 0; leftOut < sortedValues.size(); leftOut++) { + std::string bucket = std::to_string(groupId) + ":" + std::to_string(leftOut) + ":"; + for (size_t position = 0; position < sortedValues.size(); position++) { + if (position != leftOut) encodeConstantScalarValue(bucket, sortedValues[position].second); } - zval *count = zend_hash_index_find(Z_ARRVAL_P(other), second); - if (count == NULL) { - zval zero; - ZVAL_LONG(&zero, 0); - count = zend_hash_index_update(Z_ARRVAL_P(other), second, &zero); + buckets[bucket].push_back(i); + } + } + for (auto &bucket : buckets) { + std::vector &members = bucket.second; + for (size_t a = 0; a + 1 < members.size(); a++) { + for (size_t b = a + 1; b < members.size(); b++) { + bucketPartners[members[a]].push_back(members[b]); } - Z_LVAL_P(count)++; + } + } + for (size_t i = 0; i < arraysCount; i++) { + for (zend_ulong keyRank : keysByRank[i]) { + arraysPerKeyAndGroup[keyRank][bucketGroups[i]].push_back(i); } } } - for (zv::ArrayEntry entry : zv::ArrRef(eligibleCombinations.raw())) { - zend_ulong i = entry.indexKey(); + std::vector> visitOrder; + for (size_t i = 0; i < arraysCount; i++) { + for (zend_ulong keyRank : keysByRank[i]) { + if (arraysToProcessPerKey[keyRank].back() != i) { + visitOrder.emplace_back(keyRank, i); + break; + } + } + } + std::sort(visitOrder.begin(), visitOrder.end()); + + std::vector nullified(arraysCount, 0); + std::vector nullifiedList; + for (auto &visit : visitOrder) { + zend_ulong i = visit.second; if (arrayAt(arraysToProcess, i) == NULL) continue; - for (zv::ArrayEntry otherEntry : zv::ArrRef(entry.value().raw())) { - zend_ulong j = otherEntry.indexKey(); - zend_long overlappingKeysCount = Z_LVAL_P(otherEntry.value().raw()); + zval *iValues = zend_hash_index_find(constantScalarValues.table(), i); + bool allPartnersVisited = bucketGroups[i] < 0 || iValues == NULL || Z_TYPE_P(iValues) != IS_ARRAY; + std::vector partners; + if (allPartnersVisited) { + partners = getReduceArraysPartners(i, keysByRank, arraysToProcessPerKey); + } else { + /* the bucket partners, the arrays not in $i's group and the + * arrays that lost their single constant scalar values */ + std::vector seen(arraysCount, 0); + std::vector candidates; + auto add = [&](zend_ulong j) { + if (j <= i || seen[j]) return; + seen[j] = 1; + candidates.push_back(j); + }; + for (zend_ulong j : bucketPartners[i]) add(j); + for (zend_ulong keyRank : keysByRank[i]) { + for (auto &groupArrays : arraysPerKeyAndGroup[keyRank]) { + if (groupArrays.first == bucketGroups[i]) continue; + for (zend_ulong j : groupArrays.second) add(j); + } + } + for (zend_ulong j : nullifiedList) { + if (sharedKeysCount(keysByRank[i], keysByRank[j]) == 0) continue; + add(j); + } + + std::vector> partnersByRank; + for (zend_ulong j : candidates) { + for (zend_ulong keyRank : keysByRank[i]) { + if (std::binary_search(keysByRank[j].begin(), keysByRank[j].end(), keyRank)) { + partnersByRank.emplace_back(keyRank, j); + break; + } + } + } + std::sort(partnersByRank.begin(), partnersByRank.end()); + for (auto &partner : partnersByRank) { + partners.push_back(partner.second); + } + } + + for (size_t p = 0; p < partners.size(); p++) { + zend_ulong j = partners[p]; if (arrayAt(arraysToProcess, j) == NULL) continue; + zend_long overlappingKeysCount = (zend_long) sharedKeysCount(keysByRank[i], keysByRank[j]); + zend_long iKeysCount = keyTypesCount(arrayAt(arraysToProcess, i)); PT_FAIL_IF_NEG(iKeysCount); @@ -2931,6 +3245,7 @@ class TypeCombinator if (!isInstance(unionValueType.raw(), pt_ce_union_type)) { if (UNEXPECTED(!mergeInto(arraysToProcess, j, i))) return zv::Val(); forgetConstantScalarValues(constantScalarValues, j); + nullify(nullified, nullifiedList, j); break; } } @@ -2953,6 +3268,7 @@ class TypeCombinator if (superset) { if (UNEXPECTED(!mergeInto(arraysToProcess, j, i))) return zv::Val(); forgetConstantScalarValues(constantScalarValues, j); + nullify(nullified, nullifiedList, j); break; } } @@ -2963,6 +3279,15 @@ class TypeCombinator if (superset) { if (UNEXPECTED(!mergeInto(arraysToProcess, i, j))) return zv::Val(); forgetConstantScalarValues(constantScalarValues, i); + nullify(nullified, nullifiedList, i); + if (!allPartnersVisited) { + /* from now on visited with every array after $j */ + std::vector allPartners = getReduceArraysPartners(i, keysByRank, arraysToProcessPerKey); + auto after = std::find(allPartners.begin(), allPartners.end(), j); + partners.assign(after == allPartners.end() ? allPartners.begin() : after + 1, allPartners.end()); + p = (size_t) -1; + allPartnersVisited = true; + } continue; } } diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index 4d4020447c3..06860f080a8 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -62,27 +62,33 @@ inline constexpr char strings[] = "processArrayAccessoryTypes\0" /* 564 */ "findEmptyArrayNextToOversizedArray\0" /* 591 */ "processArrayTypes\0" /* 626 */ - "optimizeConstantArrays\0" /* 644 */ - "countConstantArrayValueTypes\0" /* 667 */ - "array\0" /* 696 */ - "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 702 */ - "getConstantScalarValuesByKey\0" /* 742 */ - "oneUnrelatedKeyAllowed\0" /* 771 */ - "haveDisjointConstantScalarValues\0" /* 794 */ - "constantArrays\0" /* 827 */ - "preserveTaggedUnions\0" /* 842 */ - "reduceArrays\0" /* 863 */ - "isPlainOrBenevolentUnion\0" /* 876 */ - "PHPStan\\Type\\UnionType\0" /* 901 */ - "intersectFiniteUnions\0" /* 924 */ - "finiteUnionMembers\0" /* 946 */ - "intersect\0" /* 965 */ - "doIntersect\0" /* 975 */ - "intersectDefiniteConstantArrays\0" /* 987 */ - "PHPStan\\Type\\IntersectionType\0" /* 1019 */ - "mergeIntersectionsForUnion\0" /* 1049 */ - "removeFalsey\0" /* 1076 */ - "removeTruthy"; /* 1089 */ + "arrays\0" /* 644 */ + "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 651 */ + "mergeConstantArraysWithSameKeys\0" /* 691 */ + "optimizeConstantArrays\0" /* 723 */ + "countConstantArrayValueTypes\0" /* 746 */ + "array\0" /* 775 */ + "getConstantScalarValuesByKey\0" /* 781 */ + "oneUnrelatedKeyAllowed\0" /* 810 */ + "haveDisjointConstantScalarValues\0" /* 833 */ + "i\0" /* 866 */ + "keysByRank\0" /* 868 */ + "arraysToProcessPerKey\0" /* 879 */ + "getReduceArraysPartners\0" /* 901 */ + "constantArrays\0" /* 925 */ + "preserveTaggedUnions\0" /* 940 */ + "reduceArrays\0" /* 961 */ + "isPlainOrBenevolentUnion\0" /* 974 */ + "PHPStan\\Type\\UnionType\0" /* 999 */ + "intersectFiniteUnions\0" /* 1022 */ + "finiteUnionMembers\0" /* 1044 */ + "intersect\0" /* 1063 */ + "doIntersect\0" /* 1073 */ + "intersectDefiniteConstantArrays\0" /* 1085 */ + "PHPStan\\Type\\IntersectionType\0" /* 1117 */ + "mergeIntersectionsForUnion\0" /* 1147 */ + "removeFalsey\0" /* 1174 */ + "removeTruthy"; /* 1187 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -137,35 +143,41 @@ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_NULL, 17), /* findEmptyArrayNextToOversizedArray return */ reg::packed(553, MAY_BE_ARRAY), /* processArrayTypes $arrayTypes */ reg::packed(0, MAY_BE_ARRAY), /* processArrayTypes return */ + reg::packed(644, MAY_BE_ARRAY), /* mergeConstantArraysWithSameKeys $arrays */ + reg::packed(0, 0, 651), /* mergeConstantArraysWithSameKeys return */ reg::packed(126, MAY_BE_ARRAY), /* optimizeConstantArrays $types */ reg::packed(0, MAY_BE_ARRAY), /* optimizeConstantArrays return */ reg::packed(126, MAY_BE_ARRAY), /* countConstantArrayValueTypes $types */ reg::packed(0, MAY_BE_LONG), /* countConstantArrayValueTypes return */ - reg::packed(696, 0, 702), /* getConstantScalarValuesByKey $array */ + reg::packed(775, 0, 651), /* getConstantScalarValuesByKey $array */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* getConstantScalarValuesByKey return */ reg::packed(263, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $a */ reg::packed(265, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */ - reg::packed(771, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ + reg::packed(810, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */ - reg::packed(827, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ - reg::packed(842, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ + reg::packed(866, MAY_BE_LONG), /* getReduceArraysPartners $i */ + reg::packed(868, MAY_BE_ARRAY), /* getReduceArraysPartners $keysByRank */ + reg::packed(879, MAY_BE_ARRAY), /* getReduceArraysPartners $arraysToProcessPerKey */ + reg::packed(0, MAY_BE_ARRAY), /* getReduceArraysPartners return */ + reg::packed(925, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ + reg::packed(940, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ reg::packed(0, MAY_BE_ARRAY), /* reduceArrays return */ reg::packed(12, 0, 17), /* isPlainOrBenevolentUnion $type */ reg::packed(0, MAY_BE_BOOL), /* isPlainOrBenevolentUnion return */ - reg::packed(263, 0, 901), /* intersectFiniteUnions $a */ - reg::packed(265, 0, 901), /* intersectFiniteUnions $b */ + reg::packed(263, 0, 999), /* intersectFiniteUnions $a */ + reg::packed(265, 0, 999), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 901), /* finiteUnionMembers $union */ + reg::packed(132, 0, 999), /* finiteUnionMembers $union */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* finiteUnionMembers return */ reg::packed(126, 0, 17, false, true), /* intersect $types */ reg::packed(0, 0, 17), /* intersect return */ reg::packed(126, 0, 17, false, true), /* doIntersect $types */ reg::packed(0, 0, 17), /* doIntersect return */ - reg::packed(263, 0, 702), /* intersectDefiniteConstantArrays $a */ - reg::packed(265, 0, 702), /* intersectDefiniteConstantArrays $b */ + reg::packed(263, 0, 651), /* intersectDefiniteConstantArrays $a */ + reg::packed(265, 0, 651), /* intersectDefiniteConstantArrays $b */ reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */ - reg::packed(263, 0, 1019), /* mergeIntersectionsForUnion $a */ - reg::packed(265, 0, 1019), /* mergeIntersectionsForUnion $b */ + reg::packed(263, 0, 1117), /* mergeIntersectionsForUnion $a */ + reg::packed(265, 0, 1117), /* mergeIntersectionsForUnion $b */ reg::packed(0, MAY_BE_NULL, 17), /* mergeIntersectionsForUnion return */ reg::packed(12, 0, 17), /* removeFalsey $type */ reg::packed(0, 0, 17), /* removeFalsey return */ @@ -199,20 +211,22 @@ inline constexpr sigtab::Sig intersectWithSubtractedType = { { 525 /* intersectW inline constexpr sigtab::Sig processArrayAccessoryTypes = { { 564 /* processArrayAccessoryTypes */, 1, 47, 1, 48, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 591 /* findEmptyArrayNextToOversizedArray */, 1, 49, 1, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig processArrayTypes = { { 626 /* processArrayTypes */, 1, 51, 1, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig optimizeConstantArrays = { { 644 /* optimizeConstantArrays */, 1, 53, 1, 54, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 667 /* countConstantArrayValueTypes */, 1, 55, 1, 56, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 742 /* getConstantScalarValuesByKey */, 1, 57, 1, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 794 /* haveDisjointConstantScalarValues */, 3, 59, 3, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig reduceArrays = { { 863 /* reduceArrays */, 2, 63, 2, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 876 /* isPlainOrBenevolentUnion */, 1, 66, 1, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectFiniteUnions = { { 924 /* intersectFiniteUnions */, 2, 68, 2, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig finiteUnionMembers = { { 946 /* finiteUnionMembers */, 1, 71, 1, 72, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersect = { { 965 /* intersect */, 0, 73, 1, 74, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig doIntersect = { { 975 /* doIntersect */, 0, 75, 1, 76, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 987 /* intersectDefiniteConstantArrays */, 2, 77, 2, 79, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1049 /* mergeIntersectionsForUnion */, 2, 80, 2, 82, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeFalsey = { { 1076 /* removeFalsey */, 1, 83, 1, 84, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeTruthy = { { 1089 /* removeTruthy */, 1, 85, 1, 86, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeConstantArraysWithSameKeys = { { 691 /* mergeConstantArraysWithSameKeys */, 1, 53, 1, 54, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig optimizeConstantArrays = { { 723 /* optimizeConstantArrays */, 1, 55, 1, 56, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 746 /* countConstantArrayValueTypes */, 1, 57, 1, 58, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 781 /* getConstantScalarValuesByKey */, 1, 59, 1, 60, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 833 /* haveDisjointConstantScalarValues */, 3, 61, 3, 64, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getReduceArraysPartners = { { 901 /* getReduceArraysPartners */, 3, 65, 3, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceArrays = { { 961 /* reduceArrays */, 2, 69, 2, 71, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 974 /* isPlainOrBenevolentUnion */, 1, 72, 1, 73, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 1022 /* intersectFiniteUnions */, 2, 74, 2, 76, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 1044 /* finiteUnionMembers */, 1, 77, 1, 78, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 1063 /* intersect */, 0, 79, 1, 80, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 1073 /* doIntersect */, 0, 81, 1, 82, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 1085 /* intersectDefiniteConstantArrays */, 2, 83, 2, 85, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1147 /* mergeIntersectionsForUnion */, 2, 86, 2, 88, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 1174 /* removeFalsey */, 1, 89, 1, 90, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 1187 /* removeTruthy */, 1, 91, 1, 92, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator