Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ class UnionType implements CompoundType

private ?TrinaryLogic $isCallable = null;

/** @var array<int, Type> */
private array $possiblyLooselyEqualTypes = [];

/** @var array<int, Type> */
private array $alwaysLooselyEqualTypes = [];

/**
* @api
* @param list<Type> $types
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 10 additions & 2 deletions src/Type/UnionTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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()) {
Expand Down
51 changes: 47 additions & 4 deletions turbo-ext/src/UnionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(&copy, type.raw());
zend_hash_index_update(Z_ARRVAL_P(cache), key, &copy);
return type;
}

zv::Val describe(zval *level) const
{
/* $level->getLevelValue() — the shadowing VerbosityLevel's slot, or
Expand Down Expand Up @@ -2989,15 +3019,28 @@ 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<zp::Obj>(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<zp::Obj>(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");
cls.privateTypedClassPropertyDefaultNull("finiteTypeSet", "PHPStan\\Type\\FiniteTypeSet", MAY_BE_FALSE);
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);

Expand Down
61 changes: 47 additions & 14 deletions turbo-ext/src/UnionTypeHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}

Expand All @@ -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)
{
Expand Down
12 changes: 6 additions & 6 deletions turbo-ext/src/generated/TemplateBenevolentUnionType.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions turbo-ext/src/generated/TemplateUnionType.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 18 additions & 6 deletions turbo-ext/src/generated/UnionType.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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<strings, args>;
} // namespace sigtab
Expand Down Expand Up @@ -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
Expand Down
Loading