diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index d8cbf328203..fcdab686b5b 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -20,7 +20,7 @@ final class TurboExtensionEnabler * version is the short SHA of the last commit touching turbo-ext/src/, * enforced by the phar.yml turbo-version job. */ - public const EXPECTED_EXTENSION_VERSION = '531d6bd'; + public const EXPECTED_EXTENSION_VERSION = '2537056'; private static bool $typeCombinatorCacheEnabled = false; diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 37965c001e9..95f70a97408 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -5,6 +5,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\GeneralizePrecision; @@ -21,6 +22,7 @@ use const PHP_INT_MIN; /** @api */ +#[ReferencedByTurboExtension(key: 'constantIntegerType')] class ConstantIntegerType extends IntegerType implements ConstantScalarType { diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 5994a945569..1db2770f405 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -63,6 +64,7 @@ use function substr_count; /** @api */ +#[ReferencedByTurboExtension(key: 'constantStringType')] #[InstanceofDeprecated(insteadUse: 'Type::getConstantStrings()')] class ConstantStringType extends StringType implements ConstantScalarType { diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index ef5596500ca..96b833f7f9c 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -15,6 +15,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\AcceptsResult; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\CompoundType; @@ -32,6 +33,7 @@ use function sprintf; /** @api */ +#[ReferencedByTurboExtension(key: 'enumCaseObjectType')] #[InstanceofDeprecated(insteadUse: 'Type::getEnumCaseObject() or Type::getEnumCases()')] class EnumCaseObjectType extends ObjectType { diff --git a/src/Type/FiniteTypeSet.php b/src/Type/FiniteTypeSet.php index 4ed2714ef95..d22ca083185 100644 --- a/src/Type/FiniteTypeSet.php +++ b/src/Type/FiniteTypeSet.php @@ -3,6 +3,8 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\TemplateType; use function array_diff_key; use function array_key_exists; @@ -29,6 +31,8 @@ * * @see UnionType::getFiniteTypeSet() */ +#[ShadowedByTurboExtension(turboClass: 'PHPStanTurbo\FiniteTypeSet', implementation: __DIR__ . '/../../turbo-ext/src/FiniteTypeSet.cpp')] +#[ReferencedByTurboExtension(key: 'finiteTypeSet')] final class FiniteTypeSet { diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index df82c5c08fd..d502e82cdf9 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\CompoundType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; @@ -10,6 +11,7 @@ * @api * @api-do-not-implement */ +#[ReferencedByTurboExtension(key: 'templateType')] interface TemplateType extends CompoundType { diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index eeedf5452c5..f5872a9902b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -27,6 +27,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; @@ -69,6 +70,7 @@ use function usort; /** @api */ +#[ReferencedByTurboExtension(key: 'intersectionType')] #[InstanceofDeprecated] class IntersectionType implements CompoundType { diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 52f05925616..fa2094e69a8 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -21,6 +22,7 @@ use PHPStan\Type\Traits\NonRemoveableTypeTrait; /** @api */ +#[ReferencedByTurboExtension(key: 'nullType')] #[InstanceofDeprecated(insteadUse: 'Type::isNull()')] class NullType implements ConstantScalarType { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f1b7f932237..f253a89b3ff 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -25,6 +25,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateIterableType; @@ -50,6 +51,7 @@ use function str_contains; /** @api */ +#[ReferencedByTurboExtension(key: 'unionType')] class UnionType implements CompoundType { diff --git a/turbo-ext/config.w32 b/turbo-ext/config.w32 index 90286e1a89b..ec3780c75d9 100644 --- a/turbo-ext/config.w32 +++ b/turbo-ext/config.w32 @@ -29,7 +29,7 @@ if (PHP_PHPSTAN_TURBO != "no") { // directory by splitting on backslashes only — with forward slashes the // objects compile flat while the link list expects the subpath EXTENSION("phpstan_turbo", - "src\\main.cpp src\\support.cpp src\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\ScopeOps.cpp src\\TrinaryLogic.cpp src\\TypeCombinatorCache.cpp", + "src\\main.cpp src\\support.cpp src\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FiniteTypeSet.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\ScopeOps.cpp src\\TrinaryLogic.cpp src\\TypeCombinatorCache.cpp", true, flags); ADD_SOURCES(configure_module_dirname + "/src/parser", diff --git a/turbo-ext/src/FiniteTypeSet.cpp b/turbo-ext/src/FiniteTypeSet.cpp new file mode 100644 index 00000000000..369302fad26 --- /dev/null +++ b/turbo-ext/src/FiniteTypeSet.cpp @@ -0,0 +1,715 @@ +/* + * PHPStanTurbo\FiniteTypeSet — native implementation of PHPStan\Type\FiniteTypeSet. + * + * Not final: a PHP stub subclass extends this class, and create() instantiates + * the configured finiteTypeSet class so userland type hints keep working. + * + * The port is about create(): every union that holds a finite value builds one + * of these, and the PHP twin pays key() + getConstantScalarValues() + kind() + + * get_class() per member — four userland frames each, on unions that routinely + * hold dozens of constants. The five classes that *are* a finite value are + * recognised here by their exact class entry, so the whole loop runs without + * crossing back into PHP; every other class takes the same + * getEnumCaseObject()/getConstantScalarValues() path the twin takes. + * + * The logic lives in the FiniteTypeSet handle class below, structured to + * mirror src/Type/FiniteTypeSet.php method for method; the lambdas at the + * bottom are only the engine ABI glue (parameter parsing + delegation). + */ + +#include "support.h" +#include "zv.h" + +#include + +namespace phpstanturbo { + +/* the twin's private key constants */ +#define PT_FTS_NULL_KEY "null" +#define PT_FTS_INTEGER_KEY_PREFIX "i:" +#define PT_FTS_BOOLEAN_KEY_PREFIX "b:" +#define PT_FTS_STRING_KEY_PREFIX "s:" +#define PT_FTS_ENUM_CASE_KEY_PREFIX "enum:" + +#define PT_FTS_LITERAL(s) s, sizeof(s) - 1 + +/* a . b . c . d in one allocation; trailing parts may be empty */ +static zend_string *ptFtsConcat(const char *a, size_t al, const char *b, size_t bl, const char *c, size_t cl, const char *d, size_t dl) +{ + zend_string *out = zend_string_alloc(al + bl + cl + dl, 0); + char *p = ZSTR_VAL(out); + memcpy(p, a, al); + p += al; + memcpy(p, b, bl); + p += bl; + memcpy(p, c, cl); + p += cl; + memcpy(p, d, dl); + ZSTR_VAL(out)[al + bl + cl + dl] = '\0'; + return out; +} + +/* offset of an instance property declared anywhere up the hierarchy — a + * private property of a parent class is not in the child's own table */ +static int32_t ptFtsPropOffset(zend_class_entry *ce, const char *name, size_t len) +{ + for (zend_class_entry *cur = ce; cur != NULL; cur = cur->parent) { + int32_t offset = pt_instance_prop_offset(cur, name, len); + if (offset >= 0) { + return offset; + } + } + return -1; +} + +/* + * The class entries and property slots key()/kind() need, resolved once per + * request rather than once per member. + * + * The five value classes are matched by their exact entry, deliberately not + * with instanceof: TemplateConstantStringType extends ConstantStringType and + * must key as null, and any subclass may override getConstantScalarValues() + * or getEnumCaseObject(). A subclass therefore falls through to the userland + * path, which answers for it exactly like the twin does. The three excluded + * classes are matched with instanceof, because the twin excludes them with + * instanceof. + */ +struct FiniteClasses +{ + zend_class_entry *nullType; + zend_class_entry *constantInteger; + zend_class_entry *constantString; + zend_class_entry *constantBoolean; + zend_class_entry *enumCase; + zend_class_entry *templateType; + zend_class_entry *unionType; + zend_class_entry *intersectionType; + int32_t constantIntegerValue; + int32_t constantStringValue; + int32_t constantBooleanValue; + int32_t enumCaseName; + int32_t enumClassName; + bool inited; +}; + +static FiniteClasses ptFtsClasses; + +/* false = pending exception */ +static bool ptFtsResolveClasses() +{ + if (EXPECTED(ptFtsClasses.inited)) { + return true; + } + + FiniteClasses resolved; + resolved.nullType = pt_class(PT_CLASS_NULL_TYPE); + resolved.constantInteger = pt_class(PT_CLASS_CONSTANT_INTEGER_TYPE); + resolved.constantString = pt_class(PT_CLASS_CONSTANT_STRING_TYPE); + resolved.constantBoolean = pt_class(PT_CLASS_CONSTANT_BOOLEAN_TYPE); + resolved.enumCase = pt_class(PT_CLASS_ENUM_CASE_OBJECT_TYPE); + resolved.templateType = pt_class(PT_CLASS_TEMPLATE_TYPE); + resolved.unionType = pt_class(PT_CLASS_UNION_TYPE); + resolved.intersectionType = pt_class(PT_CLASS_INTERSECTION_TYPE); + if (UNEXPECTED(resolved.nullType == NULL || resolved.constantInteger == NULL || resolved.constantString == NULL + || resolved.constantBoolean == NULL || resolved.enumCase == NULL || resolved.templateType == NULL + || resolved.unionType == NULL || resolved.intersectionType == NULL)) { + return false; + } + + resolved.constantIntegerValue = ptFtsPropOffset(resolved.constantInteger, PT_FTS_LITERAL("value")); + resolved.constantStringValue = ptFtsPropOffset(resolved.constantString, PT_FTS_LITERAL("value")); + resolved.constantBooleanValue = ptFtsPropOffset(resolved.constantBoolean, PT_FTS_LITERAL("value")); + resolved.enumCaseName = ptFtsPropOffset(resolved.enumCase, PT_FTS_LITERAL("enumCaseName")); + resolved.enumClassName = ptFtsPropOffset(resolved.enumCase, PT_FTS_LITERAL("className")); + resolved.inited = true; + + ptFtsClasses = resolved; + return true; +} + +/* $object->method(); false = pending exception */ +static bool ptFtsCall(zval *object, const char *lcname, size_t len, zval *result) +{ + zend_class_entry *ce = Z_OBJCE_P(object); + zend_function *fn = pt_find_method(ce, lcname, len); + if (UNEXPECTED(fn == NULL)) { + return false; + } + zend_call_known_function(fn, Z_OBJ_P(object), ce, result, 0, NULL, NULL); + return !EG(exception); +} + +/* getClassName() and getEnumCaseName() of an already-known enum case object */ +static zend_string *ptFtsEnumKey(const char *prefix, size_t prefixLen, zval *enumCaseObject, bool withCaseName) +{ + zval className, caseName; + if (UNEXPECTED(!ptFtsCall(enumCaseObject, PT_FTS_LITERAL("getclassname"), &className))) { + return NULL; + } + if (UNEXPECTED(Z_TYPE(className) != IS_STRING)) { + zval_ptr_dtor(&className); + zend_type_error("getClassName() must return a string"); + return NULL; + } + if (!withCaseName) { + zend_string *out = ptFtsConcat(prefix, prefixLen, ZSTR_VAL(Z_STR(className)), Z_STRLEN(className), "", 0, "", 0); + zval_ptr_dtor(&className); + return out; + } + if (UNEXPECTED(!ptFtsCall(enumCaseObject, PT_FTS_LITERAL("getenumcasename"), &caseName))) { + zval_ptr_dtor(&className); + return NULL; + } + if (UNEXPECTED(Z_TYPE(caseName) != IS_STRING)) { + zval_ptr_dtor(&className); + zval_ptr_dtor(&caseName); + zend_type_error("getEnumCaseName() must return a string"); + return NULL; + } + zend_string *out = ptFtsConcat( + prefix, prefixLen, + ZSTR_VAL(Z_STR(className)), Z_STRLEN(className), + PT_FTS_LITERAL("::"), + ZSTR_VAL(Z_STR(caseName)), Z_STRLEN(caseName)); + zval_ptr_dtor(&className); + zval_ptr_dtor(&caseName); + return out; +} + +/* Mirrors PHPStan\Type\FiniteTypeSet. State lives in the PHP object's + * hasClassStringMember/members/membersByKind/others properties. */ +class FiniteTypeSet +{ +public: + explicit FiniteTypeSet(zend_object *self) : self(self) {} + + void construct(zv::Ref members, zv::Ref membersByKind, zv::Ref others) + { + zv::ObjRef obj(self); + obj.propAtWrite(PT_FTS_PROP_MEMBERS, zv::Val::copyOf(members)); + obj.propAtWrite(PT_FTS_PROP_MEMBERS_BY_KIND, zv::Val::copyOf(membersByKind)); + obj.propAtWrite(PT_FTS_PROP_OTHERS, zv::Val::copyOf(others)); + } + + /* null when nothing keyed; UNDEF = pending exception */ + static zv::Val create(zv::ArrRef types) + { + if (UNEXPECTED(!ptFtsResolveClasses())) { + return zv::Val(); + } + + /* the tables stay unallocated until the first keyed member: a union + * with no finite value at all — the common case — must not pay for + * three empty hashtables to answer null */ + zv::Arr members; + zv::Arr membersByKind; + zv::Arr others = zv::Arr::empty(); + + for (auto entry : types) { + zv::Ref type = entry.value().deref(); + if (UNEXPECTED(!type.isObject())) { + zend_type_error("%s::create(): Argument #1 ($types) must be a list of objects", ZSTR_VAL(pt_ce_finite_type_set->name)); + return zv::Val(); + } + + zv::Str key; + zv::Str kind; + if (UNEXPECTED(!classify(type.raw(), KEY_AND_KIND, key, kind))) { + return zv::Val(); + } + + if (key.isNull() || (!members.isUndef() && zend_symtable_exists(members.table(), key.get()))) { + others.push(type); + continue; + } + + if (members.isUndef()) { + members = zv::Arr::create(types.size()); + membersByKind = zv::Arr::create(KINDS_SIZE_HINT); + } + members.set(key.get(), zv::Val::copyOf(type)); + if (!zend_symtable_exists(membersByKind.table(), kind.get())) { + membersByKind.set(kind.get(), zv::Val::copyOf(type)); + } + } + + if (members.isUndef()) { + return zv::Val::null(); + } + + zend_class_entry *ce = pt_impl_class(PT_CLASS_FINITE_TYPE_SET, pt_ce_finite_type_set); + if (UNEXPECTED(ce == NULL)) { + return zv::Val(); + } + zval set; + if (UNEXPECTED(object_init_ex(&set, ce) != SUCCESS)) { + return zv::Val(); + } + zv::ObjRef obj(Z_OBJ(set)); + obj.propAtWrite(PT_FTS_PROP_MEMBERS, zv::Val(std::move(members))); + obj.propAtWrite(PT_FTS_PROP_MEMBERS_BY_KIND, zv::Val(std::move(membersByKind))); + obj.propAtWrite(PT_FTS_PROP_OTHERS, zv::Val(std::move(others))); + return zv::Val::adopt(set); + } + + /* null string = not a finite value; false = pending exception */ + static bool key(zval *type, zv::Str &out) + { + if (UNEXPECTED(!ptFtsResolveClasses())) { + return false; + } + zv::Str kind; + return classify(type, KEY_ONLY, out, kind); + } + + /* the twin's private kind(); false = pending exception */ + static bool kind(zval *type, zv::Str &out) + { + if (UNEXPECTED(!ptFtsResolveClasses())) { + return false; + } + zv::Str key; + return classify(type, KIND_ONLY, key, out); + } + + /* UNDEF = pending exception */ + zv::Val getRepresentativesOfOtherKinds(zval *type) const + { + zv::Str wanted; + if (UNEXPECTED(!kind(type, wanted))) { + return zv::Val(); + } + + zv::Ref membersByKind = zv::ObjRef(self).propAt(PT_FTS_PROP_MEMBERS_BY_KIND); + zv::Arr representatives = zv::Arr::empty(); + for (auto entry : zv::ArrRef(membersByKind.raw())) { + zend_string *memberKind = entry.stringKeyOrNull(); + if (memberKind != NULL && zend_string_equals(memberKind, wanted.get())) { + continue; + } + representatives.push(entry.value()); + } + return zv::Val(std::move(representatives)); + } + + bool has(zend_string *key) const + { + return zv::ArrRef(zv::ObjRef(self).propAt(PT_FTS_PROP_MEMBERS).raw()).exists(key); + } + + bool isComplete() const + { + return zv::ArrRef(zv::ObjRef(self).propAt(PT_FTS_PROP_OTHERS).raw()).size() == 0; + } + + zv::Val getMembers() const { return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_FTS_PROP_MEMBERS)); } + + zv::Val getOthers() const { return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_FTS_PROP_OTHERS)); } + + /* the twin's array_diff_key() count, as one hash join that stops as soon + * as both a missing and a shared key have been seen */ + zv::Val containedIn(zend_object *other) const + { + HashTable *mine = Z_ARRVAL_P(zv::ObjRef(self).propAt(PT_FTS_PROP_MEMBERS).raw()); + HashTable *theirs = Z_ARRVAL_P(zv::ObjRef(other).propAt(PT_FTS_PROP_MEMBERS).raw()); + + bool missing = false; + bool shared = false; + for (auto entry : zv::TableRef(mine)) { + if (pt_ht_exists(theirs, entry.stringKeyOrNull(), entry.indexKey())) { + shared = true; + } else { + missing = true; + } + if (missing && shared) { + return trinary(PT_TRI_MAYBE); + } + } + + return trinary(missing ? PT_TRI_NO : PT_TRI_YES); + } + + zv::Val containedInKey(zend_string *key) const + { + if (!has(key)) { + return trinary(PT_TRI_NO); + } + if (zv::ArrRef(zv::ObjRef(self).propAt(PT_FTS_PROP_MEMBERS).raw()).size() == 1) { + return trinary(PT_TRI_YES); + } + return trinary(PT_TRI_MAYBE); + } + + /* false = pending exception */ + bool hasClassStringMember(bool &out) const + { + zv::ObjRef obj(self); + zv::Ref memo = obj.propAt(PT_FTS_PROP_HAS_CLASS_STRING_MEMBER); + if (memo.isBool()) { + out = memo.isTrue(); + return true; + } + if (UNEXPECTED(!ptFtsResolveClasses())) { + return false; + } + + out = false; + for (auto entry : zv::ArrRef(obj.propAt(PT_FTS_PROP_MEMBERS).raw())) { + bool no; + if (UNEXPECTED(!isClassStringNo(entry.value().deref().raw(), no))) { + return false; + } + if (no) { + continue; + } + out = true; + break; + } + + obj.propAtWrite(PT_FTS_PROP_HAS_CLASS_STRING_MEMBER, zv::Val::boolean(out)); + return true; + } + +private: + /* KEY_AND_KIND fills the kind only for a keyed type — the only member of + * membersByKind create() can ever want */ + enum ClassifyMode + { + KEY_ONLY, + KEY_AND_KIND, + KIND_ONLY, + }; + + /* a union of finite values is overwhelmingly of one kind */ + static const uint32_t KINDS_SIZE_HINT = 4; + + zend_object *self; + + static zv::Val trinary(zend_long value) { return zv::Val::copyOf(zv::Ref(pt_trinary_singleton(value))); } + + /* + * key() and kind() in one pass: create() needs both per member, and the + * userland path answers both from the single getEnumCaseObject() call the + * twin would otherwise make twice. + * + * false = pending exception. A null key means the type is not a finite + * value — create() then never looks at the kind, so KEY_AND_KIND leaves it + * unset in that case. + */ + static bool classify(zval *type, ClassifyMode mode, zv::Str &key, zv::Str &kind) + { + const FiniteClasses &classes = ptFtsClasses; + zend_class_entry *ce = Z_OBJCE_P(type); + bool wantKey = mode != KIND_ONLY; + bool wantKind = mode != KEY_ONLY; + + if (ce == classes.constantString) { + if (wantKey) { + zend_string *value = Z_STR_P(OBJ_PROP(Z_OBJ_P(type), (uint32_t) classes.constantStringValue)); + key = zv::Str::adopt(ptFtsConcat(PT_FTS_LITERAL(PT_FTS_STRING_KEY_PREFIX), ZSTR_VAL(value), ZSTR_LEN(value), "", 0, "", 0)); + } + if (wantKind) { + kind = zv::Str::copyOf(ce->name); + } + return true; + } + if (ce == classes.constantInteger) { + if (wantKey) { + char buf[MAX_LENGTH_OF_LONG + 1]; + char *digits = zend_print_long_to_buf(buf + sizeof(buf) - 1, Z_LVAL_P(OBJ_PROP(Z_OBJ_P(type), (uint32_t) classes.constantIntegerValue))); + key = zv::Str::adopt(ptFtsConcat(PT_FTS_LITERAL(PT_FTS_INTEGER_KEY_PREFIX), digits, (size_t) (buf + sizeof(buf) - 1 - digits), "", 0, "", 0)); + } + if (wantKind) { + kind = zv::Str::copyOf(ce->name); + } + return true; + } + if (ce == classes.enumCase) { + zend_string *className = Z_STR_P(OBJ_PROP(Z_OBJ_P(type), (uint32_t) classes.enumClassName)); + if (wantKey) { + zend_string *caseName = Z_STR_P(OBJ_PROP(Z_OBJ_P(type), (uint32_t) classes.enumCaseName)); + key = zv::Str::adopt(ptFtsConcat( + PT_FTS_LITERAL(PT_FTS_ENUM_CASE_KEY_PREFIX), + ZSTR_VAL(className), ZSTR_LEN(className), + PT_FTS_LITERAL("::"), + ZSTR_VAL(caseName), ZSTR_LEN(caseName))); + } + if (wantKind) { + kind = zv::Str::adopt(ptFtsConcat(PT_FTS_LITERAL(PT_FTS_ENUM_CASE_KEY_PREFIX), ZSTR_VAL(className), ZSTR_LEN(className), "", 0, "", 0)); + } + return true; + } + if (ce == classes.nullType) { + if (wantKey) { + key = zv::Str::adopt(zend_string_init(PT_FTS_LITERAL(PT_FTS_NULL_KEY), 0)); + } + if (wantKind) { + kind = zv::Str::copyOf(ce->name); + } + return true; + } + if (ce == classes.constantBoolean) { + if (wantKey) { + bool value = zv::Ref(OBJ_PROP(Z_OBJ_P(type), (uint32_t) classes.constantBooleanValue)).isTrue(); + key = zv::Str::adopt(zend_string_init(value ? PT_FTS_BOOLEAN_KEY_PREFIX "1" : PT_FTS_BOOLEAN_KEY_PREFIX "0", sizeof(PT_FTS_BOOLEAN_KEY_PREFIX "1") - 1, 0)); + } + if (wantKind) { + kind = zv::Str::copyOf(ce->name); + } + return true; + } + + return classifySlow(type, ce, mode, key, kind); + } + + /* the twin's own path, for every class that is not one of the five */ + static bool classifySlow(zval *type, zend_class_entry *ce, ClassifyMode mode, zv::Str &key, zv::Str &kind) + { + const FiniteClasses &classes = ptFtsClasses; + + /* kind() keeps get_class() for a union or an intersection, key() + * refuses both outright — one instanceof pair answers for both */ + if (instanceof_function(ce, classes.unionType) || instanceof_function(ce, classes.intersectionType)) { + if (mode != KEY_ONLY) { + kind = zv::Str::copyOf(ce->name); + } + return true; + } + /* a template type has no key, and create() asks for its kind only for + * keyed members — so there is nothing left to compute */ + if (mode != KIND_ONLY && instanceof_function(ce, classes.templateType)) { + return true; + } + + zval enumCaseObject; + if (UNEXPECTED(!ptFtsCall(type, PT_FTS_LITERAL("getenumcaseobject"), &enumCaseObject))) { + return false; + } + if (Z_TYPE(enumCaseObject) == IS_OBJECT) { + bool ok = true; + if (mode != KIND_ONLY) { + key = zv::Str::adopt(ptFtsEnumKey(PT_FTS_LITERAL(PT_FTS_ENUM_CASE_KEY_PREFIX), &enumCaseObject, true)); + ok = !key.isNull(); + } + if (ok && mode != KEY_ONLY) { + kind = zv::Str::adopt(ptFtsEnumKey(PT_FTS_LITERAL(PT_FTS_ENUM_CASE_KEY_PREFIX), &enumCaseObject, false)); + ok = !kind.isNull(); + } + zval_ptr_dtor(&enumCaseObject); + return ok; + } + zval_ptr_dtor(&enumCaseObject); + + if (mode != KEY_ONLY) { + kind = zv::Str::copyOf(ce->name); + } + if (mode == KIND_ONLY) { + return true; + } + + zval scalarValues; + if (UNEXPECTED(!ptFtsCall(type, PT_FTS_LITERAL("getconstantscalarvalues"), &scalarValues))) { + return false; + } + if (Z_TYPE(scalarValues) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL(scalarValues)) != 1) { + zval_ptr_dtor(&scalarValues); + return true; + } + zval *value = zend_hash_index_find(Z_ARRVAL(scalarValues), 0); + if (value != NULL) { + ZVAL_DEREF(value); + key = zv::Str::adopt(scalarKey(value)); + } + zval_ptr_dtor(&scalarValues); + return true; + } + + /* the twin's null/int/bool/string branches over a scalar value; floats + * and anything else stay unkeyed */ + static zend_string *scalarKey(zval *value) + { + switch (Z_TYPE_P(value)) { + case IS_NULL: + return zend_string_init(PT_FTS_LITERAL(PT_FTS_NULL_KEY), 0); + case IS_LONG: { + char buf[MAX_LENGTH_OF_LONG + 1]; + char *digits = zend_print_long_to_buf(buf + sizeof(buf) - 1, Z_LVAL_P(value)); + return ptFtsConcat(PT_FTS_LITERAL(PT_FTS_INTEGER_KEY_PREFIX), digits, (size_t) (buf + sizeof(buf) - 1 - digits), "", 0, "", 0); + } + case IS_TRUE: + return zend_string_init(PT_FTS_LITERAL(PT_FTS_BOOLEAN_KEY_PREFIX "1"), 0); + case IS_FALSE: + return zend_string_init(PT_FTS_LITERAL(PT_FTS_BOOLEAN_KEY_PREFIX "0"), 0); + case IS_STRING: + return ptFtsConcat(PT_FTS_LITERAL(PT_FTS_STRING_KEY_PREFIX), Z_STRVAL_P(value), Z_STRLEN_P(value), "", 0, "", 0); + default: + return NULL; + } + } + + /* $member->isClassString()->no(); false = pending exception */ + static bool isClassStringNo(zval *member, bool &out) + { + const FiniteClasses &classes = ptFtsClasses; + zend_class_entry *ce = Z_OBJCE_P(member); + /* of the five classes key() keys, only a constant string can answer + * anything but no — and only it pays a reflection lookup for it */ + if (ce == classes.nullType || ce == classes.constantInteger || ce == classes.constantBoolean || ce == classes.enumCase) { + out = true; + return true; + } + + zval result; + if (UNEXPECTED(!ptFtsCall(member, PT_FTS_LITERAL("isclassstring"), &result))) { + return false; + } + if (UNEXPECTED(Z_TYPE(result) != IS_OBJECT)) { + zval_ptr_dtor(&result); + zend_type_error("isClassString() must return a %s", ZSTR_VAL(pt_ce_trinary->name)); + return false; + } + if (EXPECTED(instanceof_function(Z_OBJCE(result), pt_ce_trinary))) { + out = pt_trinary_value(Z_OBJ(result)) == PT_TRI_NO; + zval_ptr_dtor(&result); + return true; + } + + /* an unshadowed PHPStan\TrinaryLogic (the differential smoke test) */ + zval no; + bool called = ptFtsCall(&result, PT_FTS_LITERAL("no"), &no); + zval_ptr_dtor(&result); + if (UNEXPECTED(!called)) { + return false; + } + out = Z_TYPE(no) == IS_TRUE; + zval_ptr_dtor(&no); + return true; + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::FiniteTypeSet; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define FTS_CLASS "PHPStanTurbo\\FiniteTypeSet" + +void pt_finite_type_set_rinit() +{ + phpstanturbo::ptFtsClasses.inited = false; +} + +void pt_register_finite_type_set() +{ + reg::Class cls("PHPStanTurbo\\FiniteTypeSet"); + /* not final: the stub subclass PHPStan\Type\FiniteTypeSet extends this + * class; the four properties must stay in this order (OBJ_PROP_NUM slots) */ + cls.privateNullProperty("hasClassStringMember"); + cls.privateArrayProperty("members"); + cls.privateArrayProperty("membersByKind"); + cls.privateArrayProperty("others"); + + cls.method("__construct", reg::Private, 3, { reg::arrayArg("members"), reg::arrayArg("membersByKind"), reg::arrayArg("others") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *members, *membersByKind, *others; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_ARRAY(members) + Z_PARAM_ARRAY(membersByKind) + Z_PARAM_ARRAY(others) + ZEND_PARSE_PARAMETERS_END(); + FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).construct(zv::Ref(members), zv::Ref(membersByKind), zv::Ref(others)); + }); + + cls.method("create", reg::PublicStatic, 1, { reg::arrayArg("types") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *types; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(types) + ZEND_PARSE_PARAMETERS_END(); + zv::Val set = FiniteTypeSet::create(zv::ArrRef(types)); + if (UNEXPECTED(set.isUndef())) { + RETURN_THROWS(); + } + set.intoReturnValue(return_value); + }); + + cls.method("key", reg::PublicStatic, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + zv::Str key; + if (UNEXPECTED(!FiniteTypeSet::key(type, key))) { + RETURN_THROWS(); + } + if (key.isNull()) { + RETURN_NULL(); + } + RETURN_STR(key.take()); + }); + + cls.method("getRepresentativesOfOtherKinds", reg::Public, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + zv::Val representatives = FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).getRepresentativesOfOtherKinds(type); + if (UNEXPECTED(representatives.isUndef())) { + RETURN_THROWS(); + } + representatives.intoReturnValue(return_value); + }); + + cls.method("has", reg::Public, 1, { reg::stringArg("key") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *key; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + RETURN_BOOL(FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).has(key)); + }); + + cls.method("isComplete", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).isComplete()); + }); + + cls.method("getMembers", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).getMembers().intoReturnValue(return_value); + }); + + cls.method("getOthers", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).getOthers().intoReturnValue(return_value); + }); + + cls.method("containedIn", reg::Public, 1, { reg::obj("other", FTS_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_finite_type_set) + ZEND_PARSE_PARAMETERS_END(); + FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).containedIn(Z_OBJ_P(other)).intoReturnValue(return_value); + }); + + cls.method("containedInKey", reg::Public, 1, { reg::stringArg("key") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *key; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).containedInKey(key).intoReturnValue(return_value); + }); + + cls.method("hasClassStringMember", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + bool out; + if (UNEXPECTED(!FiniteTypeSet(Z_OBJ_P(ZEND_THIS)).hasClassStringMember(out))) { + RETURN_THROWS(); + } + RETURN_BOOL(out); + }); + + pt_ce_finite_type_set = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index d231cfa3af3..ff34b5a56fc 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -82,6 +82,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) pt_register_type_combinator_cache(); pt_register_arena_cache(); pt_register_expression_result_storage(); + pt_register_finite_type_set(); return SUCCESS; } @@ -103,6 +104,7 @@ static PHP_RINIT_FUNCTION(phpstan_turbo) pt_node_traverser_rinit(); pt_scope_ops_rinit(); pt_type_combinator_cache_rinit(); + pt_finite_type_set_rinit(); return SUCCESS; } diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 0fad5118070..f9d7adb18b4 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -7,6 +7,7 @@ pt_globals_t pt_globals; zend_class_entry *pt_ce_trinary = nullptr; zend_class_entry *pt_ce_expr_type_holder = nullptr; zend_class_entry *pt_ce_cond_expr_holder = nullptr; +zend_class_entry *pt_ce_finite_type_set = nullptr; /* {{{ class map */ @@ -49,9 +50,17 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_ARROW_FUNCTION */ {"arrowFunction", "PhpParser\\Node\\Expr\\ArrowFunction"}, /* PT_CLASS_TYPE */ {"type", "PHPStan\\Type\\Type"}, /* PT_CLASS_RECURSION_GUARD */ {"recursionGuard", "PHPStan\\Type\\RecursionGuard"}, + /* PT_CLASS_NULL_TYPE */ {"nullType", "PHPStan\\Type\\NullType"}, + /* PT_CLASS_CONSTANT_INTEGER_TYPE */ {"constantIntegerType", "PHPStan\\Type\\Constant\\ConstantIntegerType"}, + /* PT_CLASS_CONSTANT_STRING_TYPE */ {"constantStringType", "PHPStan\\Type\\Constant\\ConstantStringType"}, + /* PT_CLASS_ENUM_CASE_OBJECT_TYPE */ {"enumCaseObjectType", "PHPStan\\Type\\Enum\\EnumCaseObjectType"}, + /* PT_CLASS_TEMPLATE_TYPE */ {"templateType", "PHPStan\\Type\\Generic\\TemplateType"}, + /* PT_CLASS_UNION_TYPE */ {"unionType", "PHPStan\\Type\\UnionType"}, + /* PT_CLASS_INTERSECTION_TYPE */ {"intersectionType", "PHPStan\\Type\\IntersectionType"}, /* PT_CLASS_TRINARY */ {"trinaryLogic", NULL}, /* PT_CLASS_ETH */ {"expressionTypeHolder", NULL}, /* PT_CLASS_CEH */ {"conditionalExpressionHolder", NULL}, + /* PT_CLASS_FINITE_TYPE_SET */ {"finiteTypeSet", NULL}, }; zend_class_entry *pt_class(int idx) diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 77c08a77081..efc794223c5 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -81,12 +81,22 @@ enum { PT_CLASS_ARROW_FUNCTION, PT_CLASS_TYPE, PT_CLASS_RECURSION_GUARD, + /* the classes FiniteTypeSet::key() recognises without crossing back into + * PHP (exact-class fast paths) and the three it excludes outright */ + PT_CLASS_NULL_TYPE, + PT_CLASS_CONSTANT_INTEGER_TYPE, + PT_CLASS_CONSTANT_STRING_TYPE, + PT_CLASS_ENUM_CASE_OBJECT_TYPE, + PT_CLASS_TEMPLATE_TYPE, + PT_CLASS_UNION_TYPE, + PT_CLASS_INTERSECTION_TYPE, /* classes the extension instantiates (their PHP twins are themselves * shadowed, hence no default name): configured to the stub subclasses * so created objects satisfy the original PHPStan type hints */ PT_CLASS_TRINARY, PT_CLASS_ETH, PT_CLASS_CEH, + PT_CLASS_FINITE_TYPE_SET, PT_CLASS_COUNT }; @@ -135,6 +145,7 @@ extern zend_class_entry *pt_ce_trinary; extern zend_class_entry *pt_ce_expr_type_holder; extern zend_class_entry *pt_ce_cond_expr_holder; extern zend_class_entry *pt_ce_type_combinator_cache; +extern zend_class_entry *pt_ce_finite_type_set; /* registration hooks, called from the extension's onStartup */ void pt_register_trinary_logic(); @@ -148,6 +159,7 @@ void pt_register_parser_runner(); void pt_register_type_combinator_cache(); void pt_register_arena_cache(); void pt_register_expression_result_storage(); +void pt_register_finite_type_set(); /* per-request hooks of individual classes */ void pt_node_traverser_rinit(); @@ -156,6 +168,7 @@ void pt_scope_ops_rinit(); void pt_scope_ops_rshutdown(); void pt_type_combinator_cache_rinit(); void pt_type_combinator_cache_rshutdown(); +void pt_finite_type_set_rinit(); /* module-shutdown backstop: destroys the arena mapping if the run skipped * ArenaCache::destroy() on a graceful exit */ @@ -175,6 +188,10 @@ void pt_arena_mshutdown(); #define PT_ETH_PROP_CERTAINTY 2 #define PT_CEH_PROP_CONDS 0 #define PT_CEH_PROP_TYPEHOLDER 1 +#define PT_FTS_PROP_HAS_CLASS_STRING_MEMBER 0 +#define PT_FTS_PROP_MEMBERS 1 +#define PT_FTS_PROP_MEMBERS_BY_KIND 2 +#define PT_FTS_PROP_OTHERS 3 /* Returns the singleton for the given value (instances of the configured * trinaryLogicImpl class). Borrowed zval; callers must copy. */ diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index a5b825ac314..11a68bae57e 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -612,6 +612,139 @@ public function enterNode(\PhpParser\Node $node) } check($matchResults['php'] === $matchResults['native'], 'ScopeOps matchConditionalExpressions: fixed point and remaining conditions'); +// ---- FiniteTypeSet ---- +// Both implementations are handed the very same Type objects, so members, +// others and representatives can be compared with === : same keys, same +// order, same instances. +$covered[\PHPStan\Type\FiniteTypeSet::class] = true; + +$ftsTemplate = new \PHPStan\Type\Generic\TemplateConstantStringType( + \PHPStan\Type\Generic\TemplateTypeScope::createWithFunction('smoke'), + new \PHPStan\Type\Generic\TemplateTypeParameterStrategy(), + \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant(), + 'T', + new \PHPStan\Type\Constant\ConstantStringType('a'), + null, +); +// a subclass of a keyed class takes the userland path natively, exactly like +// the twin does — it may have overridden getConstantScalarValues() +$ftsStringSubclass = new class ('sub') extends \PHPStan\Type\Constant\ConstantStringType {}; +$ftsEnumA = new \PHPStan\Type\Enum\EnumCaseObjectType('SmokeEnum', 'A'); +$ftsEnumB = new \PHPStan\Type\Enum\EnumCaseObjectType('SmokeEnum', 'B'); +$ftsOtherEnumA = new \PHPStan\Type\Enum\EnumCaseObjectType('OtherSmokeEnum', 'A'); + +$ftsInputs = [ + 'constant ints' => [new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\Constant\ConstantIntegerType(-2), new \PHPStan\Type\Constant\ConstantIntegerType(PHP_INT_MIN)], + 'constant strings' => [new \PHPStan\Type\Constant\ConstantStringType('a'), new \PHPStan\Type\Constant\ConstantStringType(''), new \PHPStan\Type\Constant\ConstantStringType('a::b')], + 'mixed kinds' => [new \PHPStan\Type\NullType(), new \PHPStan\Type\Constant\ConstantBooleanType(true), new \PHPStan\Type\Constant\ConstantBooleanType(false), new \PHPStan\Type\Constant\ConstantIntegerType(0), new \PHPStan\Type\Constant\ConstantStringType('0')], + 'enum cases' => [$ftsEnumA, $ftsEnumB, $ftsOtherEnumA], + 'duplicate values' => [new \PHPStan\Type\Constant\ConstantStringType('a'), new \PHPStan\Type\Constant\ConstantStringType('a'), $ftsEnumA, $ftsEnumA], + 'with others' => [new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\StringType(), new \PHPStan\Type\Constant\ConstantFloatType(1.0), $ftsTemplate], + 'subclass of a keyed class' => [new \PHPStan\Type\Constant\ConstantStringType('sub'), $ftsStringSubclass], + 'nothing keyed' => [new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType()], + 'union and intersection members' => [new \PHPStan\Type\UnionType([new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\Constant\ConstantIntegerType(2)]), new \PHPStan\Type\Constant\ConstantIntegerType(3)], + 'empty' => [], +]; + +$ftsSets = []; +foreach ($ftsInputs as $label => $types) { + $php = \PHPStan\Type\FiniteTypeSet::create($types); + $native = \PHPStanTurbo\FiniteTypeSet::create($types); + check(($php === null) === ($native === null), "FiniteTypeSet create($label): nullness"); + if ($php === null || $native === null) { + continue; + } + $ftsSets[$label] = [$php, $native]; + check($php->getMembers() === $native->getMembers(), "FiniteTypeSet create($label): members"); + check($php->getOthers() === $native->getOthers(), "FiniteTypeSet create($label): others"); + check($php->isComplete() === $native->isComplete(), "FiniteTypeSet create($label): isComplete"); +} + +// key() over every shape, keyed and unkeyed alike +$ftsKeyProbes = [ + new \PHPStan\Type\NullType(), + new \PHPStan\Type\Constant\ConstantIntegerType(42), + new \PHPStan\Type\Constant\ConstantIntegerType(PHP_INT_MAX), + new \PHPStan\Type\Constant\ConstantBooleanType(true), + new \PHPStan\Type\Constant\ConstantBooleanType(false), + new \PHPStan\Type\Constant\ConstantStringType('a'), + new \PHPStan\Type\Constant\ConstantStringType('Foo\\Bar', true), + new \PHPStan\Type\Constant\ConstantFloatType(1.5), + $ftsEnumA, + $ftsStringSubclass, + $ftsTemplate, + new \PHPStan\Type\StringType(), + new \PHPStan\Type\UnionType([new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\Constant\ConstantIntegerType(2)]), + new \PHPStan\Type\IntersectionType([new \PHPStan\Type\StringType(), new \PHPStan\Type\Accessory\AccessoryNonEmptyStringType()]), +]; +foreach ($ftsKeyProbes as $i => $probe) { + check( + \PHPStan\Type\FiniteTypeSet::key($probe) === \PHPStanTurbo\FiniteTypeSet::key($probe), + "FiniteTypeSet key(#$i " . get_class($probe) . ')', + ); +} + +// has()/containedInKey() over keys that exist, keys that do not, and a +// numeric-looking key (PHP array keys coerce those to integers) +[$ftsPhpMixed, $ftsNativeMixed] = $ftsSets['mixed kinds']; +foreach (['null', 'i:0', 's:0', 'b:1', 'b:0', 'i:1', 's:missing', '0', ''] as $probeKey) { + check($ftsPhpMixed->has($probeKey) === $ftsNativeMixed->has($probeKey), "FiniteTypeSet has($probeKey)"); + check( + $ftsPhpMixed->containedInKey($probeKey)->describe() === $ftsNativeMixed->containedInKey($probeKey)->describe(), + "FiniteTypeSet containedInKey($probeKey)", + ); +} +[$ftsPhpSingle, $ftsNativeSingle] = [\PHPStan\Type\FiniteTypeSet::create([new \PHPStan\Type\NullType()]), \PHPStanTurbo\FiniteTypeSet::create([new \PHPStan\Type\NullType()])]; +check( + $ftsPhpSingle->containedInKey('null')->describe() === $ftsNativeSingle->containedInKey('null')->describe(), + 'FiniteTypeSet containedInKey: single-member set', +); + +// containedIn(): yes / no / maybe across every pair of sets +foreach ($ftsSets as $label => [$php, $native]) { + foreach ($ftsSets as $otherLabel => [$otherPhp, $otherNative]) { + check( + $php->containedIn($otherPhp)->describe() === $native->containedIn($otherNative)->describe(), + "FiniteTypeSet containedIn($label, $otherLabel)", + ); + } +} + +// getRepresentativesOfOtherKinds(): the kind of the queried type is skipped, +// including the enum-per-class kinds +foreach ($ftsSets as $label => [$php, $native]) { + foreach ($ftsKeyProbes as $i => $probe) { + check( + $php->getRepresentativesOfOtherKinds($probe) === $native->getRepresentativesOfOtherKinds($probe), + "FiniteTypeSet getRepresentativesOfOtherKinds($label, #$i " . get_class($probe) . ')', + ); + } +} + +// hasClassStringMember(): a class-string member, a member answering no +// through userland, and the memoized second call. Members answering no +// natively without a call are what the other sets are made of. +$ftsNoClassString = new class ('x') extends \PHPStan\Type\Constant\ConstantStringType { + + public function isClassString(): \PHPStan\TrinaryLogic + { + return \PHPStan\TrinaryLogic::createNo(); + } + +}; +$ftsClassStringInputs = [ + 'class-string first' => [new \PHPStan\Type\Constant\ConstantStringType('Foo\\Bar', true), new \PHPStan\Type\NullType()], + 'no class-string' => [new \PHPStan\Type\NullType(), new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\Constant\ConstantBooleanType(true), $ftsEnumA], + 'userland no' => [$ftsNoClassString, new \PHPStan\Type\NullType()], + 'userland yes' => [$ftsNoClassString, new \PHPStan\Type\Constant\ConstantStringType('Foo\\Bar', true)], +]; +foreach ($ftsClassStringInputs as $label => $types) { + $php = \PHPStan\Type\FiniteTypeSet::create($types); + $native = \PHPStanTurbo\FiniteTypeSet::create($types); + check($php->hasClassStringMember() === $native->hasClassStringMember(), "FiniteTypeSet hasClassStringMember($label)"); + check($php->hasClassStringMember() === $native->hasClassStringMember(), "FiniteTypeSet hasClassStringMember($label): memoized"); +} + // ---- differential coverage completeness ---- // Every shadowed class must be exercised by one of the tests/ scripts; the // classes not covered above have their own dedicated script.