From dc020c24cea3573a4c0efdb287bd9baf3d45567a Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 14:55:42 +0000 Subject: [PATCH 1/3] Look up already normalized union members by object identity doUnion() skips comparing two members that come from the same flattened union, but finding that out scanned every flattened union for each pair, so normalizing a union of k members cost O(k^3). Repeatedly subtracting classes from object (object~(C1|...|Ck) growing by one class per `||` arm) hit this on every step. The members are now indexed by object id once per union - only when the pairwise loop runs - and each pair is answered by a hash lookup. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-fc8f-7115-81cd-f19bb1fdbf46 --- src/Type/TypeCombinator.php | 54 ++++-- turbo-ext/src/TypeCombinator.cpp | 71 ++++++-- turbo-ext/src/generated/TypeCombinator.h | 212 ++++++++++++----------- 3 files changed, 207 insertions(+), 130 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 8fa5b970dc3..3df54c05cfe 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -598,11 +598,29 @@ public static function doUnion(Type ...$types): Type ); $typesCount = count($types); + // $alreadyNormalized keeps the members alive, so their object ids stay + // unique while this lookup refers to them + $normalizedGroupsByType = []; + $normalizedTwice = []; + if ($typesCount > 1) { + foreach ($alreadyNormalized as $group => $normalizedTypes) { + foreach ($normalizedTypes as $normalizedType) { + $normalizedTypeId = spl_object_id($normalizedType); + if (isset($normalizedGroupsByType[$normalizedTypeId][$group])) { + $normalizedTwice[$normalizedTypeId] = true; + continue; + } + $normalizedGroupsByType[$normalizedTypeId][$group] = $group; + } + } + } + // transform A | A to A // transform A | never to A for ($i = 0; $i < $typesCount; $i++) { + $iGroups = $normalizedGroupsByType[spl_object_id($types[$i])] ?? null; for ($j = $i + 1; $j < $typesCount; $j++) { - if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) { + if ($iGroups !== null && self::isAlreadyNormalized($iGroups, $normalizedGroupsByType, $normalizedTwice, $types[$i], $types[$j])) { continue; } $compareResult = self::compareTypesInUnion($types[$i], $types[$j]); @@ -613,6 +631,7 @@ public static function doUnion(Type ...$types): Type [$a, $b] = $compareResult; if ($a !== null) { $types[$i] = $a; + $iGroups = $normalizedGroupsByType[spl_object_id($a)] ?? null; array_splice($types, $j--, 1); $typesCount--; continue 1; @@ -757,24 +776,27 @@ private static function unionWithMarkers(Type $rest, array $markers): Type } /** - * @param array $alreadyNormalized + * Whether $a and $b are members of one union that was flattened into this + * one - such members were already compared against each other. + * + * @param array $aGroups the groups of $a in $normalizedGroupsByType + * @param array> $normalizedGroupsByType member object id => (index of a flattened union holding it => the same index) + * @param array $normalizedTwice member object ids a flattened union held twice */ - private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool + private static function isAlreadyNormalized(array $aGroups, array $normalizedGroupsByType, array $normalizedTwice, Type $a, Type $b): bool { - foreach ($alreadyNormalized as $normalizedTypes) { - foreach ($normalizedTypes as $i => $normalizedType) { - if ($normalizedType !== $a) { - continue; - } + if ($a === $b) { + return isset($normalizedTwice[spl_object_id($a)]); + } - foreach ($normalizedTypes as $j => $anotherNormalizedType) { - if ($i === $j) { - continue; - } - if ($anotherNormalizedType === $b) { - return true; - } - } + $bGroups = $normalizedGroupsByType[spl_object_id($b)] ?? null; + if ($bGroups === null) { + return false; + } + + foreach ($aGroups as $group) { + if (isset($bGroups[$group])) { + return true; } } diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 77161fd82ff..f2dbf78be4c 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -36,6 +36,7 @@ namespace sigs = ptdecl::TypeCombinator::sig; #include #include #include +#include #include zend_class_entry *pt_ce_type_combinator = nullptr; @@ -1256,11 +1257,26 @@ class TypeCombinator listFrom(processedArrayTypes.raw(), types); typesCount = types.size(); + /* alreadyNormalized keeps the members alive, so their objects stay + * unique while this lookup refers to them */ + NormalizedGroupsByType normalizedGroupsByType; + if (typesCount > 1) { + for (size_t group = 0; group < alreadyNormalized.size(); group++) { + for (zv::ArrayEntry entry : zv::ArrRef(alreadyNormalized[group].raw())) { + zval *normalizedType = entry.value().raw(); + if (Z_TYPE_P(normalizedType) != IS_OBJECT) continue; + normalizedGroupsByType.emplace_back(Z_OBJ_P(normalizedType), group); + } + } + std::sort(normalizedGroupsByType.begin(), normalizedGroupsByType.end()); + } + /* transform A | A to A * transform A | never to A */ for (size_t i = 0; i < typesCount; i++) { + NormalizedGroups iGroups = normalizedGroupsOf(normalizedGroupsByType, types[i].raw()); for (size_t j = i + 1; j < typesCount; j++) { - if (isAlreadyNormalized(alreadyNormalized, types[i].raw(), types[j].raw())) continue; + if (iGroups.first != iGroups.second && isAlreadyNormalized(iGroups, normalizedGroupsByType, types[i].raw(), types[j].raw())) continue; zv::Val merged; int compareResult = compareTypesInUnion(types[i].raw(), types[j].raw(), merged); PT_FAIL_IF_NEG(compareResult); @@ -1268,6 +1284,7 @@ class TypeCombinator if (compareResult == 1) { types[i] = std::move(merged); + iGroups = normalizedGroupsOf(normalizedGroupsByType, types[i].raw()); types.erase(types.begin() + (ptrdiff_t) j); j--; typesCount--; @@ -1489,17 +1506,51 @@ class TypeCombinator return 0; } - /* private static isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool */ - static bool isAlreadyNormalized(std::vector &alreadyNormalized, zval *a, zval *b) + /* the $normalizedGroupsByType lookup of doUnion(): (member object, + * flattened union index) pairs, sorted - a member a union holds twice is + * there twice */ + typedef std::vector> NormalizedGroupsByType; + typedef std::pair NormalizedGroups; + + static NormalizedGroups normalizedGroupsOf(const NormalizedGroupsByType &normalizedGroupsByType, zval *type) { - for (zv::Val &normalizedTypes : alreadyNormalized) { - for (zv::ArrayEntry entry : zv::ArrRef(normalizedTypes.raw())) { - if (!sameObject(entry.value().raw(), a)) continue; + if (normalizedGroupsByType.empty() || Z_TYPE_P(type) != IS_OBJECT) { + return NormalizedGroups(normalizedGroupsByType.end(), normalizedGroupsByType.end()); + } + const zend_object *object = Z_OBJ_P(type); + NormalizedGroupsByType::const_iterator first = std::lower_bound( + normalizedGroupsByType.begin(), + normalizedGroupsByType.end(), + object, + [](const std::pair &entry, const zend_object *key) { return entry.first < key; } + ); + NormalizedGroupsByType::const_iterator last = first; + while (last != normalizedGroupsByType.end() && last->first == object) last++; + return NormalizedGroups(first, last); + } - for (zv::ArrayEntry another : zv::ArrRef(normalizedTypes.raw())) { - if (another.stringKeyOrNull() == entry.stringKeyOrNull() && another.indexKey() == entry.indexKey()) continue; - if (sameObject(another.value().raw(), b)) return true; - } + /* private static isAlreadyNormalized(array $aGroups, array $normalizedGroupsByType, Type $a, Type $b): bool */ + static bool isAlreadyNormalized(const NormalizedGroups &aGroups, const NormalizedGroupsByType &normalizedGroupsByType, zval *a, zval *b) + { + if (sameObject(a, b)) { + /* sorted, so a union holding the member twice lists it twice in a row */ + for (NormalizedGroupsByType::const_iterator it = aGroups.first; it != aGroups.second; it++) { + NormalizedGroupsByType::const_iterator next = it + 1; + if (next != aGroups.second && next->second == it->second) return true; + } + + return false; + } + + NormalizedGroups bGroups = normalizedGroupsOf(normalizedGroupsByType, b); + NormalizedGroupsByType::const_iterator ai = aGroups.first; + NormalizedGroupsByType::const_iterator bi = bGroups.first; + while (ai != aGroups.second && bi != bGroups.second) { + if (ai->second == bi->second) return true; + if (ai->second < bi->second) { + ai++; + } else { + bi++; } } diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index 06860f080a8..c898839f065 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -43,52 +43,54 @@ inline constexpr char strings[] = "addMarker\0" /* 213 */ "rest\0" /* 223 */ "unionWithMarkers\0" /* 228 */ - "alreadyNormalized\0" /* 245 */ - "a\0" /* 263 */ - "b\0" /* 265 */ - "isAlreadyNormalized\0" /* 267 */ - "intersections\0" /* 287 */ - "scalarTypes\0" /* 301 */ - "reduceRefinedConstantScalars\0" /* 313 */ - "compareTypesInUnion\0" /* 342 */ - "getAccessoryCaseStringTypes\0" /* 362 */ - "downgradeNonFalsyStringToNonEmpty\0" /* 390 */ - "removeDecimalIntStringAccessory\0" /* 424 */ - "subtractedType\0" /* 456 */ - "unionWithSubtractedType\0" /* 471 */ - "PHPStan\\Type\\SubtractableType\0" /* 495 */ - "intersectWithSubtractedType\0" /* 525 */ - "arrayTypes\0" /* 553 */ - "processArrayAccessoryTypes\0" /* 564 */ - "findEmptyArrayNextToOversizedArray\0" /* 591 */ - "processArrayTypes\0" /* 626 */ - "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 */ + "aGroups\0" /* 245 */ + "normalizedGroupsByType\0" /* 253 */ + "normalizedTwice\0" /* 276 */ + "a\0" /* 292 */ + "b\0" /* 294 */ + "isAlreadyNormalized\0" /* 296 */ + "intersections\0" /* 316 */ + "scalarTypes\0" /* 330 */ + "reduceRefinedConstantScalars\0" /* 342 */ + "compareTypesInUnion\0" /* 371 */ + "getAccessoryCaseStringTypes\0" /* 391 */ + "downgradeNonFalsyStringToNonEmpty\0" /* 419 */ + "removeDecimalIntStringAccessory\0" /* 453 */ + "subtractedType\0" /* 485 */ + "unionWithSubtractedType\0" /* 500 */ + "PHPStan\\Type\\SubtractableType\0" /* 524 */ + "intersectWithSubtractedType\0" /* 554 */ + "arrayTypes\0" /* 582 */ + "processArrayAccessoryTypes\0" /* 593 */ + "findEmptyArrayNextToOversizedArray\0" /* 620 */ + "processArrayTypes\0" /* 655 */ + "arrays\0" /* 673 */ + "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 680 */ + "mergeConstantArraysWithSameKeys\0" /* 720 */ + "optimizeConstantArrays\0" /* 752 */ + "countConstantArrayValueTypes\0" /* 775 */ + "array\0" /* 804 */ + "getConstantScalarValuesByKey\0" /* 810 */ + "oneUnrelatedKeyAllowed\0" /* 839 */ + "haveDisjointConstantScalarValues\0" /* 862 */ + "i\0" /* 895 */ + "keysByRank\0" /* 897 */ + "arraysToProcessPerKey\0" /* 908 */ + "getReduceArraysPartners\0" /* 930 */ + "constantArrays\0" /* 954 */ + "preserveTaggedUnions\0" /* 969 */ + "reduceArrays\0" /* 990 */ + "isPlainOrBenevolentUnion\0" /* 1003 */ + "PHPStan\\Type\\UnionType\0" /* 1028 */ + "intersectFiniteUnions\0" /* 1051 */ + "finiteUnionMembers\0" /* 1073 */ + "intersect\0" /* 1092 */ + "doIntersect\0" /* 1102 */ + "intersectDefiniteConstantArrays\0" /* 1114 */ + "PHPStan\\Type\\IntersectionType\0" /* 1146 */ + "mergeIntersectionsForUnion\0" /* 1176 */ + "removeFalsey\0" /* 1203 */ + "removeTruthy"; /* 1216 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -115,15 +117,17 @@ inline constexpr reg::PackedArg args[] = { reg::packed(223, 0, 17), /* unionWithMarkers $rest */ reg::packed(146, MAY_BE_ARRAY), /* unionWithMarkers $markers */ reg::packed(0, 0, 17), /* unionWithMarkers return */ - reg::packed(245, MAY_BE_ARRAY), /* isAlreadyNormalized $alreadyNormalized */ - reg::packed(263, 0, 17), /* isAlreadyNormalized $a */ - reg::packed(265, 0, 17), /* isAlreadyNormalized $b */ + reg::packed(245, MAY_BE_ARRAY), /* isAlreadyNormalized $aGroups */ + reg::packed(253, MAY_BE_ARRAY), /* isAlreadyNormalized $normalizedGroupsByType */ + reg::packed(276, MAY_BE_ARRAY), /* isAlreadyNormalized $normalizedTwice */ + reg::packed(292, 0, 17), /* isAlreadyNormalized $a */ + reg::packed(294, 0, 17), /* isAlreadyNormalized $b */ reg::packed(0, MAY_BE_BOOL), /* isAlreadyNormalized return */ - reg::packed(287, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $intersections */ - reg::packed(301, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $scalarTypes */ + reg::packed(316, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $intersections */ + reg::packed(330, MAY_BE_ARRAY), /* reduceRefinedConstantScalars $scalarTypes */ reg::packed(0, MAY_BE_ARRAY), /* reduceRefinedConstantScalars return */ - reg::packed(263, 0, 17), /* compareTypesInUnion $a */ - reg::packed(265, 0, 17), /* compareTypesInUnion $b */ + reg::packed(292, 0, 17), /* compareTypesInUnion $a */ + reg::packed(294, 0, 17), /* compareTypesInUnion $b */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* compareTypesInUnion return */ reg::packed(12, 0, 17), /* getAccessoryCaseStringTypes $type */ reg::packed(0, MAY_BE_ARRAY), /* getAccessoryCaseStringTypes return */ @@ -132,52 +136,52 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 17), /* removeDecimalIntStringAccessory $type */ reg::packed(0, 0, 17), /* removeDecimalIntStringAccessory return */ reg::packed(12, 0, 17), /* unionWithSubtractedType $type */ - reg::packed(456, MAY_BE_NULL, 17), /* unionWithSubtractedType $subtractedType */ + reg::packed(485, MAY_BE_NULL, 17), /* unionWithSubtractedType $subtractedType */ reg::packed(0, 0, 17), /* unionWithSubtractedType return */ - reg::packed(263, 0, 495), /* intersectWithSubtractedType $a */ - reg::packed(265, 0, 17), /* intersectWithSubtractedType $b */ + reg::packed(292, 0, 524), /* intersectWithSubtractedType $a */ + reg::packed(294, 0, 17), /* intersectWithSubtractedType $b */ reg::packed(0, 0, 17), /* intersectWithSubtractedType return */ - reg::packed(553, MAY_BE_ARRAY), /* processArrayAccessoryTypes $arrayTypes */ + reg::packed(582, MAY_BE_ARRAY), /* processArrayAccessoryTypes $arrayTypes */ reg::packed(0, MAY_BE_ARRAY), /* processArrayAccessoryTypes return */ - reg::packed(553, MAY_BE_ARRAY), /* findEmptyArrayNextToOversizedArray $arrayTypes */ + reg::packed(582, MAY_BE_ARRAY), /* findEmptyArrayNextToOversizedArray $arrayTypes */ reg::packed(0, MAY_BE_NULL, 17), /* findEmptyArrayNextToOversizedArray return */ - reg::packed(553, MAY_BE_ARRAY), /* processArrayTypes $arrayTypes */ + reg::packed(582, 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(673, MAY_BE_ARRAY), /* mergeConstantArraysWithSameKeys $arrays */ + reg::packed(0, 0, 680), /* 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(775, 0, 651), /* getConstantScalarValuesByKey $array */ + reg::packed(804, 0, 680), /* 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(810, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ + reg::packed(292, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $a */ + reg::packed(294, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */ + reg::packed(839, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */ - 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(895, MAY_BE_LONG), /* getReduceArraysPartners $i */ + reg::packed(897, MAY_BE_ARRAY), /* getReduceArraysPartners $keysByRank */ + reg::packed(908, 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(954, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ + reg::packed(969, 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, 999), /* intersectFiniteUnions $a */ - reg::packed(265, 0, 999), /* intersectFiniteUnions $b */ + reg::packed(292, 0, 1028), /* intersectFiniteUnions $a */ + reg::packed(294, 0, 1028), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 999), /* finiteUnionMembers $union */ + reg::packed(132, 0, 1028), /* 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, 651), /* intersectDefiniteConstantArrays $a */ - reg::packed(265, 0, 651), /* intersectDefiniteConstantArrays $b */ + reg::packed(292, 0, 680), /* intersectDefiniteConstantArrays $a */ + reg::packed(294, 0, 680), /* intersectDefiniteConstantArrays $b */ reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */ - reg::packed(263, 0, 1117), /* mergeIntersectionsForUnion $a */ - reg::packed(265, 0, 1117), /* mergeIntersectionsForUnion $b */ + reg::packed(292, 0, 1146), /* mergeIntersectionsForUnion $a */ + reg::packed(294, 0, 1146), /* 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 */ @@ -200,33 +204,33 @@ inline constexpr sigtab::Sig union_ = { { 132 /* union */, 0, 15, 1, 16, ZEND_AC inline constexpr sigtab::Sig doUnion = { { 138 /* doUnion */, 0, 17, 1, 18, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig addMarker = { { 213 /* addMarker */, 2, 19, 2, 21, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig unionWithMarkers = { { 228 /* unionWithMarkers */, 2, 22, 2, 24, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isAlreadyNormalized = { { 267 /* isAlreadyNormalized */, 3, 25, 3, 28, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig reduceRefinedConstantScalars = { { 313 /* reduceRefinedConstantScalars */, 2, 29, 2, 31, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig compareTypesInUnion = { { 342 /* compareTypesInUnion */, 2, 32, 2, 34, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getAccessoryCaseStringTypes = { { 362 /* getAccessoryCaseStringTypes */, 1, 35, 1, 36, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig downgradeNonFalsyStringToNonEmpty = { { 390 /* downgradeNonFalsyStringToNonEmpty */, 1, 37, 1, 38, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeDecimalIntStringAccessory = { { 424 /* removeDecimalIntStringAccessory */, 1, 39, 1, 40, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig unionWithSubtractedType = { { 471 /* unionWithSubtractedType */, 2, 41, 2, 43, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectWithSubtractedType = { { 525 /* intersectWithSubtractedType */, 2, 44, 2, 46, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -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 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 } }; +inline constexpr sigtab::Sig isAlreadyNormalized = { { 296 /* isAlreadyNormalized */, 5, 25, 5, 30, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceRefinedConstantScalars = { { 342 /* reduceRefinedConstantScalars */, 2, 31, 2, 33, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig compareTypesInUnion = { { 371 /* compareTypesInUnion */, 2, 34, 2, 36, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getAccessoryCaseStringTypes = { { 391 /* getAccessoryCaseStringTypes */, 1, 37, 1, 38, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig downgradeNonFalsyStringToNonEmpty = { { 419 /* downgradeNonFalsyStringToNonEmpty */, 1, 39, 1, 40, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeDecimalIntStringAccessory = { { 453 /* removeDecimalIntStringAccessory */, 1, 41, 1, 42, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig unionWithSubtractedType = { { 500 /* unionWithSubtractedType */, 2, 43, 2, 45, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectWithSubtractedType = { { 554 /* intersectWithSubtractedType */, 2, 46, 2, 48, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig processArrayAccessoryTypes = { { 593 /* processArrayAccessoryTypes */, 1, 49, 1, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 620 /* findEmptyArrayNextToOversizedArray */, 1, 51, 1, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig processArrayTypes = { { 655 /* processArrayTypes */, 1, 53, 1, 54, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeConstantArraysWithSameKeys = { { 720 /* mergeConstantArraysWithSameKeys */, 1, 55, 1, 56, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig optimizeConstantArrays = { { 752 /* optimizeConstantArrays */, 1, 57, 1, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 775 /* countConstantArrayValueTypes */, 1, 59, 1, 60, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 810 /* getConstantScalarValuesByKey */, 1, 61, 1, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 862 /* haveDisjointConstantScalarValues */, 3, 63, 3, 66, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getReduceArraysPartners = { { 930 /* getReduceArraysPartners */, 3, 67, 3, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceArrays = { { 990 /* reduceArrays */, 2, 71, 2, 73, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 1003 /* isPlainOrBenevolentUnion */, 1, 74, 1, 75, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 1051 /* intersectFiniteUnions */, 2, 76, 2, 78, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 1073 /* finiteUnionMembers */, 1, 79, 1, 80, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 1092 /* intersect */, 0, 81, 1, 82, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 1102 /* doIntersect */, 0, 83, 1, 84, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 1114 /* intersectDefiniteConstantArrays */, 2, 85, 2, 87, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1176 /* mergeIntersectionsForUnion */, 2, 88, 2, 90, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 1203 /* removeFalsey */, 1, 91, 1, 92, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 1216 /* removeTruthy */, 1, 93, 1, 94, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator From c2890cd2abd068db4ce1ee9def03d87f06f6eaa4 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 14:55:52 +0000 Subject: [PATCH 2/3] Remove a union from a value it does not hold with one lookup Removing a union peels its members off one at a time, a full remove() per member, even when the value removed from is a constant none of them stands for. Deriving conditional guards from the branch set difference asks exactly that at every branch merge of a growing constant chain (remove(3, 0|1|2)). When the value has a FiniteTypeSet key and the union's set is complete, a key the set does not hold proves every member disjoint, so the value comes back unchanged without the per-member walk. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-fc8f-7115-81cd-f19bb1fdbf46 --- src/Type/TypeCombinator.php | 10 ++++++++++ turbo-ext/src/TypeCombinator.cpp | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 3df54c05cfe..f965be52410 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -176,6 +176,16 @@ public static function doRemove(Type $fromType, Type $typeToRemove): Type return $fromType->subtract(count($remainingTypes) === 1 ? $remainingTypes[0] : new UnionType($remainingTypes)); } + // a value none of the union's keyed members stands for overlaps none + // of them - one lookup instead of a removal per member + $fromTypeKey = FiniteTypeSet::key($fromType); + if ($fromTypeKey !== null) { + $finiteTypeSet = $typeToRemove->getFiniteTypeSet(); + if ($finiteTypeSet !== null && $finiteTypeSet->isComplete() && !$finiteTypeSet->has($fromTypeKey)) { + return $fromType; + } + } + foreach ($typeToRemove->getTypes() as $unionTypeToRemove) { $fromType = self::remove($fromType, $unionTypeToRemove); if ($fromType instanceof NeverType) { diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index f2dbf78be4c..776fd8ca416 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -646,6 +646,24 @@ class TypeCombinator if (isInstance(fromType, pt_ce_union_type)) return copy(fromType); } + /* a value none of the union's keyed members stands for overlaps + * none of them - one lookup instead of a removal per member */ + zv::Val fromTypeKey = pt_type_finite_type_set_key(fromType); + PT_FAIL_IF_UNDEF(fromTypeKey); + if (!isNull(fromTypeKey)) { + zv::Val finiteTypeSet = call(typeToRemove, PT_LC("getfinitetypeset")); + PT_FAIL_IF_UNDEF(finiteTypeSet); + if (!isNull(finiteTypeSet)) { + int complete = callBool(finiteTypeSet.raw(), PT_LC("iscomplete")); + PT_FAIL_IF_NEG(complete); + if (complete) { + int has = callBool(finiteTypeSet.raw(), PT_LC("has"), 1, fromTypeKey.raw()); + PT_FAIL_IF_NEG(has); + if (!has) return copy(fromType); + } + } + } + zv::Val types = getTypes(typeToRemove); PT_FAIL_IF_UNDEF(types); From da9d9171057f027765c885b1ac2f3af17940f39c Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 15:05:19 +0000 Subject: [PATCH 3/3] Answer overwrite markers against the live set instead of per write Each write that replaces a variable leaves a marker the previous replacing write of the same slot observes and kills. compileOverwrites() materialized, for every write, the list of every other write's marker it observes and kills, so a variable assigned W times cost O(W^2) in memory and time, and the backward walk then unset all W markers at each write. Only the markers are compiled now, by variable and slot. A write intersects the live set with the markers of its slots when it is reached, walking the smaller of the two, so the live sets, and with them what is reported, stay the same. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-fc8f-7115-81cd-f19bb1fdbf46 --- src/Analyser/VariableLivenessResolver.php | 127 +++++++++----- turbo-ext/src/VariableLivenessResolver.cpp | 156 ++++++++++-------- .../src/generated/VariableLivenessResolver.h | 19 ++- 3 files changed, 186 insertions(+), 116 deletions(-) diff --git a/src/Analyser/VariableLivenessResolver.php b/src/Analyser/VariableLivenessResolver.php index af9cf05481e..ee119a3c98c 100644 --- a/src/Analyser/VariableLivenessResolver.php +++ b/src/Analyser/VariableLivenessResolver.php @@ -86,8 +86,8 @@ final class VariableLivenessResolver /** @var array */ private array $overwriteMarkers = []; - /** @var array> */ - private array $overwriteKeys = []; + /** @var array>> variable name => replaced slot => marker => id of the write leaving it */ + private array $markersBySlot = []; /** @var array */ private array $overwrittenIds = []; @@ -258,15 +258,16 @@ private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContex $id = $flow->write->getId(); if ($flow->kind !== VariableFlow::DISCARD) { $this->observeWrite($id, $next); - $this->observeOverwrite($id, $next); + $this->observeOverwrite($flow->write, $next); foreach ($this->literalItems[$id] ?? [] as $item) { $this->observeWrite($item->getId(), $next); - $this->observeOverwrite($item->getId(), $next); + $this->observeOverwrite($item, $next); } } foreach (array_keys($this->killedKeys[$id] ?? []) as $key) { unset($next[$key]); } + $next = $this->killOverwriteMarkers($flow->write, $next); if (isset($this->overwriteMarkers[$id])) { $next[$this->overwriteMarkers[$id]] = true; } @@ -569,6 +570,10 @@ private function compileAccesses(): void * write finding another write's marker after it reaches that replacement * on some path, with no replacement in between. * + * Only the markers are compiled, by slot: which of them a write observes + * or kills is answered against the live set when the write is reached, so + * the work stays linear in the variable's writes. + * * @param list $accesses */ private function compileOverwrites(string $name, array $accesses): void @@ -590,38 +595,8 @@ private function compileOverwrites(string $name, array $accesses): void if ($markersBySlot === []) { return; } - foreach ($accesses as $access) { - $write = $access->write; - if ($write === null) { - continue; - } - $id = $write->getId(); - $offset = $write->getOffset(); - $observed = $markersBySlot[''] ?? []; - if ($write->isOffsetWrite() && $offset !== null) { - $observed += $markersBySlot[self::offsetKey($offset)] ?? []; - } - foreach ($observed as $marker => $writerId) { - // a loop running the same write again replaces nothing it - // wrote - nor the items of the array literal it assigns - if ($writerId === $id || $writerId === $write->getParentId()) { - continue; - } - $this->overwriteKeys[$id][$marker] = true; - } - $slot = self::replacedSlot($write); - if ($slot === null) { - continue; - } - foreach ($markersBySlot as $markerSlot => $markers) { - if ($slot !== '' && $markerSlot !== $slot) { - continue; - } - foreach (array_keys($markers) as $marker) { - $this->killedKeys[$id][$marker] = true; - } - } - } + + $this->markersBySlot[$name] = $markersBySlot; } /** The slot a write replaces: '' for the whole variable, null when it keeps the old value. */ @@ -655,15 +630,85 @@ private function observeWrite(int $id, array $next): void } /** @param array $next */ - private function observeOverwrite(int $id, array $next): void + private function observeOverwrite(VariableWrite $write, array $next): void { - foreach (array_keys($this->overwriteKeys[$id] ?? []) as $key) { - if (!isset($next[$key])) { + $markersBySlot = $this->markersBySlot[$write->getVariableName()] ?? null; + if ($markersBySlot === null) { + return; + } + $id = $write->getId(); + $parentId = $write->getParentId(); + $observedSlots = ['']; + $offset = $write->getOffset(); + if ($write->isOffsetWrite() && $offset !== null) { + $observedSlots[] = self::offsetKey($offset); + } + foreach ($observedSlots as $slot) { + $markers = $markersBySlot[$slot] ?? []; + // walk the smaller of the two sets + if (count($markers) <= count($next)) { + foreach ($markers as $marker => $writerId) { + if (!isset($next[$marker])) { + continue; + } + // a loop running the same write again replaces nothing it + // wrote - nor the items of the array literal it assigns + if ($writerId === $id || $writerId === $parentId) { + continue; + } + $this->overwrittenIds[$id] = true; + return; + } continue; } - $this->overwrittenIds[$id] = true; - return; + foreach (array_keys($next) as $key) { + $writerId = $markers[$key] ?? null; + if ($writerId === null || $writerId === $id || $writerId === $parentId) { + continue; + } + $this->overwrittenIds[$id] = true; + return; + } + } + } + + /** + * Drops the markers of the slots $write replaces - all of the variable's + * when it replaces the whole variable. + * + * @param array $next + * @return array + */ + private function killOverwriteMarkers(VariableWrite $write, array $next): array + { + $markersBySlot = $this->markersBySlot[$write->getVariableName()] ?? null; + if ($markersBySlot === null) { + return $next; + } + $slot = self::replacedSlot($write); + if ($slot === null) { + return $next; + } + foreach ($markersBySlot as $markerSlot => $markers) { + if ($slot !== '' && $markerSlot !== $slot) { + continue; + } + // walk the smaller of the two sets + if (count($markers) <= count($next)) { + foreach (array_keys($markers) as $marker) { + unset($next[$marker]); + } + continue; + } + foreach (array_keys($next) as $key) { + if (!isset($markers[$key])) { + continue; + } + unset($next[$key]); + } } + + return $next; } private function resolveDependencies(): void diff --git a/turbo-ext/src/VariableLivenessResolver.cpp b/turbo-ext/src/VariableLivenessResolver.cpp index 87a648f9a8b..4f55fcede0a 100644 --- a/turbo-ext/src/VariableLivenessResolver.cpp +++ b/turbo-ext/src/VariableLivenessResolver.cpp @@ -548,7 +548,7 @@ class VariableLivenessResolver zv::Val literalItems = emptyArray(); zv::Val coveredIds = emptyArray(); zv::Val overwriteMarkers = emptyArray(); - zv::Val overwriteKeys = emptyArray(); + zv::Val markersBySlot = emptyArray(); /* name => slot => marker => writer id */ zv::Val overwrittenIds = emptyArray(); zv::Val allReadKeys = emptyArray(); zv::Val loopStatements = emptyArray(); @@ -816,14 +816,14 @@ class VariableLivenessResolver zend_long id = view.id; if (kind != PT_VLR_DISCARD) { observeWrite(id, next); - observeOverwrite(id, next); + observeOverwrite(view, next); HashTable *items = innerTable(literalItems, id); if (items != NULL) { for (auto entry : zv::TableRef(items)) { - zend_long itemId; - if (UNEXPECTED(!writeId(entry.value().deref().raw(), itemId))) return zv::Val(); - observeWrite(itemId, next); - observeOverwrite(itemId, next); + WriteView item; + if (UNEXPECTED(!item.load(entry.value().deref().raw()))) return zv::Val(); + observeWrite(item.id, next); + observeOverwrite(item, next); } } } @@ -836,6 +836,7 @@ class VariableLivenessResolver } } } + killOverwriteMarkers(view, next); zval *marker = zend_hash_index_find(tableOf(overwriteMarkers), (zend_ulong) id); if (marker != NULL) { setTrue(next, Z_STR_P(marker)); @@ -1415,7 +1416,7 @@ class VariableLivenessResolver /* Mirrors compileOverwrites(); false = pending exception. */ [[nodiscard]] bool compileOverwrites(zend_string *name, HashTable *accessTable) { - zv::Val markersBySlot = emptyArray(); + zv::Val slotMarkers = emptyArray(); for (auto entry : zv::TableRef(accessTable)) { zend_object *access = Z_OBJ_P(entry.value().deref().raw()); zval *write = slot(access, slots.accessWrite, "write"); @@ -1438,7 +1439,7 @@ class VariableLivenessResolver smart_str_append_long(&key, view.id); smart_str_0(&key); zv::Str marker = zv::Str::adopt(key.s); - zval *bySlot = innerSlotByKey(markersBySlot, slotName.get()); + zval *bySlot = innerSlotByKey(slotMarkers, slotName.get()); zval idValue; ZVAL_LONG(&idValue, view.id); zend_hash_update(Z_ARRVAL_P(bySlot), marker.get(), &idValue); @@ -1446,60 +1447,12 @@ class VariableLivenessResolver ZVAL_STR(&markerValue, marker.get()); setIndex(overwriteMarkers, view.id, &markerValue); } - if (countOf(markersBySlot) == 0) return true; - for (auto entry : zv::TableRef(accessTable)) { - zend_object *access = Z_OBJ_P(entry.value().deref().raw()); - zval *write = slot(access, slots.accessWrite, "write"); - if (UNEXPECTED(write == NULL)) return false; - if (Z_TYPE_P(write) != IS_OBJECT) continue; - WriteView view; - if (UNEXPECTED(!view.load(write))) return false; - zend_long id = view.id; - observeOverwrites(view, innerTableByKey(markersBySlot, ZSTR_EMPTY_ALLOC())); - if (view.offsetWrite && !view.offsetIsNull) { - zv::Str offsetSlot = offsetKey(view.offset.raw()); - observeOverwrites(view, innerTableByKey(markersBySlot, offsetSlot.get())); - } - zv::Str slotName; - if (!replacedSlot(view, slotName)) continue; - bool whole = ZSTR_LEN(slotName.get()) == 0; - for (auto slotEntry : zv::TableRef(tableOf(markersBySlot))) { - zend_string *markerSlot = slotEntry.stringKeyOrNull(); - if (markerSlot == NULL) continue; - if (!whole && !zend_string_equals(markerSlot, slotName.get())) continue; - zv::Ref markers = slotEntry.value().deref(); - if (!markers.isArray()) continue; - for (auto markerEntry : zv::TableRef(markers.asArrayTable())) { - zend_string *marker = markerEntry.stringKeyOrNull(); - if (marker == NULL) continue; - zval *killed = innerSlot(killedKeys, id); - zval trueValue; - ZVAL_TRUE(&trueValue); - zend_hash_update(Z_ARRVAL_P(killed), marker, &trueValue); - } - } - } - return true; - } + if (countOf(slotMarkers) == 0) return true; - /* The markers of $markers written by another write than $write and the - * array literal it is an item of. */ - void observeOverwrites(const WriteView &write, HashTable *markers) - { - if (markers == NULL) return; - zend_long id = write.id; - for (auto entry : zv::TableRef(markers)) { - zend_string *marker = entry.stringKeyOrNull(); - if (marker == NULL) continue; - // a loop running the same write again replaces nothing it - // wrote - nor the items of the array literal it assigns - zend_long writerId = entry.value().deref().toLong(); - if (writerId == id || (!write.parentIdIsNull && writerId == write.parentId)) continue; - zval *observed = innerSlot(overwriteKeys, id); - zval trueValue; - ZVAL_TRUE(&trueValue); - zend_hash_update(Z_ARRVAL_P(observed), marker, &trueValue); - } + zval *stored = innerSlotByKey(markersBySlot, name); + zval_ptr_dtor(stored); + ZVAL_COPY(stored, slotMarkers.raw()); + return true; } /* Mirrors replacedSlot(); false for null. */ @@ -1515,16 +1468,83 @@ class VariableLivenessResolver } /* Mirrors observeOverwrite(). */ - void observeOverwrite(zend_long id, const zv::Val &next) + void observeOverwrite(WriteView &write, const zv::Val &next) { - HashTable *overwrites = innerTable(overwriteKeys, id); - if (overwrites == NULL) return; - for (auto entry : zv::TableRef(overwrites)) { - zend_string *key = entry.stringKeyOrNull(); - if (key == NULL || !isTrueAt(next, key)) continue; - setTrueIndex(overwrittenIds, id); + HashTable *bySlot = innerTableByKey(markersBySlot, write.nameStr); + if (bySlot == NULL) return; + if (observesOverwriteMarker(write, zend_hash_find(bySlot, ZSTR_EMPTY_ALLOC()), next)) { + setTrueIndex(overwrittenIds, write.id); return; } + if (write.offsetWrite && !write.offsetIsNull) { + zv::Str offsetSlot = offsetKey(write.offset.raw()); + if (observesOverwriteMarker(write, zend_hash_find(bySlot, offsetSlot.get()), next)) { + setTrueIndex(overwrittenIds, write.id); + } + } + } + + /* whether $next holds one of $markers left by another write than $write + * and the array literal it is an item of - walking the smaller of the two */ + static bool observesOverwriteMarker(const WriteView &write, zval *markersValue, const zv::Val &next) + { + if (markersValue == NULL) return false; + ZVAL_DEREF(markersValue); + if (Z_TYPE_P(markersValue) != IS_ARRAY) return false; + HashTable *markers = Z_ARRVAL_P(markersValue); + bool walkMarkers = zend_hash_num_elements(markers) <= countOf(next); + for (auto entry : zv::TableRef(walkMarkers ? markers : tableOf(next))) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) continue; + zval *writer; + if (walkMarkers) { + if (!isTrueAt(next, key)) continue; + writer = entry.value().deref().raw(); + } else { + writer = zend_hash_find(markers, key); + if (writer == NULL) continue; + ZVAL_DEREF(writer); + } + // a loop running the same write again replaces nothing it + // wrote - nor the items of the array literal it assigns + zend_long writerId = zval_get_long(writer); + if (writerId == write.id || (!write.parentIdIsNull && writerId == write.parentId)) continue; + return true; + } + return false; + } + + /* Mirrors killOverwriteMarkers(). */ + void killOverwriteMarkers(WriteView &write, zv::Val &next) + { + HashTable *bySlot = innerTableByKey(markersBySlot, write.nameStr); + if (bySlot == NULL) return; + zv::Str slotName; + if (!replacedSlot(write, slotName)) return; + bool whole = ZSTR_LEN(slotName.get()) == 0; + for (auto slotEntry : zv::TableRef(bySlot)) { + zend_string *markerSlot = slotEntry.stringKeyOrNull(); + if (markerSlot == NULL) continue; + if (!whole && !zend_string_equals(markerSlot, slotName.get())) continue; + zv::Ref markersRef = slotEntry.value().deref(); + if (!markersRef.isArray()) continue; + HashTable *markers = markersRef.asArrayTable(); + if (zend_hash_num_elements(markers) <= countOf(next)) { + for (auto markerEntry : zv::TableRef(markers)) { + zend_string *marker = markerEntry.stringKeyOrNull(); + if (marker == NULL) continue; + unsetKey(next, marker); + } + continue; + } + /* a snapshot to walk while the live set loses keys */ + zv::Val live = shareArray(next); + for (auto entry : zv::TableRef(tableOf(live))) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL || !zend_hash_exists(markers, key)) continue; + unsetKey(next, key); + } + } } /* Mirrors observeWrite(). */ diff --git a/turbo-ext/src/generated/VariableLivenessResolver.h b/turbo-ext/src/generated/VariableLivenessResolver.h index 58688e0ebee..9c62ee75255 100644 --- a/turbo-ext/src/generated/VariableLivenessResolver.h +++ b/turbo-ext/src/generated/VariableLivenessResolver.h @@ -28,7 +28,7 @@ inline constexpr uint32_t inputSinks = 14; inline constexpr uint32_t literalItems = 15; inline constexpr uint32_t coveredIds = 16; inline constexpr uint32_t overwriteMarkers = 17; -inline constexpr uint32_t overwriteKeys = 18; +inline constexpr uint32_t markersBySlot = 18; inline constexpr uint32_t overwrittenIds = 19; inline constexpr uint32_t allReadKeys = 20; inline constexpr uint32_t loopStatements = 21; @@ -66,7 +66,7 @@ inline void declareProperties(reg::Class &cls) cls.property("literalItems", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("coveredIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("overwriteMarkers", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); - cls.property("overwriteKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("markersBySlot", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("overwrittenIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("allReadKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("loopStatements", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); @@ -112,8 +112,9 @@ inline constexpr char strings[] = "id\0" /* 373 */ "observeWrite\0" /* 376 */ "observeOverwrite\0" /* 389 */ - "resolveDependencies\0" /* 406 */ - "resolveCoverage"; /* 426 */ + "killOverwriteMarkers\0" /* 406 */ + "resolveDependencies\0" /* 427 */ + "resolveCoverage"; /* 447 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 21), /* resolve $function */ reg::packed(49, MAY_BE_NULL, 54), /* resolve $flow */ @@ -143,9 +144,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(373, MAY_BE_LONG), /* observeWrite $id */ reg::packed(133, MAY_BE_ARRAY), /* observeWrite $next */ reg::packed(84, MAY_BE_VOID), /* observeWrite return */ - reg::packed(373, MAY_BE_LONG), /* observeOverwrite $id */ + reg::packed(262, 0, 202), /* observeOverwrite $write */ reg::packed(133, MAY_BE_ARRAY), /* observeOverwrite $next */ reg::packed(84, MAY_BE_VOID), /* observeOverwrite return */ + reg::packed(262, 0, 202), /* killOverwriteMarkers $write */ + reg::packed(133, MAY_BE_ARRAY), /* killOverwriteMarkers $next */ + reg::packed(84, MAY_BE_ARRAY), /* killOverwriteMarkers return */ reg::packed(84, MAY_BE_VOID), /* resolveDependencies return */ reg::packed(84, MAY_BE_VOID), /* resolveCoverage return */ }; @@ -166,8 +170,9 @@ inline constexpr sigtab::Sig compileOverwrites = { { 342 /* compileOverwrites */ inline constexpr sigtab::Sig replacedSlot = { { 360 /* replacedSlot */, 1, 23, 1, 24, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig observeWrite = { { 376 /* observeWrite */, 2, 25, 2, 27, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig observeOverwrite = { { 389 /* observeOverwrite */, 2, 28, 2, 30, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveDependencies = { { 406 /* resolveDependencies */, 0, 31, 0, 31, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveCoverage = { { 426 /* resolveCoverage */, 0, 32, 0, 32, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig killOverwriteMarkers = { { 406 /* killOverwriteMarkers */, 2, 31, 2, 33, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveDependencies = { { 427 /* resolveDependencies */, 0, 34, 0, 34, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveCoverage = { { 447 /* resolveCoverage */, 0, 35, 0, 35, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::VariableLivenessResolver