From 4acb057b0e989a362bb9b06ca289767699598ef9 Mon Sep 17 00:00:00 2001 From: Niharika Devanathan Date: Thu, 12 Dec 2024 05:38:50 -0800 Subject: [PATCH] Add defaultValue method on type intersection constraint Summary: As title. The logic for computing the default value is straightforward - choose all possible default values that satisfy all constraints in the intersection. If there is more than one possible default, choose one in the specified precedence order. Reviewed By: jano Differential Revision: D64982596 fbshipit-source-id: 641f92cf51ce12c75b2fcc1393862486c8dafee1 --- hphp/hhbbc/index.cpp | 11 ++-- hphp/runtime/base/annot-type.h | 53 ++++++++++++++++++ hphp/runtime/test/type-check-test.cpp | 48 +++++++++++++++++ hphp/runtime/vm/type-constraint.cpp | 78 +++++++++++++++++++++++++-- hphp/runtime/vm/type-constraint.h | 11 ++-- 5 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 hphp/runtime/test/type-check-test.cpp diff --git a/hphp/hhbbc/index.cpp b/hphp/hhbbc/index.cpp index 3c811e17b0228a..3f854cf0ac3ff6 100644 --- a/hphp/hhbbc/index.cpp +++ b/hphp/hhbbc/index.cpp @@ -11767,13 +11767,14 @@ struct FlattenJob { cinfo.hasBadRedeclareProp = true; } + auto const dv = prop.typeConstraints.defaultValue(); auto const nullable = [&] { if (isClosure) return true; if (!(prop.attrs & AttrSystemInitialValue)) return false; - return prop.typeConstraints.main().defaultValue().m_type == KindOfNull; + return dv && dv->m_type == KindOfNull; }(); - attribute_setter(prop.attrs, !nullable, AttrNoImplicitNullable); + attribute_setter(prop.attrs, dv && !nullable, AttrNoImplicitNullable); if (!(prop.attrs & AttrSystemInitialValue)) continue; if (prop.val.m_type == KindOfUninit) { assertx(isClosure || bool(prop.attrs & AttrLateInit)); @@ -11788,7 +11789,8 @@ struct FlattenJob { if (prop.name == s_86reified_prop.get()) { return get_default_value_of_reified_list(cls.userAttributes); } - return prop.typeConstraints.main().defaultValue(); + + return dv ? dv.value() : make_tv(); }(); } } @@ -14443,7 +14445,8 @@ struct BuildSubclassListJob { if (prop.name == s_86reified_prop.get()) { return get_default_value_of_reified_list(cls.userAttributes); } - return prop.typeConstraints.main().defaultValue(); + auto dv = prop.typeConstraints.defaultValue(); + return dv ? dv.value() : make_tv(); }(); } } diff --git a/hphp/runtime/base/annot-type.h b/hphp/runtime/base/annot-type.h index 302cabcd055e7e..701cdce0c9beb3 100644 --- a/hphp/runtime/base/annot-type.h +++ b/hphp/runtime/base/annot-type.h @@ -117,6 +117,59 @@ constexpr const char* annotNullableTypeName(AnnotType ty) { } } +// Encodes possible default values for a given AnnotType. +enum class AnnotTypeDefault : uint8_t { + None = 0b00000000, + Null = 0b00000001, + ZeroInt = 0b00000010, + False = 0b00000100, + ZeroDouble = 0b00001000, + EmptyString = 0b00010000, + EmptyVec = 0b00100000, + EmptyDict = 0b01000000, + EmptyKeyset = 0b10000000, + // The following are simply unions of the above, defined for convenience + AnyNonNull = 0b11111110, + Any = 0b11111111, + ZeroNumber = 0b00001010, + ZeroIntOrEmptyString = 0b00010010, + EmptyArray = 0b11100000, + EmptyVecOrDict = 0b01100000 +}; + +constexpr AnnotTypeDefault operator&(AnnotTypeDefault a, AnnotTypeDefault b) { + return AnnotTypeDefault(static_cast(a) & static_cast(b)); +} + +constexpr AnnotTypeDefault operator|(AnnotTypeDefault a, AnnotTypeDefault b) { + return AnnotTypeDefault(static_cast(a) | static_cast(b)); +} + +constexpr AnnotTypeDefault operator~(AnnotTypeDefault a) { + return AnnotTypeDefault(~static_cast(a)); +} + +constexpr bool has_flag(AnnotTypeDefault flags, AnnotTypeDefault flag) { + return (flags & flag) != AnnotTypeDefault::None; +} + +static_assert(AnnotTypeDefault::ZeroNumber + == (AnnotTypeDefault::ZeroInt | AnnotTypeDefault::ZeroDouble)); +static_assert(uint8_t(AnnotTypeDefault::Any) == 0xFF); +static_assert(uint8_t(AnnotTypeDefault::AnyNonNull) + == uint8_t(AnnotTypeDefault::Any & ~AnnotTypeDefault::Null)); +static_assert(AnnotTypeDefault::EmptyArray + == (AnnotTypeDefault::EmptyVec + | AnnotTypeDefault::EmptyDict + | AnnotTypeDefault::EmptyKeyset) +); +static_assert(AnnotTypeDefault::ZeroIntOrEmptyString + == (AnnotTypeDefault::EmptyString | AnnotTypeDefault::ZeroInt) +); +static_assert(AnnotTypeDefault::EmptyVecOrDict + == (AnnotTypeDefault::EmptyVec | AnnotTypeDefault::EmptyDict) +); + constexpr const char* annotTypeName(AnnotType ty) { const char* name = annotNullableTypeName(ty); assertx(*name == '?'); diff --git a/hphp/runtime/test/type-check-test.cpp b/hphp/runtime/test/type-check-test.cpp new file mode 100644 index 00000000000000..1834c48f6515b3 --- /dev/null +++ b/hphp/runtime/test/type-check-test.cpp @@ -0,0 +1,48 @@ +/* + +----------------------------------------------------------------------+ + | HipHop for PHP | + +----------------------------------------------------------------------+ + | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ +*/ + +#include "hphp/runtime/vm/type-constraint.h" + +#include + +namespace HPHP { + TEST(TypeChecks, TypeVar) { + /* + * Test the following types of type constraints + * main: TypeConstraint{flags:TypeVar, type:Mixed, clsName:, typeName:T} + */ + auto const typeVar = TypeConstraint( + AnnotType::Mixed, + TypeConstraintFlags::TypeVar, + LowStringPtr(StringData::MakeStatic("T")) + ); + auto const tic = TypeIntersectionConstraint(std::vector({typeVar})); + EXPECT_EQ(KindOfNull, tic.defaultValue()->m_type); + } + + TEST(TypeChecks, NullableString) { + auto nullableString = TypeConstraint( + AnnotType::String, + (TypeConstraintFlags::Nullable + | TypeConstraintFlags::Resolved + | TypeConstraintFlags::DisplayNullable + | TypeConstraintFlags::UpperBound), + LowStringPtr(StringData::MakeStatic("HH\\string")) + ); + auto const tic = TypeIntersectionConstraint(std::vector({nullableString})); + EXPECT_EQ(KindOfNull, tic.defaultValue()->m_type); + } +} diff --git a/hphp/runtime/vm/type-constraint.cpp b/hphp/runtime/vm/type-constraint.cpp index ce86afd810aa6b..73eb7cbb384e07 100644 --- a/hphp/runtime/vm/type-constraint.cpp +++ b/hphp/runtime/vm/type-constraint.cpp @@ -25,11 +25,14 @@ #include "hphp/util/configs/eval.h" #include "hphp/util/match.h" #include "hphp/util/trace.h" +#include "hphp/runtime/base/annot-type.h" #include "hphp/runtime/base/autoload-handler.h" #include "hphp/runtime/base/datatype.h" #include "hphp/runtime/base/execution-context.h" #include "hphp/runtime/base/runtime-error.h" +#include "hphp/runtime/base/tv-type.h" +#include "hphp/runtime/base/typed-value.h" #include "hphp/runtime/base/type-structure-helpers.h" #include "hphp/runtime/vm/act-rec.h" #include "hphp/runtime/vm/class.h" @@ -544,6 +547,38 @@ size_t stableHashVec(R&& v) { } return h; } + +// Returns the set of all possible default values for a given annot-type. +AnnotTypeDefault annotTypeDefaultValues(AnnotType at) { + switch (at) { + case AnnotType::This: + case AnnotType::Callable: + case AnnotType::Resource: + case AnnotType::Object: + case AnnotType::SubObject: + case AnnotType::Nothing: + case AnnotType::NoReturn: + case AnnotType::Classname: + case AnnotType::Class: + case AnnotType::ClassOrClassname: + case AnnotType::Null: return AnnotTypeDefault::Null; + case AnnotType::Mixed: return AnnotTypeDefault::Any; + case AnnotType::Nonnull: return AnnotTypeDefault::AnyNonNull; + case AnnotType::Number: return AnnotTypeDefault::ZeroNumber; + case AnnotType::ArrayKey: return AnnotTypeDefault::ZeroIntOrEmptyString; + case AnnotType::Int: return AnnotTypeDefault::ZeroInt; + case AnnotType::Bool: return AnnotTypeDefault::False; + case AnnotType::Float: return AnnotTypeDefault::ZeroDouble; + case AnnotType::ArrayLike: return AnnotTypeDefault::EmptyArray; + case AnnotType::VecOrDict: return AnnotTypeDefault::EmptyVecOrDict; + case AnnotType::Vec: return AnnotTypeDefault::EmptyVec; + case AnnotType::String: return AnnotTypeDefault::EmptyString; + case AnnotType::Dict: return AnnotTypeDefault::EmptyDict; + case AnnotType::Keyset: return AnnotTypeDefault::EmptyKeyset; + case AnnotType::Unresolved: return AnnotTypeDefault::None; + } + always_assert(false); +} } // anonymous namespace size_t UnionClassList::stableHash() const { @@ -884,12 +919,47 @@ std::string TypeConstraint::debugName() const { } } -TypedValue TypeConstraint::defaultValue() const { +AnnotTypeDefault TypeConstraint::getPossibleDefaultValues() const { // Nullable type-constraints should always default to null, as Hack // guarantees this. - if (!isCheckable() || isNullable()) return make_tv(); - AnnotType annotType = (*eachTypeConstraintInUnion(*this).begin()).type(); - return annotDefaultValue(annotType); + if (isNullable()) return AnnotTypeDefault::Null; + AnnotTypeDefault dv = AnnotTypeDefault::None; + for (const auto& tc : eachTypeConstraintInUnion(*this)) { + dv = (dv | annotTypeDefaultValues(tc.type())); + } + return dv; +} + +/* + * Choose a default value that satisfies all the constraints in the given + * intersection. Returns nullopt if no such default value exists. + */ +HPHP::Optional TypeIntersectionConstraint::defaultValue() const { + AnnotTypeDefault dv = AnnotTypeDefault::Any; + for (auto const& tc : range()) { + dv = (dv & tc.getPossibleDefaultValues()); + } + + // It is possible that multiple default values satisfy the given + // intersection. Choose one in the following order of precedence. + if (has_flag(dv, AnnotTypeDefault::Null)) return make_tv(); + if (has_flag(dv, AnnotTypeDefault::ZeroInt)) return make_tv(0); + if (has_flag(dv, AnnotTypeDefault::False)) return make_tv(false); + if (has_flag(dv, AnnotTypeDefault::ZeroDouble)) return make_tv(0.0); + if (has_flag(dv, AnnotTypeDefault::EmptyString)) { + return make_tv(staticEmptyString()); + } + if (has_flag(dv, AnnotTypeDefault::EmptyVec)) { + return make_tv(staticEmptyVec()); + } + if (has_flag(dv, AnnotTypeDefault::EmptyDict)) { + return make_tv(staticEmptyDictArray()); + } + if (has_flag(dv, AnnotTypeDefault::EmptyKeyset)) { + return make_tv(staticEmptyKeysetArray()); + } + assertx(dv == AnnotTypeDefault::None); + return std::nullopt; } namespace { diff --git a/hphp/runtime/vm/type-constraint.h b/hphp/runtime/vm/type-constraint.h index f7eaf46794839d..cc1220ac9b82fb 100644 --- a/hphp/runtime/vm/type-constraint.h +++ b/hphp/runtime/vm/type-constraint.h @@ -307,7 +307,7 @@ struct TypeConstraint { && !isMixed() && !isTypeVar() && !isTypeConstant()) - || isUnion(); + || isUnion(); } /* @@ -408,12 +408,7 @@ struct TypeConstraint { std::string debugName() const; - /* - * Obtain an initial value suitable for this type-constraint. Where possible, - * the initial value is chosen to satisfy the type-constraint, but this isn't - * always possible (for example, for objects). - */ - TypedValue defaultValue() const; + AnnotTypeDefault getPossibleDefaultValues() const; /* * Returns whether this and another type-constraint might not be equivalent at @@ -896,6 +891,8 @@ struct TypeIntersectionConstraint { } } + HPHP::Optional defaultValue() const; + private: folly::Range mutableRange() { if (isSimple()) {