From e139bc95f3194f833a9544896ff47cb6c64cf49f Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 15:12:15 +0000 Subject: [PATCH 1/2] Merge same-key constant array variants without a chain of growing unions optimizeConstantArrays() collapsed each group of constant arrays with the same keys by merging them one by one into the result, so every merge unioned the growing value union with one more value - quadratic in the number of variants. At a key where every variant holds only constant scalars (or plain unions of them), one union of all the values gives the same type, members in the same order, as that chain; the chain now merges only the rest of the shape, with a placeholder at those keys. bug-12671 merges ~750 list{code, name, null} variants this way. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-d1e7-7395-b029-dd3c2510fac2 --- src/Type/TypeCombinator.php | 69 +++++++++++- turbo-ext/src/TypeCombinator.cpp | 138 ++++++++++++++++++++++- turbo-ext/src/generated/TypeCombinator.h | 97 ++++++++-------- 3 files changed, 247 insertions(+), 57 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index f42af28f3f2..5f075b4aed7 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1368,6 +1368,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 +1474,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) { diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 0f5f69d4ce0..86b06cfb4dd 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -33,6 +33,7 @@ namespace sigs = ptdecl::TypeCombinator::sig; +#include #include zend_class_entry *pt_ce_type_combinator = nullptr; @@ -2458,6 +2459,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 +2629,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; } diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index 4d4020447c3..dc0440b8a04 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -62,27 +62,29 @@ 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 */ + "constantArrays\0" /* 866 */ + "preserveTaggedUnions\0" /* 881 */ + "reduceArrays\0" /* 902 */ + "isPlainOrBenevolentUnion\0" /* 915 */ + "PHPStan\\Type\\UnionType\0" /* 940 */ + "intersectFiniteUnions\0" /* 963 */ + "finiteUnionMembers\0" /* 985 */ + "intersect\0" /* 1004 */ + "doIntersect\0" /* 1014 */ + "intersectDefiniteConstantArrays\0" /* 1026 */ + "PHPStan\\Type\\IntersectionType\0" /* 1058 */ + "mergeIntersectionsForUnion\0" /* 1088 */ + "removeFalsey\0" /* 1115 */ + "removeTruthy"; /* 1128 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -137,35 +139,37 @@ 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_ARRAY), /* reduceArrays $constantArrays */ + reg::packed(881, 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, 940), /* intersectFiniteUnions $a */ + reg::packed(265, 0, 940), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 901), /* finiteUnionMembers $union */ + reg::packed(132, 0, 940), /* 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, 1058), /* mergeIntersectionsForUnion $a */ + reg::packed(265, 0, 1058), /* 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 +203,21 @@ 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 reduceArrays = { { 902 /* reduceArrays */, 2, 65, 2, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 915 /* isPlainOrBenevolentUnion */, 1, 68, 1, 69, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 963 /* intersectFiniteUnions */, 2, 70, 2, 72, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 985 /* finiteUnionMembers */, 1, 73, 1, 74, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 1004 /* intersect */, 0, 75, 1, 76, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 1014 /* doIntersect */, 0, 77, 1, 78, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 1026 /* intersectDefiniteConstantArrays */, 2, 79, 2, 81, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1088 /* mergeIntersectionsForUnion */, 2, 82, 2, 84, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 1115 /* removeFalsey */, 1, 85, 1, 86, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 1128 /* removeTruthy */, 1, 87, 1, 88, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator From 3dd322c5558fa35430e93de246ea0de44756fbf8 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 15:13:06 +0000 Subject: [PATCH 2/2] 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 --- src/Type/TypeCombinator.php | 293 +++++++++++++++++------ turbo-ext/src/TypeCombinator.cpp | 265 +++++++++++++++++--- turbo-ext/src/generated/TypeCombinator.h | 71 +++--- 3 files changed, 495 insertions(+), 134 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 5f075b4aed7..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; @@ -1690,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 @@ -1745,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 86b06cfb4dd..0c13591fd42 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -34,6 +34,8 @@ namespace sigs = ptdecl::TypeCombinator::sig; #include +#include +#include #include zend_class_entry *pt_ce_type_combinator = nullptr; @@ -2921,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) { @@ -2963,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 @@ -2997,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); @@ -3057,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; } } @@ -3079,6 +3268,7 @@ class TypeCombinator if (superset) { if (UNEXPECTED(!mergeInto(arraysToProcess, j, i))) return zv::Val(); forgetConstantScalarValues(constantScalarValues, j); + nullify(nullified, nullifiedList, j); break; } } @@ -3089,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 dc0440b8a04..06860f080a8 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -71,20 +71,24 @@ inline constexpr char strings[] = "getConstantScalarValuesByKey\0" /* 781 */ "oneUnrelatedKeyAllowed\0" /* 810 */ "haveDisjointConstantScalarValues\0" /* 833 */ - "constantArrays\0" /* 866 */ - "preserveTaggedUnions\0" /* 881 */ - "reduceArrays\0" /* 902 */ - "isPlainOrBenevolentUnion\0" /* 915 */ - "PHPStan\\Type\\UnionType\0" /* 940 */ - "intersectFiniteUnions\0" /* 963 */ - "finiteUnionMembers\0" /* 985 */ - "intersect\0" /* 1004 */ - "doIntersect\0" /* 1014 */ - "intersectDefiniteConstantArrays\0" /* 1026 */ - "PHPStan\\Type\\IntersectionType\0" /* 1058 */ - "mergeIntersectionsForUnion\0" /* 1088 */ - "removeFalsey\0" /* 1115 */ - "removeTruthy"; /* 1128 */ + "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 */ @@ -151,15 +155,19 @@ inline constexpr reg::PackedArg args[] = { reg::packed(265, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */ reg::packed(810, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */ - reg::packed(866, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ - reg::packed(881, 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, 940), /* intersectFiniteUnions $a */ - reg::packed(265, 0, 940), /* 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, 940), /* 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 */ @@ -168,8 +176,8 @@ inline constexpr reg::PackedArg args[] = { 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, 1058), /* mergeIntersectionsForUnion $a */ - reg::packed(265, 0, 1058), /* 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 */ @@ -208,16 +216,17 @@ inline constexpr sigtab::Sig optimizeConstantArrays = { { 723 /* optimizeConstan 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 reduceArrays = { { 902 /* reduceArrays */, 2, 65, 2, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 915 /* isPlainOrBenevolentUnion */, 1, 68, 1, 69, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectFiniteUnions = { { 963 /* intersectFiniteUnions */, 2, 70, 2, 72, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig finiteUnionMembers = { { 985 /* finiteUnionMembers */, 1, 73, 1, 74, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersect = { { 1004 /* intersect */, 0, 75, 1, 76, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig doIntersect = { { 1014 /* doIntersect */, 0, 77, 1, 78, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 1026 /* intersectDefiniteConstantArrays */, 2, 79, 2, 81, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1088 /* mergeIntersectionsForUnion */, 2, 82, 2, 84, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeFalsey = { { 1115 /* removeFalsey */, 1, 85, 1, 86, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeTruthy = { { 1128 /* removeTruthy */, 1, 87, 1, 88, ZEND_ACC_PUBLIC | 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