From d0b1cf1453694fab4bcfb6b7dfc8934952efd463 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 22:15:05 +0000 Subject: [PATCH 1/2] Share the observed keys of whole-variable writes and walk the smaller set Every write replacing a whole variable observes and kills the read keys of all its slots, and compileAccesses() copied them into each such write: quadratic in the writes and reads of a variable. They are now built once per variable and shared. Removing them, the markers of a variable and the observed keys from the live set walks the smaller of the two sets, like the per-slot markers already did. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ad6c-dec2-72ff-bbe8-5b3b62746c94 --- src/Analyser/VariableLivenessResolver.php | 104 ++++++---- turbo-ext/src/VariableLivenessResolver.cpp | 187 ++++++++++++------ .../src/generated/VariableLivenessResolver.h | 34 ++-- 3 files changed, 217 insertions(+), 108 deletions(-) diff --git a/src/Analyser/VariableLivenessResolver.php b/src/Analyser/VariableLivenessResolver.php index ee119a3c98c..24225d45efa 100644 --- a/src/Analyser/VariableLivenessResolver.php +++ b/src/Analyser/VariableLivenessResolver.php @@ -12,6 +12,9 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use Throwable; +use function array_diff_key; +use function array_fill_keys; +use function array_key_exists; use function array_keys; use function array_pop; use function array_reverse; @@ -89,6 +92,9 @@ final class VariableLivenessResolver /** @var array>> variable name => replaced slot => marker => id of the write leaving it */ private array $markersBySlot = []; + /** @var array> variable name => marker => id of the write leaving it */ + private array $markersByName = []; + /** @var array */ private array $overwrittenIds = []; @@ -264,9 +270,7 @@ private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContex $this->observeOverwrite($item, $next); } } - foreach (array_keys($this->killedKeys[$id] ?? []) as $key) { - unset($next[$key]); - } + $next = self::withoutKeys($next, $this->killedKeys[$id] ?? []); $next = $this->killOverwriteMarkers($flow->write, $next); if (isset($this->overwriteMarkers[$id])) { $next[$this->overwriteMarkers[$id]] = true; @@ -532,6 +536,14 @@ private function compileAccesses(): void $this->nameKeys[$name][$key] = true; } } + // the keys of every slot, shared by all writes of the whole variable + // instead of copied into each of them + $allKeys = []; + foreach ($keysBySlot as $keys) { + $allKeys += $keys; + } + $offsetKeys = array_diff_key($allKeys, $keysBySlot['container'] ?? []); + $allKills = array_fill_keys(array_keys($allKeys), true); foreach ($accesses as $access) { $write = $access->write; if ($write === null) { @@ -540,25 +552,21 @@ private function compileAccesses(): void $id = $write->getId(); $offset = $write->getOffset(); if ($write->isOffsetWrite() && $offset !== null) { - $slot = self::offsetKey($offset); - $selectedKeys = [$slot => $keysBySlot[$slot] ?? []]; + $keys = $keysBySlot[self::offsetKey($offset)] ?? []; + $kills = $write->replacesOffset(); } else { - $selectedKeys = $keysBySlot; - if ($write->isOffsetWrite()) { - unset($selectedKeys['container']); - } + $keys = $write->isOffsetWrite() ? $offsetKeys : $allKeys; + $kills = !$write->isOffsetWrite(); } - $kills = !$write->isOffsetWrite() || ($offset !== null && $write->replacesOffset()); - foreach ($selectedKeys as $keys) { - foreach ($keys as $key => $targetId) { - $this->observedKeys[$id][$key] = $targetId; - if (!$kills) { - continue; - } - - $this->killedKeys[$id][$key] = true; - } + if ($keys === []) { + continue; } + $this->observedKeys[$id] = isset($this->observedKeys[$id]) ? $this->observedKeys[$id] + $keys : $keys; + if (!$kills) { + continue; + } + $killedKeys = $keys === $allKeys ? $allKills : array_fill_keys(array_keys($keys), true); + $this->killedKeys[$id] = isset($this->killedKeys[$id]) ? $this->killedKeys[$id] + $killedKeys : $killedKeys; } $this->compileOverwrites($name, $accesses); } @@ -597,6 +605,11 @@ private function compileOverwrites(string $name, array $accesses): void } $this->markersBySlot[$name] = $markersBySlot; + $markersOfName = []; + foreach ($markersBySlot as $markers) { + $markersOfName += $markers; + } + $this->markersByName[$name] = $markersOfName; } /** The slot a write replaces: '' for the whole variable, null when it keeps the old value. */ @@ -616,7 +629,19 @@ private static function replacedSlot(VariableWrite $write): ?string /** @param array $next */ private function observeWrite(int $id, array $next): void { - foreach ($this->observedKeys[$id] ?? [] as $key => $targetId) { + $observedKeys = $this->observedKeys[$id] ?? []; + // walk the smaller of the two sets + if (count($observedKeys) > count($next)) { + $live = []; + foreach (array_keys($next) as $key) { + if (!array_key_exists($key, $observedKeys)) { + continue; + } + $live[$key] = $observedKeys[$key]; + } + $observedKeys = $live; + } + foreach ($observedKeys as $key => $targetId) { if (!isset($next[$key])) { continue; } @@ -689,23 +714,34 @@ private function killOverwriteMarkers(VariableWrite $write, array $next): array if ($slot === null) { return $next; } - foreach ($markersBySlot as $markerSlot => $markers) { - if ($slot !== '' && $markerSlot !== $slot) { - continue; + if ($slot === '') { + return self::withoutKeys($next, $this->markersByName[$write->getVariableName()]); + } + + return self::withoutKeys($next, $markersBySlot[$slot] ?? []); + } + + /** + * Walks the smaller of the two sets. + * + * @param array $next + * @param array $keys + * @return array + */ + private static function withoutKeys(array $next, array $keys): array + { + if (count($keys) <= count($next)) { + foreach (array_keys($keys) as $key) { + unset($next[$key]); } - // walk the smaller of the two sets - if (count($markers) <= count($next)) { - foreach (array_keys($markers) as $marker) { - unset($next[$marker]); - } + + return $next; + } + foreach (array_keys($next) as $key) { + if (!array_key_exists($key, $keys)) { continue; } - foreach (array_keys($next) as $key) { - if (!isset($markers[$key])) { - continue; - } - unset($next[$key]); - } + unset($next[$key]); } return $next; diff --git a/turbo-ext/src/VariableLivenessResolver.cpp b/turbo-ext/src/VariableLivenessResolver.cpp index 4f55fcede0a..2366ed3857f 100644 --- a/turbo-ext/src/VariableLivenessResolver.cpp +++ b/turbo-ext/src/VariableLivenessResolver.cpp @@ -358,6 +358,58 @@ zv::Val unionOf(const zv::Val &a, const zv::Val &b) return unionOf(a, tableOf(b)); } +/* Mirrors withoutKeys(): unset($next[$key]) for every key of $keys, + * walking the smaller of the two sets */ +void withoutKeys(zv::Val &next, HashTable *keys) +{ + if (keys == NULL || zend_hash_num_elements(keys) == 0) return; + if (zend_hash_num_elements(keys) <= countOf(next)) { + for (auto entry : zv::TableRef(keys)) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(next, key); + } + } + return; + } + /* 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(keys, key)) continue; + unsetKey(next, key); + } +} + +/* $array[$index] = isset($array[$index]) ? $array[$index] + $keys : $keys + * ($keys shared, not copied, when the entry is new) */ +void addKeysAt(zv::Val &array, zend_long index, const zv::Val &keys) +{ + SEPARATE_ARRAY(array.raw()); + zval *slot = zend_hash_index_find(Z_ARRVAL_P(array.raw()), (zend_ulong) index); + if (slot == NULL) { + zval shared; + ZVAL_COPY(&shared, const_cast(keys).raw()); + zend_hash_index_add_new(Z_ARRVAL_P(array.raw()), (zend_ulong) index, &shared); + return; + } + SEPARATE_ARRAY(slot); + zend_hash_merge(Z_ARRVAL_P(slot), tableOf(keys), zval_add_ref, 0); +} + +/* array_fill_keys(array_keys($keys), true) */ +zv::Val trueForKeys(const zv::Val &keys) +{ + zv::Val result = emptyArray(); + for (auto entry : zv::TableRef(tableOf(keys))) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + setTrue(result, key); + } + } + return result; +} + bool isTrueAt(const zv::Val &array, zend_string *key) { return zend_hash_exists(tableOf(array), key); @@ -549,6 +601,7 @@ class VariableLivenessResolver zv::Val coveredIds = emptyArray(); zv::Val overwriteMarkers = emptyArray(); zv::Val markersBySlot = emptyArray(); /* name => slot => marker => writer id */ + zv::Val markersByName = emptyArray(); /* name => marker => writer id */ zv::Val overwrittenIds = emptyArray(); zv::Val allReadKeys = emptyArray(); zv::Val loopStatements = emptyArray(); @@ -827,15 +880,7 @@ class VariableLivenessResolver } } } - HashTable *killed = innerTable(killedKeys, id); - if (killed != NULL) { - for (auto entry : zv::TableRef(killed)) { - zend_string *key = entry.stringKeyOrNull(); - if (key != NULL) { - unsetKey(next, key); - } - } - } + withoutKeys(next, innerTable(killedKeys, id)); killOverwriteMarkers(view, next); zval *marker = zend_hash_index_find(tableOf(overwriteMarkers), (zend_ulong) id); if (marker != NULL) { @@ -1366,6 +1411,26 @@ class VariableLivenessResolver zend_hash_update(Z_ARRVAL_P(byName), key.get(), &trueValue); } } + /* the keys of every slot, shared by all writes of the whole + * variable instead of copied into each of them */ + zv::Val allKeys = emptyArray(); + for (auto slotEntry : zv::TableRef(tableOf(keysBySlot))) { + zv::Ref keys = slotEntry.value().deref(); + if (!keys.isArray()) continue; + SEPARATE_ARRAY(allKeys.raw()); + zend_hash_merge(Z_ARRVAL_P(allKeys.raw()), keys.asArrayTable(), zval_add_ref, 0); + } + zv::Val offsetKeys = shareArray(allKeys); + HashTable *containerKeys = innerTableByKey(keysBySlot, containerSlot); + if (containerKeys != NULL) { + for (auto keyEntry : zv::TableRef(containerKeys)) { + zend_string *key = keyEntry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(offsetKeys, key); + } + } + } + zv::Val allKills = trueForKeys(allKeys); for (auto entry : zv::TableRef(accessTable)) { zend_object *access = Z_OBJ_P(entry.value().deref().raw()); zval *write = slot(access, slots.accessWrite, "write"); @@ -1374,38 +1439,28 @@ class VariableLivenessResolver WriteView view; if (UNEXPECTED(!view.load(write))) return false; zend_long id = view.id; - bool offsetIsNull = view.offsetIsNull; - zv::Val selectedKeys; - if (view.offsetWrite && !offsetIsNull) { + zv::Val keys; + bool kills; + bool whole = false; + if (view.offsetWrite && !view.offsetIsNull) { zv::Str slotName = offsetKey(view.offset.raw()); - selectedKeys = emptyArray(); - HashTable *keys = innerTableByKey(keysBySlot, slotName.get()); - zval keysValue = (keys != NULL ? zv::Arr::copyOfTable(keys) : zv::Arr::empty()).take(); - SEPARATE_ARRAY(selectedKeys.raw()); - zend_hash_update(Z_ARRVAL_P(selectedKeys.raw()), slotName.get(), &keysValue); + HashTable *slotKeys = innerTableByKey(keysBySlot, slotName.get()); + zval keysValue = (slotKeys != NULL ? zv::Arr::copyOfTable(slotKeys) : zv::Arr::empty()).take(); + keys = zv::Val::adopt(keysValue); + kills = view.replacesOffset; } else { - selectedKeys = shareArray(keysBySlot); - if (view.offsetWrite) { - unsetKey(selectedKeys, containerSlot); - } + whole = !view.offsetWrite; + keys = shareArray(whole ? allKeys : offsetKeys); + kills = whole; } - bool kills = !view.offsetWrite || (!offsetIsNull && view.replacesOffset); - for (auto slotEntry : zv::TableRef(tableOf(selectedKeys))) { - zv::Ref keys = slotEntry.value().deref(); - if (!keys.isArray()) continue; - for (auto keyEntry : zv::TableRef(keys.asArrayTable())) { - zend_string *key = keyEntry.stringKeyOrNull(); - if (key == NULL) continue; - zval *observed = innerSlot(observedKeys, id); - Z_TRY_ADDREF_P(keyEntry.value().raw()); - zend_hash_update(Z_ARRVAL_P(observed), key, keyEntry.value().raw()); - if (!kills) continue; - - zval *killed = innerSlot(killedKeys, id); - zval trueValue; - ZVAL_TRUE(&trueValue); - zend_hash_update(Z_ARRVAL_P(killed), key, &trueValue); - } + if (countOf(keys) == 0) continue; + addKeysAt(observedKeys, id, keys); + if (!kills) continue; + + if (whole) { + addKeysAt(killedKeys, id, allKills); + } else { + addKeysAt(killedKeys, id, trueForKeys(keys)); } } if (UNEXPECTED(!compileOverwrites(name, accessTable))) return false; @@ -1452,6 +1507,16 @@ class VariableLivenessResolver zval *stored = innerSlotByKey(markersBySlot, name); zval_ptr_dtor(stored); ZVAL_COPY(stored, slotMarkers.raw()); + zv::Val markersOfName = emptyArray(); + for (auto slotEntry : zv::TableRef(tableOf(slotMarkers))) { + zv::Ref markers = slotEntry.value().deref(); + if (!markers.isArray()) continue; + SEPARATE_ARRAY(markersOfName.raw()); + zend_hash_merge(Z_ARRVAL_P(markersOfName.raw()), markers.asArrayTable(), zval_add_ref, 0); + } + zval *storedOfName = innerSlotByKey(markersByName, name); + zval_ptr_dtor(storedOfName); + ZVAL_COPY(storedOfName, markersOfName.raw()); return true; } @@ -1521,30 +1586,15 @@ class VariableLivenessResolver 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); - } + if (ZSTR_LEN(slotName.get()) == 0) { + withoutKeys(next, innerTableByKey(markersByName, write.nameStr)); + return; } + zval *markers = zend_hash_find(bySlot, slotName.get()); + if (markers == NULL) return; + ZVAL_DEREF(markers); + if (Z_TYPE_P(markers) != IS_ARRAY) return; + withoutKeys(next, Z_ARRVAL_P(markers)); } /* Mirrors observeWrite(). */ @@ -1552,6 +1602,21 @@ class VariableLivenessResolver { HashTable *observed = innerTable(observedKeys, id); if (observed == NULL) return; + /* walk the smaller of the two sets */ + zv::Val live; + if (zend_hash_num_elements(observed) > countOf(next)) { + live = emptyArray(); + for (auto entry : zv::TableRef(tableOf(next))) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) continue; + zval *targetId = zend_hash_find(observed, key); + if (targetId == NULL) continue; + SEPARATE_ARRAY(live.raw()); + Z_TRY_ADDREF_P(targetId); + zend_hash_update(Z_ARRVAL_P(live.raw()), key, targetId); + } + observed = tableOf(live); + } for (auto entry : zv::TableRef(observed)) { zend_string *key = entry.stringKeyOrNull(); if (key == NULL || !isTrueAt(next, key)) continue; diff --git a/turbo-ext/src/generated/VariableLivenessResolver.h b/turbo-ext/src/generated/VariableLivenessResolver.h index 9c62ee75255..421254b07fd 100644 --- a/turbo-ext/src/generated/VariableLivenessResolver.h +++ b/turbo-ext/src/generated/VariableLivenessResolver.h @@ -29,15 +29,16 @@ inline constexpr uint32_t literalItems = 15; inline constexpr uint32_t coveredIds = 16; inline constexpr uint32_t overwriteMarkers = 17; inline constexpr uint32_t markersBySlot = 18; -inline constexpr uint32_t overwrittenIds = 19; -inline constexpr uint32_t allReadKeys = 20; -inline constexpr uint32_t loopStatements = 21; -inline constexpr uint32_t ownWriteIds = 22; -inline constexpr uint32_t variableOverwritingLoops = 23; -inline constexpr uint32_t opaque = 24; -inline constexpr uint32_t readsAllVariables = 25; -inline constexpr uint32_t allNamesMentioned = 26; -inline constexpr uint32_t returnsByReference = 27; +inline constexpr uint32_t markersByName = 19; +inline constexpr uint32_t overwrittenIds = 20; +inline constexpr uint32_t allReadKeys = 21; +inline constexpr uint32_t loopStatements = 22; +inline constexpr uint32_t ownWriteIds = 23; +inline constexpr uint32_t variableOverwritingLoops = 24; +inline constexpr uint32_t opaque = 25; +inline constexpr uint32_t readsAllVariables = 26; +inline constexpr uint32_t allNamesMentioned = 27; +inline constexpr uint32_t returnsByReference = 28; } // namespace slot inline void declareClass(reg::Class &cls) @@ -67,6 +68,7 @@ inline void declareProperties(reg::Class &cls) 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("markersBySlot", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("markersByName", 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); @@ -113,8 +115,10 @@ inline constexpr char strings[] = "observeWrite\0" /* 376 */ "observeOverwrite\0" /* 389 */ "killOverwriteMarkers\0" /* 406 */ - "resolveDependencies\0" /* 427 */ - "resolveCoverage"; /* 447 */ + "keys\0" /* 427 */ + "withoutKeys\0" /* 432 */ + "resolveDependencies\0" /* 444 */ + "resolveCoverage"; /* 464 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 21), /* resolve $function */ reg::packed(49, MAY_BE_NULL, 54), /* resolve $flow */ @@ -150,6 +154,9 @@ inline constexpr reg::PackedArg args[] = { 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(133, MAY_BE_ARRAY), /* withoutKeys $next */ + reg::packed(427, MAY_BE_ARRAY), /* withoutKeys $keys */ + reg::packed(84, MAY_BE_ARRAY), /* withoutKeys return */ reg::packed(84, MAY_BE_VOID), /* resolveDependencies return */ reg::packed(84, MAY_BE_VOID), /* resolveCoverage return */ }; @@ -171,8 +178,9 @@ inline constexpr sigtab::Sig replacedSlot = { { 360 /* replacedSlot */, 1, 23, 1 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 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 } }; +inline constexpr sigtab::Sig withoutKeys = { { 432 /* withoutKeys */, 2, 34, 2, 36, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolveDependencies = { { 444 /* resolveDependencies */, 0, 37, 0, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveCoverage = { { 464 /* resolveCoverage */, 0, 38, 0, 38, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::VariableLivenessResolver From 339d0092e654816a82866f4512ac6f1ae5544838 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 22:15:05 +0000 Subject: [PATCH 2/2] Memoize UnionType::hasTemplateOrLateResolvableType() TypeUtils::resolveLateResolvableTypes() asks for every resolved type, so an array keyed by a union of thousands of literals scanned the whole union on every assignment. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ad6c-dec2-72ff-bbe8-5b3b62746c94 --- src/Type/UnionType.php | 9 ++++++-- turbo-ext/src/UnionType.cpp | 22 ++++++++++++++----- .../generated/TemplateBenevolentUnionType.h | 12 +++++----- turbo-ext/src/generated/TemplateUnionType.h | 12 +++++----- turbo-ext/src/generated/UnionType.h | 10 +++++---- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 283be4e74e5..baafb406973 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -94,6 +94,8 @@ class UnionType implements CompoundType private ?TrinaryLogic $isCallable = null; + private ?bool $hasTemplateOrLateResolvableType = null; + /** @var array */ private array $possiblyLooselyEqualTypes = []; @@ -1814,15 +1816,18 @@ private function notBenevolentPickFromTypes(callable $getValues): array public function hasTemplateOrLateResolvableType(): bool { + if ($this->hasTemplateOrLateResolvableType !== null) { + return $this->hasTemplateOrLateResolvableType; + } foreach ($this->types as $type) { if (!$type->hasTemplateOrLateResolvableType()) { continue; } - return true; + return $this->hasTemplateOrLateResolvableType = true; } - return false; + return $this->hasTemplateOrLateResolvableType = false; } public function getPossiblyLooselyEqualType(PhpVersion $phpVersion): Type diff --git a/turbo-ext/src/UnionType.cpp b/turbo-ext/src/UnionType.cpp index 02b06c82711..4d593d1a5d6 100644 --- a/turbo-ext/src/UnionType.cpp +++ b/turbo-ext/src/UnionType.cpp @@ -5,10 +5,11 @@ * native BenevolentUnionType and the PHP TemplateUnionType extend it — their * constructors call parent::__construct(), so the constructor is a proper * method), implementing PHPStan\Type\CompoundType. State is the twin's - * ten private properties, declared typed property slots in the twin's - * declaration order — the eight memos ($sortedTypesCache, $cachedDescriptions, + * eleven private properties, declared typed property slots in the twin's + * declaration order — the nine memos ($sortedTypesCache, $cachedDescriptions, * $finiteTypeSet, $finiteTypes, $isNull, $isCallable, - * $possiblyLooselyEqualTypes, $alwaysLooselyEqualTypes) first, the promoted + * $hasTemplateOrLateResolvableType, $possiblyLooselyEqualTypes, + * $alwaysLooselyEqualTypes) first, the promoted * $types and $normalized after them — so the std object handlers do * GC/clone and a PHP subclass's own properties follow them. The one trait * the twin uses (NonGeneralizableTypeTrait) comes from the shared registrar @@ -2145,18 +2146,26 @@ class UnionType } /* whether any member has one; -1 = pending exception */ + /* memoized in $hasTemplateOrLateResolvableType */ int hasTemplateOrLateResolvableType() const { + zval *memo = OBJ_PROP_NUM(self, slots::hasTemplateOrLateResolvableType); + if (Z_TYPE_P(memo) == IS_TRUE) return 1; + if (Z_TYPE_P(memo) == IS_FALSE) return 0; zval *types = this->types(); if (UNEXPECTED(types == NULL)) return -1; zv::Val typesCopy = zv::Val::copyOf(zv::Ref(types)); + int result = 0; for (zv::ArrayEntry entry : zv::ArrRef(typesCopy.raw())) { int has = pt_type_call_is_true(entry.value().deref().asObject(), PT_LC("hastemplateorlateresolvabletype"), 0, NULL); if (UNEXPECTED(has < 0)) return -1; if (has == 0) continue; - return 1; + result = 1; + break; } - return 0; + memo = OBJ_PROP_NUM(self, slots::hasTemplateOrLateResolvableType); + zv::Ref(memo).assign(zv::Val::boolean(result == 1)); + return result; } /* the three protected methods: this class's own body when the object's @@ -3031,7 +3040,7 @@ PT_MINIT_REGISTRATION(pt_register_union_type) PT_RETURN_VAL(PT_THIS.memoizedLooselyEqualType(slots::alwaysLooselyEqualTypes, phpVersion, PT_LC("getalwayslooselyequaltype"))); }); cls.classConstantValue("EQUAL_UNION_CLASSES", pt_ut_equal_union_classes); - /* the slots PT_UT_PROP_*: the eight memo properties first, the promoted + /* the slots PT_UT_PROP_*: the nine memo properties first, the promoted * $types and $normalized after them */ cls.privateTypedPropertyDefaultNull("sortedTypesCache", MAY_BE_ARRAY); cls.privateTypedArrayPropertyDefaultEmpty("cachedDescriptions"); @@ -3039,6 +3048,7 @@ PT_MINIT_REGISTRATION(pt_register_union_type) cls.privateTypedPropertyDefaultNull("finiteTypes", MAY_BE_ARRAY); cls.privateTypedClassPropertyDefaultNull("isNull", ptcls::trinaryLogic); cls.privateTypedClassPropertyDefaultNull("isCallable", ptcls::trinaryLogic); + cls.privateTypedPropertyDefaultNull("hasTemplateOrLateResolvableType", MAY_BE_BOOL); cls.privateTypedArrayPropertyDefaultEmpty("possiblyLooselyEqualTypes"); cls.privateTypedArrayPropertyDefaultEmpty("alwaysLooselyEqualTypes"); cls.privateTypedProperty("types", MAY_BE_ARRAY); diff --git a/turbo-ext/src/generated/TemplateBenevolentUnionType.h b/turbo-ext/src/generated/TemplateBenevolentUnionType.h index 89814d6c14f..61a9b8da4d8 100644 --- a/turbo-ext/src/generated/TemplateBenevolentUnionType.h +++ b/turbo-ext/src/generated/TemplateBenevolentUnionType.h @@ -10,12 +10,12 @@ namespace ptdecl::TemplateBenevolentUnionType { /* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ namespace slot { -inline constexpr uint32_t name = 10; -inline constexpr uint32_t scope = 11; -inline constexpr uint32_t strategy = 12; -inline constexpr uint32_t variance = 13; -inline constexpr uint32_t bound = 14; -inline constexpr uint32_t default_ = 15; +inline constexpr uint32_t name = 11; +inline constexpr uint32_t scope = 12; +inline constexpr uint32_t strategy = 13; +inline constexpr uint32_t variance = 14; +inline constexpr uint32_t bound = 15; +inline constexpr uint32_t default_ = 16; } // namespace slot inline void declareClass(reg::Class &cls) diff --git a/turbo-ext/src/generated/TemplateUnionType.h b/turbo-ext/src/generated/TemplateUnionType.h index 436c58ba4f3..9faf074b406 100644 --- a/turbo-ext/src/generated/TemplateUnionType.h +++ b/turbo-ext/src/generated/TemplateUnionType.h @@ -10,12 +10,12 @@ namespace ptdecl::TemplateUnionType { /* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ namespace slot { -inline constexpr uint32_t name = 10; -inline constexpr uint32_t scope = 11; -inline constexpr uint32_t strategy = 12; -inline constexpr uint32_t variance = 13; -inline constexpr uint32_t bound = 14; -inline constexpr uint32_t default_ = 15; +inline constexpr uint32_t name = 11; +inline constexpr uint32_t scope = 12; +inline constexpr uint32_t strategy = 13; +inline constexpr uint32_t variance = 14; +inline constexpr uint32_t bound = 15; +inline constexpr uint32_t default_ = 16; } // namespace slot inline void declareClass(reg::Class &cls) diff --git a/turbo-ext/src/generated/UnionType.h b/turbo-ext/src/generated/UnionType.h index a3af2bb89c8..0508030abb6 100644 --- a/turbo-ext/src/generated/UnionType.h +++ b/turbo-ext/src/generated/UnionType.h @@ -16,10 +16,11 @@ inline constexpr uint32_t finiteTypeSet = 2; inline constexpr uint32_t finiteTypes = 3; inline constexpr uint32_t isNull = 4; inline constexpr uint32_t isCallable = 5; -inline constexpr uint32_t possiblyLooselyEqualTypes = 6; -inline constexpr uint32_t alwaysLooselyEqualTypes = 7; -inline constexpr uint32_t types = 8; -inline constexpr uint32_t normalized = 9; +inline constexpr uint32_t hasTemplateOrLateResolvableType = 6; +inline constexpr uint32_t possiblyLooselyEqualTypes = 7; +inline constexpr uint32_t alwaysLooselyEqualTypes = 8; +inline constexpr uint32_t types = 9; +inline constexpr uint32_t normalized = 10; } // namespace slot inline void declareClass(reg::Class &cls) @@ -36,6 +37,7 @@ inline void declareProperties(reg::Class &cls) cls.property("finiteTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); cls.property("isNull", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\TrinaryLogic"); cls.property("isCallable", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\TrinaryLogic"); + cls.property("hasTemplateOrLateResolvableType", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); cls.property("possiblyLooselyEqualTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("alwaysLooselyEqualTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("types", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY);