diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 3df4945517f..283be4e74e5 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -94,6 +94,12 @@ class UnionType implements CompoundType private ?TrinaryLogic $isCallable = null; + /** @var array */ + private array $possiblyLooselyEqualTypes = []; + + /** @var array */ + private array $alwaysLooselyEqualTypes = []; + /** * @api * @param list $types @@ -1819,6 +1825,16 @@ public function hasTemplateOrLateResolvableType(): bool return false; } + public function getPossiblyLooselyEqualType(PhpVersion $phpVersion): Type + { + return $this->possiblyLooselyEqualTypes[$phpVersion->getVersionId()] ??= $this->toValueSet()->getPossiblyLooselyEqualType($phpVersion); + } + + public function getAlwaysLooselyEqualType(PhpVersion $phpVersion): Type + { + return $this->alwaysLooselyEqualTypes[$phpVersion->getVersionId()] ??= $this->toValueSet()->getAlwaysLooselyEqualType($phpVersion); + } + public function toValueSet(): ValueSet { $valueSet = ValueSet::createEmpty(); diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index b537ca2af82..bbd90c1970b 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -12,6 +12,7 @@ use function count; use function is_float; use function is_nan; +use function spl_object_id; use function strcasecmp; use function usort; use const PHP_INT_MIN; @@ -31,7 +32,11 @@ public static function sortTypes(array $types): array return $types; } - usort($types, static function (Type $a, Type $b): int { + // usort() compares a member with many others - an array shape, whose description + // is costly, is described once + $arrayDescriptions = []; + + usort($types, static function (Type $a, Type $b) use (&$arrayDescriptions): int { if ($a instanceof NullType) { return 1; } elseif ($b instanceof NullType) { @@ -136,7 +141,10 @@ public static function sortTypes(array $types): array return 1; } - return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + return self::compareStrings( + $arrayDescriptions[spl_object_id($a)] ??= $a->describe(VerbosityLevel::value()), + $arrayDescriptions[spl_object_id($b)] ??= $b->describe(VerbosityLevel::value()), + ); } if ($a->isString()->yes() && $b->isString()->yes()) { diff --git a/turbo-ext/src/UnionType.cpp b/turbo-ext/src/UnionType.cpp index 8b96a40c81c..02b06c82711 100644 --- a/turbo-ext/src/UnionType.cpp +++ b/turbo-ext/src/UnionType.cpp @@ -5,9 +5,10 @@ * 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 - * eight private properties, declared typed property slots in the twin's - * declaration order — the six memos ($sortedTypesCache, $cachedDescriptions, - * $finiteTypeSet, $finiteTypes, $isNull, $isCallable) first, the promoted + * ten private properties, declared typed property slots in the twin's + * declaration order — the eight memos ($sortedTypesCache, $cachedDescriptions, + * $finiteTypeSet, $finiteTypes, $isNull, $isCallable, + * $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 @@ -952,6 +953,35 @@ class UnionType * union of the members with their constant values generalized, every * other level the sorted members joined with '|'; an owned string, UNDEF * = pending exception */ + /* $this->{memo}[$phpVersion->getVersionId()] ??= $this->toValueSet()->{method}($phpVersion) + * — getPossiblyLooselyEqualType() / getAlwaysLooselyEqualType(); + * UNDEF = pending exception */ + zv::Val memoizedLooselyEqualType(uint32_t slotIndex, zval *phpVersion, const char *method, size_t methodLength) const + { + zv::Val versionId = pt_php_version_get_version_id(phpVersion); + if (UNEXPECTED(versionId.isUndef())) return zv::Val(); + zend_long key = zval_get_long(versionId.raw()); + zval *cache = OBJ_PROP_NUM(self, slotIndex); + if (EXPECTED(Z_TYPE_P(cache) == IS_ARRAY)) { + zval *cached = zend_hash_index_find(Z_ARRVAL_P(cache), key); + if (cached != NULL && Z_TYPE_P(cached) != IS_NULL) return zv::Val::copyOf(zv::Ref(cached)); + } + zv::Val valueSet = pt_type_call(self, PT_LC("tovalueset"), 0, NULL); + if (UNEXPECTED(valueSet.isUndef())) return zv::Val(); + zv::Val type = pt_type_call(Z_OBJ_P(valueSet.raw()), method, methodLength, 1, phpVersion); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + + cache = OBJ_PROP_NUM(self, slotIndex); + if (Z_TYPE_P(cache) != IS_ARRAY) { + zv::Ref(cache).assign(zv::Val(zv::Arr::create(1))); + } + SEPARATE_ARRAY(cache); + zval copy; + ZVAL_COPY(©, type.raw()); + zend_hash_index_update(Z_ARRVAL_P(cache), key, ©); + return type; + } + zv::Val describe(zval *level) const { /* $level->getLevelValue() — the shadowing VerbosityLevel's slot, or @@ -2989,8 +3019,19 @@ PT_MINIT_REGISTRATION(pt_register_union_type) ptdecl::UnionType::declareClass(cls); /* mirrors toValueSet() */ cls.method(sigs::toValueSet, [](INTERNAL_FUNCTION_PARAMETERS) { pt_value_set_of_members(INTERNAL_FUNCTION_PARAM_PASSTHRU, false); }); + /* mirror getPossiblyLooselyEqualType() and getAlwaysLooselyEqualType(), memoized per PHP version */ + cls.method(sigs::getPossiblyLooselyEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *phpVersion; + if (!zp::parse(execute_data, phpVersion)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.memoizedLooselyEqualType(slots::possiblyLooselyEqualTypes, phpVersion, PT_LC("getpossiblylooselyequaltype"))); + }); + cls.method(sigs::getAlwaysLooselyEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *phpVersion; + if (!zp::parse(execute_data, phpVersion)) RETURN_THROWS(); + 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 six memo properties first, the promoted + /* the slots PT_UT_PROP_*: the eight memo properties first, the promoted * $types and $normalized after them */ cls.privateTypedPropertyDefaultNull("sortedTypesCache", MAY_BE_ARRAY); cls.privateTypedArrayPropertyDefaultEmpty("cachedDescriptions"); @@ -2998,6 +3039,8 @@ PT_MINIT_REGISTRATION(pt_register_union_type) cls.privateTypedPropertyDefaultNull("finiteTypes", MAY_BE_ARRAY); cls.privateTypedClassPropertyDefaultNull("isNull", ptcls::trinaryLogic); cls.privateTypedClassPropertyDefaultNull("isCallable", ptcls::trinaryLogic); + cls.privateTypedArrayPropertyDefaultEmpty("possiblyLooselyEqualTypes"); + cls.privateTypedArrayPropertyDefaultEmpty("alwaysLooselyEqualTypes"); cls.privateTypedProperty("types", MAY_BE_ARRAY); cls.privateTypedProperty("normalized", MAY_BE_BOOL); diff --git a/turbo-ext/src/UnionTypeHelper.cpp b/turbo-ext/src/UnionTypeHelper.cpp index c323595ebb2..3a51776d446 100644 --- a/turbo-ext/src/UnionTypeHelper.cpp +++ b/turbo-ext/src/UnionTypeHelper.cpp @@ -9,7 +9,8 @@ * original positions that PHP 8 usort() applies), so the members come out * in exactly the order the twin produces, comparator calls included — the * comparator describes members through their own describe() and the - * shadowed VerbosityLevel's singletons. + * shadowed VerbosityLevel's singletons, an array shape once per sort, as + * the twin's $arrayDescriptions memo does. * * The shadowed classes the comparator tests (NullType, ConstantBooleanType, * ConstantIntegerType, ConstantFloatType, IntegerRangeType, IntegerType, @@ -46,7 +47,14 @@ class UnionTypeHelper if (zend_hash_num_elements(source) > PT_UTH_SORT_LIMIT) return zv::Val::copyOf(zv::Ref(types)); /* usort() separates its by-reference argument: a private copy */ zv::Arr sorted = zv::Arr::adoptTable(zend_array_dup(source)); + /* the twin's $arrayDescriptions: each array shape described once; + * a describe() inside the sort can sort a nested union, which + * brings its own */ + zv::Arr descriptions = zv::Arr::create(0); + HashTable *outerDescriptions = currentDescriptions; + currentDescriptions = descriptions.table(); zend_hash_sort(sorted.table(), compare, 1); + currentDescriptions = outerDescriptions; if (UNEXPECTED(EG(exception))) return zv::Val(); return zv::Val(std::move(sorted)); } @@ -63,6 +71,10 @@ class UnionTypeHelper } private: + /* the twin's $arrayDescriptions of the sort in progress, keyed by object + * handle and level — zend_hash_sort() passes the comparator no context */ + static inline thread_local HashTable *currentDescriptions = nullptr; + /* the bucket comparator usort() runs: the closure's answer normalized * to -1/0/1 (ZEND_THREEWAY_COMPARE), ties broken by the original * positions zend_hash_sort() stored in Z_EXTRA (PHP 8's stable sort); @@ -113,7 +125,7 @@ class UnionTypeHelper return false; } if (aAccessory) { - if (bAccessory) return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, out); + if (bAccessory) return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, false, out); out = 1; return true; } @@ -227,7 +239,7 @@ class UnionTypeHelper if ((instanceof_function(a->ce, pt_ce_callable_type) || instanceof_function(a->ce, pt_ce_closure_type)) && (instanceof_function(b->ce, pt_ce_callable_type) || instanceof_function(b->ce, pt_ce_closure_type))) { - return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, out); + return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, false, out); } zend_long aConstantArray = pt_type_op_trinary(a, PT_OP_IS_CONSTANT_ARRAY, 0, NULL); @@ -254,7 +266,7 @@ class UnionTypeHelper out = 1; return true; } - return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, out); + return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_VALUE, true, out); } } @@ -263,27 +275,48 @@ class UnionTypeHelper if (aString == PT_TRI_YES) { zend_long bString = pt_type_op_trinary(b, PT_OP_IS_STRING, 0, NULL); if (UNEXPECTED(bString < 0)) return false; - if (bString == PT_TRI_YES) return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_PRECISE, out); + if (bString == PT_TRI_YES) return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_PRECISE, false, out); } - return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_TYPE_ONLY, out); + return compareDescriptions(a, b, PT_VERBOSITY_LEVEL_TYPE_ONLY, false, out); } /* self::compareStrings($a->describe($level), $b->describe($level)) for - * one of the shadowed VerbosityLevel's singletons; false = pending - * exception */ - [[nodiscard]] static bool compareDescriptions(zend_object *a, zend_object *b, zend_long level, zend_long &out) + * one of the shadowed VerbosityLevel's singletons, through the twin's + * $arrayDescriptions memo when memoized; false = pending exception */ + [[nodiscard]] static bool compareDescriptions(zend_object *a, zend_object *b, zend_long level, bool memoized, zend_long &out) { zval *levelZv = pt_verbosity_level_singleton(level); if (UNEXPECTED(levelZv == NULL)) return false; - zv::Val aDescription = describe(a, levelZv); - if (UNEXPECTED(aDescription.isUndef())) return false; - zv::Val bDescription = describe(b, levelZv); - if (UNEXPECTED(bDescription.isUndef())) return false; - out = compareStrings(Z_STR_P(aDescription.raw()), Z_STR_P(bDescription.raw())); + if (!memoized) { + zv::Val aDescription = describe(a, levelZv); + if (UNEXPECTED(aDescription.isUndef())) return false; + zv::Val bDescription = describe(b, levelZv); + if (UNEXPECTED(bDescription.isUndef())) return false; + out = compareStrings(Z_STR_P(aDescription.raw()), Z_STR_P(bDescription.raw())); + return true; + } + zend_string *aDescription = memoizedDescription(a, level, levelZv); + if (UNEXPECTED(aDescription == NULL)) return false; + zend_string *bDescription = memoizedDescription(b, level, levelZv); + if (UNEXPECTED(bDescription == NULL)) return false; + out = compareStrings(aDescription, bDescription); return true; } + /* $arrayDescriptions[spl_object_id($type)] ??= $type->describe($level), + * borrowed from the sort's table; NULL = pending exception */ + static zend_string *memoizedDescription(zend_object *type, zend_long level, zval *levelZv) + { + zend_ulong key = ((zend_ulong) type->handle << 3) | (zend_ulong) level; + zval *found = zend_hash_index_find(currentDescriptions, key); + if (found != NULL) return Z_STR_P(found); + zv::Val description = describe(type, levelZv); + if (UNEXPECTED(description.isUndef())) return NULL; + zval owned = description.take(); + return Z_STR_P(zend_hash_index_add_new(currentDescriptions, key, &owned)); + } + /* $type->describe($level), a string; UNDEF = pending exception */ static zv::Val describe(zend_object *type, zval *level) { diff --git a/turbo-ext/src/generated/TemplateBenevolentUnionType.h b/turbo-ext/src/generated/TemplateBenevolentUnionType.h index cdbcf1af813..89814d6c14f 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 = 8; -inline constexpr uint32_t scope = 9; -inline constexpr uint32_t strategy = 10; -inline constexpr uint32_t variance = 11; -inline constexpr uint32_t bound = 12; -inline constexpr uint32_t default_ = 13; +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; } // 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 bca0ba21e46..436c58ba4f3 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 = 8; -inline constexpr uint32_t scope = 9; -inline constexpr uint32_t strategy = 10; -inline constexpr uint32_t variance = 11; -inline constexpr uint32_t bound = 12; -inline constexpr uint32_t default_ = 13; +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; } // 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 f7b46d6ed73..a3af2bb89c8 100644 --- a/turbo-ext/src/generated/UnionType.h +++ b/turbo-ext/src/generated/UnionType.h @@ -16,8 +16,10 @@ 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 types = 6; -inline constexpr uint32_t normalized = 7; +inline constexpr uint32_t possiblyLooselyEqualTypes = 6; +inline constexpr uint32_t alwaysLooselyEqualTypes = 7; +inline constexpr uint32_t types = 8; +inline constexpr uint32_t normalized = 9; } // namespace slot inline void declareClass(reg::Class &cls) @@ -34,6 +36,8 @@ 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("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); cls.property("normalized", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); } @@ -276,8 +280,10 @@ inline constexpr char strings[] = "toNativePhpTypeNode\0" /* 3839 */ "notBenevolentPickFromTypes\0" /* 3859 */ "hasTemplateOrLateResolvableType\0" /* 3886 */ - "PHPStan\\Type\\ValueSet\0" /* 3918 */ - "toValueSet"; /* 3940 */ + "getPossiblyLooselyEqualType\0" /* 3918 */ + "getAlwaysLooselyEqualType\0" /* 3946 */ + "PHPStan\\Type\\ValueSet\0" /* 3972 */ + "toValueSet"; /* 3994 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_ARRAY), /* __construct $types */ reg::packed(6, MAY_BE_BOOL, reg::NoString, false, false, 17), /* __construct $normalized */ @@ -536,7 +542,11 @@ inline constexpr reg::PackedArg args[] = { reg::packed(3713, MAY_BE_CALLABLE), /* notBenevolentPickFromTypes $getValues */ reg::packed(35, MAY_BE_ARRAY), /* notBenevolentPickFromTypes return */ reg::packed(35, MAY_BE_BOOL), /* hasTemplateOrLateResolvableType return */ - reg::packed(35, 0, 3918), /* toValueSet return */ + reg::packed(1803, 0, 1814), /* getPossiblyLooselyEqualType $phpVersion */ + reg::packed(35, 0, 54), /* getPossiblyLooselyEqualType return */ + reg::packed(1803, 0, 1814), /* getAlwaysLooselyEqualType $phpVersion */ + reg::packed(35, 0, 54), /* getAlwaysLooselyEqualType return */ + reg::packed(35, 0, 3972), /* toValueSet return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -696,7 +706,9 @@ inline constexpr sigtab::Sig toPhpDocNode = { { 3785 /* toPhpDocNode */, 0, 252, inline constexpr sigtab::Sig toNativePhpTypeNode = { { 3839 /* toNativePhpTypeNode */, 1, 253, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig notBenevolentPickFromTypes = { { 3859 /* notBenevolentPickFromTypes */, 1, 254, 1, 255, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 3886 /* hasTemplateOrLateResolvableType */, 0, 256, 0, 256, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toValueSet = { { 3940 /* toValueSet */, 0, 257, 0, 257, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPossiblyLooselyEqualType = { { 3918 /* getPossiblyLooselyEqualType */, 1, 257, 1, 258, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAlwaysLooselyEqualType = { { 3946 /* getAlwaysLooselyEqualType */, 1, 259, 1, 260, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toValueSet = { { 3994 /* toValueSet */, 0, 261, 0, 261, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::UnionType