Skip to content

Commit

Permalink
Add defaultValue method on type intersection constraint
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Niharika Devanathan authored and facebook-github-bot committed Dec 12, 2024
1 parent 149ff37 commit 4acb057
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 15 deletions.
11 changes: 7 additions & 4 deletions hphp/hhbbc/index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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<KindOfNull>();
}();
}
}
Expand Down Expand Up @@ -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<KindOfNull>();
}();
}
}
Expand Down
53 changes: 53 additions & 0 deletions hphp/runtime/base/annot-type.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>(a) & static_cast<uint8_t>(b));
}

constexpr AnnotTypeDefault operator|(AnnotTypeDefault a, AnnotTypeDefault b) {
return AnnotTypeDefault(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}

constexpr AnnotTypeDefault operator~(AnnotTypeDefault a) {
return AnnotTypeDefault(~static_cast<uint8_t>(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 == '?');
Expand Down
48 changes: 48 additions & 0 deletions hphp/runtime/test/type-check-test.cpp
Original file line number Diff line number Diff line change
@@ -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 |
| [email protected] so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/

#include "hphp/runtime/vm/type-constraint.h"

#include <gtest/gtest.h>

namespace HPHP {
TEST(TypeChecks, TypeVar) {
/*
* Test the following types of type constraints
* main: TypeConstraint{flags:TypeVar, type:Mixed, clsName:<null>, typeName:T}
*/
auto const typeVar = TypeConstraint(
AnnotType::Mixed,
TypeConstraintFlags::TypeVar,
LowStringPtr(StringData::MakeStatic("T"))
);
auto const tic = TypeIntersectionConstraint(std::vector<TypeConstraint>({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<TypeConstraint>({nullableString}));
EXPECT_EQ(KindOfNull, tic.defaultValue()->m_type);
}
}
78 changes: 74 additions & 4 deletions hphp/runtime/vm/type-constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<KindOfNull>();
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<TypedValue> 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<KindOfNull>();
if (has_flag(dv, AnnotTypeDefault::ZeroInt)) return make_tv<KindOfInt64>(0);
if (has_flag(dv, AnnotTypeDefault::False)) return make_tv<KindOfBoolean>(false);
if (has_flag(dv, AnnotTypeDefault::ZeroDouble)) return make_tv<KindOfDouble>(0.0);
if (has_flag(dv, AnnotTypeDefault::EmptyString)) {
return make_tv<KindOfPersistentString>(staticEmptyString());
}
if (has_flag(dv, AnnotTypeDefault::EmptyVec)) {
return make_tv<KindOfPersistentVec>(staticEmptyVec());
}
if (has_flag(dv, AnnotTypeDefault::EmptyDict)) {
return make_tv<KindOfPersistentDict>(staticEmptyDictArray());
}
if (has_flag(dv, AnnotTypeDefault::EmptyKeyset)) {
return make_tv<KindOfPersistentKeyset>(staticEmptyKeysetArray());
}
assertx(dv == AnnotTypeDefault::None);
return std::nullopt;
}

namespace {
Expand Down
11 changes: 4 additions & 7 deletions hphp/runtime/vm/type-constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ struct TypeConstraint {
&& !isMixed()
&& !isTypeVar()
&& !isTypeConstant())
|| isUnion();
|| isUnion();
}

/*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -896,6 +891,8 @@ struct TypeIntersectionConstraint {
}
}

HPHP::Optional<TypedValue> defaultValue() const;

private:
folly::Range<TypeConstraint*> mutableRange() {
if (isSimple()) {
Expand Down

0 comments on commit 4acb057

Please sign in to comment.