hphp/hhbbc/index.cpp (5,707 lines of code) (raw):

/* +----------------------------------------------------------------------+ | 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/hhbbc/index.h" #include <algorithm> #include <cstdio> #include <cstdlib> #include <iterator> #include <map> #include <memory> #include <mutex> #include <unordered_map> #include <utility> #include <vector> #include <boost/dynamic_bitset.hpp> #include <tbb/concurrent_hash_map.h> #include <tbb/concurrent_unordered_map.h> #include <folly/Format.h> #include <folly/Hash.h> #include <folly/Lazy.h> #include <folly/MapUtil.h> #include <folly/Memory.h> #include <folly/Range.h> #include <folly/String.h> #include <folly/concurrency/ConcurrentHashMap.h> #include "hphp/runtime/base/array-iterator.h" #include "hphp/runtime/base/runtime-option.h" #include "hphp/runtime/base/tv-comparisons.h" #include "hphp/runtime/base/type-structure-helpers-defs.h" #include "hphp/runtime/vm/native.h" #include "hphp/runtime/vm/preclass-emitter.h" #include "hphp/runtime/vm/runtime.h" #include "hphp/runtime/vm/trait-method-import-data.h" #include "hphp/runtime/vm/unit-util.h" #include "hphp/hhbbc/analyze.h" #include "hphp/hhbbc/class-util.h" #include "hphp/hhbbc/context.h" #include "hphp/hhbbc/func-util.h" #include "hphp/hhbbc/options.h" #include "hphp/hhbbc/options-util.h" #include "hphp/hhbbc/parallel.h" #include "hphp/hhbbc/representation.h" #include "hphp/hhbbc/type-builtins.h" #include "hphp/hhbbc/type-structure.h" #include "hphp/hhbbc/type-system.h" #include "hphp/hhbbc/unit-util.h" #include "hphp/hhbbc/wide-func.h" #include "hphp/util/algorithm.h" #include "hphp/util/assertions.h" #include "hphp/util/hash-set.h" #include "hphp/util/lock-free-lazy.h" #include "hphp/util/match.h" #include "hphp/zend/zend-string.h" namespace HPHP::HHBBC { TRACE_SET_MOD(hhbbc_index); ////////////////////////////////////////////////////////////////////// namespace { ////////////////////////////////////////////////////////////////////// const StaticString s_construct("__construct"); const StaticString s_toBoolean("__toBoolean"); const StaticString s_invoke("__invoke"); const StaticString s_Closure("Closure"); const StaticString s_AsyncGenerator("HH\\AsyncGenerator"); const StaticString s_Generator("Generator"); ////////////////////////////////////////////////////////////////////// // HHBBC consumes a LOT of memory, so we keep representation types small. template <typename T, size_t Expected, size_t Actual = sizeof(T)> constexpr bool CheckSize() { static_assert(Expected == Actual); return true; }; static_assert(CheckSize<php::Block, 24>(), ""); static_assert(CheckSize<php::Local, use_lowptr ? 12 : 16>(), ""); static_assert(CheckSize<php::Param, use_lowptr ? 64 : 88>(), ""); static_assert(CheckSize<php::Func, use_lowptr ? 176 : 216>(), ""); // Likewise, we also keep the bytecode and immediate types small. static_assert(CheckSize<Bytecode, use_lowptr ? 32 : 40>(), ""); static_assert(CheckSize<MKey, 16>(), ""); static_assert(CheckSize<IterArgs, 16>(), ""); static_assert(CheckSize<FCallArgs, 8>(), ""); static_assert(CheckSize<RepoAuthType, 8>(), ""); ////////////////////////////////////////////////////////////////////// /* * One-to-many case insensitive map, where the keys are static strings * and the values are some kind of pointer. */ template<class T> using ISStringToMany = std::unordered_multimap< SString, T*, string_data_hash, string_data_isame >; template<class T> using SStringToMany = std::unordered_multimap< SString, T*, string_data_hash, string_data_same >; /* * One-to-one case insensitive map, where the keys are static strings * and the values are some T. */ template<class T> using ISStringToOneT = hphp_hash_map< SString, T, string_data_hash, string_data_isame >; template<class T> using SStringToOneT = hphp_hash_map< SString, T, string_data_hash, string_data_same >; /* * One-to-one case sensitive map, where the keys are static strings * and the values are some T. * * Elements are not stable under insert/erase. */ template<class T> using SStringToOneFastT = hphp_fast_map< SString, T, string_data_hash, string_data_same >; template<class T> using SStringToOneFastT = hphp_fast_map< SString, T, string_data_hash, string_data_same >; /* * One-to-one case insensitive map, where the keys are static strings * and the values are some kind of pointer. */ template<class T> using ISStringToOne = ISStringToOneT<T*>; ////////////////////////////////////////////////////////////////////// Dep operator|(Dep a, Dep b) { return static_cast<Dep>( static_cast<uintptr_t>(a) | static_cast<uintptr_t>(b) ); } bool has_dep(Dep m, Dep t) { return static_cast<uintptr_t>(m) & static_cast<uintptr_t>(t); } /* * Maps functions to contexts that depend on information about that * function, with information about the type of dependency. */ using DepMap = tbb::concurrent_hash_map< DependencyContext, std::map<DependencyContext,Dep,DependencyContextLess>, DependencyContextHashCompare >; ////////////////////////////////////////////////////////////////////// /* * Each ClassInfo has a table of public static properties with these entries. * The `initializerType' is for use during refine_public_statics, and * inferredType will always be a supertype of initializerType. */ struct PublicSPropEntry { Type inferredType; Type initializerType; const php::Prop* prop; uint32_t refinements; /* * This flag is set during analysis to indicate that we resolved the * initial value (and updated it on the php::Class). This doesn't * need to be atomic, because only one thread can resolve the value * (the one processing the 86sinit), and it's been joined by the * time we read the flag in refine_public_statics. */ bool initialValueResolved; bool everModified; }; /* * Entries in the ClassInfo method table need to track some additional * information. * * The reason for this is that we need to record attributes of the * class hierarchy. */ struct MethTabEntry { MethTabEntry(const php::Func* func, Attr a, bool hpa, bool tl) : func(func), attrs(a), hasPrivateAncestor(hpa), topLevel(tl) {} const php::Func* func = nullptr; // A method could be imported from a trait, and its attributes changed Attr attrs {}; bool hasAncestor = false; bool hasPrivateAncestor = false; // This method came from the ClassInfo that owns the MethTabEntry, // or one of its used traits. bool topLevel = false; uint32_t idx = 0; }; } struct res::Func::MethTabEntryPair : SStringToOneT<MethTabEntry>::value_type {}; namespace { using MethTabEntryPair = res::Func::MethTabEntryPair; inline MethTabEntryPair* mteFromElm( SStringToOneT<MethTabEntry>::value_type& elm) { return static_cast<MethTabEntryPair*>(&elm); } inline const MethTabEntryPair* mteFromElm( const SStringToOneT<MethTabEntry>::value_type& elm) { return static_cast<const MethTabEntryPair*>(&elm); } inline MethTabEntryPair* mteFromIt(SStringToOneT<MethTabEntry>::iterator it) { return static_cast<MethTabEntryPair*>(&*it); } using ContextRetTyMap = tbb::concurrent_hash_map< CallContext, Type, CallContextHashCompare >; ////////////////////////////////////////////////////////////////////// template<class Filter> PropState make_unknown_propstate(const php::Class* cls, Filter filter) { auto ret = PropState{}; for (auto& prop : cls->properties) { if (filter(prop)) { auto& elem = ret[prop.name]; elem.ty = TCell; elem.tc = &prop.typeConstraint; elem.attrs = prop.attrs; elem.everModified = true; } } return ret; } } /* * Currently inferred information about a PHP function. * * Nothing in this structure can ever be untrue. The way the * algorithm works, whatever is in here must be factual (even if it is * not complete information), because we may deduce other facts based * on it. */ struct res::Func::FuncInfo { const php::Func* func = nullptr; /* * The best-known return type of the function, if we have any * information. May be TBottom if the function is known to never * return (e.g. always throws). */ Type returnTy = TInitCell; /* * If the function always returns the same parameter, this will be * set to its id; otherwise it will be NoLocalId. */ LocalId retParam{NoLocalId}; /* * The number of times we've refined returnTy. */ uint32_t returnRefinements{0}; /* * Whether the function is effectFree. */ bool effectFree{false}; /* * Bitset representing which parameters definitely don't affect the * result of the function, assuming it produces one. Note that * VerifyParamType does not count as a use in this context. */ std::bitset<64> unusedParams; /* * List of all func families this function belongs to. */ CompactVector<FuncFamily*> families; }; namespace { ////////////////////////////////////////////////////////////////////// /* * Known information about a particular constant: * - if system is true, it's a system constant and other definitions * will be ignored. * - for non-system constants, if func is non-null it's the unique * pseudomain defining the constant; otherwise there was more than * one definition, or a non-pseudomain definition, and the type will * be TInitCell * - readonly is true if we've only seen uses of the constant, and no * definitions (this could change during the first pass, but not after * that). */ struct ConstInfo { const php::Func* func; Type type; bool system; bool readonly; }; using FuncFamily = res::Func::FuncFamily; using FuncInfo = res::Func::FuncInfo; using MethTabEntryPair = res::Func::MethTabEntryPair; ////////////////////////////////////////////////////////////////////// } ////////////////////////////////////////////////////////////////////// /* * Sometimes function resolution can't determine which function * something will call, but can restrict it to a family of functions. * * For example, if you want to call an abstract function on a base * class with all unique derived classes, we will resolve the function * to a FuncFamily that contains references to all the possible * overriding-functions. */ struct res::Func::FuncFamily { using PFuncVec = CompactVector<const MethTabEntryPair*>; explicit FuncFamily(PFuncVec&& v) : m_v{std::move(v)} {} FuncFamily(FuncFamily&& o) noexcept : m_v(std::move(o.m_v)) {} FuncFamily& operator=(FuncFamily&& o) noexcept { m_v = std::move(o.m_v); return *this; } FuncFamily(const FuncFamily&) = delete; FuncFamily& operator=(const FuncFamily&) = delete; const PFuncVec& possibleFuncs() const { return m_v; }; friend auto begin(const FuncFamily& ff) { return ff.m_v.begin(); } friend auto end(const FuncFamily& ff) { return ff.m_v.end(); } PFuncVec m_v; LockFreeLazy<Type> m_returnTy; Optional<uint32_t> m_numInOut; TriBool m_isReadonlyReturn; TriBool m_isReadonlyThis; }; namespace { struct PFuncVecHasher { size_t operator()(const FuncFamily::PFuncVec& v) const { return folly::hash::hash_range( v.begin(), v.end(), 0, pointer_hash<MethTabEntryPair>{} ); } }; } ////////////////////////////////////////////////////////////////////// /* * Known information about a particular possible instantiation of a * PHP class. The php::Class will be marked AttrUnique if there is a * unique ClassInfo with the same name. */ struct ClassInfo { /* * A pointer to the underlying php::Class that we're storing * information about. */ const php::Class* cls = nullptr; /* * The info for the parent of this Class. */ ClassInfo* parent = nullptr; /* * A vector of the declared interfaces class info structures. This is in * declaration order mirroring the php::Class interfaceNames vector, and does * not include inherited interfaces. */ CompactVector<const ClassInfo*> declInterfaces; /* * A (case-insensitive) map from interface names supported by this class to * their ClassInfo structures, flattened across the hierarchy. */ ISStringToOneT<const ClassInfo*> implInterfaces; /* * A vector of the included enums, in class order, mirroring the * php::Class includedEnums vector. */ CompactVector<const ClassInfo*> includedEnums; struct ConstIndex { const php::Const& operator*() const { return cls->constants[idx]; } const php::Const* operator->() const { return get(); } const php::Const* get() const { return &cls->constants[idx]; } const php::Class* cls; uint32_t idx; }; /* * A (case-sensitive) map from class constant name to the php::Class* and * index into the constants vector that it came from. This map is flattened * across the inheritance hierarchy. Use a vector_map for stable iteration. */ hphp_vector_map<SString, ConstIndex> clsConstants; /* * A vector of the used traits, in class order, mirroring the * php::Class usedTraitNames vector. */ CompactVector<const ClassInfo*> usedTraits; /* * A list of extra properties supplied by this class's used traits. */ CompactVector<php::Prop> traitProps; /* * A list of extra consts supplied by this class's used traits. */ CompactVector<php::Const> traitConsts; /* * A (case-sensitive) map from class method names to the php::Func * associated with it. This map is flattened across the inheritance * hierarchy. */ SStringToOneT<MethTabEntry> methods; /* * A (case-sensitive) map from class method names to associated * FuncFamily objects that group the set of possibly-overriding * methods. * * Note that this does not currently encode anything for interface * methods. * * Invariant: methods on this class with AttrNoOverride or * AttrPrivate will not have an entry in this map. */ SStringToOneFastT<FuncFamily*> methodFamilies; // Resolutions to single entries do not require a FuncFamily (this // saves space). SStringToOneFastT<const MethTabEntryPair*> singleMethodFamilies; /* * Subclasses of this class, including this class itself. * * For interfaces, this is the list of instantiable classes that * implement this interface. * * For traits, this is the list of classes that use the trait where * the trait wasn't flattened into the class (including the trait * itself). * * Note, unlike baseList, the order of the elements in this vector * is unspecified. */ CompactVector<ClassInfo*> subclassList; /* * A vector of ClassInfo that encodes the inheritance hierarchy, * unless this ClassInfo represents an interface. * * This is the list of base classes for this class in inheritance * order. */ CompactVector<ClassInfo*> baseList; /* * Property types for public static properties, declared on this exact class * (i.e. not flattened in the hierarchy). * * These maps always have an entry for each public static property declared * in this class, so it can also be used to check if this class declares a * public static property of a given name. * * Note: the effective type we can assume a given static property may hold is * not just the value in these maps. */ hphp_hash_map<SString,PublicSPropEntry> publicStaticProps; struct PreResolveState { hphp_fast_map<SString, std::pair<php::Prop, const ClassInfo*>> pbuildNoTrait; hphp_fast_map<SString, std::pair<php::Prop, const ClassInfo*>> pbuildTrait; hphp_fast_set<SString> constsFromTraits; }; std::unique_ptr<PreResolveState> preResolveState; /* * Flags to track if this class is mocked, or if any of its dervied classes * are mocked. */ bool isMocked{false}; bool isDerivedMocked{false}; /* * Track if this class has a property which might redeclare a property in a * parent class with an inequivalent type-hint. */ bool hasBadRedeclareProp{true}; /* * Track if this class has any properties with initial values that might * violate their type-hints. */ bool hasBadInitialPropValues{true}; /* * Track if this class has any const props (including inherited ones). */ bool hasConstProp{false}; /* * Track if any derived classes (including this one) have any const props. */ bool derivedHasConstProp{false}; const php::Class* phpType() const { return cls; } /* * Return true if this is derived from o. */ bool derivedFrom(const ClassInfo& o) const { if (this == &o) return true; // If o is an interface, see if this declared it. if (o.cls->attrs & AttrInterface) return implInterfaces.count(o.cls->name); // Otherwise check for direct inheritance. if (baseList.size() >= o.baseList.size()) { return baseList[o.baseList.size() - 1] == &o; } return false; } /* * Flags about the existence of various magic methods, or whether * any derived classes may have those methods. The non-derived * flags imply the derived flags, even if the class is final, so you * don't need to check both in those situations. */ struct MagicFnInfo { bool thisHas{false}; bool derivedHas{false}; }; MagicFnInfo magicBool; }; struct MagicMapInfo { StaticString name; ClassInfo::MagicFnInfo ClassInfo::*pmem; }; const MagicMapInfo magicMethods[] { { StaticString{"__toBoolean"}, &ClassInfo::magicBool }, }; ////////////////////////////////////////////////////////////////////// namespace res { Class::Class(Either<SString,ClassInfo*> val) : val(val) {} // Class type operations here are very conservative for now. bool Class::same(const Class& o) const { return val == o.val; } template <bool returnTrueOnMaybe> bool Class::subtypeOfImpl(const Class& o) const { auto s1 = val.left(); auto s2 = o.val.left(); if (s1 || s2) return returnTrueOnMaybe || s1 == s2; auto c1 = val.right(); auto c2 = o.val.right(); return c1->derivedFrom(*c2); } bool Class::mustBeSubtypeOf(const Class& o) const { return subtypeOfImpl<false>(o); } bool Class::maybeSubtypeOf(const Class& o) const { return subtypeOfImpl<true>(o); } bool Class::couldBe(const Class& o) const { if (same(o)) return true; // If either types are not unique return true if (val.left() || o.val.left()) return true; auto c1 = val.right(); auto c2 = o.val.right(); // if one or the other is an interface return true for now. // TODO(#3621433): better interface stuff if (c1->cls->attrs & AttrInterface || c2->cls->attrs & AttrInterface) { return true; } // Both types are unique classes so they "could be" if they are in an // inheritance relationship if (c1->baseList.size() >= c2->baseList.size()) { return c1->baseList[c2->baseList.size() - 1] == c2; } else { return c2->baseList[c1->baseList.size() - 1] == c1; } } SString Class::name() const { return val.match( [] (SString s) { return s; }, [] (ClassInfo* ci) { return ci->cls->name.get(); } ); } bool Class::couldBeInterface() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return cinfo->cls->attrs & AttrInterface; } ); } bool Class::mustBeInterface() const { return val.match( [] (SString) { return false; }, [] (ClassInfo* cinfo) { return cinfo->cls->attrs & AttrInterface; } ); } bool Class::couldBeOverriden() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return !(cinfo->cls->attrs & AttrNoOverride); } ); } bool Class::couldHaveMagicBool() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return cinfo->magicBool.derivedHas; } ); } bool Class::couldHaveMockedDerivedClass() const { return val.match( [] (SString) { return true;}, [] (ClassInfo* cinfo) { return cinfo->isDerivedMocked; } ); } bool Class::couldBeMocked() const { return val.match( [] (SString) { return true;}, [] (ClassInfo* cinfo) { return cinfo->isMocked; } ); } bool Class::couldHaveReifiedGenerics() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return cinfo->cls->hasReifiedGenerics; } ); } bool Class::mightCareAboutDynConstructs() const { if (RuntimeOption::EvalForbidDynamicConstructs > 0) { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return !(cinfo->cls->attrs & AttrDynamicallyConstructible); } ); } return false; } bool Class::couldHaveConstProp() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return cinfo->hasConstProp; } ); } bool Class::derivedCouldHaveConstProp() const { return val.match( [] (SString) { return true; }, [] (ClassInfo* cinfo) { return cinfo->derivedHasConstProp; } ); } Optional<Class> Class::commonAncestor(const Class& o) const { if (val.left() || o.val.left()) return std::nullopt; auto const c1 = val.right(); auto const c2 = o.val.right(); if (c1 == c2) return res::Class { c1 }; // Walk the arrays of base classes until they match. For common ancestors // to exist they must be on both sides of the baseList at the same positions ClassInfo* ancestor = nullptr; auto it1 = c1->baseList.begin(); auto it2 = c2->baseList.begin(); while (it1 != c1->baseList.end() && it2 != c2->baseList.end()) { if (*it1 != *it2) break; ancestor = *it1; ++it1; ++it2; } if (ancestor == nullptr) { return std::nullopt; } return res::Class { ancestor }; } Optional<res::Class> Class::parent() const { if (!val.right()) return std::nullopt; auto parent = val.right()->parent; if (!parent) return std::nullopt; return res::Class { parent }; } const php::Class* Class::cls() const { return val.right() ? val.right()->cls : nullptr; } void Class::forEachSubclass(const std::function<void(const php::Class*)>& f) const { auto const cinfo = val.right(); assertx(cinfo); for (auto const& s : cinfo->subclassList) f(s->cls); } std::string show(const Class& c) { return c.val.match( [] (SString s) -> std::string { return s->data(); }, [] (ClassInfo* cinfo) { return folly::sformat("{}*", cinfo->cls->name); } ); } Func::Func(Rep val) : val(val) {} SString Func::name() const { return match<SString>( val, [&] (FuncName s) { return s.name; }, [&] (MethodName s) { return s.name; }, [&] (FuncInfo* fi) { return fi->func->name; }, [&] (const MethTabEntryPair* mte) { return mte->first; }, [&] (FuncFamily* fa) -> SString { auto const name = fa->possibleFuncs().front()->first; if (debug) { for (DEBUG_ONLY auto const f : fa->possibleFuncs()) { assertx(f->first->isame(name)); } } return name; } ); } const php::Func* Func::exactFunc() const { using Ret = const php::Func*; return match<Ret>( val, [&](FuncName) { return Ret{}; }, [&](MethodName) { return Ret{}; }, [&](FuncInfo* fi) { return fi->func; }, [&](const MethTabEntryPair* mte) { return mte->second.func; }, [&](FuncFamily* /*fa*/) { return Ret{}; } ); } bool Func::isFoldable() const { return match<bool>( val, [&](FuncName) { return false; }, [&](MethodName) { return false; }, [&](FuncInfo* fi) { return fi->func->attrs & AttrIsFoldable; }, [&](const MethTabEntryPair* mte) { return mte->second.func->attrs & AttrIsFoldable; }, [&](FuncFamily* fa) { return false; } ); } bool Func::couldHaveReifiedGenerics() const { return match<bool>( val, [&](FuncName s) { return true; }, [&](MethodName) { return true; }, [&](FuncInfo* fi) { return fi->func->isReified; }, [&](const MethTabEntryPair* mte) { return mte->second.func->isReified; }, [&](FuncFamily* fa) { for (auto const pf : fa->possibleFuncs()) { if (pf->second.func->isReified) return true; } return false; } ); } bool Func::mightCareAboutDynCalls() const { if (RuntimeOption::EvalNoticeOnBuiltinDynamicCalls && mightBeBuiltin()) { return true; } auto const mightCareAboutFuncs = RuntimeOption::EvalForbidDynamicCallsToFunc > 0; auto const mightCareAboutInstMeth = RuntimeOption::EvalForbidDynamicCallsToInstMeth > 0; auto const mightCareAboutClsMeth = RuntimeOption::EvalForbidDynamicCallsToClsMeth > 0; return match<bool>( val, [&](FuncName) { return mightCareAboutFuncs; }, [&](MethodName) { return mightCareAboutClsMeth || mightCareAboutInstMeth; }, [&](FuncInfo* fi) { return dyn_call_error_level(fi->func) > 0; }, [&](const MethTabEntryPair* mte) { return dyn_call_error_level(mte->second.func) > 0; }, [&](FuncFamily* fa) { for (auto const pf : fa->possibleFuncs()) { if (dyn_call_error_level(pf->second.func) > 0) return true; } return false; } ); } bool Func::mightBeBuiltin() const { return match<bool>( val, // Builtins are always uniquely resolvable unless renaming is // involved. [&](FuncName s) { return s.renamable; }, [&](MethodName) { return true; }, [&](FuncInfo* fi) { return fi->func->attrs & AttrBuiltin; }, [&](const MethTabEntryPair* mte) { return mte->second.func->attrs & AttrBuiltin; }, [&](FuncFamily* fa) { for (auto const pf : fa->possibleFuncs()) { if (pf->second.func->attrs & AttrBuiltin) return true; } return false; } ); } namespace { uint32_t numNVArgs(const php::Func& f) { uint32_t cnt = f.params.size(); return cnt && f.params[cnt - 1].isVariadic ? cnt - 1 : cnt; } } uint32_t Func::minNonVariadicParams() const { return match<uint32_t>( val, [&] (FuncName) { return 0; }, [&] (MethodName) { return 0; }, [&] (FuncInfo* fi) { return numNVArgs(*fi->func); }, [&] (const MethTabEntryPair* mte) { return numNVArgs(*mte->second.func); }, [&] (FuncFamily* fa) { auto c = std::numeric_limits<uint32_t>::max(); for (auto const pf : fa->possibleFuncs()) { c = std::min(c, numNVArgs(*pf->second.func)); } return c; } ); } uint32_t Func::maxNonVariadicParams() const { return match<uint32_t>( val, [&] (FuncName) { return std::numeric_limits<uint32_t>::max(); }, [&] (MethodName) { return std::numeric_limits<uint32_t>::max(); }, [&] (FuncInfo* fi) { return numNVArgs(*fi->func); }, [&] (const MethTabEntryPair* mte) { return numNVArgs(*mte->second.func); }, [&] (FuncFamily* fa) { uint32_t c = 0; for (auto const pf : fa->possibleFuncs()) { c = std::max(c, numNVArgs(*pf->second.func)); } return c; } ); } const RuntimeCoeffects* Func::requiredCoeffects() const { auto const get = [&](const php::Func* f) { return &f->requiredCoeffects; }; return match<const RuntimeCoeffects*>( val, [&] (FuncName) { return nullptr; }, [&] (MethodName) { return nullptr; }, [&] (FuncInfo* fi) { return get(fi->func); }, [&] (const MethTabEntryPair* mte) { return get(mte->second.func); }, [&] (FuncFamily* fa) -> const RuntimeCoeffects* { const RuntimeCoeffects* result = nullptr; for (auto const pf : fa->possibleFuncs()) { if (!result) { result = get(pf->second.func); continue; } auto const cur = get(pf->second.func); assertx(cur); if (result->value() != cur->value()) return nullptr; } return result; } ); } const CompactVector<CoeffectRule>* Func::coeffectRules() const { auto const get = [&](const php::Func* f) { return &f->coeffectRules; }; return match<const CompactVector<CoeffectRule>*>( val, [&] (FuncName) { return nullptr; }, [&] (MethodName) { return nullptr; }, [&] (FuncInfo* fi) { return get(fi->func); }, [&] (const MethTabEntryPair* mte) { return get(mte->second.func); }, [&] (FuncFamily* fa) -> const CompactVector<CoeffectRule>* { const CompactVector<CoeffectRule>* result = nullptr; for (auto const pf : fa->possibleFuncs()) { if (!result) { result = get(pf->second.func); continue; } auto const cur = get(pf->second.func); if (!std::is_permutation(cur->begin(), cur->end(), result->begin(), result->end())) { return nullptr; } } return result; } ); } std::string show(const Func& f) { auto ret = f.name()->toCppString(); match<void>( f.val, [&](Func::FuncName s) { if (s.renamable) ret += '?'; }, [&](Func::MethodName) {}, [&](FuncInfo*) { ret += "*"; }, [&](const MethTabEntryPair*) { ret += "*"; }, [&](FuncFamily*) { ret += "+"; } ); return ret; } } ////////////////////////////////////////////////////////////////////// using IfaceSlotMap = hphp_hash_map<const php::Class*, Slot>; // Inferred class constant type from a 86cinit. struct ClsConstInfo { Type type; size_t refinements = 0; }; struct Index::IndexData { explicit IndexData(Index* index) : m_index{index} {} IndexData(const IndexData&) = delete; IndexData& operator=(const IndexData&) = delete; ~IndexData() { if (compute_iface_vtables.joinable()) { compute_iface_vtables.join(); } } Index* m_index; bool frozen{false}; bool ever_frozen{false}; std::unique_ptr<ArrayTypeTable::Builder> arrTableBuilder; ISStringToOneT<const php::Class*> classes; SStringToMany<const php::Func> methods; SStringToOneFastT<uint64_t> method_inout_params_by_name; // 0th index is the return value // 1st index is whether function is marked readonly SStringToOneFastT<uint64_t> method_readonly_by_name; ISStringToOneT<const php::Func*> funcs; ISStringToOneT<const php::TypeAlias*> typeAliases; ISStringToOneT<const php::Class*> enums; SStringToOneT<const php::Constant*> constants; // Map from each class to all the closures that are allocated in // functions of that class. hphp_hash_map< const php::Class*, CompactVector<const php::Class*> > classClosureMap; hphp_hash_map< const php::Class*, hphp_fast_set<const php::Func*> > classExtraMethodMap; /* * Map from each class name to ClassInfo objects if one exists. * * It may not exists if we would fatal when defining the class. That could * happen for if the inheritance is bad or __Sealed or other things. */ ISStringToOneT<ClassInfo*> classInfo; /* * All the ClassInfos, sorted topologically (ie all the parents, * interfaces and traits used by the ClassInfo at index K will have * indices less than K). This mostly drops out of the way ClassInfos * are created; it would be hard to create the ClassInfos for the * php::Class X (or even know how many to create) without knowing * all the ClassInfos that were created for X's dependencies. */ std::vector<std::unique_ptr<ClassInfo>> allClassInfos; std::vector<FuncInfo> funcInfo; // Private instance and static property types are stored separately // from ClassInfo, because you don't need to resolve a class to get // at them. hphp_hash_map< const php::Class*, PropState > privatePropInfo; hphp_hash_map< const php::Class*, PropState > privateStaticPropInfo; /* * Public static property information: */ // If this is true, we've seen mutations to public static // properties. Once this is true, it's no longer legal to report a // pessimistic static property set (unknown class and // property). Doing so is a monotonicity violation. bool seenPublicSPropMutations{false}; // The set of gathered public static property mutations for each function. The // inferred types for the public static properties is the union of all these // mutations. If a function is not analyzed in a particular analysis round, // its mutations are left unchanged from the previous round. folly_concurrent_hash_map_simd< const php::Func*, PublicSPropMutations, pointer_hash<const php::Func>> publicSPropMutations; // All FuncFamilies. These are stored globally so we can avoid // generating duplicates. struct FuncFamilyPtrHasher { using is_transparent = void; size_t operator()(const std::unique_ptr<FuncFamily>& ff) const { return PFuncVecHasher{}(ff->possibleFuncs()); } size_t operator()(const FuncFamily::PFuncVec& pf) const { return PFuncVecHasher{}(pf); } }; struct FuncFamilyPtrEquals { using is_transparent = void; bool operator()(const std::unique_ptr<FuncFamily>& a, const std::unique_ptr<FuncFamily>& b) const { return a->possibleFuncs() == b->possibleFuncs(); } bool operator()(const FuncFamily::PFuncVec& pf, const std::unique_ptr<FuncFamily>& ff) const { return pf == ff->possibleFuncs(); } }; folly_concurrent_hash_map_simd< std::unique_ptr<FuncFamily>, bool, FuncFamilyPtrHasher, FuncFamilyPtrEquals > funcFamilies; /* * Map from interfaces to their assigned vtable slots, computed in * compute_iface_vtables(). */ IfaceSlotMap ifaceSlotMap; hphp_hash_map< const php::Class*, CompactVector<Type> > closureUseVars; struct ClsConstTypesHasher { bool operator()(const std::pair<const php::Class*, SString>& k) const { return hash_int64_pair(uintptr_t(k.first), k.second->hash()); } }; struct ClsConstTypesEquals { bool operator()(const std::pair<const php::Class*, SString>& a, const std::pair<const php::Class*, SString>& b) const { return a.first == b.first && a.second->same(b.second); } }; folly_concurrent_hash_map_simd< std::pair<const php::Class*, SString>, ClsConstInfo, ClsConstTypesHasher, ClsConstTypesEquals > clsConstTypes; // Cache for lookup_class_constant folly_concurrent_hash_map_simd< std::pair<const php::Class*, SString>, ClsConstLookupResult<>, ClsConstTypesHasher, ClsConstTypesEquals > clsConstLookupCache; bool useClassDependencies{}; DepMap dependencyMap; /* * If a function is effect-free when called with a particular set of * literal arguments, and produces a literal result, there will be * an entry here representing the type. * * The map isn't just an optimization; we can't call * analyze_func_inline during the optimization phase, because the * bytecode could be modified while we do so. */ ContextRetTyMap foldableReturnTypeMap; /* * Call-context sensitive return types are cached here. This is not * an optimization. * * The reason we need to retain this information about the * calling-context-sensitive return types is that once the Index is * frozen (during the final optimization pass), calls to * lookup_return_type with a CallContext can't look at the bytecode * bodies of functions other than the calling function. So we need * to know what we determined the last time we were alloewd to do * that so we can return it again. */ ContextRetTyMap contextualReturnTypes{}; std::thread compute_iface_vtables; }; ////////////////////////////////////////////////////////////////////// namespace { ////////////////////////////////////////////////////////////////////// using IndexData = Index::IndexData; std::mutex closure_use_vars_mutex; std::mutex private_propstate_mutex; DependencyContext make_dep(const php::Func* func) { return DependencyContext{DependencyContextType::Func, func}; } DependencyContext make_dep(const php::Class* cls) { return DependencyContext{DependencyContextType::Class, cls}; } DependencyContext make_dep(const php::Prop* prop) { return DependencyContext{DependencyContextType::Prop, prop}; } DependencyContext make_dep(const FuncFamily* family) { return DependencyContext{DependencyContextType::FuncFamily, family}; } DependencyContext dep_context(IndexData& data, const Context& ctx) { if (!ctx.cls || !data.useClassDependencies) return make_dep(ctx.func); auto const cls = ctx.cls->closureContextCls ? ctx.cls->closureContextCls : ctx.cls; if (is_used_trait(*cls)) return make_dep(ctx.func); return make_dep(cls); } template <typename T> void add_dependency(IndexData& data, T src, const Context& dst, Dep newMask) { if (data.frozen) return; auto d = dep_context(data, dst); DepMap::accessor acc; data.dependencyMap.insert(acc, make_dep(src)); auto& current = acc->second[d]; current = current | newMask; } std::mutex func_info_mutex; FuncInfo* create_func_info(IndexData& data, const php::Func* f) { auto fi = &data.funcInfo[f->idx]; if (UNLIKELY(fi->func == nullptr)) { if (f->nativeInfo) { std::lock_guard<std::mutex> g{func_info_mutex}; if (fi->func) { assertx(fi->func == f); return fi; } // We'd infer this anyway when we look at the bytecode body // (NativeImpl) for the HNI function, but just initializing it // here saves on whole-program iterations. fi->returnTy = native_function_return_type(f); } fi->func = f; } assertx(fi->func == f); return fi; } FuncInfo* func_info(IndexData& data, const php::Func* f) { auto const fi = &data.funcInfo[f->idx]; return fi; } template <typename T> void find_deps(IndexData& data, T src, Dep mask, DependencyContextSet& deps) { auto const srcDep = make_dep(src); { DepMap::const_accessor acc; if (data.dependencyMap.find(acc, srcDep)) { for (auto const& kv : acc->second) { if (has_dep(kv.second, mask)) deps.insert(kv.first); } } } // If this is a Func dep, we need to also check if any FuncFamily // dependencies need to be added. if (srcDep.tag() != DependencyContextType::Func) return; auto const fi = func_info(data, static_cast<const php::Func*>(srcDep.ptr())); if (!fi->func) return; // Add any associated FuncFamilies for (auto const ff : fi->families) { DepMap::const_accessor acc; if (data.dependencyMap.find(acc, make_dep(ff))) { for (auto const& kv : acc->second) { if (has_dep(kv.second, mask)) deps.insert(kv.first); } } } } struct TraitMethod { using class_type = const ClassInfo*; using method_type = const php::Func*; using origin_type = const php::Class*; TraitMethod(class_type trait_, method_type method_, Attr modifiers_) : trait(trait_) , method(method_) , modifiers(modifiers_) {} class_type trait; method_type method; Attr modifiers; }; struct TMIOps { using string_type = LSString; using class_type = TraitMethod::class_type; using method_type = TraitMethod::method_type; using origin_type = TraitMethod::origin_type; struct TMIException : std::exception { explicit TMIException(std::string msg) : msg(msg) {} const char* what() const noexcept override { return msg.c_str(); } private: std::string msg; }; // Return the name for the trait class. static const string_type clsName(class_type traitCls) { return traitCls->cls->name; } // Return the name for the trait method. static const string_type methName(method_type meth) { return meth->name; } // Return the name of the trait where the method was originally defined static origin_type originalClass(method_type meth) { return meth->originalClass; } // Is-a methods. static bool isTrait(class_type traitCls) { return traitCls->cls->attrs & AttrTrait; } static bool isAbstract(Attr modifiers) { return modifiers & AttrAbstract; } // Whether to exclude methods with name `methName' when adding. static bool exclude(string_type methName) { return Func::isSpecial(methName); } // TraitMethod constructor. static TraitMethod traitMethod(class_type traitCls, method_type traitMeth, const PreClass::TraitAliasRule& rule) { return TraitMethod { traitCls, traitMeth, rule.modifiers() }; } // Register a trait alias once the trait class is found. static void addTraitAlias(const ClassInfo* /*cls*/, const PreClass::TraitAliasRule& /*rule*/, class_type /*traitCls*/) { // purely a runtime thing... nothing to do } // Trait class/method finders. static class_type findSingleTraitWithMethod(class_type cls, string_type origMethName) { class_type traitCls = nullptr; for (auto const t : cls->usedTraits) { // Note: m_methods includes methods from parents/traits recursively. if (t->methods.count(origMethName)) { if (traitCls != nullptr) { return nullptr; } traitCls = t; } } return traitCls; } static class_type findTraitClass(class_type cls, string_type traitName) { for (auto const t : cls->usedTraits) { if (traitName->isame(t->cls->name)) return t; } return nullptr; } static method_type findTraitMethod(class_type traitCls, string_type origMethName) { auto it = traitCls->methods.find(origMethName); if (it == traitCls->methods.end()) return nullptr; return it->second.func; } // Errors. static void errorUnknownMethod(string_type methName) { throw TMIException(folly::sformat("Unknown method '{}'", methName)); } static void errorUnknownTrait(string_type traitName) { throw TMIException(folly::sformat("Unknown trait '{}'", traitName)); } static void errorDuplicateMethod(class_type cls, string_type methName, const std::vector<const StringData*>&) { auto const& m = cls->cls->methods; if (std::find_if(m.begin(), m.end(), [&] (auto const& f) { return f->name->isame(methName); }) != m.end()) { // the duplicate methods will be overridden by the class method. return; } throw TMIException(folly::sformat("DuplicateMethod: {}", methName)); } static void errorInconsistentInsteadOf(class_type cls, string_type methName) { throw TMIException(folly::sformat("InconsistentInsteadOf: {} {}", methName, cls->cls->name)); } static void errorMultiplyExcluded(string_type traitName, string_type methName) { throw TMIException(folly::sformat("MultiplyExcluded: {}::{}", traitName, methName)); } }; using TMIData = TraitMethodImportData<TraitMethod, TMIOps>; struct ClsPreResolveUpdates { TinyVector<std::unique_ptr<ClassInfo>> newInfos; TinyVector<ClassInfo*> updateDeps; struct CnsHash { size_t operator()(const ClassInfo::ConstIndex& cns) const { return hash_int64_pair((uintptr_t)cns.cls, cns.idx); } }; struct CnsEquals { bool operator()(const ClassInfo::ConstIndex& cns1, const ClassInfo::ConstIndex& cns2) const { return cns1.cls == cns2.cls && cns1.idx == cns2.idx; } }; hphp_hash_map< const php::Class*, hphp_fast_set<const php::Func*> > extraMethods; hphp_hash_map< const php::Class*, CompactVector<const php::Class*> > closures; CompactVector<const php::Class*> newClosures; CompactVector< std::tuple<std::unique_ptr<php::Class>, php::Unit*, uint32_t> > newClasses; uint32_t nextClassId = 0; }; // Keep track of order of closure creation to make the logic more // deterministic. struct ClonedClosureMap { using Tuple = std::tuple<const php::Class*, std::unique_ptr<php::Class>, uint32_t>; bool empty() const { return ordered.empty(); } CompactVector<Tuple>::iterator find(const php::Class* cls) { auto const it = map.find(cls); if (it == map.end()) return ordered.end(); auto const idx = it->second; assertx(idx < ordered.size()); assertx(std::get<0>(ordered[idx]) == cls); return ordered.begin() + idx; } bool emplace(const php::Class* cls, std::unique_ptr<php::Class> clo, uint32_t id) { auto const inserted = map.emplace(cls, ordered.size()).second; if (!inserted) return false; ordered.emplace_back(cls, std::move(clo), id); return true; } CompactVector<Tuple>::iterator begin() { return ordered.begin(); } CompactVector<Tuple>::iterator end() { return ordered.end(); } private: hphp_fast_map<const php::Class*, size_t> map; CompactVector<Tuple> ordered; }; std::unique_ptr<php::Func> clone_meth(php::Unit* unit, php::Class* newContext, const php::Func* origMeth, SString name, Attr attrs, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures); /* * Make a flattened table of the constants on this class. */ bool build_class_constants(const php::Program* program, ClassInfo* cinfo, ClsPreResolveUpdates& updates) { if (cinfo->parent) cinfo->clsConstants = cinfo->parent->clsConstants; auto const add = [&] (const ClassInfo::ConstIndex& cns, bool fromTrait) { auto insert = cinfo->clsConstants.emplace(cns->name, cns); if (insert.second) { if (fromTrait) { cinfo->preResolveState->constsFromTraits.emplace(cns->name); } return true; } auto& existing = insert.first->second; // Same constant (from an interface via two different paths) is ok if (existing->cls == cns->cls) return true; if (existing->kind != cns->kind) { ITRACE( 2, "build_class_constants failed for `{}' because `{}' was defined by " "`{}' as a {} and by `{}' as a {}\n", cinfo->cls->name, cns->name, cns->cls->name, ConstModifiers::show(cns->kind), existing->cls->name, ConstModifiers::show(existing->kind) ); return false; } // Ignore abstract constants if (cns->isAbstract && !cns->val) return true; // if the existing constant in the map is concrete, then don't overwrite it with an incoming // abstract constant's default if (!existing->isAbstract && cns->isAbstract) { return true; } if (existing->val) { // A constant from a declared interface collides with a constant // (Excluding constants from interfaces a trait implements) // Need this check otherwise constants from traits that conflict with // declared interfaces will silently lose and not conflict in the runtime // Type and Context constants can be overriden. if (cns->kind == ConstModifiers::Kind::Value && !existing->isAbstract && existing->cls->attrs & AttrInterface && !(cns->cls->attrs & AttrInterface && fromTrait)) { for (auto const& interface : cinfo->declInterfaces) { if (existing->cls == interface->cls) { ITRACE( 2, "build_class_constants failed for `{}' because " "`{}' was defined by both `{}' and `{}'\n", cinfo->cls->name, cns->name, cns->cls->name, existing->cls->name ); return false; } } } if (!RO::EvalTraitConstantInterfaceBehavior) { // Constants from traits silently lose if (fromTrait) return true; } if ((cns->cls->attrs & AttrInterface || (RO::EvalTraitConstantInterfaceBehavior && (cns->cls->attrs & AttrTrait))) && existing->isAbstract) { // because existing has val, this covers the case where it is abstract with default // allow incoming to win } else { // A constant from an interface or from an included enum collides // with an existing constant. if (cns->cls->attrs & (AttrInterface | AttrEnum | AttrEnumClass) || (RO::EvalTraitConstantInterfaceBehavior && (cns->cls->attrs & AttrTrait))) { ITRACE( 2, "build_class_constants failed for `{}' because " "`{}' was defined by both `{}' and `{}'\n", cinfo->cls->name, cns->name, cns->cls->name, existing->cls->name ); return false; } } } existing = cns; if (fromTrait) { cinfo->preResolveState->constsFromTraits.emplace(cns->name); } else { cinfo->preResolveState->constsFromTraits.erase(cns->name); } return true; }; for (auto const iface : cinfo->declInterfaces) { for (auto const& cns : iface->clsConstants) { if (!add(cns.second, iface->preResolveState->constsFromTraits.count(cns.first))) { return false; } } } auto const addShallowConstants = [&]() { for (uint32_t idx = 0; idx < cinfo->cls->constants.size(); ++idx) { auto const cns = ClassInfo::ConstIndex { cinfo->cls, idx }; if (!add(cns, false)) return false; } return true; }; auto const addTraitConstants = [&]() { for (auto const trait : cinfo->usedTraits) { for (auto const& cns : trait->clsConstants) { if (!add(cns.second, true)) return false; } } return true; }; if (RO::EvalTraitConstantInterfaceBehavior) { // trait constants must be inserted before constants shallowly declared on the class // to match the interface semantics if (!addTraitConstants()) return false; if (!addShallowConstants()) return false; } else { if (!addShallowConstants()) return false; if (!addTraitConstants()) return false; } for (auto const ienum : cinfo->includedEnums) { for (auto const& cns : ienum->clsConstants) { if (!add(cns.second, false)) return false; } } auto const addTraitConst = [&] (const php::Const& c) { /* * Only copy in constants that win. Otherwise, in the runtime, if * we have a constant from an interface implemented by a trait * that wins over this fromTrait constant, we won't know which * trait it came from, and therefore won't know which constant * should win. Dropping losing constants here works because if * they fatal with constants in declared interfaces, we catch that * above. */ auto const& existing = cinfo->clsConstants.find(c.name); if (existing->second->cls == c.cls) { cinfo->traitConsts.emplace_back(c); cinfo->traitConsts.back().isFromTrait = true; } }; for (auto const t : cinfo->usedTraits) { for (auto const& c : t->cls->constants) addTraitConst(c); for (auto const& c : t->traitConsts) addTraitConst(c); } if (!(cinfo->cls->attrs & (AttrAbstract | AttrInterface | AttrTrait))) { // If we are in a concrete class, concretize the defaults of inherited abstract constants auto const cls = const_cast<php::Class*>(cinfo->cls); for (auto t : cinfo->clsConstants) { auto const& cns = *t.second; if (cns.isAbstract && cns.val) { if (cns.val.value().m_type == KindOfUninit) { // We need to copy the constant's initializer into this class auto const& cns_86cinit = cns.cls->methods.back().get(); assertx(cns_86cinit->name == s_86cinit.get()); std::unique_ptr<php::Func> empty; auto& current_86cinit = [&] () -> std::unique_ptr<php::Func>& { for (auto& m : cls->methods) { if (m->name == cns_86cinit->name) return m; } return empty; }(); if (!current_86cinit) { ClonedClosureMap clonedClosures; auto& nextFuncId = const_cast<php::Program*>(program)->nextFuncId; current_86cinit = clone_meth(cls->unit, cls, cns_86cinit, cns_86cinit->name, cns_86cinit->attrs, nextFuncId, updates, clonedClosures); assertx(clonedClosures.empty()); DEBUG_ONLY auto res = cinfo->methods.emplace( current_86cinit->name, MethTabEntry { current_86cinit.get(), current_86cinit->attrs, false, true } ); assertx(res.second); cls->methods.push_back(std::move(current_86cinit)); } else { append_86cinit(current_86cinit.get(), *cns_86cinit); } } auto concretizedCns = cns; concretizedCns.cls = cls; concretizedCns.isAbstract = false; // this is similar to trait constant flattening cls->constants.push_back(concretizedCns); cinfo->clsConstants[concretizedCns.name].cls = cls; cinfo->clsConstants[concretizedCns.name].idx = cls->constants.size() - 1; } } } return true; } bool build_class_impl_interfaces(ClassInfo* cinfo) { if (cinfo->parent) cinfo->implInterfaces = cinfo->parent->implInterfaces; for (auto const ienum : cinfo->includedEnums) { cinfo->implInterfaces.insert( ienum->implInterfaces.begin(), ienum->implInterfaces.end() ); } for (auto const iface : cinfo->declInterfaces) { cinfo->implInterfaces.insert( iface->implInterfaces.begin(), iface->implInterfaces.end() ); } for (auto const trait : cinfo->usedTraits) { cinfo->implInterfaces.insert( trait->implInterfaces.begin(), trait->implInterfaces.end() ); } if (cinfo->cls->attrs & AttrInterface) { cinfo->implInterfaces.emplace(cinfo->cls->name, cinfo); } return true; } bool build_class_properties(ClassInfo* cinfo) { if (cinfo->parent) { cinfo->preResolveState->pbuildNoTrait = cinfo->parent->preResolveState->pbuildNoTrait; cinfo->preResolveState->pbuildTrait = cinfo->parent->preResolveState->pbuildNoTrait; } auto const add = [&] (auto& m, SString name, const php::Prop& p, const ClassInfo* cls, bool add) { auto res = m.emplace(name, std::make_pair(p, cls)); if (res.second) { if (add) cinfo->traitProps.emplace_back(p); return true; } auto const& prev = res.first->second.first; if (cinfo == res.first->second.second) { if ((prev.attrs ^ p.attrs) & (AttrStatic | AttrPublic | AttrProtected | AttrPrivate) || (!(p.attrs & AttrSystemInitialValue) && !(prev.attrs & AttrSystemInitialValue) && !Class::compatibleTraitPropInit(prev.val, p.val))) { ITRACE(2, "build_class_properties failed for `{}' because " "two declarations of `{}' at the same level had " "different attributes\n", cinfo->cls->name, p.name); return false; } return true; } if (!(prev.attrs & AttrPrivate)) { if ((prev.attrs ^ p.attrs) & AttrStatic) { ITRACE(2, "build_class_properties failed for `{}' because " "`{}' was defined both static and non-static\n", cinfo->cls->name, p.name); return false; } if (p.attrs & AttrPrivate) { ITRACE(2, "build_class_properties failed for `{}' because " "`{}' was re-declared private\n", cinfo->cls->name, p.name); return false; } if (p.attrs & AttrProtected && !(prev.attrs & AttrProtected)) { ITRACE(2, "build_class_properties failed for `{}' because " "`{}' was redeclared protected from public\n", cinfo->cls->name, p.name); return false; } } if (add) cinfo->traitProps.emplace_back(p); res.first->second = std::make_pair(p, cls); return true; }; auto const merge = [&] (const ClassInfo::PreResolveState& src) { for (auto const& p : src.pbuildNoTrait) { if (!add(cinfo->preResolveState->pbuildNoTrait, p.first, p.second.first, p.second.second, false)) { return false; } } for (auto const& p : src.pbuildTrait) { if (!add(cinfo->preResolveState->pbuildTrait, p.first, p.second.first, p.second.second, false)) { return false; } } return true; }; for (auto const iface : cinfo->declInterfaces) { if (!merge(*iface->preResolveState)) return false; } for (auto const trait : cinfo->usedTraits) { if (!merge(*trait->preResolveState)) return false; } for (auto const ienum : cinfo->includedEnums) { if (!merge(*ienum->preResolveState)) return false; } if (!(cinfo->cls->attrs & AttrInterface)) { for (auto const& p : cinfo->cls->properties) { if (!add(cinfo->preResolveState->pbuildNoTrait, p.name, p, cinfo, false)) { return false; } } // There's no need to do this work if traits have been flattened // already, or if the top level class has no traits. In those // cases, we might be able to rule out some ClassInfo // instantiations, but it doesn't seem worth it. if (!(cinfo->cls->attrs & AttrNoExpandTrait)) { for (auto const trait : cinfo->usedTraits) { for (auto const& p : trait->cls->properties) { if (!add(cinfo->preResolveState->pbuildNoTrait, p.name, p, cinfo, true)) { return false; } } for (auto const& p : trait->traitProps) { if (!add(cinfo->preResolveState->pbuildNoTrait, p.name, p, cinfo, true)) { return false; } } } } } return true; } const StaticString s___EnableMethodTraitDiamond("__EnableMethodTraitDiamond"); bool enable_method_trait_diamond(const ClassInfo* cinfo) { assertx(cinfo->cls); auto const cls_attrs = cinfo->cls->userAttributes; return cls_attrs.find(s___EnableMethodTraitDiamond.get()) != cls_attrs.end(); } /* * Make a flattened table of the methods on this class. * * Duplicate method names override parent methods, unless the parent method * is final and the class is not a __MockClass, in which case this class * definitely would fatal if ever defined. * * Note: we're leaving non-overridden privates in their subclass method * table, here. This isn't currently "wrong", because calling it would be a * fatal, but note that resolve_method needs to be pretty careful about * privates and overriding in general. */ bool build_class_methods(const IndexData& index, ClassInfo* cinfo, ClsPreResolveUpdates& updates) { if (cinfo->cls->attrs & AttrInterface) return true; auto const methodOverride = [&] (auto& it, const php::Func* meth, Attr attrs, SString name) { if (it->second.func->attrs & AttrFinal) { if (!is_mock_class(cinfo->cls)) { ITRACE(2, "build_class_methods failed for `{}' because " "it tried to override final method `{}::{}'\n", cinfo->cls->name, it->second.func->cls->name, name); return false; } } ITRACE(9, " {}: overriding method {}::{} with {}::{}\n", cinfo->cls->name, it->second.func->cls->name, it->second.func->name, meth->cls->name, name); if (it->second.func->attrs & AttrPrivate) { it->second.hasPrivateAncestor = true; } it->second.func = meth; it->second.attrs = attrs; it->second.hasAncestor = true; it->second.topLevel = true; assertx(it->first == name); return true; }; // If there's a parent, start by copying its methods if (auto const rparent = cinfo->parent) { for (auto& mte : rparent->methods) { // don't inherit the 86* methods. if (HPHP::Func::isSpecial(mte.first)) continue; auto const res = cinfo->methods.emplace(mte.first, mte.second); assertx(res.second); res.first->second.topLevel = false; ITRACE(9, " {}: inheriting method {}::{}\n", cinfo->cls->name, rparent->cls->name, mte.first); continue; } } uint32_t idx = cinfo->methods.size(); // Now add our methods. for (auto& m : cinfo->cls->methods) { auto res = cinfo->methods.emplace( m->name, MethTabEntry { m.get(), m->attrs, false, true } ); if (res.second) { res.first->second.idx = idx++; ITRACE(9, " {}: adding method {}::{}\n", cinfo->cls->name, cinfo->cls->name, m->name); continue; } if (m->attrs & AttrTrait && m->attrs & AttrAbstract) { // abstract methods from traits never override anything. continue; } if (!methodOverride(res.first, m.get(), m->attrs, m->name)) return false; } // If our traits were previously flattened, we're done. if (cinfo->cls->attrs & AttrNoExpandTrait) return true; try { TMIData tmid; for (auto const t : cinfo->usedTraits) { std::vector<const MethTabEntryPair*> methods(t->methods.size()); for (auto& m : t->methods) { if (HPHP::Func::isSpecial(m.first)) continue; assertx(!methods[m.second.idx]); methods[m.second.idx] = mteFromElm(m); } for (auto const m : methods) { if (!m) continue; TraitMethod traitMethod { t, m->second.func, m->second.attrs }; tmid.add(traitMethod, m->first); } if (auto const it = index.classClosureMap.find(t->cls); it != index.classClosureMap.end()) { for (auto const& c : it->second) { auto const invoke = find_method(c, s_invoke.get()); assertx(invoke); updates.extraMethods[cinfo->cls].emplace(invoke); } } } for (auto const& precRule : cinfo->cls->traitPrecRules) { tmid.applyPrecRule(precRule, cinfo); } for (auto const& aliasRule : cinfo->cls->traitAliasRules) { tmid.applyAliasRule(aliasRule, cinfo); } auto traitMethods = tmid.finish(cinfo, enable_method_trait_diamond(cinfo)); // Import the methods. for (auto const& mdata : traitMethods) { auto const method = mdata.tm.method; auto attrs = mdata.tm.modifiers; if (attrs == AttrNone) { attrs = method->attrs; } else { Attr attrMask = (Attr)(AttrPublic | AttrProtected | AttrPrivate | AttrAbstract | AttrFinal); attrs = (Attr)((attrs & attrMask) | (method->attrs & ~attrMask)); } auto res = cinfo->methods.emplace( mdata.name, MethTabEntry { method, attrs, false, true } ); if (res.second) { res.first->second.idx = idx++; ITRACE(9, " {}: adding trait method {}::{} as {}\n", cinfo->cls->name, method->cls->name, method->name, mdata.name); } else { if (attrs & AttrAbstract) continue; if (res.first->second.func->cls == cinfo->cls) continue; if (!methodOverride(res.first, method, attrs, mdata.name)) { return false; } res.first->second.idx = idx++; } updates.extraMethods[cinfo->cls].emplace(method); } } catch (TMIOps::TMIException& ex) { ITRACE(2, "build_class_methods failed for `{}' importing traits: {}\n", cinfo->cls->name, ex.what()); return false; } return true; } const StaticString s___Sealed("__Sealed"); bool enforce_in_maybe_sealed_parent_whitelist( const ClassInfo* cls, const ClassInfo* parent) { // if our parent isn't sealed, then we're fine. if (!parent || !(parent->cls->attrs & AttrSealed)) return true; const UserAttributeMap& parent_attrs = parent->cls->userAttributes; assertx(parent_attrs.find(s___Sealed.get()) != parent_attrs.end()); const auto& parent_sealed_attr = parent_attrs.find(s___Sealed.get())->second; bool in_sealed_whitelist = false; IterateV(parent_sealed_attr.m_data.parr, [&in_sealed_whitelist, cls](TypedValue v) -> bool { if (v.m_data.pstr->same(cls->cls->name)) { in_sealed_whitelist = true; return true; } return false; }); return in_sealed_whitelist; } /* * This function return false if instantiating the cinfo would be a * fatal at runtime. */ bool build_cls_info(const php::Program* program, const IndexData& index, ClassInfo* cinfo, ClsPreResolveUpdates& updates) { if (!enforce_in_maybe_sealed_parent_whitelist(cinfo, cinfo->parent)) { return false; } for (auto const iface : cinfo->declInterfaces) { if (!enforce_in_maybe_sealed_parent_whitelist(cinfo, iface)) { return false; } } for (auto const trait : cinfo->usedTraits) { if (!enforce_in_maybe_sealed_parent_whitelist(cinfo, trait)) { return false; } } for (auto const ienum : cinfo->includedEnums) { if (!enforce_in_maybe_sealed_parent_whitelist(cinfo, ienum)) { return false; } } if (!build_class_constants(program, cinfo, updates)) return false; if (!build_class_impl_interfaces(cinfo)) return false; if (!build_class_properties(cinfo)) return false; if (!build_class_methods(index, cinfo, updates)) return false; return true; } ////////////////////////////////////////////////////////////////////// void add_system_constants_to_index(IndexData& index) { for (auto cnsPair : Native::getConstants()) { assertx(cnsPair.second.m_type != KindOfUninit || cnsPair.second.dynamic()); auto pc = new php::Constant { nullptr, cnsPair.first, cnsPair.second, AttrUnique | AttrPersistent }; add_symbol(index.constants, pc, "constant"); } } ////////////////////////////////////////////////////////////////////// Optional<uint32_t> func_num_inout(const php::Func* func) { if (!func->hasInOutArgs) return 0; uint32_t count = 0; for (auto& p : func->params) count += p.inout; return count; } template<typename PossibleFuncRange> Optional<uint32_t> num_inout_from_set(PossibleFuncRange range) { if (begin(range) == end(range)) return 0; struct FuncFind { using F = const php::Func*; static F get(std::pair<SString,F> p) { return p.second; } static F get(const MethTabEntryPair* mte) { return mte->second.func; } }; Optional<uint32_t> num; for (auto const& item : range) { auto const n = func_num_inout(FuncFind::get(item)); if (!n.has_value()) return std::nullopt; if (num.has_value() && n != num) return std::nullopt; num = n; } return num; } template<typename PossibleFuncRange, typename Fn> TriBool is_readonly_helper_from_set(PossibleFuncRange range, Fn fn) { if (begin(range) == end(range)) return TriBool::No; struct FuncFind { using F = const php::Func*; static F get(std::pair<SString,F> p) { return p.second; } static F get(const MethTabEntryPair* mte) { return mte->second.func; } }; Optional<bool> result = false; for (auto const& item : range) { auto const b = fn(FuncFind::get(item)); if (!result) { result = b; continue; } if (*result != b) return TriBool::Maybe; } return yesOrNo(*result); } template<typename PossibleFuncRange> TriBool is_readonly_return_from_set(PossibleFuncRange range) { return is_readonly_helper_from_set( range, [](const php::Func* f) { return f->isReadonlyReturn; }); } template<typename PossibleFuncRange> TriBool is_readonly_this_from_set(PossibleFuncRange range) { return is_readonly_helper_from_set( range, [](const php::Func* f) { return f->isReadonlyThis; }); } ////////////////////////////////////////////////////////////////////// struct ClassInfoData { // Map from name to types that directly use that name (as parent, // interface or trait). hphp_hash_map<SString, CompactVector<const php::Class*>, string_data_hash, string_data_isame> users; // Map from types to number of dependencies, used in // conjunction with users field above. hphp_hash_map<const php::Class*, uint32_t> depCounts; uint32_t cqFront{}; uint32_t cqBack{}; std::vector<const php::Class*> queue; bool hasPseudoCycles{}; }; // We want const qualifiers on various index data structures for php // object pointers, but during index creation time we need to // manipulate some of their attributes (changing the representation). // This little wrapper keeps the const_casting out of the main line of // code below. void attribute_setter(const Attr& attrs, bool set, Attr attr) { attrSetter(const_cast<Attr&>(attrs), set, attr); } void add_unit_to_index(IndexData& index, php::Unit& unit) { hphp_hash_map< const php::Class*, hphp_hash_set<const php::Class*> > closureMap; for (auto& c : unit.classes) { assertx(!(c->attrs & AttrNoOverride)); if (c->attrs & AttrEnum) { add_symbol(index.enums, c.get(), "enum"); } add_symbol(index.classes, c.get(), "class", index.typeAliases); for (auto& m : c->methods) { attribute_setter(m->attrs, false, AttrNoOverride); index.methods.insert({m->name, m.get()}); uint64_t inOutBits = 0, readonlyBits = 0, cur = 1; bool anyInOut = false, anyReadonly = false; for (auto& p : m->params) { if (p.inout) { inOutBits |= cur; anyInOut = true; } if (p.readonly) { readonlyBits |= cur; anyReadonly = true; } // It doesn't matter that we lose parameters beyond the 64th, // for those, we'll conservatively check everything anyway. cur <<= 1; } if (anyInOut) { // Multiple methods with the same name will be combined in the same // cell, thus we use |=. This only makes sense in WholeProgram mode // since we use this field to check that no functions has its n-th // parameter as inout, which requires global knowledge. index.method_inout_params_by_name[m->name] |= inOutBits; } if (anyReadonly || m->isReadonlyReturn || m->isReadonlyThis) { index.method_readonly_by_name[m->name] |= ((readonlyBits << 2) | ((m->isReadonlyThis ? 1 : 0) << 1) | (m->isReadonlyReturn ? 1 : 0)); } } if (c->closureContextCls) { closureMap[c->closureContextCls].insert(c.get()); } } if (!closureMap.empty()) { for (auto const& c1 : closureMap) { auto& s = index.classClosureMap[c1.first]; for (auto const& c2 : c1.second) { s.push_back(c2); } } } for (auto i = unit.funcs.begin(); i != unit.funcs.end();) { auto& f = *i; // Deduplicate meth_caller wrappers- We just take the first one we see. if (f->attrs & AttrIsMethCaller && index.funcs.count(f->name)) { unit.funcs.erase(i); continue; } add_symbol(index.funcs, f.get(), "function"); ++i; } for (auto& ta : unit.typeAliases) { add_symbol(index.typeAliases, ta.get(), "type alias", index.classes); } for (auto& c : unit.constants) { add_symbol(index.constants, c.get(), "constant"); } } std::unique_ptr<php::Func> clone_meth_helper( php::Unit* unit, php::Class* newContext, const php::Func* origMeth, std::unique_ptr<php::Func> cloneMeth, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures ); std::unique_ptr<php::Class> clone_closure(php::Unit* unit, php::Class* newContext, php::Class* cls, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures) { auto clone = std::make_unique<php::Class>(*cls); assertx(clone->closureContextCls); clone->closureContextCls = newContext; clone->unit = newContext->unit; auto i = 0; for (auto& cloneMeth : clone->methods) { cloneMeth = clone_meth_helper(unit, clone.get(), cls->methods[i++].get(), std::move(cloneMeth), nextFuncId, updates, clonedClosures); if (!cloneMeth) return nullptr; } return clone; } std::unique_ptr<php::Func> clone_meth_helper( php::Unit* unit, php::Class* newContext, const php::Func* origMeth, std::unique_ptr<php::Func> cloneMeth, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& preResolveUpdates, ClonedClosureMap& clonedClosures) { cloneMeth->cls = newContext; cloneMeth->idx = nextFuncId.fetch_add(1, std::memory_order_relaxed); if (!cloneMeth->originalFilename) { cloneMeth->originalFilename = origMeth->unit->filename; } if (!cloneMeth->originalUnit) { cloneMeth->originalUnit = origMeth->unit; } cloneMeth->unit = newContext->unit; cloneMeth->originalClass = origMeth->originalClass; if (!origMeth->hasCreateCl) return cloneMeth; auto const recordClosure = [&] (uint32_t& clsId) { auto const cls = origMeth->unit->classes[clsId].get(); auto it = clonedClosures.find(cls); if (it == clonedClosures.end()) { auto cloned = clone_closure( unit, newContext->closureContextCls ? newContext->closureContextCls : newContext, cls, nextFuncId, preResolveUpdates, clonedClosures ); if (!cloned) return false; clsId = preResolveUpdates.nextClassId++; always_assert(clonedClosures.emplace(cls, std::move(cloned), clsId)); } else { clsId = std::get<2>(*it); } return true; }; auto mf = php::WideFunc::mut(cloneMeth.get()); hphp_fast_map<size_t, hphp_fast_map<size_t, uint32_t>> updates; for (size_t bid = 0; bid < mf.blocks().size(); bid++) { auto const b = mf.blocks()[bid].get(); for (size_t ix = 0; ix < b->hhbcs.size(); ix++) { auto const& bc = b->hhbcs[ix]; switch (bc.op) { case Op::CreateCl: { auto clsId = bc.CreateCl.arg2; if (!recordClosure(clsId)) return nullptr; updates[bid][ix] = clsId; break; } default: break; } } } for (auto const& elm : updates) { auto const blk = mf.blocks()[elm.first].mutate(); for (auto const& ix : elm.second) { blk->hhbcs[ix.first].CreateCl.arg2 = ix.second; } } return cloneMeth; } std::unique_ptr<php::Func> clone_meth(php::Unit* unit, php::Class* newContext, const php::Func* origMeth, SString name, Attr attrs, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures) { auto cloneMeth = std::make_unique<php::Func>(*origMeth); cloneMeth->name = name; cloneMeth->attrs = attrs | AttrTrait; return clone_meth_helper(unit, newContext, origMeth, std::move(cloneMeth), nextFuncId, updates, clonedClosures); } bool merge_inits(std::vector<std::unique_ptr<php::Func>>& clones, php::Unit* unit, ClassInfo* cinfo, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures, SString xinitName) { auto const cls = const_cast<php::Class*>(cinfo->cls); std::unique_ptr<php::Func> empty; auto& xinit = [&] () -> std::unique_ptr<php::Func>& { for (auto& m : cls->methods) { if (m->name == xinitName) return m; } return empty; }(); auto merge_one = [&] (const php::Func* func) { if (!xinit) { ITRACE(5, " - cloning {}::{} as {}::{}\n", func->cls->name, func->name, cls->name, xinitName); xinit = clone_meth(unit, cls, func, func->name, func->attrs, nextFuncId, updates, clonedClosures); return xinit != nullptr; } ITRACE(5, " - appending {}::{} into {}::{}\n", func->cls->name, func->name, cls->name, xinitName); if (xinitName == s_86cinit.get()) { return append_86cinit(xinit.get(), *func); } else { return append_func(xinit.get(), *func); } }; for (auto t : cinfo->usedTraits) { auto it = t->methods.find(xinitName); if (it != t->methods.end()) { if (!merge_one(it->second.func)) { ITRACE(5, "merge_xinits: failed to merge {}::{}\n", it->second.func->cls->name, it->second.func->name); return false; } } } assertx(xinit); if (empty) { ITRACE(5, "merge_xinits: adding {}::{} to method table\n", xinit->cls->name, xinit->name); assertx(&empty == &xinit); clones.push_back(std::move(xinit)); } return true; } bool merge_xinits(Attr attr, std::vector<std::unique_ptr<php::Func>>& clones, php::Unit* unit, ClassInfo* cinfo, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures) { auto const xinitName = [&]() { switch (attr) { case AttrNone : return s_86pinit.get(); case AttrStatic: return s_86sinit.get(); case AttrLSB : return s_86linit.get(); default: always_assert(false); } }(); auto const xinitMatch = [&](Attr prop_attrs) { auto mask = AttrStatic | AttrLSB; switch (attr) { case AttrNone: return (prop_attrs & mask) == AttrNone; case AttrStatic: return (prop_attrs & mask) == AttrStatic; case AttrLSB: return (prop_attrs & mask) == mask; default: always_assert(false); } }; for (auto const& p : cinfo->traitProps) { if (xinitMatch(p.attrs) && p.val.m_type == KindOfUninit && !(p.attrs & AttrLateInit)) { ITRACE(5, "merge_xinits: {}: Needs merge for {}{}prop `{}'\n", cinfo->cls->name, attr & AttrStatic ? "static " : "", attr & AttrLSB ? "lsb " : "", p.name); return merge_inits(clones, unit, cinfo, nextFuncId, updates, clonedClosures, xinitName); } } return true; } bool merge_cinits(std::vector<std::unique_ptr<php::Func>>& clones, php::Unit* unit, ClassInfo* cinfo, std::atomic<uint32_t>& nextFuncId, ClsPreResolveUpdates& updates, ClonedClosureMap& clonedClosures) { auto const xinitName = s_86cinit.get(); for (auto const& c : cinfo->traitConsts) { if (c.val && c.val->m_type == KindOfUninit) { return merge_inits(clones, unit, cinfo, nextFuncId, updates, clonedClosures, xinitName); } } return true; } void rename_closure(const IndexData& index, php::Class* cls, ClsPreResolveUpdates& updates, size_t idx) { auto n = cls->name->slice(); auto const p = n.find(';'); if (p != std::string::npos) { n = n.subpiece(0, p); } auto const newName = makeStaticString( folly::sformat( "{};{}-{}", n, idx+1, string_sha1(cls->unit->filename->slice()) ) ); always_assert(!index.classes.count(newName)); cls->name = newName; updates.newClosures.emplace_back(cls); } void preresolve(const php::Program*, const IndexData&, const php::Class*, ClsPreResolveUpdates&); void flatten_traits(const php::Program* program, const IndexData& index, ClassInfo* cinfo, ClsPreResolveUpdates& updates) { bool hasConstProp = false; for (auto const t : cinfo->usedTraits) { if (t->usedTraits.size() && !(t->cls->attrs & AttrNoExpandTrait)) { ITRACE(5, "Not flattening {} because of {}\n", cinfo->cls->name, t->cls->name); return; } if (is_noflatten_trait(t->cls)) { ITRACE(5, "Not flattening {} because {} is annotated with __NoFlatten\n", cinfo->cls->name, t->cls->name); return; } if (t->cls->hasConstProp) hasConstProp = true; } auto const cls = const_cast<php::Class*>(cinfo->cls); if (hasConstProp) cls->hasConstProp = true; std::vector<MethTabEntryPair*> methodsToAdd; for (auto& ent : cinfo->methods) { if (!ent.second.topLevel || ent.second.func->cls == cinfo->cls) { continue; } always_assert(ent.second.func->cls->attrs & AttrTrait); methodsToAdd.push_back(mteFromElm(ent)); } auto const it = updates.extraMethods.find(cinfo->cls); if (!methodsToAdd.empty()) { assertx(it != updates.extraMethods.end()); std::sort(begin(methodsToAdd), end(methodsToAdd), [] (const MethTabEntryPair* a, const MethTabEntryPair* b) { return a->second.idx < b->second.idx; }); } else if (debug && it != updates.extraMethods.end()) { // When building the ClassInfos, we proactively added all closures // from usedTraits to classExtraMethodMap; but now we're going to // start from the used methods, and deduce which closures actually // get pulled in. Its possible *none* of the methods got used, in // which case, we won't need their closures either. To be safe, // verify that the only things in classExtraMethodMap are // closures. for (DEBUG_ONLY auto const f : it->second) { assertx(f->isClosureBody); } } std::vector<std::unique_ptr<php::Func>> clones; ClonedClosureMap clonedClosures; auto& nextFuncId = const_cast<php::Program*>(program)->nextFuncId; for (auto const ent : methodsToAdd) { auto clone = clone_meth(cls->unit, cls, ent->second.func, ent->first, ent->second.attrs, nextFuncId, updates, clonedClosures); if (!clone) { ITRACE(5, "Not flattening {} because {}::{} could not be cloned\n", cls->name, ent->second.func->cls->name, ent->first); return; } clone->attrs |= AttrTrait; ent->second.attrs |= AttrTrait; ent->second.func = clone.get(); clones.push_back(std::move(clone)); } if (cinfo->traitProps.size()) { if (!merge_xinits(AttrNone, clones, cls->unit, cinfo, nextFuncId, updates, clonedClosures) || !merge_xinits(AttrStatic, clones, cls->unit, cinfo, nextFuncId, updates, clonedClosures) || !merge_xinits(AttrLSB, clones, cls->unit, cinfo, nextFuncId, updates, clonedClosures)) { ITRACE(5, "Not flattening {} because we couldn't merge the 86xinits\n", cls->name); return; } } // flatten initializers for constants in traits if (cinfo->traitConsts.size()) { if (!merge_cinits(clones, cls->unit, cinfo, nextFuncId, updates, clonedClosures)) { ITRACE(5, "Not flattening {} because we couldn't merge the 86cinits\n", cls->name); return; } } // We're now committed to flattening. ITRACE(3, "Flattening {}\n", cls->name); if (it != updates.extraMethods.end()) it->second.clear(); for (auto const& p : cinfo->traitProps) { ITRACE(5, " - prop {}\n", p.name); cls->properties.push_back(p); cls->properties.back().attrs |= AttrTrait; } cinfo->traitProps.clear(); for (auto const& c : cinfo->traitConsts) { ITRACE(5, " - const {}\n", c.name); cls->constants.push_back(c); cls->constants.back().cls = cls; cinfo->clsConstants[c.name].cls = cls; cinfo->clsConstants[c.name].idx = cls->constants.size()-1; cinfo->preResolveState->constsFromTraits.erase(c.name); } cinfo->traitConsts.clear(); if (clones.size()) { auto cinit = cls->methods.size() && cls->methods.back()->name == s_86cinit.get() ? std::move(cls->methods.back()) : nullptr; if (cinit) cls->methods.pop_back(); for (auto& clone : clones) { if (is_special_method_name(clone->name)) { DEBUG_ONLY auto res = cinfo->methods.emplace( clone->name, MethTabEntry { clone.get(), clone->attrs, false, true } ); assertx(res.second); } ITRACE(5, " - meth {}\n", clone->name); cinfo->methods.find(clone->name)->second.func = clone.get(); if (clone->name == s_86cinit.get()) { cinit = std::move(clone); continue; } cls->methods.push_back(std::move(clone)); } if (cinit) cls->methods.push_back(std::move(cinit)); if (!clonedClosures.empty()) { auto& closures = updates.closures[cls]; for (auto& [orig, clo, idx] : clonedClosures) { rename_closure(index, clo.get(), updates, idx); ITRACE(5, " - closure {} as {}\n", orig->name, clo->name); assertx(clo->closureContextCls == cls); assertx(clo->unit == cls->unit); closures.emplace_back(clo.get()); updates.newClasses.emplace_back( std::move(clo), cls->unit, idx ); preresolve(program, index, closures.back(), updates); } } } struct EqHash { bool operator()(const PreClass::ClassRequirement& a, const PreClass::ClassRequirement& b) const { return a.is_same(&b); } size_t operator()(const PreClass::ClassRequirement& a) const { return a.hash(); } }; hphp_hash_set<PreClass::ClassRequirement, EqHash, EqHash> reqs; for (auto const t : cinfo->usedTraits) { for (auto const& req : t->cls->requirements) { if (reqs.empty()) { for (auto const& r : cls->requirements) { reqs.insert(r); } } if (reqs.insert(req).second) cls->requirements.push_back(req); } } cls->attrs |= AttrNoExpandTrait; } /* * Given a static representation of a Hack class, find a possible resolution * of the class along with all classes, interfaces and traits in its hierarchy. * * Returns the resultant ClassInfo, or nullptr if the Hack class * cannot be instantiated at runtime. */ ClassInfo* resolve_combinations(const php::Program* program, const IndexData& index, const php::Class* cls, ClsPreResolveUpdates& updates) { auto cinfo = std::make_unique<ClassInfo>(); cinfo->cls = cls; auto const& map = index.classInfo; if (cls->parentName) { cinfo->parent = map.at(cls->parentName); cinfo->baseList = cinfo->parent->baseList; if (cinfo->parent->cls->attrs & (AttrInterface | AttrTrait)) { ITRACE(2, "Resolve combinations failed for `{}' because " "its parent `{}' is not a class\n", cls->name, cls->parentName); return nullptr; } } cinfo->baseList.push_back(cinfo.get()); for (auto& iname : cls->interfaceNames) { auto const iface = map.at(iname); if (!(iface->cls->attrs & AttrInterface)) { ITRACE(2, "Resolve combinations failed for `{}' because `{}' " "is not an interface\n", cls->name, iname); return nullptr; } cinfo->declInterfaces.push_back(iface); } for (auto& included_enum_name : cls->includedEnumNames) { auto const included_enum = map.at(included_enum_name); auto const want_attr = cls->attrs & (AttrEnum | AttrEnumClass); if (!(included_enum->cls->attrs & want_attr)) { ITRACE(2, "Resolve combinations failed for `{}' because `{}' " "is not an enum{}\n", cls->name, included_enum_name, want_attr & AttrEnumClass ? " class" : ""); return nullptr; } cinfo->includedEnums.push_back(included_enum); } for (auto& tname : cls->usedTraitNames) { auto const trait = map.at(tname); if (!(trait->cls->attrs & AttrTrait)) { ITRACE(2, "Resolve combinations failed for `{}' because `{}' " "is not a trait\n", cls->name, tname); return nullptr; } cinfo->usedTraits.push_back(trait); } cinfo->preResolveState = std::make_unique<ClassInfo::PreResolveState>(); if (!build_cls_info(program, index, cinfo.get(), updates)) return nullptr; ITRACE(2, " resolved: {}\n", cls->name); if (Trace::moduleEnabled(Trace::hhbbc_index, 3)) { for (auto const DEBUG_ONLY& iface : cinfo->implInterfaces) { ITRACE(3, " implements: {}\n", iface.second->cls->name); } for (auto const DEBUG_ONLY& trait : cinfo->usedTraits) { ITRACE(3, " uses: {}\n", trait->cls->name); } } cinfo->baseList.shrink_to_fit(); updates.newInfos.emplace_back(std::move(cinfo)); return updates.newInfos.back().get(); } void preresolve(const php::Program* program, const IndexData& index, const php::Class* type, ClsPreResolveUpdates& updates) { ITRACE(2, "preresolve class: {}:{}\n", type->name, (void*)type); auto const resolved = [&] { Trace::Indent indent; if (debug) { if (type->parentName) { assertx(index.classInfo.count(type->parentName)); } for (DEBUG_ONLY auto& i : type->interfaceNames) { assertx(index.classInfo.count(i)); } for (DEBUG_ONLY auto& t : type->usedTraitNames) { assertx(index.classInfo.count(t)); } } return resolve_combinations(program, index, type, updates); }(); ITRACE(3, "preresolve: {}:{} ({} resolutions)\n", type->name, (void*)type, resolved ? 1 : 0); if (resolved) { updates.updateDeps.emplace_back(resolved); if (options.FlattenTraits && !(type->attrs & AttrNoExpandTrait) && !type->usedTraitNames.empty() && index.classes.count(type->name) == 1) { Trace::Indent indent; flatten_traits(program, index, resolved, updates); } } } void compute_subclass_list_rec(IndexData& index, ClassInfo* cinfo, ClassInfo* csub) { for (auto const ctrait : csub->usedTraits) { auto const ct = const_cast<ClassInfo*>(ctrait); ct->subclassList.push_back(cinfo); compute_subclass_list_rec(index, cinfo, ct); } } void compute_included_enums_list_rec(IndexData& index, ClassInfo* cinfo, ClassInfo* csub) { for (auto const cincluded_enum : csub->includedEnums) { auto const cie = const_cast<ClassInfo*>(cincluded_enum); cie->subclassList.push_back(cinfo); compute_included_enums_list_rec(index, cinfo, cie); } } void compute_subclass_list(IndexData& index) { trace_time _("compute subclass list"); auto fixupTraits = false; auto fixupEnums = false; auto const AnyEnum = AttrEnum | AttrEnumClass; for (auto& cinfo : index.allClassInfos) { if (cinfo->cls->attrs & AttrInterface) continue; for (auto& cparent : cinfo->baseList) { cparent->subclassList.push_back(cinfo.get()); } if (!(cinfo->cls->attrs & AttrNoExpandTrait) && cinfo->usedTraits.size()) { fixupTraits = true; compute_subclass_list_rec(index, cinfo.get(), cinfo.get()); } // Add the included enum lists if cinfo is an enum if ((cinfo->cls->attrs & AnyEnum) && cinfo->cls->includedEnumNames.size()) { fixupEnums = true; compute_included_enums_list_rec(index, cinfo.get(), cinfo.get()); } // Also add instantiable classes to their interface's subclassLists if (cinfo->cls->attrs & (AttrTrait | AnyEnum | AttrAbstract)) continue; for (auto& ipair : cinfo->implInterfaces) { auto impl = const_cast<ClassInfo*>(ipair.second); impl->subclassList.push_back(cinfo.get()); } } for (auto& cinfo : index.allClassInfos) { auto& sub = cinfo->subclassList; if ((fixupTraits && cinfo->cls->attrs & AttrTrait) || (fixupEnums && cinfo->cls->attrs & AnyEnum)) { // traits and enums can be reached by multiple paths, so we need to // uniquify their subclassLists. std::sort(begin(sub), end(sub)); sub.erase( std::unique(begin(sub), end(sub)), end(sub) ); } sub.shrink_to_fit(); } } bool define_func_family(IndexData& index, ClassInfo* cinfo, SString name, const php::Func* func = nullptr) { FuncFamily::PFuncVec funcs{}; for (auto const cleaf : cinfo->subclassList) { auto const leafFn = [&] () -> const MethTabEntryPair* { auto const leafFnIt = cleaf->methods.find(name); if (leafFnIt == end(cleaf->methods)) return nullptr; return mteFromIt(leafFnIt); }(); if (!leafFn) continue; funcs.push_back(leafFn); } if (funcs.empty()) return false; std::sort( begin(funcs), end(funcs), [&] (const MethTabEntryPair* a, const MethTabEntryPair* b) { // We want a canonical order for the family. Putting the // one corresponding to cinfo first makes sense, because // the first one is used as the name for FCall*Method* hint, // after that, sort by name so that different case spellings // come in the same order. if (a->second.func == b->second.func) return false; if (func) { if (b->second.func == func) return false; if (a->second.func == func) return true; } if (auto d = a->first->compare(b->first)) { if (!func) { if (b->first == name) return false; if (a->first == name) return true; } return d < 0; } return std::less<const void*>{}(a->second.func, b->second.func); } ); funcs.erase( std::unique( begin(funcs), end(funcs), [] (const MethTabEntryPair* a, const MethTabEntryPair* b) { return a->second.func == b->second.func; } ), end(funcs) ); funcs.shrink_to_fit(); if (Trace::moduleEnabled(Trace::hhbbc_index, 4)) { FTRACE(4, "define_func_family: {}::{}:\n", cinfo->cls->name, name); for (auto const DEBUG_ONLY func : funcs) { FTRACE(4, " {}::{}\n", func->second.func->cls->name, func->second.func->name); } } // Single func resolutions are stored separately. They don't need a // FuncFamily and this saves space. if (funcs.size() == 1) { cinfo->singleMethodFamilies.emplace(name, funcs[0]); return true; } // Otherwise re-use an existing identical FuncFamily, or create a // new one. auto const ff = [&] { auto it = index.funcFamilies.find(funcs); if (it != index.funcFamilies.end()) return it->first.get(); return index.funcFamilies.insert( std::make_unique<FuncFamily>(std::move(funcs)), false ).first->first.get(); }(); cinfo->methodFamilies.emplace( std::piecewise_construct, std::forward_as_tuple(name), std::forward_as_tuple(ff) ); return true; } void build_abstract_func_families(IndexData& data, ClassInfo* cinfo) { std::vector<SString> extras; // We start by collecting the list of methods shared across all // subclasses of cinfo (including indirectly). And then add the // public methods which are not constructors and have no private // ancestors to the method families of cinfo. Note that this set // may be larger than the methods declared on cinfo and may also // be missing methods declared on cinfo. In practice this is the // set of methods we can depend on having accessible given any // object which is known to implement cinfo. auto it = cinfo->subclassList.begin(); while (true) { if (it == cinfo->subclassList.end()) return; auto const sub = *it++; assertx(!(sub->cls->attrs & AttrInterface)); if (sub == cinfo || (sub->cls->attrs & AttrAbstract)) continue; for (auto& par : sub->methods) { if (!par.second.hasPrivateAncestor && (par.second.attrs & AttrPublic) && !cinfo->methodFamilies.count(par.first) && !cinfo->singleMethodFamilies.count(par.first) && !cinfo->methods.count(par.first)) { extras.push_back(par.first); } } if (!extras.size()) return; break; } auto end = extras.end(); while (it != cinfo->subclassList.end()) { auto const sub = *it++; assertx(!(sub->cls->attrs & AttrInterface)); if (sub == cinfo || (sub->cls->attrs & AttrAbstract)) continue; for (auto nameIt = extras.begin(); nameIt != end;) { auto const meth = sub->methods.find(*nameIt); if (meth == sub->methods.end() || !(meth->second.attrs & AttrPublic) || meth->second.hasPrivateAncestor) { *nameIt = *--end; if (end == extras.begin()) return; } else { ++nameIt; } } } extras.erase(end, extras.end()); if (Trace::moduleEnabled(Trace::hhbbc_index, 5)) { FTRACE(5, "Adding extra methods to {}:\n", cinfo->cls->name); for (auto const DEBUG_ONLY extra : extras) { FTRACE(5, " {}\n", extra); } } hphp_fast_set<SString> added; for (auto name : extras) { if (define_func_family(data, cinfo, name) && (cinfo->cls->attrs & AttrInterface)) { added.emplace(name); } } if (cinfo->cls->attrs & AttrInterface) { for (auto& m : cinfo->cls->methods) { if (added.count(m->name)) { cinfo->methods.emplace( m->name, MethTabEntry { m.get(), m->attrs, false, true } ); } } } return; } void define_func_families(IndexData& index) { trace_time tracer("define_func_families"); parallel::for_each( index.allClassInfos, [&] (const std::unique_ptr<ClassInfo>& cinfo) { if (cinfo->cls->attrs & AttrTrait) return; FTRACE(4, "Defining func families for {}\n", cinfo->cls->name); if (!(cinfo->cls->attrs & AttrInterface)) { for (auto& kv : cinfo->methods) { auto const mte = mteFromElm(kv); if (mte->second.attrs & AttrNoOverride) continue; if (is_special_method_name(mte->first)) continue; // We need function family for constructor even if it is private, // as `new static()` may still call a non-private constructor from // subclass. if (!mte->first->isame(s_construct.get()) && mte->second.attrs & AttrPrivate) { continue; } define_func_family(index, cinfo.get(), mte->first, mte->second.func); } } if (cinfo->cls->attrs & (AttrInterface | AttrAbstract)) { build_abstract_func_families(index, cinfo.get()); } } ); // Now that all of the FuncFamilies have been created, generate the // back links from FuncInfo to their FuncFamilies. std::vector<FuncFamily*> work; work.reserve(index.funcFamilies.size()); for (auto const& kv : index.funcFamilies) work.emplace_back(kv.first.get()); // Different threads can touch the same FuncInfo, so use sharded // locking scheme. std::array<std::mutex, 256> locks; parallel::for_each( work, [&] (FuncFamily* ff) { ff->m_numInOut = num_inout_from_set(ff->possibleFuncs()); ff->m_isReadonlyReturn = is_readonly_return_from_set(ff->possibleFuncs()); ff->m_isReadonlyThis = is_readonly_this_from_set(ff->possibleFuncs()); for (auto const pf : ff->possibleFuncs()) { auto finfo = create_func_info(index, pf->second.func); auto& lock = locks[pointer_hash<FuncInfo>{}(finfo) % locks.size()]; std::lock_guard<std::mutex> _{lock}; finfo->families.emplace_back(ff); } } ); parallel::for_each( index.funcInfo, [&] (FuncInfo& fi) { fi.families.shrink_to_fit(); } ); } /* * ConflictGraph maintains lists of interfaces that conflict with each other * due to being implemented by the same class. */ struct ConflictGraph { void add(const php::Class* i, const php::Class* j) { if (i == j) return; map[i].insert(j); } hphp_hash_map<const php::Class*, hphp_fast_set<const php::Class*>> map; }; /* * Trace information about interface conflict sets and the vtables computed * from them. */ void trace_interfaces(const IndexData& index, const ConflictGraph& cg) { // Compute what the vtable for each Class will look like, and build up a list // of all interfaces. struct Cls { const ClassInfo* cinfo; std::vector<const php::Class*> vtable; }; std::vector<Cls> classes; std::vector<const php::Class*> ifaces; size_t total_slots = 0, empty_slots = 0; for (auto& cinfo : index.allClassInfos) { if (cinfo->cls->attrs & AttrInterface) { ifaces.emplace_back(cinfo->cls); continue; } if (cinfo->cls->attrs & (AttrTrait | AttrEnum | AttrEnumClass)) continue; classes.emplace_back(Cls{cinfo.get()}); auto& vtable = classes.back().vtable; for (auto& pair : cinfo->implInterfaces) { auto it = index.ifaceSlotMap.find(pair.second->cls); assertx(it != end(index.ifaceSlotMap)); auto const slot = it->second; if (slot >= vtable.size()) vtable.resize(slot + 1); vtable[slot] = pair.second->cls; } total_slots += vtable.size(); for (auto iface : vtable) if (iface == nullptr) ++empty_slots; } Slot max_slot = 0; for (auto const& pair : index.ifaceSlotMap) { max_slot = std::max(max_slot, pair.second); } // Sort the list of class vtables so the largest ones come first. auto class_cmp = [&](const Cls& a, const Cls& b) { return a.vtable.size() > b.vtable.size(); }; std::sort(begin(classes), end(classes), class_cmp); // Sort the list of interfaces so the biggest conflict sets come first. auto iface_cmp = [&](const php::Class* a, const php::Class* b) { return cg.map.at(a).size() > cg.map.at(b).size(); }; std::sort(begin(ifaces), end(ifaces), iface_cmp); std::string out; folly::format(&out, "{} interfaces, {} classes\n", ifaces.size(), classes.size()); folly::format(&out, "{} vtable slots, {} empty vtable slots, max slot {}\n", total_slots, empty_slots, max_slot); folly::format(&out, "\n{:-^80}\n", " interface slots & conflict sets"); for (auto iface : ifaces) { auto cgIt = cg.map.find(iface); if (cgIt == end(cg.map)) break; auto& conflicts = cgIt->second; folly::format(&out, "{:>40} {:3} {:2} [", iface->name, conflicts.size(), folly::get_default(index.ifaceSlotMap, iface)); auto sep = ""; for (auto conflict : conflicts) { folly::format(&out, "{}{}", sep, conflict->name); sep = ", "; } folly::format(&out, "]\n"); } folly::format(&out, "\n{:-^80}\n", " class vtables "); for (auto& item : classes) { if (item.vtable.empty()) break; folly::format(&out, "{:>30}: [", item.cinfo->cls->name); auto sep = ""; for (auto iface : item.vtable) { folly::format(&out, "{}{}", sep, iface ? iface->name->data() : "null"); sep = ", "; } folly::format(&out, "]\n"); } Trace::traceRelease("%s", out.c_str()); } /* * Find the lowest Slot that doesn't conflict with anything in the conflict set * for iface. */ Slot find_min_slot(const php::Class* iface, const IfaceSlotMap& slots, const ConflictGraph& cg) { auto const& cit = cg.map.find(iface); if (cit == cg.map.end() || cit->second.empty()) { // No conflicts. This is the only interface implemented by the classes that // implement it. return 0; } boost::dynamic_bitset<> used; for (auto const& c : cit->second) { auto const it = slots.find(c); if (it == slots.end()) continue; auto const slot = it->second; if (used.size() <= slot) used.resize(slot + 1); used.set(slot); } used.flip(); return used.any() ? used.find_first() : used.size(); } /* * Compute vtable slots for all interfaces. No two interfaces implemented by * the same class will share the same vtable slot. */ void compute_iface_vtables(IndexData& index) { trace_time tracer("compute interface vtables"); ConflictGraph cg; std::vector<const php::Class*> ifaces; hphp_hash_map<const php::Class*, int> iface_uses; // Build up the conflict sets. for (auto& cinfo : index.allClassInfos) { // Gather interfaces. if (cinfo->cls->attrs & AttrInterface) { ifaces.emplace_back(cinfo->cls); // Make sure cg.map has an entry for every interface - this simplifies // some code later on. cg.map[cinfo->cls]; continue; } // Only worry about classes with methods that can be called. if (cinfo->cls->attrs & (AttrTrait | AttrEnum | AttrEnumClass)) continue; for (auto& ipair : cinfo->implInterfaces) { ++iface_uses[ipair.second->cls]; for (auto& jpair : cinfo->implInterfaces) { cg.add(ipair.second->cls, jpair.second->cls); } } } if (ifaces.size() == 0) return; // Sort interfaces by usage frequencies. // We assign slots greedily, so sort the interface list so the most // frequently implemented ones come first. auto iface_cmp = [&](const php::Class* a, const php::Class* b) { return iface_uses[a] > iface_uses[b]; }; std::sort(begin(ifaces), end(ifaces), iface_cmp); // Assign slots, keeping track of the largest assigned slot and the total // number of uses for each slot. Slot max_slot = 0; hphp_hash_map<Slot, int> slot_uses; for (auto* iface : ifaces) { auto const slot = find_min_slot(iface, index.ifaceSlotMap, cg); index.ifaceSlotMap[iface] = slot; max_slot = std::max(max_slot, slot); // Interfaces implemented by the same class never share a slot, so normal // addition is fine here. slot_uses[slot] += iface_uses[iface]; } // Make sure we have an initialized entry for each slot for the sort below. for (Slot slot = 0; slot < max_slot; ++slot) { assertx(slot_uses.count(slot)); } // Finally, sort and reassign slots so the most frequently used slots come // first. This slightly reduces the number of wasted vtable vector entries at // runtime. auto const slots = sort_keys_by_value( slot_uses, [&] (int a, int b) { return a > b; } ); std::vector<Slot> slots_permute(max_slot + 1, 0); for (size_t i = 0; i <= max_slot; ++i) slots_permute[slots[i]] = i; // re-map interfaces to permuted slots for (auto& pair : index.ifaceSlotMap) { pair.second = slots_permute[pair.second]; } if (Trace::moduleEnabledRelease(Trace::hhbbc_iface)) { trace_interfaces(index, cg); } } void mark_magic_on_parents(ClassInfo& cinfo, ClassInfo& derived) { auto any = false; for (const auto& mm : magicMethods) { if ((derived.*mm.pmem).thisHas) { auto& derivedHas = (cinfo.*mm.pmem).derivedHas; if (!derivedHas) { derivedHas = any = true; } } } if (!any) return; if (cinfo.parent) mark_magic_on_parents(*cinfo.parent, derived); for (auto iface : cinfo.declInterfaces) { mark_magic_on_parents(*const_cast<ClassInfo*>(iface), derived); } } bool has_magic_method(const ClassInfo* cinfo, SString name) { if (name == s_toBoolean.get()) { // note that "having" a magic method includes the possibility that // a parent class has it. This can't happen for the collection // classes, because they're all final; but for SimpleXMLElement, // we need to search. while (cinfo->parent) cinfo = cinfo->parent; return has_magic_bool_conversion(cinfo->cls->name); } return cinfo->methods.find(name) != end(cinfo->methods); } void find_magic_methods(IndexData& index) { trace_time tracer("find magic methods"); for (auto& cinfo : index.allClassInfos) { bool any = false; for (const auto& mm : magicMethods) { bool const found = has_magic_method(cinfo.get(), mm.name.get()); any = any || found; (cinfo.get()->*mm.pmem).thisHas = found; } if (any) mark_magic_on_parents(*cinfo, *cinfo); } } void find_mocked_classes(IndexData& index) { trace_time tracer("find mocked classes"); for (auto& cinfo : index.allClassInfos) { if (is_mock_class(cinfo->cls) && cinfo->parent) { cinfo->parent->isMocked = true; for (auto c = cinfo->parent; c; c = c->parent) { c->isDerivedMocked = true; } } } } void mark_const_props(IndexData& index) { trace_time tracer("mark const props"); for (auto& cinfo : index.allClassInfos) { auto const hasConstProp = [&]() { if (cinfo->cls->hasConstProp) return true; if (cinfo->parent && cinfo->parent->hasConstProp) return true; if (!(cinfo->cls->attrs & AttrNoExpandTrait)) { for (auto t : cinfo->usedTraits) { if (t->cls->hasConstProp) return true; } } return false; }(); if (hasConstProp) { cinfo->hasConstProp = true; for (auto c = cinfo.get(); c; c = c->parent) { if (c->derivedHasConstProp) break; c->derivedHasConstProp = true; } } } } void mark_no_override_classes(IndexData& index) { trace_time tracer("mark no override classes"); for (auto& cinfo : index.allClassInfos) { // We cleared all the NoOverride flags while building the // index. Set them as necessary. if (!(cinfo->cls->attrs & AttrInterface) && cinfo->subclassList.size() == 1) { attribute_setter(cinfo->cls->attrs, true, AttrNoOverride); } } } void mark_no_override_methods(IndexData& index) { trace_time tracer("mark no override methods"); // We removed any AttrNoOverride flags from all methods while adding // the units to the index. Now start by marking every // (non-interface, non-special) method as AttrNoOverride. parallel::for_each( index.allClassInfos, [&] (const std::unique_ptr<ClassInfo>& cinfo) { if (cinfo->cls->attrs & AttrInterface) return; for (auto& m : cinfo->methods) { if (!(is_special_method_name(m.first))) { FTRACE(9, "Pre-setting AttrNoOverride on {}::{}\n", m.second.func->cls->name, m.first); attribute_setter(m.second.attrs, true, AttrNoOverride); attribute_setter(m.second.func->attrs, true, AttrNoOverride); } } } ); // Then run through every ClassInfo, and for each of its parent // classes clear the AttrNoOverride flag if it has a different Func // with the same name. auto const updates = parallel::map( index.allClassInfos, [&] (const std::unique_ptr<ClassInfo>& cinfo) { hphp_fast_set<MethTabEntry*> changes; for (auto const& ancestor : cinfo->baseList) { if (ancestor == cinfo.get()) continue; for (auto const& derivedMethod : cinfo->methods) { auto const it = ancestor->methods.find(derivedMethod.first); if (it == end(ancestor->methods)) continue; if (it->second.func != derivedMethod.second.func) { FTRACE(2, "Removing AttrNoOverride on {}::{}\n", it->second.func->cls->name, it->first); changes.emplace(&it->second); } } } return changes; } ); for (auto const& u : updates) { for (auto& mte : u) { assertx(mte->attrs & AttrNoOverride || !(mte->func->attrs & AttrNoOverride)); if (mte->attrs & AttrNoOverride) { attribute_setter(mte->attrs, false, AttrNoOverride); attribute_setter(mte->func->attrs, false, AttrNoOverride); } } } } const StaticString s__Reified("__Reified"); /* * Emitter adds a 86reifiedinit method to all classes that have reified * generics. All base classes also need to have this method so that when we * call parent::86reifeidinit(...), there is a stopping point. * Since while emitting we do not know whether a base class will have * reified parents, during JIT time we need to add 86reifiedinit * unless AttrNoReifiedInit attribute is set. At this phase, * we set AttrNoReifiedInit attribute on classes do not have any * reified classes that extend it. */ void clean_86reifiedinit_methods(IndexData& index) { trace_time tracer("clean 86reifiedinit methods"); hphp_fast_set<const php::Class*> needsinit; // Find all classes that still need their 86reifiedinit methods for (auto const& cinfo : index.allClassInfos) { auto const& ual = cinfo->cls->userAttributes; // Each class that has at least one reified generic has an attribute // __Reified added by the emitter auto has_reification = ual.find(s__Reified.get()) != ual.end(); if (!has_reification) continue; // Add the base class for this reified class needsinit.emplace(cinfo->baseList[0]->cls); } // Add AttrNoReifiedInit to the base classes that do not need this method for (auto& cinfo : index.allClassInfos) { if (cinfo->parent == nullptr && needsinit.count(cinfo->cls) == 0) { FTRACE(2, "Adding AttrNoReifiedInit on class {}\n", cinfo->cls->name); attribute_setter(cinfo->cls->attrs, true, AttrNoReifiedInit); } } } ////////////////////////////////////////////////////////////////////// void check_invariants(const ClassInfo* cinfo) { // All the following invariants only apply to classes if (cinfo->cls->attrs & AttrInterface) return; if (!(cinfo->cls->attrs & AttrTrait)) { // For non-interface classes, each method in a php class has an // entry in its ClassInfo method table, and if it's not special, // AttrNoOverride, or private, an entry in the family table. for (auto& m : cinfo->cls->methods) { auto const it = cinfo->methods.find(m->name); always_assert(it != cinfo->methods.end()); if (it->second.attrs & (AttrNoOverride|AttrPrivate)) continue; if (is_special_method_name(m->name)) continue; always_assert( cinfo->methodFamilies.count(m->name) || cinfo->singleMethodFamilies.count(m->name) ); } } // The subclassList is non-empty, contains this ClassInfo, and // contains only unique elements. always_assert(!cinfo->subclassList.empty()); always_assert(std::find(begin(cinfo->subclassList), end(cinfo->subclassList), cinfo) != end(cinfo->subclassList)); auto cpy = cinfo->subclassList; std::sort(begin(cpy), end(cpy)); cpy.erase( std::unique(begin(cpy), end(cpy)), end(cpy) ); always_assert(cpy.size() == cinfo->subclassList.size()); // The baseList is non-empty, and the last element is this class. always_assert(!cinfo->baseList.empty()); always_assert(cinfo->baseList.back() == cinfo); for (const auto& mm : magicMethods) { const auto& info = cinfo->*mm.pmem; // Magic method flags should be consistent with the method table. always_assert(info.thisHas == has_magic_method(cinfo, mm.name.get())); // Non-'derived' flags (thisHas) about magic methods imply the derived // ones. always_assert(!info.thisHas || info.derivedHas); } // Every FuncFamily has more than function and contain functions // with the same name (unless its a family of ctors). methodFamilies // and singleMethodFamilies should have disjoint keys. for (auto const& mfam: cinfo->methodFamilies) { always_assert(mfam.second->possibleFuncs().size() > 1); auto const name = mfam.second->possibleFuncs().front()->first; for (auto const pf : mfam.second->possibleFuncs()) { always_assert(pf->first->isame(name)); } always_assert(!cinfo->singleMethodFamilies.count(mfam.first)); } for (auto const& mfam : cinfo->singleMethodFamilies) { always_assert(!cinfo->methodFamilies.count(mfam.first)); } } void check_invariants(IndexData& data) { if (!debug) return; for (auto& cinfo : data.allClassInfos) { check_invariants(cinfo.get()); } } ////////////////////////////////////////////////////////////////////// Type context_sensitive_return_type(IndexData& data, CallContext callCtx, Type returnType) { constexpr auto max_interp_nexting_level = 2; static __thread uint32_t interp_nesting_level; auto const finfo = func_info(data, callCtx.callee); returnType = return_with_context(std::move(returnType), callCtx.context); auto checkParam = [&] (int i) { auto const constraint = finfo->func->params[i].typeConstraint; if (constraint.hasConstraint() && !constraint.isTypeVar() && !constraint.isTypeConstant()) { auto ctx = Context { finfo->func->unit, finfo->func, finfo->func->cls }; auto t = data.m_index->lookup_constraint(ctx, constraint); return callCtx.args[i].strictlyMoreRefined(t); } return callCtx.args[i].strictSubtypeOf(TInitCell); }; // TODO(#3788877): more heuristics here would be useful. bool const tryContextSensitive = [&] { if (finfo->func->noContextSensitiveAnalysis || finfo->func->params.empty() || interp_nesting_level + 1 >= max_interp_nexting_level || returnType == TBottom) { return false; } if (finfo->retParam != NoLocalId && callCtx.args.size() > finfo->retParam && checkParam(finfo->retParam)) { return true; } if (!options.ContextSensitiveInterp) return false; if (callCtx.args.size() < finfo->func->params.size()) return true; for (auto i = 0; i < finfo->func->params.size(); i++) { if (checkParam(i)) return true; } return false; }(); if (!tryContextSensitive) { return returnType; } { ContextRetTyMap::const_accessor acc; if (data.contextualReturnTypes.find(acc, callCtx)) { if (data.frozen || acc->second == TBottom || is_scalar(acc->second)) { return acc->second; } } } if (data.frozen) { return returnType; } auto contextType = [&] { ++interp_nesting_level; SCOPE_EXIT { --interp_nesting_level; }; auto const func = finfo->func; auto const wf = php::WideFunc::cns(func); auto const calleeCtx = AnalysisContext { func->unit, wf, func->cls }; auto const ty = analyze_func_inline(*data.m_index, calleeCtx, callCtx.context, callCtx.args).inferredReturn; return return_with_context(ty, callCtx.context); }(); if (!interp_nesting_level) { FTRACE(3, "Context sensitive type: {}\n" "Context insensitive type: {}\n", show(contextType), show(returnType)); } if (!returnType.subtypeOf(BUnc)) { // If the context insensitive return type could be non-static, staticness // could be a result of temporary context sensitive bytecode optimizations. contextType = loosen_staticness(std::move(contextType)); } auto ret = intersection_of(std::move(returnType), std::move(contextType)); if (!interp_nesting_level) { FTRACE(3, "Context sensitive result: {}\n", show(ret)); } ContextRetTyMap::accessor acc; if (data.contextualReturnTypes.insert(acc, callCtx) || ret.strictSubtypeOf(acc->second)) { acc->second = ret; } return ret; } ////////////////////////////////////////////////////////////////////// PrepKind func_param_prep_func_doesnt_exist() { // since function doesnt exist, we will fail anyway, it is fine to remove // readonly checks return PrepKind{TriBool::No, TriBool::Yes}; } PrepKind func_param_prep(const php::Func* f, uint32_t paramId) { auto const sz = f->params.size(); if (paramId >= sz) return PrepKind{TriBool::No, TriBool::No}; PrepKind kind; kind.inOut = yesOrNo(f->params[paramId].inout); kind.readonly = yesOrNo(f->params[paramId].readonly); return kind; } template<class PossibleFuncRange> PrepKind prep_kind_from_set(PossibleFuncRange range, uint32_t paramId) { if (begin(range) == end(range)) return func_param_prep_func_doesnt_exist(); struct FuncFind { using F = const php::Func*; static F get(std::pair<SString,F> p) { return p.second; } static F get(const MethTabEntryPair* mte) { return mte->second.func; } }; Optional<PrepKind> prep; for (auto& item : range) { auto const kind = func_param_prep(FuncFind::get(item), paramId); if (!prep) { prep = kind; continue; } prep->inOut |= kind.inOut; prep->readonly |= kind.readonly; } assertx(prep); return *prep; } template<typename F> auto visit_parent_cinfo(const ClassInfo* cinfo, F fun) -> decltype(fun(cinfo)) { for (auto ci = cinfo; ci != nullptr; ci = ci->parent) { if (auto const ret = fun(ci)) return ret; if (ci->cls->attrs & AttrNoExpandTrait) continue; for (auto ct : ci->usedTraits) { if (auto const ret = visit_parent_cinfo(ct, fun)) { return ret; } } } return {}; } Type lookup_public_prop_impl( const IndexData& data, const ClassInfo* cinfo, SString propName ) { // Find a property declared in this class (or a parent) with the same name. const php::Class* knownCls = nullptr; auto const prop = visit_parent_cinfo( cinfo, [&] (const ClassInfo* ci) -> const php::Prop* { for (auto const& prop : ci->cls->properties) { if (prop.name == propName) { knownCls = ci->cls; return &prop; } } return nullptr; } ); if (!prop) return TCell; // Make sure its non-static and public. Otherwise its another function's // problem. if (prop->attrs & (AttrStatic | AttrPrivate)) return TCell; // Get a type corresponding to its declared type-hint (if any). auto ty = adjust_type_for_prop( *data.m_index, *knownCls, &prop->typeConstraint, TCell ); // We might have to include the initial value which might be outside of the // type-hint. auto initialTy = loosen_all(from_cell(prop->val)); if (!initialTy.subtypeOf(TUninit) && (prop->attrs & AttrSystemInitialValue)) { ty |= initialTy; } return ty; } // Test if the given property (declared in `cls') is accessible in the // given context (null if we're not in a class). bool static_is_accessible(const ClassInfo* clsCtx, const ClassInfo* cls, const php::Prop& prop) { assertx(prop.attrs & AttrStatic); switch (prop.attrs & (AttrPublic|AttrProtected|AttrPrivate)) { case AttrPublic: // Public is accessible everywhere return true; case AttrProtected: // Protected is accessible from both derived classes and parent // classes return clsCtx && (clsCtx->derivedFrom(*cls) || cls->derivedFrom(*clsCtx)); case AttrPrivate: // Private is only accessible from within the declared class return clsCtx == cls; } always_assert(false); } // Return true if the given class can possibly throw when its // initialized. Initialization can happen when an object of that class // is instantiated, or (more importantly) when static properties are // accessed. bool class_init_might_raise(IndexData& data, Context ctx, const ClassInfo* cinfo) { // Check this class and all of its parents for possible inequivalent // redeclarations or bad initial values. do { // Be conservative for now if we have unflattened traits. if (!cinfo->traitProps.empty()) return true; if (cinfo->hasBadRedeclareProp) return true; if (cinfo->hasBadInitialPropValues) { add_dependency(data, cinfo->cls, ctx, Dep::PropBadInitialValues); return true; } cinfo = cinfo->parent; } while (cinfo); return false; } /* * Calculate the effects of applying the given type against the * type-constraints for the given prop. This includes the subtype * which will succeed (if any), and if the type-constraint check might * throw. */ PropMergeResult<> prop_tc_effects(const Index& index, const ClassInfo* ci, const php::Prop& prop, const Type& val, bool checkUB) { assertx(prop.typeConstraint.validForProp()); using R = PropMergeResult<>; // If we're not actually checking property type-hints, everything // goes if (RuntimeOption::EvalCheckPropTypeHints <= 0) return R{ val, TriBool::No }; auto const ctx = Context { nullptr, nullptr, ci->cls }; auto const check = [&] (const TypeConstraint& tc, const Type& t) { // If the type as is satisfies the constraint, we won't throw and // the type is unchanged. if (index.satisfies_constraint(ctx, t, tc)) return R{ t, TriBool::No }; // Otherwise adjust the type. If we get a Bottom we'll definitely // throw. We already know the type doesn't completely satisfy the // constraint, so we'll at least maybe throw. auto adjusted = adjust_type_for_prop(index, *ctx.cls, &tc, t); auto const throws = yesOrMaybe(adjusted.subtypeOf(BBottom)); return R{ std::move(adjusted), throws }; }; // First check the main type-constraint. auto result = check(prop.typeConstraint, val); // If we're not checking generics upper-bounds, or if we already // know we'll fail, we're done. if (!checkUB || RuntimeOption::EvalEnforceGenericsUB <= 0 || result.throws == TriBool::Yes) { return result; } // Otherwise check every generic upper-bound. We'll feed the // narrowed type into each successive round. If we reach the point // where we'll know we'll definitely fail, just stop. for (auto ub : prop.ubs) { applyFlagsToUB(ub, prop.typeConstraint); auto r = check(ub, result.adjusted); result.throws &= r.throws; result.adjusted = std::move(r.adjusted); if (result.throws == TriBool::Yes) break; } return result; } /* * Lookup data for the static property named `propName', starting from * the specified class `start'. If `propName' is nullptr, then any * accessible static property in the class hierarchy is considered. If * `startOnly' is specified, if the property isn't found in `start', * it is treated as a lookup failure. Otherwise the lookup continues * in all parent classes of `start', until a property is found, or * until all parent classes have been exhausted (`startOnly' is used * to avoid redundant class hierarchy walks). `clsCtx' is the current * context, converted to a ClassInfo* (or nullptr if not in a class). */ PropLookupResult<> lookup_static_impl(IndexData& data, Context ctx, const ClassInfo* clsCtx, const PropertiesInfo& privateProps, const ClassInfo* start, SString propName, bool startOnly) { ITRACE( 6, "lookup_static_impl: {} {} {}\n", clsCtx ? clsCtx->cls->name->toCppString() : std::string{"-"}, start->cls->name, propName ? propName->toCppString() : std::string{"*"} ); Trace::Indent _; auto const type = [&] (const php::Prop& prop, const ClassInfo* ci) { switch (prop.attrs & (AttrPublic|AttrProtected|AttrPrivate)) { case AttrPublic: case AttrProtected: { if (ctx.unit) add_dependency(data, &prop, ctx, Dep::PublicSProp); auto const it = ci->publicStaticProps.find(propName); assertx(it != end(ci->publicStaticProps)); return remove_uninit(it->second.inferredType); } case AttrPrivate: { assertx(clsCtx == ci); auto const elem = privateProps.readPrivateStatic(prop.name); if (!elem) return TInitCell; return remove_uninit(elem->ty); } } always_assert(false); }; auto const initMightRaise = class_init_might_raise(data, ctx, start); auto const fromProp = [&] (const php::Prop& prop, const ClassInfo* ci) { // The property was definitely found. Compute its attributes // from the prop metadata. return PropLookupResult<>{ type(prop, ci), propName, TriBool::Yes, yesOrNo(prop.attrs & AttrIsConst), yesOrNo(prop.attrs & AttrIsReadonly), yesOrNo(prop.attrs & AttrLateInit), initMightRaise }; }; auto const notFound = [&] { // The property definitely wasn't found. return PropLookupResult<>{ TBottom, propName, TriBool::No, TriBool::No, TriBool::No, TriBool::No, false }; }; if (!propName) { // We don't statically know the prop name. Walk up the hierarchy // and union the data for any accessible static property. ITRACE(4, "no prop name, considering all accessible\n"); auto result = notFound(); visit_parent_cinfo( start, [&] (const ClassInfo* ci) { for (auto const& prop : ci->cls->properties) { if (!(prop.attrs & AttrStatic) || !static_is_accessible(clsCtx, ci, prop)) { ITRACE( 6, "skipping inaccessible {}::${}\n", ci->cls->name, prop.name ); continue; } auto const r = fromProp(prop, ci); ITRACE(6, "including {}:${} {}\n", ci->cls->name, prop.name, show(r)); result |= r; } // If we're only interested in the starting class, don't walk // up to the parents. return startOnly; } ); return result; } // We statically know the prop name. Walk up the hierarchy and stop // at the first matching property and use that data. assertx(!startOnly); auto const result = visit_parent_cinfo( start, [&] (const ClassInfo* ci) -> Optional<PropLookupResult<>> { for (auto const& prop : ci->cls->properties) { if (prop.name != propName) continue; // We have a matching prop. If its not static or not // accessible, the access will not succeed. if (!(prop.attrs & AttrStatic) || !static_is_accessible(clsCtx, ci, prop)) { ITRACE( 6, "{}::${} found but inaccessible, stopping\n", ci->cls->name, propName ); return notFound(); } // Otherwise its a match auto const r = fromProp(prop, ci); ITRACE(6, "found {}:${} {}\n", ci->cls->name, propName, show(r)); return r; } return std::nullopt; } ); if (!result) { // We walked up to all of the base classes and didn't find a // property with a matching name. The access will fail. ITRACE(6, "nothing found\n"); return notFound(); } return *result; } /* * Lookup the static property named `propName', starting from the * specified class `start'. If an accessible property is found, then * merge the given type `val' into the already known type for that * property. If `propName' is nullptr, then any accessible static * property in the class hierarchy is considered. If `startOnly' is * specified, if the property isn't found in `start', then the nothing * is done. Otherwise the lookup continues in all parent classes of * `start', until a property is found, or until all parent classes * have been exhausted (`startOnly' is to avoid redundant class * hierarchy walks). `clsCtx' is the current context, converted to a * ClassInfo* (or nullptr if not in a class). If `ignoreConst' is * false, then AttrConst properties will not have their type * modified. `mergePublic' is a lambda with the logic to merge a type * for a public property (this is needed to avoid cyclic * dependencies). */ template <typename F> PropMergeResult<> merge_static_type_impl(IndexData& data, Context ctx, F mergePublic, PropertiesInfo& privateProps, const ClassInfo* clsCtx, const ClassInfo* start, SString propName, const Type& val, bool checkUB, bool ignoreConst, bool mustBeReadOnly, bool startOnly) { ITRACE( 6, "merge_static_type_impl: {} {} {} {}\n", clsCtx ? clsCtx->cls->name->toCppString() : std::string{"-"}, start->cls->name, propName ? propName->toCppString() : std::string{"*"}, show(val) ); Trace::Indent _; assertx(!val.subtypeOf(BBottom)); // Perform the actual merge for a given property, returning the // effects of that merge. auto const merge = [&] (const php::Prop& prop, const ClassInfo* ci) { // First calculate the effects of the type-constraint. auto const effects = prop_tc_effects(*data.m_index, ci, prop, val, checkUB); // No point in merging if the type-constraint will always fail. if (effects.throws == TriBool::Yes) { ITRACE( 6, "tc would throw on {}::${} with {}, skipping\n", ci->cls->name, prop.name, show(val) ); return effects; } assertx(!effects.adjusted.subtypeOf(BBottom)); ITRACE( 6, "merging {} into {}::${}\n", show(effects.adjusted), ci->cls->name, prop.name ); switch (prop.attrs & (AttrPublic|AttrProtected|AttrPrivate)) { case AttrPublic: case AttrProtected: mergePublic(ci, prop, unctx(effects.adjusted)); return effects; case AttrPrivate: { assertx(clsCtx == ci); privateProps.mergeInPrivateStaticPreAdjusted( prop.name, unctx(effects.adjusted) ); return effects; } } always_assert(false); }; // If we don't find a property, then the mutation will definitely // fail. auto const notFound = [&] { return PropMergeResult<>{ TBottom, TriBool::Yes }; }; if (!propName) { // We don't statically know the prop name. Walk up the hierarchy // and merge the type for any accessible static property. ITRACE(6, "no prop name, considering all accessible\n"); auto result = notFound(); visit_parent_cinfo( start, [&] (const ClassInfo* ci) { for (auto const& prop : ci->cls->properties) { if (!(prop.attrs & AttrStatic) || !static_is_accessible(clsCtx, ci, prop)) { ITRACE( 6, "skipping inaccessible {}::${}\n", ci->cls->name, prop.name ); continue; } if (!ignoreConst && (prop.attrs & AttrIsConst)) { ITRACE(6, "skipping const {}::${}\n", ci->cls->name, prop.name); continue; } if (mustBeReadOnly && !(prop.attrs & AttrIsReadonly)) { ITRACE(6, "skipping mutable property that must be readonly {}::${}\n", ci->cls->name, prop.name); continue; } result |= merge(prop, ci); } return startOnly; } ); return result; } // We statically know the prop name. Walk up the hierarchy and stop // at the first matching property and merge the type there. assertx(!startOnly); auto result = visit_parent_cinfo( start, [&] (const ClassInfo* ci) -> Optional<PropMergeResult<>> { for (auto const& prop : ci->cls->properties) { if (prop.name != propName) continue; // We found a property with the right name, but its // inaccessible from this context (or not even static). This // mutation will fail, so we don't need to modify the type. if (!(prop.attrs & AttrStatic) || !static_is_accessible(clsCtx, ci, prop)) { ITRACE( 6, "{}::${} found but inaccessible, stopping\n", ci->cls->name, propName ); return notFound(); } // Mutations to AttrConst properties will fail as well, unless // it we want to override that behavior. if (!ignoreConst && (prop.attrs & AttrIsConst)) { ITRACE( 6, "{}:${} found but const, stopping\n", ci->cls->name, propName ); return notFound(); } if (mustBeReadOnly && !(prop.attrs & AttrIsReadonly)) { ITRACE( 6, "{}:${} found but is mutable and must be readonly, stopping\n", ci->cls->name, propName ); return notFound(); } return merge(prop, ci); } return std::nullopt; } ); if (!result) { ITRACE(6, "nothing found\n"); return notFound(); } // If the mutation won't throw, we still need to check if the class // initialization can throw. If we might already throw (or // definitely will throw), this doesn't matter. if (result->throws == TriBool::No) { return PropMergeResult<>{ std::move(result->adjusted), maybeOrNo(class_init_might_raise(data, ctx, start)) }; } return *result; } ////////////////////////////////////////////////////////////////////// void buildTypeInfoData(IndexData& data, ClassInfoData& cid) { for (auto const& elm : data.classes) { auto const cls = elm.second; auto const addUser = [&] (SString rName) { cid.users[rName].push_back(cls); ++cid.depCounts[cls]; }; if (cls->parentName) addUser(cls->parentName); for (auto& i : cls->interfaceNames) addUser(i); for (auto& t : cls->usedTraitNames) addUser(t); for (auto& t : cls->includedEnumNames) addUser(t); if (!cid.depCounts.count(cls)) { FTRACE(5, "Adding no-dep class {}:{} to queue\n", cls->name, (void*)cls); // make sure that closure is first, because we end up calling // preresolve directly on closures created by trait // flattening, which assumes all dependencies are satisfied. if (cid.queue.size() && cls->name == s_Closure.get()) { cid.queue.push_back(cid.queue[0]); cid.queue[0] = cls; } else { cid.queue.push_back(cls); } } else { FTRACE(6, "class {}:{} has {} deps\n", cls->name, (void*)cls, cid.depCounts[cls]); } } cid.cqBack = cid.queue.size(); cid.queue.resize(data.classes.size()); } void updatePreResolveDeps(ClassInfoData& cid, const ClsPreResolveUpdates& updates) { for (auto const info : updates.updateDeps) { auto const& users = cid.users[info->cls->name]; for (auto const tu : users) { auto const it = cid.depCounts.find(tu); if (it == cid.depCounts.end()) { assertx(cid.hasPseudoCycles); continue; } auto& depCount = it->second; assertx(depCount); if (!--depCount) { cid.depCounts.erase(it); ITRACE(5, " enqueue: {}:{}\n", tu->name, (void*)tu); cid.queue[cid.cqBack++] = tu; } else { ITRACE(6, " depcount: {}:{} = {}\n", tu->name, (void*)tu, depCount); } } } } void commitPreResolveUpdates(IndexData& index, ClassInfoData& tid, std::vector<ClsPreResolveUpdates>& updates) { parallel::parallel( [&] { for (auto const& u : updates) updatePreResolveDeps(tid, u); }, [&] { for (auto& u : updates) { for (size_t i = 0; i < u.newInfos.size(); ++i) { auto& cinfo = u.newInfos[i]; auto const UNUSED it = index.classInfo.emplace(cinfo->cls->name, cinfo.get()); assertx(it.second); index.allClassInfos.emplace_back(std::move(cinfo)); } } }, [&] { for (auto& u : updates) { for (auto const& p : u.extraMethods) { index.classExtraMethodMap[p.first].insert( p.second.begin(), p.second.end() ); } } }, [&] { for (auto& u : updates) { for (auto const& p : u.closures) { auto& map = index.classClosureMap[p.first]; map.insert(map.end(), p.second.begin(), p.second.end()); } } }, [&] { for (auto& u : updates) { for (auto const c : u.newClosures) index.classes.emplace(c->name, c); } }, [&] { for (auto& u : updates) { for (auto& p : u.newClasses) { auto unit = std::get<1>(p); auto const idx = std::get<2>(p); if (unit->classes.size() <= idx) unit->classes.resize(idx+1); unit->classes[idx] = std::move(std::get<0>(p)); } } } ); } void preresolveTypes(php::Program* program, IndexData& index, ClassInfoData& cid) { auto round = uint32_t{0}; while (true) { if (cid.cqFront == cid.cqBack) { // we've consumed everything where all dependencies are // satisfied. There may still be some pseudo-cycles that can // be broken though. // // eg if A extends B and B' extends A', we'll resolve B and // A', and then end up here, since both A and B' still have // one dependency. But both A and B' can be resolved at this // point for (auto it = cid.depCounts.begin(); it != cid.depCounts.end();) { auto canResolve = true; auto const checkCanResolve = [&] (SString name) { if (canResolve) canResolve = index.classInfo.count(name); }; auto const cls = it->first; if (cls->parentName) checkCanResolve(cls->parentName); for (auto& i : cls->interfaceNames) checkCanResolve(i); for (auto& t : cls->usedTraitNames) checkCanResolve(t); for (auto& t : cls->includedEnumNames) checkCanResolve(t); if (canResolve) { FTRACE(2, "Breaking pseudo-cycle for class {}:{}\n", it->first->name, (void*)it->first); cid.queue[cid.cqBack++] = it->first; it = cid.depCounts.erase(it); cid.hasPseudoCycles = true; } else { ++it; } } if (cid.cqFront == cid.cqBack) break; } auto const workitems = cid.cqBack - cid.cqFront; auto updates = [&] { trace_time trace( "preresolve", folly::sformat("round {} -- {} work items", round, workitems) ); // Aggregate the types together by their Unit. This means only // one thread will be processing a particular Unit at a // time. This lets us avoid locking access to the Unit, and also // keeps the flattening logic deterministic. using UnitGroup = std::pair<const php::Unit*, CompactVector<const php::Class*>>; hphp_fast_map<const php::Unit*, CompactVector<const php::Class*>> group; for (auto idx = cid.cqFront; idx < cid.cqBack; ++idx) { auto const t = cid.queue[idx]; group[t->unit].emplace_back(t); } std::vector<UnitGroup> worklist{group.begin(), group.end()}; return parallel::map( worklist, [&] (UnitGroup& group) { Trace::Bump bumper{ Trace::hhbbc_index, kSystemLibBump, is_systemlib_part(*group.first) }; (void)bumper; std::sort( group.second.begin(), group.second.end(), [&] (const php::Class* a, const php::Class* b) { return strcmp(a->name->data(), b->name->data()) < 0; } ); // NB: Even though we can freely access the Unit, we cannot // modify it in preresolve because other threads might also // be accessing it. ClsPreResolveUpdates updates; updates.nextClassId = group.first->classes.size(); for (auto const t : group.second) { preresolve(program, index, t, updates); } return updates; } ); }(); ++round; cid.cqFront += workitems; trace_time trace("update"); commitPreResolveUpdates(index, cid, updates); } trace_time trace("preresolve clear state"); parallel::for_each( index.allClassInfos, [&] (const std::unique_ptr<ClassInfo>& cinfo) { cinfo->preResolveState.reset(); } ); } } //namespace Index::Index(php::Program* program) : m_data(std::make_unique<IndexData>(this)) { trace_time tracer("create index"); m_data->arrTableBuilder.reset(new ArrayTypeTable::Builder()); add_system_constants_to_index(*m_data); { trace_time trace_add_units("add units to index"); for (auto& u : program->units) { add_unit_to_index(*m_data, *u); } } ClassInfoData cid; { trace_time build_class_info_data("build classinfo data"); buildTypeInfoData(*m_data, cid); } { trace_time preresolve_classes("preresolve classes"); preresolveTypes(program, *m_data, cid); } m_data->funcInfo.resize(program->nextFuncId); // Part of the index building routines happens before the various asserted // index invariants hold. These each may depend on computations from // previous functions, so be careful changing the order here. compute_subclass_list(*m_data); clean_86reifiedinit_methods(*m_data); // uses the base class lists mark_no_override_methods(*m_data); find_magic_methods(*m_data); // uses the subclass lists find_mocked_classes(*m_data); mark_const_props(*m_data); auto const logging = Trace::moduleEnabledRelease(Trace::hhbbc_time, 1); m_data->compute_iface_vtables = std::thread([&] { HphpSessionAndThread _{Treadmill::SessionKind::HHBBC}; auto const enable = logging && !Trace::moduleEnabledRelease(Trace::hhbbc_time, 1); Trace::BumpRelease bumper(Trace::hhbbc_time, -1, enable); compute_iface_vtables(*m_data); } ); define_func_families(*m_data); // AttrNoOverride, iface_vtables, // subclass_list check_invariants(*m_data); mark_no_override_classes(*m_data); trace_time tracer_2("initialize return types"); std::vector<const php::Func*> all_funcs; all_funcs.reserve(m_data->funcs.size() + m_data->methods.size()); for (auto const fn : m_data->funcs) { all_funcs.push_back(fn.second); } for (auto const fn : m_data->methods) { all_funcs.push_back(fn.second); } parallel::for_each( all_funcs, [&] (const php::Func* f) { init_return_type(f); } ); } // Defined here so IndexData is a complete type for the unique_ptr // destructor. Index::~Index() {} ////////////////////////////////////////////////////////////////////// void Index::mark_no_bad_redeclare_props(php::Class& cls) const { /* * Keep a list of properties which have not yet been found to redeclare * anything inequivalently. Start out by putting everything on the list. Then * walk up the inheritance chain, removing collisions as we find them. */ std::vector<php::Prop*> props; for (auto& prop : cls.properties) { if (prop.attrs & (AttrStatic | AttrPrivate)) { // Static and private properties never redeclare anything so need not be // considered. attribute_setter(prop.attrs, true, AttrNoBadRedeclare); continue; } attribute_setter(prop.attrs, false, AttrNoBadRedeclare); props.emplace_back(&prop); } auto currentCls = [&]() -> const ClassInfo* { auto const rcls = resolve_class(&cls); if (rcls.val.left()) return nullptr; return rcls.val.right(); }(); // If there's one more than one resolution for the class, be conservative and // we'll treat everything as possibly redeclaring. if (!currentCls) props.clear(); while (!props.empty()) { auto const parent = currentCls->parent; if (!parent) { // No parent. We're done, so anything left on the prop list is // AttrNoBadRedeclare. for (auto& prop : props) { attribute_setter(prop->attrs, true, AttrNoBadRedeclare); } break; } auto const findParentProp = [&] (SString name) -> const php::Prop* { for (auto& prop : parent->cls->properties) { if (prop.name == name) return &prop; } for (auto& prop : parent->traitProps) { if (prop.name == name) return &prop; } return nullptr; }; // Remove any properties which collide with the current class. auto const propRedeclares = [&] (php::Prop* prop) { auto const pprop = findParentProp(prop->name); if (!pprop) return false; // We found a property being redeclared. Check if the type-hints on // the two are equivalent. auto const equivOneTCPair = [&](const TypeConstraint& tc1, const TypeConstraint& tc2) { // Try the cheap check first, use the index otherwise. Two // type-constraints are equivalent if all the possible values of one // satisfies the other, and vice-versa. if (!tc1.maybeInequivalentForProp(tc2)) return true; return satisfies_constraint( Context{}, lookup_constraint(Context{}, tc1), tc2 ) && satisfies_constraint( Context{}, lookup_constraint(Context{}, tc2), tc1 ); }; auto const equiv = [&] { if (!equivOneTCPair(prop->typeConstraint, pprop->typeConstraint)) { return false; } for (auto ub : prop->ubs) { applyFlagsToUB(ub, prop->typeConstraint); auto foundEquiv = false; for (auto pub : pprop->ubs) { applyFlagsToUB(pub, pprop->typeConstraint); if (equivOneTCPair(ub, pub)) { foundEquiv = true; break; } } if (!foundEquiv) return false; } return true; }; // If the property in the parent is static or private, the property in // the child isn't actually redeclaring anything. Otherwise, if the // type-hints are equivalent, remove this property from further // consideration and mark it as AttrNoBadRedeclare. if ((pprop->attrs & (AttrStatic | AttrPrivate)) || equiv()) { attribute_setter(prop->attrs, true, AttrNoBadRedeclare); } return true; }; props.erase( std::remove_if(props.begin(), props.end(), propRedeclares), props.end() ); currentCls = parent; } auto const possibleOverride = std::any_of( cls.properties.begin(), cls.properties.end(), [&](const php::Prop& prop) { return !(prop.attrs & AttrNoBadRedeclare); } ); // Mark all resolutions of this class as having any possible bad redeclaration // props, even if there's not an unique resolution. auto const it = m_data->classInfo.find(cls.name); if (it != end(m_data->classInfo)) { auto const cinfo = it->second; if (cinfo->cls == &cls) { cinfo->hasBadRedeclareProp = possibleOverride; } } } /* * Rewrite the initial values for any AttrSystemInitialValue properties. If the * properties' type-hint does not admit null values, change the initial value to * one (if possible) to one that is not null. This is only safe to do so if the * property is not redeclared in a derived class or if the redeclaration does * not have a null system provided default value. Otherwise, a property can have * a null value (even if its type-hint doesn't allow it) without the JIT * realizing that its possible. * * Note that this ignores any unflattened traits. This is okay because * properties pulled in from traits which match an already existing property * can't change the initial value. The runtime will clear AttrNoImplicitNullable * on any property pulled from the trait if it doesn't match an existing * property. */ void Index::rewrite_default_initial_values(php::Program& program) const { trace_time tracer("rewrite default initial values"); /* * Use dataflow across the whole program class hierarchy. Start from the * classes which have no derived classes and flow up the hierarchy. We flow * the set of properties which have been assigned a null system provided * default value. If a property with such a null value flows into a class * which declares a property with the same name (and isn't static or private), * than that property is forced to be null as well. */ using PropSet = folly::F14FastSet<SString>; using OutState = folly::F14FastMap<const ClassInfo*, PropSet>; using Worklist = folly::F14FastSet<const ClassInfo*>; OutState outStates; outStates.reserve(m_data->allClassInfos.size()); // List of Class' still to process this iteration using WorkList = std::vector<const ClassInfo*>; using WorkSet = folly::F14FastSet<const ClassInfo*>; WorkList workList; WorkSet workSet; auto const enqueue = [&] (const ClassInfo& cls) { auto const result = workSet.insert(&cls); if (!result.second) return; workList.emplace_back(&cls); }; // Start with all the leaf classes for (auto const& cinfo : m_data->allClassInfos) { auto const isLeaf = [&] { for (auto const& sub : cinfo->subclassList) { if (sub != cinfo.get()) return false; } return true; }(); if (isLeaf) enqueue(*cinfo); } WorkList oldWorkList; int iter = 1; while (!workList.empty()) { FTRACE( 4, "rewrite_default_initial_values round #{}: {} items\n", iter, workList.size() ); ++iter; std::swap(workList, oldWorkList); workList.clear(); workSet.clear(); for (auto const& cinfo : oldWorkList) { // Retrieve the set of properties which are flowing into this Class and // have to be null. auto inState = [&] () -> Optional<PropSet> { PropSet in; for (auto const& sub : cinfo->subclassList) { if (sub == cinfo || sub->parent != cinfo) continue; auto const it = outStates.find(sub); if (it == outStates.end()) return std::nullopt; in.insert(it->second.begin(), it->second.end()); } return in; }(); if (!inState) continue; // Modify the in-state depending on the properties declared on this Class auto const cls = cinfo->cls; for (auto const& prop : cls->properties) { if (prop.attrs & (AttrStatic | AttrPrivate)) { // Private or static properties can't be redeclared inState->erase(prop.name); continue; } // Ignore properties which have actual user provided initial values or // are LateInit. if (!(prop.attrs & AttrSystemInitialValue) || (prop.attrs & AttrLateInit)) { continue; } // Forced to be null, nothing to do if (inState->count(prop.name) > 0) continue; // Its not forced to be null. Find a better default value. If its null // anyways, force any properties this redeclares to be null as well. auto const defaultValue = prop.typeConstraint.defaultValue(); if (defaultValue.m_type == KindOfNull) inState->insert(prop.name); } // Push the in-state to the out-state. auto const result = outStates.emplace(std::make_pair(cinfo, *inState)); if (result.second) { if (cinfo->parent) enqueue(*cinfo->parent); } else { // There shouldn't be cycles in the inheritance tree, so the out state // of Class', once set, should never change. assertx(result.first->second == *inState); } } } // Now that we've processed all the classes, rewrite the property initial // values, unless they are forced to be nullable. for (auto& unit : program.units) { for (auto& c : unit->classes) { if (is_closure(*c)) continue; auto const out = [&] () -> Optional<PropSet> { Optional<PropSet> props; auto const range = m_data->classInfo.equal_range(c->name); for (auto it = range.first; it != range.second; ++it) { if (it->second->cls != c.get()) continue; auto const outStateIt = outStates.find(it->second); if (outStateIt == outStates.end()) return std::nullopt; if (!props) props.emplace(); props->insert(outStateIt->second.begin(), outStateIt->second.end()); } return props; }(); for (auto& prop : c->properties) { auto const nullable = [&] { if (!(prop.attrs & (AttrStatic | AttrPrivate))) { if (!out || out->count(prop.name)) return true; } if (!(prop.attrs & AttrSystemInitialValue)) return false; return prop.typeConstraint.defaultValue().m_type == KindOfNull; }(); attribute_setter(prop.attrs, !nullable, AttrNoImplicitNullable); if (!(prop.attrs & AttrSystemInitialValue)) continue; if (prop.val.m_type == KindOfUninit) { assertx(prop.attrs & AttrLateInit); continue; } prop.val = [&] { if (nullable) return make_tv<KindOfNull>(); // Give the 86reified_prop a special default value to avoid // pessimizing the inferred type (we want it to always be a // vec of a specific size). if (prop.name->isame(s_86reified_prop.get())) { return get_default_value_of_reified_list(c->userAttributes); } return prop.typeConstraint.defaultValue(); }(); } } } } void Index::preinit_bad_initial_prop_values() { trace_time tracer("preinit bad initial prop values"); parallel::for_each( m_data->allClassInfos, [&] (std::unique_ptr<ClassInfo>& cinfo) { if (is_used_trait(*cinfo->cls)) return; cinfo->hasBadInitialPropValues = false; for (auto& prop : const_cast<php::Class*>(cinfo->cls)->properties) { if (prop_might_have_bad_initial_value(*this, *cinfo->cls, prop)) { cinfo->hasBadInitialPropValues = true; prop.attrs = (Attr)(prop.attrs & ~AttrInitialSatisfiesTC); } else { prop.attrs |= AttrInitialSatisfiesTC; } } } ); } void Index::preresolve_type_structures(php::Program& program) { trace_time tracer("pre-resolve type-structures"); // First resolve and update type-aliases. We do this first because // the resolutions may help us resolve the type-constants below // faster. struct TAUpdate { php::TypeAlias* typeAlias; SArray ts; }; auto const taUpdates = parallel::map( program.units, [&] (const std::unique_ptr<php::Unit>& unit) { CompactVector<TAUpdate> updates; for (auto const& typeAlias : unit->typeAliases) { assertx(typeAlias->resolvedTypeStructure.isNull()); if (auto const ts = resolve_type_structure(*this, *typeAlias).sarray()) { updates.emplace_back(TAUpdate{ typeAlias.get(), ts }); } } return updates; } ); parallel::for_each( taUpdates, [&] (const CompactVector<TAUpdate>& updates) { for (auto const& u : updates) { assertx(u.ts->isStatic()); assertx(u.ts->isDictType()); assertx(!u.ts->empty()); u.typeAlias->resolvedTypeStructure = Array::attach(const_cast<ArrayData*>(u.ts)); } } ); // Then do the type-constants. Here we not only resolve the // type-structures, we make a copy of each type-constant for each // class. The reason is that a type-structure may be resolved // differently for each class in the inheritance hierarchy (due to // this::). By making a separate copy for each class, we can resolve // the type-structure specifically for that class. struct CnsUpdate { ClassInfo* to; ClassInfo::ConstIndex from; php::Const cns; }; auto const cnsUpdates = parallel::map( m_data->allClassInfos, [&] (const std::unique_ptr<ClassInfo>& cinfo) { CompactVector<CnsUpdate> updates; for (auto const& kv : cinfo->clsConstants) { auto const& cns = *kv.second; assertx(!cns.resolvedTypeStructure); if (!cns.val.has_value()) continue; if (cns.kind != ConstModifiers::Kind::Type) continue; assertx(tvIsDict(*cns.val)); // If we can resolve it, schedule an update if (auto const resolved = resolve_type_structure(*this, cns, *cinfo->cls).sarray()) { auto newCns = cns; newCns.resolvedTypeStructure = resolved; updates.emplace_back(CnsUpdate{ cinfo.get(), kv.second, newCns }); } else if (cinfo->cls != kv.second.cls) { // Even if we can't, we need to copy it anyways (unless it's // already in it's original location). updates.emplace_back(CnsUpdate{ cinfo.get(), kv.second, cns }); } } return updates; } ); parallel::for_each( cnsUpdates, [&] (const CompactVector<CnsUpdate>& updates) { for (auto const& u : updates) { assertx(u.cns.val.has_value()); assertx(u.cns.kind == ConstModifiers::Kind::Type); if (u.to->cls == u.from.cls) { assertx(u.from.idx < u.to->cls->constants.size()); const_cast<php::Class*>(u.to->cls)->constants[u.from.idx] = u.cns; } else { auto const idx = u.to->cls->constants.size(); const_cast<php::Class*>(u.to->cls)->constants.emplace_back(u.cns); u.to->clsConstants.insert_or_assign( u.cns.name, ClassInfo::ConstIndex{ u.to->cls, (uint32_t)idx } ); } } } ); // Now that everything has been updated, calculate the invariance // for each resolved type-structure. For each class constant, // examine all subclasses and see how the resolved type-structure // changes. parallel::for_each( m_data->allClassInfos, [&] (std::unique_ptr<ClassInfo>& cinfo) { for (auto& cns : const_cast<php::Class*>(cinfo->cls)->constants) { assertx(cns.invariance == php::Const::Invariance::None); if (cns.kind != ConstModifiers::Kind::Type) continue; if (!cns.val.has_value()) continue; if (!cns.resolvedTypeStructure) continue; auto const checkClassname = tvIsString(cns.resolvedTypeStructure->get(s_classname)); // Assume it doesn't change auto invariance = php::Const::Invariance::Same; for (auto const& s : cinfo->subclassList) { assertx(invariance != php::Const::Invariance::None); assertx( IMPLIES(!checkClassname, invariance != php::Const::Invariance::ClassnamePresent) ); if (s == cinfo.get()) continue; auto const it = s->clsConstants.find(cns.name); assertx(it != s->clsConstants.end()); if (it->second.cls != s->cls) continue; auto const& scns = *it->second; // Overridden in some strange way. Be pessimistic. if (!scns.val.has_value() || scns.kind != ConstModifiers::Kind::Type) { invariance = php::Const::Invariance::None; break; } // The resolved type structure in this subclass is not the // same. if (scns.resolvedTypeStructure != cns.resolvedTypeStructure) { if (!scns.resolvedTypeStructure) { // It's not even resolved here, so we can't assume // anything. invariance = php::Const::Invariance::None; break; } // We might still be able to assert that a classname is // always present, or a resolved type structure at least // exists. if (invariance == php::Const::Invariance::Same || invariance == php::Const::Invariance::ClassnamePresent) { invariance = (checkClassname && tvIsString(scns.resolvedTypeStructure->get(s_classname))) ? php::Const::Invariance::ClassnamePresent : php::Const::Invariance::Present; } } } if (invariance != php::Const::Invariance::None) { cns.invariance = invariance; } } } ); } ////////////////////////////////////////////////////////////////////// const CompactVector<const php::Class*>* Index::lookup_closures(const php::Class* cls) const { auto const it = m_data->classClosureMap.find(cls); if (it != end(m_data->classClosureMap)) { return &it->second; } return nullptr; } const hphp_fast_set<const php::Func*>* Index::lookup_extra_methods(const php::Class* cls) const { if (cls->attrs & AttrNoExpandTrait) return nullptr; auto const it = m_data->classExtraMethodMap.find(cls); if (it != end(m_data->classExtraMethodMap)) { return &it->second; } return nullptr; } ////////////////////////////////////////////////////////////////////// res::Class Index::resolve_class(const php::Class* cls) const { auto const it = m_data->classInfo.find(cls->name); if (it != end(m_data->classInfo)) { auto const cinfo = it->second; if (cinfo->cls == cls) { return res::Class { cinfo }; } } // We know its a class, not an enum or type alias, so return // by name return res::Class { cls->name.get() }; } Optional<res::Class> Index::resolve_class(Context ctx, SString clsName) const { clsName = normalizeNS(clsName); if (ctx.cls) { if (ctx.cls->name->isame(clsName)) { return resolve_class(ctx.cls); } if (ctx.cls->parentName && ctx.cls->parentName->isame(clsName)) { if (auto const parent = resolve_class(ctx.cls).parent()) return parent; } } auto const it = m_data->classInfo.find(clsName); if (it != end(m_data->classInfo)) { auto const tinfo = it->second; /* * If the preresolved ClassInfo is Unique we can give it out. */ assertx(tinfo->phpType()->attrs & AttrUnique); if (debug && m_data->typeAliases.count(clsName)) { std::fprintf(stderr, "non unique \"unique\" class: %s\n", tinfo->phpType()->name->data()); auto const ta = m_data->typeAliases.find(clsName); if (ta != end(m_data->typeAliases)) { std::fprintf(stderr, " and type-alias %s\n", ta->second->name->data()); } always_assert(false); } return res::Class { tinfo }; } // We refuse to have name-only resolutions of enums and typeAliases, // so that all name only resolutions can be treated as classes. if (!m_data->enums.count(clsName) && !m_data->typeAliases.count(clsName)) { return res::Class { clsName }; } return std::nullopt; } Optional<res::Class> Index::selfCls(const Context& ctx) const { if (!ctx.cls || is_used_trait(*ctx.cls)) return std::nullopt; return resolve_class(ctx.cls); } Optional<res::Class> Index::parentCls(const Context& ctx) const { if (!ctx.cls || !ctx.cls->parentName) return std::nullopt; if (auto const parent = resolve_class(ctx.cls).parent()) return parent; return resolve_class(ctx, ctx.cls->parentName); } const php::TypeAlias* Index::lookup_type_alias(SString name) const { auto const it = m_data->typeAliases.find(name); if (it == m_data->typeAliases.end()) return nullptr; return it->second; } Index::ResolvedInfo<Optional<res::Class>> Index::resolve_type_name(SString inName) const { auto const res = resolve_type_name_internal(inName); using Ret = Optional<res::Class>; auto const val = match<Ret>( res.value, [&] (boost::blank) { return Ret{}; }, [&] (SString s) { return res::Class{s}; }, [&] (ClassInfo* c) { return res::Class{c}; } ); return { res.type, res.nullable, val }; } Index::ResolvedInfo<boost::variant<boost::blank,SString,ClassInfo*>> Index::resolve_type_name_internal(SString inName) const { Optional<hphp_fast_set<const void*>> seen; auto nullable = false; auto name = inName; for (unsigned i = 0; ; ++i) { name = normalizeNS(name); auto const cls_it = m_data->classInfo.find(name); if (cls_it != end(m_data->classInfo)) { auto const cinfo = cls_it->second; assertx(cinfo->cls->attrs & AttrUnique); if (!(cinfo->cls->attrs & AttrEnum)) { return { AnnotType::Object, nullable, cinfo }; } auto const& tc = cinfo->cls->enumBaseTy; assertx(!tc.isNullable()); if (tc.type() != AnnotType::Object) { auto const type = tc.type() == AnnotType::Mixed ? AnnotType::ArrayKey : tc.type(); return { type, nullable, tc.typeName() }; } name = tc.typeName(); } else { auto const ta_it = m_data->typeAliases.find(name); if (ta_it == end(m_data->typeAliases)) break; auto const ta = ta_it->second; assertx(ta->attrs & AttrUnique); nullable = nullable || ta->nullable; if (ta->type != AnnotType::Object) { return { ta->type, nullable, ta->value.get() }; } name = ta->value; } // deal with cycles. Since we don't expect to // encounter them, just use a counter until we hit a chain length // of 10, then start tracking the names we resolve. if (i == 10) { seen.emplace(); seen->insert(name); } else if (i > 10) { if (!seen->insert(name).second) { return { AnnotType::Object, false, {} }; } } } return { AnnotType::Object, nullable, name }; } struct Index::ConstraintResolution { /* implicit */ ConstraintResolution(Type type) : type{std::move(type)} , maybeMixed{false} {} ConstraintResolution(Optional<Type> type, bool maybeMixed) : type{std::move(type)} , maybeMixed{maybeMixed} {} Optional<Type> type; bool maybeMixed; }; Index::ConstraintResolution Index::resolve_named_type( const Context& ctx, SString name, const Type& candidate) const { auto const res = resolve_type_name_internal(name); if (res.nullable && candidate.subtypeOf(BInitNull)) return TInitNull; if (res.type == AnnotType::Object) { auto resolve = [&] (const res::Class& rcls) -> Optional<Type> { if (!interface_supports_non_objects(rcls.name()) || candidate.subtypeOf(BOptObj)) { return subObj(rcls); } if (candidate.subtypeOf(BOptVec)) { if (interface_supports_arrlike(rcls.name())) return TVec; } else if (candidate.subtypeOf(BOptDict)) { if (interface_supports_arrlike(rcls.name())) return TDict; } else if (candidate.subtypeOf(BOptKeyset)) { if (interface_supports_arrlike(rcls.name())) return TKeyset; } else if (candidate.subtypeOf(BOptStr)) { if (interface_supports_string(rcls.name())) return TStr; } else if (candidate.subtypeOf(BOptInt)) { if (interface_supports_int(rcls.name())) return TInt; } else if (candidate.subtypeOf(BOptDbl)) { if (interface_supports_double(rcls.name())) return TDbl; } return std::nullopt; }; auto const val = match<Either<SString, ClassInfo*>>( res.value, [&] (boost::blank) { return nullptr; }, [&] (SString s) { return s; }, [&] (ClassInfo* c) { return c; } ); if (val.isNull()) return ConstraintResolution{ std::nullopt, true }; auto ty = resolve(res::Class { val }); if (ty && res.nullable) *ty = opt(std::move(*ty)); return ConstraintResolution{ std::move(ty), false }; } return get_type_for_annotated_type(ctx, res.type, res.nullable, boost::get<SString>(res.value), candidate); } std::pair<res::Class,php::Class*> Index::resolve_closure_class(Context ctx, int32_t idx) const { auto const cls = ctx.unit->classes[idx].get(); auto const rcls = resolve_class(cls); // Closure classes must be unique and defined in the unit that uses // the CreateCl opcode, so resolution must succeed. always_assert_flog( rcls.resolved(), "A Closure class ({}) failed to resolve", cls->name ); return { rcls, cls }; } res::Class Index::builtin_class(SString name) const { auto const rcls = resolve_class(Context {}, name); always_assert_flog( rcls.has_value() && rcls->val.right() && (rcls->val.right()->cls->attrs & AttrBuiltin), "A builtin class ({}) failed to resolve", name->data() ); return *rcls; } res::Func Index::resolve_method(Context ctx, Type clsType, SString name) const { auto name_only = [&] { return res::Func { res::Func::MethodName { name } }; }; if (!is_specialized_cls(clsType)) { return name_only(); } auto const dcls = dcls_of(clsType); auto const cinfo = dcls.cls.val.right(); if (!cinfo) return name_only(); // Classes may have more method families than methods. Any such // method families are guaranteed to all be public so we can do this // lookup as a last gasp before resorting to name_only(). auto const find_extra_method = [&] { auto singleMethIt = cinfo->singleMethodFamilies.find(name); if (singleMethIt != cinfo->singleMethodFamilies.end()) { return res::Func { singleMethIt->second }; } auto methIt = cinfo->methodFamilies.find(name); if (methIt == end(cinfo->methodFamilies)) return name_only(); // If there was a sole implementer we can resolve to a single method, even // if the method was not declared on the interface itself. assertx(methIt->second->possibleFuncs().size() > 1); return res::Func { methIt->second }; }; // Interfaces *only* have the extra methods defined for all // subclasses if (cinfo->cls->attrs & AttrInterface) return find_extra_method(); /* * Whether or not the context class has a private method with the * same name as the method we're trying to call. */ auto const contextMayHavePrivateWithSameName = folly::lazy([&]() -> bool { if (!ctx.cls) return false; auto const cls_it = m_data->classInfo.find(ctx.cls->name); if (cls_it == end(m_data->classInfo)) { // This class had no pre-resolved ClassInfos, which means it // always fatals in any way it could be defined, so it doesn't // matter what we return here (as all methods in the context // class are unreachable code). return true; } // Because of traits, each instantiation of the class could have // different private methods; we need to check them all. auto const iter = cls_it->second->methods.find(name); if (iter != end(cls_it->second->methods) && iter->second.attrs & AttrPrivate && iter->second.topLevel) { return true; } return false; }); /* * Look up the method in the target class. */ auto const methIt = cinfo->methods.find(name); if (methIt == end(cinfo->methods)) return find_extra_method(); auto const ftarget = methIt->second.func; // We need to revisit the hasPrivateAncestor code if we start being // able to look up methods on interfaces (currently they have empty // method tables). assertx(!(cinfo->cls->attrs & AttrInterface)); /* * If our candidate method has a private ancestor, unless it is * defined on this class, we need to make sure we don't erroneously * resolve the overriding method if the call is coming from the * context the defines the private method. * * For now this just gives up if the context and the callee class * could be related and the context defines a private of the same * name. (We should actually try to resolve that method, though.) */ if (methIt->second.hasPrivateAncestor && ctx.cls && ctx.cls != ftarget->cls) { if (could_be_related(ctx.cls, cinfo->cls)) { if (contextMayHavePrivateWithSameName()) { return name_only(); } } } auto resolve = [&] { create_func_info(*m_data, ftarget); return res::Func { mteFromIt(methIt) }; }; switch (dcls.type) { case DCls::Exact: return resolve(); case DCls::Sub: if (methIt->second.attrs & AttrNoOverride) { return resolve(); } if (!options.FuncFamilies) return name_only(); { auto const singleFamIt = cinfo->singleMethodFamilies.find(name); if (singleFamIt != cinfo->singleMethodFamilies.end()) { return res::Func { singleFamIt->second }; } auto const famIt = cinfo->methodFamilies.find(name); if (famIt == end(cinfo->methodFamilies)) return name_only(); assertx(famIt->second->possibleFuncs().size() > 1); return res::Func { famIt->second }; } } not_reached(); } Optional<res::Func> Index::resolve_ctor(Context /*ctx*/, res::Class rcls, bool exact) const { auto const cinfo = rcls.val.right(); if (!cinfo) return std::nullopt; if (cinfo->cls->attrs & (AttrInterface|AttrTrait)) return std::nullopt; auto const cit = cinfo->methods.find(s_construct.get()); if (cit == end(cinfo->methods)) return std::nullopt; auto const ctor = mteFromIt(cit); if (exact || ctor->second.attrs & AttrNoOverride) { create_func_info(*m_data, ctor->second.func); return res::Func { ctor }; } if (!options.FuncFamilies) return std::nullopt; auto const singleFamIt = cinfo->singleMethodFamilies.find(s_construct.get()); if (singleFamIt != cinfo->singleMethodFamilies.end()) { return res::Func { singleFamIt->second}; } auto const famIt = cinfo->methodFamilies.find(s_construct.get()); if (famIt == end(cinfo->methodFamilies)) return std::nullopt; assertx(famIt->second->possibleFuncs().size() > 1); return res::Func { famIt->second }; } res::Func Index::resolve_func_helper(const php::Func* func, SString name) const { auto name_only = [&] (bool renamable) { return res::Func { res::Func::FuncName { name, renamable } }; }; // no resolution if (func == nullptr) return name_only(false); // single resolution, in whole-program mode, that's it assertx(func->attrs & AttrUnique); return do_resolve(func); } res::Func Index::resolve_func(Context /*ctx*/, SString name) const { name = normalizeNS(name); auto const it = m_data->funcs.find(name); return resolve_func_helper((it != end(m_data->funcs)) ? it->second : nullptr, name); } /* * Gets a type for the constraint. * * If getSuperType is true, the type could be a super-type of the * actual type constraint (eg TCell). Otherwise its guaranteed that * for any t, t.subtypeOf(get_type_for_constraint<false>(ctx, tc, t) * implies t would pass the constraint. * * The candidate type is used to disambiguate; if we're applying a * Traversable constraint to a TObj, we should return * subObj(Traversable). If we're applying it to an Array, we should * return Array. */ template<bool getSuperType> Type Index::get_type_for_constraint(Context ctx, const TypeConstraint& tc, const Type& candidate) const { assertx(IMPLIES(!tc.isCheckable(), tc.isMixed() || (tc.isUpperBound() && RuntimeOption::EvalEnforceGenericsUB == 0))); if (getSuperType) { /* * Soft hints (@Foo) are not checked. * Also upper-bound type hints are not checked when they do not error. */ if (tc.isSoft() || (RuntimeOption::EvalEnforceGenericsUB < 2 && tc.isUpperBound())) { return TCell; } } auto const res = get_type_for_annotated_type( ctx, tc.type(), tc.isNullable(), tc.typeName(), candidate ); if (res.type) return *res.type; // If the type constraint might be mixed, then the value could be // uninit. Any other type constraint implies TInitCell. return getSuperType ? (res.maybeMixed ? TCell : TInitCell) : TBottom; } bool Index::prop_tc_maybe_unenforced(const php::Class& propCls, const TypeConstraint& tc) const { assertx(tc.validForProp()); if (RuntimeOption::EvalCheckPropTypeHints <= 2) return true; if (!tc.isCheckable()) return true; if (tc.isSoft()) return true; if (tc.isUpperBound() && RuntimeOption::EvalEnforceGenericsUB < 2) { return true; } auto const res = get_type_for_annotated_type( Context { nullptr, nullptr, &propCls }, tc.type(), tc.isNullable(), tc.typeName(), TCell ); return res.maybeMixed; } Index::ConstraintResolution Index::get_type_for_annotated_type( Context ctx, AnnotType annot, bool nullable, SString name, const Type& candidate) const { if (candidate.subtypeOf(BInitNull) && nullable) { return TInitNull; } auto mainType = [&]() -> ConstraintResolution { switch (getAnnotMetaType(annot)) { case AnnotMetaType::Precise: { auto const dt = getAnnotDataType(annot); switch (dt) { case KindOfNull: return TNull; case KindOfBoolean: return TBool; case KindOfInt64: return TInt; case KindOfDouble: return TDbl; case KindOfPersistentString: case KindOfString: return TStr; case KindOfPersistentVec: case KindOfVec: return TVec; case KindOfPersistentDict: case KindOfDict: return TDict; case KindOfPersistentKeyset: case KindOfKeyset: return TKeyset; case KindOfResource: return TRes; case KindOfClsMeth: return TClsMeth; case KindOfObject: return resolve_named_type(ctx, name, candidate); case KindOfUninit: case KindOfRFunc: case KindOfFunc: case KindOfRClsMeth: case KindOfClass: case KindOfLazyClass: always_assert_flog(false, "Unexpected DataType"); break; } break; } case AnnotMetaType::Mixed: /* * Here we handle "mixed", typevars, and some other ignored * typehints (ex. "(function(..): ..)" typehints). */ return { TCell, true }; case AnnotMetaType::Nothing: case AnnotMetaType::NoReturn: return TBottom; case AnnotMetaType::Nonnull: if (candidate.subtypeOf(BInitNull)) return TBottom; if (!candidate.couldBe(BInitNull)) return candidate; return unopt(candidate); case AnnotMetaType::This: if (auto s = selfCls(ctx)) return setctx(subObj(*s)); break; case AnnotMetaType::Callable: break; case AnnotMetaType::Number: return TNum; case AnnotMetaType::ArrayKey: if (candidate.subtypeOf(BInt)) return TInt; if (candidate.subtypeOf(BStr)) return TStr; return TArrKey; case AnnotMetaType::VecOrDict: if (candidate.subtypeOf(BVec)) return TVec; if (candidate.subtypeOf(BDict)) return TDict; return union_of(TVec, TDict); case AnnotMetaType::ArrayLike: if (candidate.subtypeOf(BVec)) return TVec; if (candidate.subtypeOf(BDict)) return TDict; if (candidate.subtypeOf(BKeyset)) return TKeyset; return TArrLike; case AnnotMetaType::Classname: if (candidate.subtypeOf(BStr)) return TStr; if (!RuntimeOption::EvalClassnameNotices) { if (candidate.subtypeOf(BCls)) return TCls; if (candidate.subtypeOf(BLazyCls)) return TLazyCls; } } return ConstraintResolution{ std::nullopt, false }; }(); if (mainType.type && nullable) { if (mainType.type->subtypeOf(BBottom)) { if (candidate.couldBe(BInitNull)) { mainType.type = TInitNull; } } else if (!mainType.type->couldBe(BInitNull)) { mainType.type = opt(*mainType.type); } } return mainType; } Type Index::lookup_constraint(Context ctx, const TypeConstraint& tc, const Type& t) const { return get_type_for_constraint<true>(ctx, tc, t); } bool Index::satisfies_constraint(Context ctx, const Type& t, const TypeConstraint& tc) const { auto const tcType = get_type_for_constraint<false>(ctx, tc, t); return t.moreRefined(tcType); } bool Index::could_have_reified_type(Context ctx, const TypeConstraint& tc) const { if (ctx.func->isClosureBody) { for (auto i = ctx.func->params.size(); i < ctx.func->locals.size(); ++i) { auto const name = ctx.func->locals[i].name; if (!name) return false; // named locals do not appear after unnamed local if (isMangledReifiedGenericInClosure(name)) return true; } return false; } if (!tc.isObject()) return false; auto const name = tc.typeName(); auto const resolved = resolve_type_name_internal(name); if (resolved.type != AnnotType::Object) return false; auto const val = match<Either<SString, ClassInfo*>>( resolved.value, [&] (boost::blank) { return nullptr; }, [&] (SString s) { return s; }, [&] (ClassInfo* c) { return c; } ); res::Class rcls{val}; return rcls.couldHaveReifiedGenerics(); } Optional<bool> Index::supports_async_eager_return(res::Func rfunc) const { auto const supportsAER = [] (const php::Func* func) { // Async functions always support async eager return. if (func->isAsync && !func->isGenerator) return true; // No other functions support async eager return yet. return false; }; return match<Optional<bool>>( rfunc.val, [&](res::Func::FuncName) { return std::nullopt; }, [&](res::Func::MethodName) { return std::nullopt; }, [&](FuncInfo* finfo) { return supportsAER(finfo->func); }, [&](const MethTabEntryPair* mte) { return supportsAER(mte->second.func); }, [&](FuncFamily* fam) -> Optional<bool> { auto ret = Optional<bool>{}; for (auto const pf : fam->possibleFuncs()) { // Abstract functions are never called. if (pf->second.attrs & AttrAbstract) continue; auto const val = supportsAER(pf->second.func); if (ret && *ret != val) return std::nullopt; ret = val; } return ret; }); } bool Index::is_effect_free(const php::Func* func) const { return func_info(*m_data, func)->effectFree; } bool Index::is_effect_free(res::Func rfunc) const { return match<bool>( rfunc.val, [&](res::Func::FuncName) { return false; }, [&](res::Func::MethodName) { return false; }, [&](FuncInfo* finfo) { return finfo->effectFree; }, [&](const MethTabEntryPair* mte) { return func_info(*m_data, mte->second.func)->effectFree; }, [&](FuncFamily* fam) { for (auto const mte : fam->possibleFuncs()) { if (!func_info(*m_data, mte->second.func)->effectFree) return false; } return true; } ); } ClsConstLookupResult<> Index::lookup_class_constant(Context ctx, const Type& cls, const Type& name) const { ITRACE(4, "lookup_class_constant: ({}) {}::{}\n", show(ctx), show(cls), show(name)); Trace::Indent _; using R = ClsConstLookupResult<>; auto const conservative = [] { ITRACE(4, "conservative\n"); return R{ TInitCell, TriBool::Maybe, true }; }; auto const notFound = [] { ITRACE(4, "not found\n"); return R{ TBottom, TriBool::No, false }; }; if (!is_specialized_cls(cls)) return conservative(); auto const dcls = dcls_of(cls); if (dcls.cls.val.left()) return conservative(); auto const cinfo = dcls.cls.val.right(); // We could easy support the case where we don't know the constant // name, but know the class (like we do for properties), by unioning // together all possible constants. However it very rarely happens, // but when it does, the set of constants to union together can be // huge and it becomes very expensive. if (!is_specialized_string(name)) return conservative(); auto const sname = sval_of(name); // If this lookup is safe to cache. Some classes can have a huge // number of subclasses and unioning together all possible constants // can become very expensive. We can aleviate some of this expense // by caching results. We cannot cache a result we use 86cinit // analysis since that can change. auto cachable = true; auto const process = [&] (const ClassInfo* ci) { ITRACE(4, "{}:\n", ci->cls->name); Trace::Indent _; // Does the constant exist on this class? auto const it = ci->clsConstants.find(sname); if (it == ci->clsConstants.end()) return notFound(); // Is it a value and is it non-abstract (we only deal with // concrete constants). auto const& cns = *it->second.get(); if (cns.kind != ConstModifiers::Kind::Value) return notFound(); if (!cns.val.has_value()) return notFound(); // Determine the constant's value and return it auto const r = [&] { if (cns.val->m_type == KindOfUninit) { // Constant is defined by a 86cinit. Use the result from // analysis and add a dependency. We cannot cache in this // case. cachable = false; if (ctx.func) { auto const cinit = cns.cls->methods.back().get(); assertx(cinit->name == s_86cinit.get()); add_dependency(*m_data, cinit, ctx, Dep::ClsConst); } ITRACE(4, "(dynamic)\n"); auto const it = m_data->clsConstTypes.find(std::make_pair(cns.cls, cns.name)); auto const type = (it == m_data->clsConstTypes.end()) ? TInitCell : it->second.type; return R{ type, TriBool::Yes, true }; } // Fully resolved constant with a known value return R{ from_cell(*cns.val), TriBool::Yes, false }; }(); ITRACE(4, "-> {}\n", show(r)); return r; }; // If we know the exact class, just look up the constant and we're // done. Otherwise, loop over all possible subclasses and do the // lookup for each. switch (dcls.type) { case DCls::Sub: { // Before anything, look up this entry in the cache. We don't // bother with the cache for the DCls::Exact case because it's // quick and there's little point. if (auto const it = m_data->clsConstLookupCache.find(std::make_pair(cinfo->cls, sname)); it != m_data->clsConstLookupCache.end()) { ITRACE(4, "cache hit: {}\n", show(it->second)); return it->second; } Optional<R> result; for (auto const sub : cinfo->subclassList) { if (result) { ITRACE(5, "-> {}\n", show(*result)); } auto r = process(sub); if (!result) { result.emplace(std::move(r)); } else { *result |= r; } } // This can happen if cls is an interface with no // implementations. if (!result) return notFound(); // Save this for future lookups if we can if (cachable) { m_data->clsConstLookupCache.emplace( std::make_pair(cinfo->cls, sname), *result ); } ITRACE(4, "-> {}\n", show(*result)); return *result; } case DCls::Exact: return process(cinfo); } always_assert(false); } ClsTypeConstLookupResult<> Index::lookup_class_type_constant( const Type& cls, const Type& name, const ClsTypeConstLookupResolver& resolver) const { ITRACE(4, "lookup_class_type_constant: {}::{}\n", show(cls), show(name)); Trace::Indent _; using R = ClsTypeConstLookupResult<>; auto const conservative = [] { ITRACE(4, "conservative\n"); return R{ TypeStructureResolution { TSDictN, true }, TriBool::Maybe, TriBool::Maybe }; }; auto const notFound = [] { ITRACE(4, "not found\n"); return R { TypeStructureResolution { TBottom, false }, TriBool::No, TriBool::No }; }; // Unlike lookup_class_constant, we distinguish abstract from // not-found, as the runtime sometimes treats them differently. auto const abstract = [] { ITRACE(4, "abstract\n"); return R { TypeStructureResolution { TBottom, false }, TriBool::No, TriBool::Yes }; }; if (!is_specialized_cls(cls)) return conservative(); auto const dcls = dcls_of(cls); if (dcls.cls.val.left()) return conservative(); auto const cinfo = dcls.cls.val.right(); // As in lookup_class_constant, we could handle this, but it's not // worth it. if (!is_specialized_string(name)) return conservative(); auto const sname = sval_of(name); auto const process = [&] (const ClassInfo* ci) { ITRACE(4, "{}:\n", ci->cls->name); Trace::Indent _; // Does the type constant exist on this class? auto const it = ci->clsConstants.find(sname); if (it == ci->clsConstants.end()) return notFound(); // Is it an actual non-abstract type-constant? auto const& cns = *it->second; if (cns.kind != ConstModifiers::Kind::Type) return notFound(); if (!cns.val.has_value()) return abstract(); assertx(tvIsDict(*cns.val)); ITRACE(4, "({}) {}\n", cns.cls->name, show(dict_val(val(*cns.val).parr))); // If we've been given a resolver, use it. Otherwise resolve it in // the normal way. auto resolved = resolver ? resolver(cns, *ci->cls) : resolve_type_structure(*this, cns, *ci->cls); // The result of resolve_type_structure isn't, in general, // static. However a type-constant will always be, so force that // here. assertx(resolved.type.is(BBottom) || resolved.type.couldBe(BUnc)); resolved.type &= TUnc; auto const r = R{ std::move(resolved), TriBool::Yes, TriBool::No }; ITRACE(4, "-> {}\n", show(r)); return r; }; // If we know the exact class, just look up the constant and we're // done. Otherwise, loop over all possible subclasses and do the // lookup for each. switch (dcls.type) { case DCls::Sub: { Optional<R> result; for (auto const sub : cinfo->subclassList) { if (result) { ITRACE(5, "-> {}\n", show(*result)); } auto r = process(sub); if (!result) { result.emplace(std::move(r)); } else { *result |= r; } } // This can happen if cls is an interface with no // implementations. if (!result) return notFound(); ITRACE(4, "-> {}\n", show(*result)); return *result; } case DCls::Exact: return process(cinfo); } always_assert(false); } Type Index::lookup_constant(Context ctx, SString cnsName) const { auto iter = m_data->constants.find(cnsName); if (iter == end(m_data->constants)) { return TInitCell; } auto constant = iter->second; if (type(constant->val) != KindOfUninit) { return from_cell(constant->val); } auto const func_name = Constant::funcNameFromName(cnsName); assertx(func_name && "func_name will never be nullptr"); auto rfunc = resolve_func(ctx, func_name); return lookup_return_type(ctx, nullptr, rfunc, Dep::ConstVal); } bool Index::func_depends_on_arg(const php::Func* func, int arg) const { auto const& finfo = *func_info(*m_data, func); return arg >= finfo.unusedParams.size() || !finfo.unusedParams.test(arg); } Type Index::lookup_foldable_return_type(Context ctx, const CallContext& calleeCtx) const { auto const func = calleeCtx.callee; auto const& ctxType = calleeCtx.context; constexpr auto max_interp_nexting_level = 2; static __thread uint32_t interp_nesting_level; static __thread Context base_ctx; // Don't fold functions when staticness mismatches if ((func->attrs & AttrStatic) && ctxType.couldBe(TObj)) return TInitCell; if (!(func->attrs & AttrStatic) && ctxType.couldBe(TCls)) return TInitCell; auto const& finfo = *func_info(*m_data, func); if (finfo.effectFree && is_scalar(finfo.returnTy)) { return finfo.returnTy; } auto showArgs DEBUG_ONLY = [] (const CompactVector<Type>& a) { std::string ret, sep; for (auto& arg : a) { folly::format(&ret, "{}{}", sep, show(arg)); sep = ","; }; return ret; }; { ContextRetTyMap::const_accessor acc; if (m_data->foldableReturnTypeMap.find(acc, calleeCtx)) { FTRACE_MOD( Trace::hhbbc, 4, "Found foldableReturnType for {}{}{} with args {} (hash: {})\n", func->cls ? func->cls->name : staticEmptyString(), func->cls ? "::" : "", func->name, showArgs(calleeCtx.args), CallContextHashCompare{}.hash(calleeCtx)); assertx(is_scalar(acc->second)); return acc->second; } } if (frozen()) { FTRACE_MOD( Trace::hhbbc, 4, "MISSING: foldableReturnType for {}{}{} with args {} (hash: {})\n", func->cls ? func->cls->name : staticEmptyString(), func->cls ? "::" : "", func->name, showArgs(calleeCtx.args), CallContextHashCompare{}.hash(calleeCtx)); return TInitCell; } if (!interp_nesting_level) { base_ctx = ctx; } else if (interp_nesting_level > max_interp_nexting_level) { add_dependency(*m_data, func, base_ctx, Dep::InlineDepthLimit); return TInitCell; } auto const contextType = [&] { ++interp_nesting_level; SCOPE_EXIT { --interp_nesting_level; }; auto const wf = php::WideFunc::cns(func); auto const fa = analyze_func_inline( *this, AnalysisContext { func->unit, wf, func->cls }, calleeCtx.context, calleeCtx.args, CollectionOpts::EffectFreeOnly ); return fa.effectFree ? fa.inferredReturn : TInitCell; }(); if (!is_scalar(contextType)) { return TInitCell; } ContextRetTyMap::accessor acc; if (m_data->foldableReturnTypeMap.insert(acc, calleeCtx)) { acc->second = contextType; } else { // someone beat us to it assertx(acc->second == contextType); } return contextType; } Type Index::lookup_return_type(Context ctx, MethodsInfo* methods, res::Func rfunc, Dep dep) const { return match<Type>( rfunc.val, [&] (res::Func::FuncName) { return TInitCell; }, [&] (res::Func::MethodName) { return TInitCell; }, [&] (FuncInfo* finfo) { add_dependency(*m_data, finfo->func, ctx, dep); return unctx(finfo->returnTy); }, [&] (const MethTabEntryPair* mte) { if (methods) { if (auto ret = methods->lookupReturnType(*mte->second.func)) { return unctx(std::move(*ret)); } } add_dependency(*m_data, mte->second.func, ctx, dep); auto const finfo = func_info(*m_data, mte->second.func); if (!finfo->func) return TInitCell; return unctx(finfo->returnTy); }, [&] (FuncFamily* fam) { add_dependency(*m_data, fam, ctx, dep); return fam->m_returnTy.get( [&] { auto ret = TBottom; for (auto const pf : fam->possibleFuncs()) { auto const finfo = func_info(*m_data, pf->second.func); if (!finfo->func) return TInitCell; ret |= unctx(finfo->returnTy); if (!ret.strictSubtypeOf(BInitCell)) return ret; } return ret; } ); } ); } Type Index::lookup_return_type(Context caller, MethodsInfo* methods, const CompactVector<Type>& args, const Type& context, res::Func rfunc, Dep dep) const { return match<Type>( rfunc.val, [&] (res::Func::FuncName) { return lookup_return_type(caller, methods, rfunc, dep); }, [&] (res::Func::MethodName) { return lookup_return_type(caller, methods, rfunc, dep); }, [&] (FuncInfo* finfo) { add_dependency(*m_data, finfo->func, caller, dep); return context_sensitive_return_type( *m_data, { finfo->func, args, context }, finfo->returnTy ); }, [&] (const MethTabEntryPair* mte) { auto const finfo = func_info(*m_data, mte->second.func); if (!finfo->func) return TInitCell; auto returnType = [&] { if (methods) { if (auto ret = methods->lookupReturnType(*mte->second.func)) { return *ret; } } add_dependency(*m_data, mte->second.func, caller, dep); return finfo->returnTy; }(); return context_sensitive_return_type( *m_data, { finfo->func, args, context }, std::move(returnType) ); }, [&] (FuncFamily* fam) { add_dependency(*m_data, fam, caller, dep); auto ret = fam->m_returnTy.get( [&] { auto ret = TBottom; for (auto const pf : fam->possibleFuncs()) { auto const finfo = func_info(*m_data, pf->second.func); if (!finfo->func) return TInitCell; ret |= finfo->returnTy; if (!ret.strictSubtypeOf(BInitCell)) return ret; } return ret; } ); return return_with_context(std::move(ret), context); } ); } CompactVector<Type> Index::lookup_closure_use_vars(const php::Func* func, bool move) const { assertx(func->isClosureBody); auto const numUseVars = closure_num_use_vars(func); if (!numUseVars) return {}; auto const it = m_data->closureUseVars.find(func->cls); if (it == end(m_data->closureUseVars)) { return CompactVector<Type>(numUseVars, TCell); } if (move) return std::move(it->second); return it->second; } std::pair<Type, size_t> Index::lookup_return_type_raw(const php::Func* f) const { auto it = func_info(*m_data, f); if (it->func) { assertx(it->func == f); return { it->returnTy, it->returnRefinements }; } return { TInitCell, 0 }; } bool Index::lookup_this_available(const php::Func* f) const { return !(f->cls->attrs & AttrTrait) && !(f->attrs & AttrStatic); } Optional<uint32_t> Index::lookup_num_inout_params( Context, res::Func rfunc ) const { return match<Optional<uint32_t>>( rfunc.val, [&] (res::Func::FuncName s) -> Optional<uint32_t> { if (s.renamable) return std::nullopt; auto const it = m_data->funcs.find(s.name); return it != end(m_data->funcs) ? func_num_inout(it->second) : 0; }, [&] (res::Func::MethodName s) -> Optional<uint32_t> { auto const it = m_data->method_inout_params_by_name.find(s.name); if (it == end(m_data->method_inout_params_by_name)) { // There was no entry, so no method by this name takes a parameter // by inout. return 0; } auto const pair = m_data->methods.equal_range(s.name); return num_inout_from_set(folly::range(pair.first, pair.second)); }, [&] (FuncInfo* finfo) { return func_num_inout(finfo->func); }, [&] (const MethTabEntryPair* mte) { return func_num_inout(mte->second.func); }, [&] (FuncFamily* fam) -> Optional<uint32_t> { return fam->m_numInOut; } ); } PrepKind Index::lookup_param_prep(Context /*ctx*/, res::Func rfunc, uint32_t paramId) const { return match<PrepKind>( rfunc.val, [&] (res::Func::FuncName s) { if (s.renamable) return PrepKind{TriBool::Maybe, TriBool::Maybe}; auto const it = m_data->funcs.find(s.name); return it != end(m_data->funcs) ? func_param_prep(it->second, paramId) : func_param_prep_func_doesnt_exist(); }, [&] (res::Func::MethodName s) { auto const couldHaveBit = [&] (auto map, auto index) { auto const it = map.find(s.name); if (it == end(map)) return false; if (index < sizeof(it->second) * CHAR_BIT && !((it->second >> index) & 1)) { return false; } return true; }; if (!couldHaveBit(m_data->method_inout_params_by_name, paramId) && !couldHaveBit(m_data->method_readonly_by_name, paramId + 2)) { return PrepKind{TriBool::No, TriBool::No}; } auto const pair = m_data->methods.equal_range(s.name); return prep_kind_from_set(folly::range(pair.first, pair.second), paramId); }, [&] (FuncInfo* finfo) { return func_param_prep(finfo->func, paramId); }, [&] (const MethTabEntryPair* mte) { return func_param_prep(mte->second.func, paramId); }, [&] (FuncFamily* fam) { return prep_kind_from_set(fam->possibleFuncs(), paramId); } ); } TriBool Index::lookup_return_readonly( Context, res::Func rfunc ) const { return match<TriBool>( rfunc.val, [&] (res::Func::FuncName s) { if (s.renamable) return TriBool::Maybe; auto const it = m_data->funcs.find(s.name); return it != end(m_data->funcs) ? yesOrNo(it->second->isReadonlyReturn) : TriBool::No; // if the function doesnt exist, we will error anyway }, [&] (res::Func::MethodName s) { auto const it = m_data->method_readonly_by_name.find(s.name); if (it == end(m_data->method_readonly_by_name)) { // There was no entry, so no method by this name returns readonly. return TriBool::No; } if (!(it->second & 1)) { // No method of this name returns readonly. return TriBool::No; } auto const pair = m_data->methods.equal_range(s.name); return is_readonly_return_from_set(folly::range(pair.first, pair.second)); }, [&] (FuncInfo* finfo) { return yesOrNo(finfo->func->isReadonlyReturn); }, [&] (const MethTabEntryPair* mte) { return yesOrNo(mte->second.func->isReadonlyReturn); }, [&] (FuncFamily* fam) { return fam->m_isReadonlyReturn; } ); } TriBool Index::lookup_readonly_this( Context, res::Func rfunc ) const { return match<TriBool>( rfunc.val, [&] (res::Func::FuncName s) { if (s.renamable) return TriBool::Maybe; auto const it = m_data->funcs.find(s.name); return it != end(m_data->funcs) ? yesOrNo(it->second->isReadonlyThis) : TriBool::Yes; // if the function doesnt exist, we will error anyway }, [&] (res::Func::MethodName s) { auto const it = m_data->method_readonly_by_name.find(s.name); if (it == end(m_data->method_readonly_by_name)) { // There was no entry, so no method by this name returns readonly. return TriBool::No; } if (!((it->second >> 1) & 1)) { // No method of this name is marked readonly return TriBool::No; } auto const pair = m_data->methods.equal_range(s.name); return is_readonly_this_from_set(folly::range(pair.first, pair.second)); }, [&] (FuncInfo* finfo) { return yesOrNo(finfo->func->isReadonlyThis); }, [&] (const MethTabEntryPair* mte) { return yesOrNo(mte->second.func->isReadonlyThis); }, [&] (FuncFamily* fam) { return fam->m_isReadonlyThis; } ); } PropState Index::lookup_private_props(const php::Class* cls, bool move) const { auto it = m_data->privatePropInfo.find(cls); if (it != end(m_data->privatePropInfo)) { if (move) return std::move(it->second); return it->second; } return make_unknown_propstate( cls, [&] (const php::Prop& prop) { return (prop.attrs & AttrPrivate) && !(prop.attrs & AttrStatic); } ); } PropState Index::lookup_private_statics(const php::Class* cls, bool move) const { auto it = m_data->privateStaticPropInfo.find(cls); if (it != end(m_data->privateStaticPropInfo)) { if (move) return std::move(it->second); return it->second; } return make_unknown_propstate( cls, [&] (const php::Prop& prop) { return (prop.attrs & AttrPrivate) && (prop.attrs & AttrStatic); } ); } PropState Index::lookup_public_statics(const php::Class* cls) const { auto const cinfo = [&] () -> const ClassInfo* { auto const it = m_data->classInfo.find(cls->name); if (it == end(m_data->classInfo)) return nullptr; return it->second; }(); PropState state; for (auto const& prop : cls->properties) { if (!(prop.attrs & (AttrPublic|AttrProtected)) || !(prop.attrs & AttrStatic)) { continue; } auto [ty, everModified] = [&] { if (!cinfo) return std::make_pair(TInitCell, true); auto const it = cinfo->publicStaticProps.find(prop.name); assertx(it != end(cinfo->publicStaticProps)); return std::make_pair( remove_uninit(it->second.inferredType), it->second.everModified ); }(); state.emplace( prop.name, PropStateElem<>{ std::move(ty), &prop.typeConstraint, prop.attrs, everModified } ); } return state; } /* * Entry point for static property lookups from the Index. Return * metadata about a `cls'::`name' static property access in the given * context. */ PropLookupResult<> Index::lookup_static(Context ctx, const PropertiesInfo& privateProps, const Type& cls, const Type& name) const { ITRACE(4, "lookup_static: {} {}::${}\n", show(ctx), show(cls), show(name)); Trace::Indent _; // First try to obtain the property name as a static string auto const sname = [&] () -> SString { // Treat non-string names conservatively, but the caller should be // checking this. if (!is_specialized_string(name)) return nullptr; return sval_of(name); }(); // Conservative result when we can't do any better. The type can be // anything, and anything might throw. auto const conservative = [&] { ITRACE(4, "conservative\n"); return PropLookupResult<>{ TInitCell, sname, TriBool::Maybe, TriBool::Maybe, TriBool::Maybe, TriBool::Maybe, true }; }; // If we don't know what `cls' is, there's not much we can do. if (!is_specialized_cls(cls)) return conservative(); auto const dcls = dcls_of(cls); if (dcls.cls.val.left()) return conservative(); auto const cinfo = dcls.cls.val.right(); // Turn the context class into a ClassInfo* for convenience. const ClassInfo* ctxCls = nullptr; if (ctx.cls) { // I don't think this can ever fail (we should always be able to // resolve the class since we're currently processing it). If it // does, be conservative. auto const rCtx = resolve_class(ctx.cls); if (rCtx.val.left()) return conservative(); ctxCls = rCtx.val.right(); } switch (dcls.type) { case DCls::Sub: { // We know that `cls' is at least dcls.type, but could be a // subclass. For every subclass (including dcls.type itself), // start the property lookup from there, and union together all // the potential results. This could potentially visit a lot of // parent classes redundently, so tell it not to look into // parent classes, unless we're processing dcls.type. Optional<PropLookupResult<>> result; for (auto const sub : cinfo->subclassList) { auto r = lookup_static_impl( *m_data, ctx, ctxCls, privateProps, sub, sname, !sname && sub != cinfo ); ITRACE(4, "{} -> {}\n", sub->cls->name, show(r)); if (!result) { result.emplace(std::move(r)); } else { *result |= r; } } assertx(result.has_value()); ITRACE(4, "union -> {}\n", show(*result)); return *result; } case DCls::Exact: { // We know what exactly `cls' is. Just do the property lookup // starting from there. auto const r = lookup_static_impl( *m_data, ctx, ctxCls, privateProps, cinfo, sname, false ); ITRACE(4, "{} -> {}\n", cinfo->cls->name, show(r)); return r; } } always_assert(false); } Type Index::lookup_public_prop(const Type& cls, const Type& name) const { if (!is_specialized_cls(cls)) return TCell; if (!is_specialized_string(name)) return TCell; auto const sname = sval_of(name); auto const dcls = dcls_of(cls); if (dcls.cls.val.left()) return TCell; auto const cinfo = dcls.cls.val.right(); switch (dcls.type) { case DCls::Sub: { auto ty = TBottom; for (auto const sub : cinfo->subclassList) { ty |= lookup_public_prop_impl( *m_data, sub, sname ); } return ty; } case DCls::Exact: return lookup_public_prop_impl( *m_data, cinfo, sname ); } always_assert(false); } Type Index::lookup_public_prop(const php::Class* cls, SString name) const { auto const it = m_data->classInfo.find(cls->name); if (it == end(m_data->classInfo)) { return TCell; } return lookup_public_prop_impl(*m_data, it->second, name); } bool Index::lookup_class_init_might_raise(Context ctx, res::Class cls) const { return cls.val.match( [] (SString) { return true; }, [&] (ClassInfo* cinfo) { return class_init_might_raise(*m_data, ctx, cinfo); } ); } void Index::join_iface_vtable_thread() const { if (m_data->compute_iface_vtables.joinable()) { m_data->compute_iface_vtables.join(); } } Slot Index::lookup_iface_vtable_slot(const php::Class* cls) const { return folly::get_default(m_data->ifaceSlotMap, cls, kInvalidSlot); } ////////////////////////////////////////////////////////////////////// /* * Entry point for static property type mutation from the Index. Merge * `val' into the known type for any accessible `cls'::`name' static * property. The mutation will be recovered into either * `publicMutations' or `privateProps' depending on the properties * found. Mutations to AttrConst properties are ignored, unless * `ignoreConst' is true. */ PropMergeResult<> Index::merge_static_type( Context ctx, PublicSPropMutations& publicMutations, PropertiesInfo& privateProps, const Type& cls, const Type& name, const Type& val, bool checkUB, bool ignoreConst, bool mustBeReadOnly) const { ITRACE( 4, "merge_static_type: {} {}::${} {}\n", show(ctx), show(cls), show(name), show(val) ); Trace::Indent _; assertx(val.subtypeOf(BInitCell)); using R = PropMergeResult<>; // In some cases we might try to merge Bottom if we're in // unreachable code. This won't affect anything, so just skip out // early. if (val.subtypeOf(BBottom)) return R{ TBottom, TriBool::No }; // Try to turn the given property name into a static string auto const sname = [&] () -> SString { // Non-string names are treated conservatively here. The caller // should be checking for these and doing the right thing. if (!is_specialized_string(name)) return nullptr; return sval_of(name); }(); // The case where we don't know `cls': auto const unknownCls = [&] { if (!sname) { // Very bad case. We don't know `cls' or the property name. This // mutation can be affecting anything, so merge it into all // properties (this drops type information for public // properties). ITRACE(4, "unknown class and prop. merging everything\n"); publicMutations.mergeUnknown(ctx); privateProps.mergeInAllPrivateStatics( *this, unctx(val), ignoreConst, mustBeReadOnly ); } else { // Otherwise we don't know `cls', but do know the property // name. We'll store this mutation separately and union it in to // any lookup with the same name. ITRACE(4, "unknown class. merging all props with name {}\n", sname); publicMutations.mergeUnknownClass(sname, unctx(val)); // Assume that it could possibly affect any private property with // the same name. privateProps.mergeInPrivateStatic( *this, sname, unctx(val), ignoreConst, mustBeReadOnly ); } // To be conservative, say we might throw and be conservative about // conversions. return PropMergeResult<>{ loosen_likeness(val), TriBool::Maybe }; }; // check if we can determine the class. if (!is_specialized_cls(cls)) return unknownCls(); auto const dcls = dcls_of(cls); if (dcls.cls.val.left()) return unknownCls(); auto const cinfo = dcls.cls.val.right(); const ClassInfo* ctxCls = nullptr; if (ctx.cls) { auto const rCtx = resolve_class(ctx.cls); // We should only be not able to resolve our own context if the // class is not instantiable. In that case, the merge can't // happen. if (rCtx.val.left()) return R{ TBottom, TriBool::No }; ctxCls = rCtx.val.right(); } auto const mergePublic = [&] (const ClassInfo* ci, const php::Prop& prop, const Type& val) { publicMutations.mergeKnown(ci, prop, val); }; switch (dcls.type) { case DCls::Sub: { // We know this class is either dcls.type, or a child class of // it. For every child of dcls.type (including dcls.type // itself), do the merge starting from it. To avoid redundant // work, only iterate into parent classes if we're dcls.type // (this is only a matter of efficiency. The merge is // idiompotent). Optional<PropMergeResult<>> result; for (auto const sub : cinfo->subclassList) { auto r = merge_static_type_impl( *m_data, ctx, mergePublic, privateProps, ctxCls, sub, sname, val, checkUB, ignoreConst, mustBeReadOnly, !sname && sub != cinfo ); ITRACE(4, "{} -> {}\n", sub->cls->name, show(r)); if (!result) { result.emplace(std::move(r)); } else { *result |= r; } } assertx(result.has_value()); ITRACE(4, "union -> {}\n", show(*result)); return *result; } case DCls::Exact: { // We know the class exactly. Do the merge starting from only // it. auto const r = merge_static_type_impl( *m_data, ctx, mergePublic, privateProps, ctxCls, cinfo, sname, val, checkUB, ignoreConst, mustBeReadOnly, false ); ITRACE(4, "{} -> {}\n", cinfo->cls->name, show(r)); return r; } } always_assert(false); } ////////////////////////////////////////////////////////////////////// DependencyContext Index::dependency_context(const Context& ctx) const { return dep_context(*m_data, ctx); } void Index::use_class_dependencies(bool f) { if (f != m_data->useClassDependencies) { m_data->dependencyMap.clear(); m_data->useClassDependencies = f; } } void Index::init_public_static_prop_types() { trace_time tracer("init public static prop types"); for (auto const& cinfo : m_data->allClassInfos) { for (auto const& prop : cinfo->cls->properties) { if (!(prop.attrs & (AttrPublic|AttrProtected)) || !(prop.attrs & AttrStatic)) { continue; } /* * If the initializer type is TUninit, it means an 86sinit provides the * actual initialization type or it is AttrLateInit. So we don't want to * include the Uninit (which isn't really a user-visible type for the * property) or by the time we union things in we'll have inferred nothing * much. */ auto const initial = [&] { auto const tyRaw = from_cell(prop.val); if (tyRaw.subtypeOf(BUninit)) return TBottom; if (prop.attrs & AttrSystemInitialValue) return tyRaw; return adjust_type_for_prop( *this, *cinfo->cls, &prop.typeConstraint, tyRaw ); }(); cinfo->publicStaticProps[prop.name] = PublicSPropEntry { union_of( adjust_type_for_prop( *this, *cinfo->cls, &prop.typeConstraint, TInitCell ), initial ), initial, &prop, 0, false, true }; } } } void Index::refine_class_constants( const Context& ctx, const CompactVector<std::pair<size_t, Type>>& resolved, DependencyContextSet& deps) { if (!resolved.size()) return; auto changed = false; auto& constants = ctx.func->cls->constants; for (auto const& c : resolved) { assertx(c.first < constants.size()); auto& cnst = constants[c.first]; assertx(cnst.kind == ConstModifiers::Kind::Value); auto const key = std::make_pair(ctx.func->cls, cnst.name); auto& types = m_data->clsConstTypes; always_assert(cnst.val && type(*cnst.val) == KindOfUninit); if (auto const val = tv(c.second)) { assertx(val->m_type != KindOfUninit); cnst.val = *val; // Deleting from the types map is too expensive, so just leave // any entry. We won't look at it if val is set. changed = true; } else { auto const old = [&] { auto const it = types.find(key); return (it == types.end()) ? ClsConstInfo{ TInitCell, 0 } : it->second; }(); if (c.second.strictlyMoreRefined(old.type)) { if (old.refinements < options.returnTypeRefineLimit) { types.insert_or_assign( key, ClsConstInfo{ c.second, old.refinements+1 } ); changed = true; } else { FTRACE( 1, "maxed out refinements for class constant {}::{}\n", ctx.func->cls->name, cnst.name ); } } else { always_assert_flog( more_refined_for_index(c.second, old.type), "Class constant type invariant violated for {}::{}\n" " {} is not at least as refined as {}\n", ctx.func->cls->name, cnst.name, show(c.second), show(old.type) ); } } } if (changed) { find_deps(*m_data, ctx.func, Dep::ClsConst, deps); } } void Index::refine_constants(const FuncAnalysisResult& fa, DependencyContextSet& deps) { auto const& func = fa.ctx.func; if (func->cls != nullptr) return; auto const val = tv(fa.inferredReturn); if (!val) return; auto const cns_name = Constant::nameFromFuncName(func->name); if (!cns_name) return; auto& cs = fa.ctx.unit->constants; auto it = std::find_if( cs.begin(), cs.end(), [&] (auto const& c) { return cns_name->same(c->name); }); assertx(it != cs.end() && "Did not find constant"); (*it)->val = val.value(); find_deps(*m_data, func, Dep::ConstVal, deps); } void Index::fixup_return_type(const php::Func* func, Type& retTy) const { if (func->isGenerator) { if (func->isAsync) { // Async generators always return AsyncGenerator object. retTy = objExact(builtin_class(s_AsyncGenerator.get())); } else { // Non-async generators always return Generator object. retTy = objExact(builtin_class(s_Generator.get())); } } else if (func->isAsync) { // Async functions always return WaitH<T>, where T is the type returned // internally. retTy = wait_handle(*this, std::move(retTy)); } } void Index::init_return_type(const php::Func* func) { if ((func->attrs & AttrBuiltin) || func->isMemoizeWrapper) { return; } auto make_type = [&] (const TypeConstraint& tc) { if (tc.isSoft() || (RuntimeOption::EvalEnforceGenericsUB < 2 && tc.isUpperBound())) { return TBottom; } auto const cls = func->cls && func->cls->closureContextCls ? func->cls->closureContextCls : func->cls; return lookup_constraint(Context { func->unit, func, cls }, tc); }; auto const finfo = create_func_info(*m_data, func); auto tcT = make_type(func->retTypeConstraint); if (tcT.is(BBottom)) return; if (func->hasInOutArgs) { std::vector<Type> types; types.emplace_back(intersection_of(TInitCell, std::move(tcT))); for (auto& p : func->params) { if (!p.inout) continue; auto t = make_type(p.typeConstraint); if (t.is(BBottom)) return; types.emplace_back(intersection_of(TInitCell, std::move(t))); } tcT = vec(std::move(types)); } tcT = loosen_interfaces(loosen_all(to_cell(std::move(tcT)))); FTRACE(4, "Pre-fixup return type for {}{}{}: {}\n", func->cls ? func->cls->name->data() : "", func->cls ? "::" : "", func->name, show(tcT)); fixup_return_type(func, tcT); FTRACE(3, "Initial return type for {}{}{}: {}\n", func->cls ? func->cls->name->data() : "", func->cls ? "::" : "", func->name, show(tcT)); finfo->returnTy = std::move(tcT); } void Index::refine_return_info(const FuncAnalysisResult& fa, DependencyContextSet& deps) { auto const& func = fa.ctx.func; auto const finfo = create_func_info(*m_data, func); auto const t = loosen_interfaces(fa.inferredReturn); auto const error_loc = [&] { return folly::sformat( "{} {}{}", func->unit->filename, func->cls ? folly::to<std::string>(func->cls->name->data(), "::") : std::string{}, func->name ); }; auto dep = Dep{}; if (finfo->retParam == NoLocalId && fa.retParam != NoLocalId) { // This is just a heuristic; it doesn't mean that the value passed // in was returned, but that the value of the parameter at the // point of the RetC was returned. We use it to make (heuristic) // decisions about whether to do inline interps, so we only allow // it to change once (otherwise later passes might not do the // inline interp, and get worse results, which could trigger other // assertions in Index::refine_*). dep = Dep::ReturnTy; finfo->retParam = fa.retParam; } auto unusedParams = ~fa.usedParams; if (finfo->unusedParams != unusedParams) { dep = Dep::ReturnTy; always_assert_flog( (finfo->unusedParams | unusedParams) == unusedParams, "Index unusedParams decreased in {}.\n", error_loc() ); finfo->unusedParams = unusedParams; } if (t.strictlyMoreRefined(finfo->returnTy)) { if (finfo->returnRefinements < options.returnTypeRefineLimit) { finfo->returnTy = t; // We've modifed the return type, so reset any cached FuncFamily // return types. for (auto const ff : finfo->families) ff->m_returnTy.reset(); dep = is_scalar(t) ? Dep::ReturnTy | Dep::InlineDepthLimit : Dep::ReturnTy; finfo->returnRefinements += fa.localReturnRefinements + 1; if (finfo->returnRefinements > options.returnTypeRefineLimit) { FTRACE(1, "maxed out return type refinements at {}\n", error_loc()); } } else { FTRACE(1, "maxed out return type refinements at {}\n", error_loc()); } } else { always_assert_flog( more_refined_for_index(t, finfo->returnTy), "Index return type invariant violated in {}.\n" " {} is not at least as refined as {}\n", error_loc(), show(t), show(finfo->returnTy) ); } always_assert_flog( !finfo->effectFree || fa.effectFree, "Index effectFree changed from true to false in {} {}{}.\n", func->unit->filename, func->cls ? folly::to<std::string>(func->cls->name->data(), "::") : std::string{}, func->name); if (finfo->effectFree != fa.effectFree) { finfo->effectFree = fa.effectFree; dep = Dep::InlineDepthLimit | Dep::ReturnTy; } if (dep != Dep{}) find_deps(*m_data, func, dep, deps); } bool Index::refine_closure_use_vars(const php::Class* cls, const CompactVector<Type>& vars) { assertx(is_closure(*cls)); for (auto i = uint32_t{0}; i < vars.size(); ++i) { always_assert_flog( vars[i].equivalentlyRefined(unctx(vars[i])), "Closure cannot have a used var with a context dependent type" ); } auto& current = [&] () -> CompactVector<Type>& { std::lock_guard<std::mutex> _{closure_use_vars_mutex}; return m_data->closureUseVars[cls]; }(); always_assert(current.empty() || current.size() == vars.size()); if (current.empty()) { current = vars; return true; } auto changed = false; for (auto i = uint32_t{0}; i < vars.size(); ++i) { if (vars[i].strictSubtypeOf(current[i])) { changed = true; current[i] = vars[i]; } else { always_assert_flog( more_refined_for_index(vars[i], current[i]), "Index closure_use_var invariant violated in {}.\n" " {} is not at least as refined as {}\n", cls->name, show(vars[i]), show(current[i]) ); } } return changed; } template<class Container> void refine_private_propstate(Container& cont, const php::Class* cls, const PropState& state) { assertx(!is_used_trait(*cls)); auto* elm = [&] () -> typename Container::value_type* { std::lock_guard<std::mutex> _{private_propstate_mutex}; auto it = cont.find(cls); if (it == end(cont)) { if (!state.empty()) cont[cls] = state; return nullptr; } return &*it; }(); if (!elm) return; for (auto& kv : state) { auto& target = elm->second[kv.first]; assertx(target.tc == kv.second.tc); always_assert_flog( more_refined_for_index(kv.second.ty, target.ty), "PropState refinement failed on {}::${} -- {} was not a subtype of {}\n", cls->name->data(), kv.first->data(), show(kv.second.ty), show(target.ty) ); target.ty = kv.second.ty; if (kv.second.everModified) { always_assert_flog( target.everModified, "PropState refinement failed on {}::${} -- " "everModified flag went from false to true\n", cls->name->data(), kv.first->data() ); } else { target.everModified = false; } } } void Index::refine_private_props(const php::Class* cls, const PropState& state) { refine_private_propstate(m_data->privatePropInfo, cls, state); } void Index::refine_private_statics(const php::Class* cls, const PropState& state) { // We can't store context dependent types in private statics since they // could be accessed using different contexts. auto cleanedState = PropState{}; for (auto const& prop : state) { auto& elem = cleanedState[prop.first]; elem.ty = unctx(prop.second.ty); elem.tc = prop.second.tc; elem.attrs = prop.second.attrs; elem.everModified = prop.second.everModified; } refine_private_propstate(m_data->privateStaticPropInfo, cls, cleanedState); } void Index::record_public_static_mutations(const php::Func& func, PublicSPropMutations mutations) { if (!mutations.m_data) { m_data->publicSPropMutations.erase(&func); return; } m_data->publicSPropMutations.insert_or_assign(&func, std::move(mutations)); } void Index::update_static_prop_init_val(const php::Class* cls, SString name) const { auto const cls_it = m_data->classInfo.find(cls->name); if (cls_it == end(m_data->classInfo)) { return; } auto const cinfo = cls_it->second; if (cinfo->cls != cls) { return; } auto const it = cinfo->publicStaticProps.find(name); if (it != cinfo->publicStaticProps.end()) { it->second.initialValueResolved = true; } } void Index::refine_public_statics(DependencyContextSet& deps) { trace_time update("update public statics"); // Union together the mutations for each function, including the functions // which weren't analyzed this round. auto nothing_known = false; PublicSPropMutations::UnknownMap unknown; PublicSPropMutations::KnownMap known; for (auto const& mutations : m_data->publicSPropMutations) { if (!mutations.second.m_data) continue; if (mutations.second.m_data->m_nothing_known) { nothing_known = true; break; } for (auto const& kv : mutations.second.m_data->m_unknown) { auto const ret = unknown.insert(kv); if (!ret.second) ret.first->second |= kv.second; } for (auto const& kv : mutations.second.m_data->m_known) { auto const ret = known.insert(kv); if (!ret.second) ret.first->second |= kv.second; } } if (nothing_known) { // We cannot go from knowing the types to not knowing the types (this is // equivalent to widening the types). always_assert(!m_data->seenPublicSPropMutations); return; } m_data->seenPublicSPropMutations = true; // Refine known class state for (auto const& cinfo : m_data->allClassInfos) { for (auto& kv : cinfo->publicStaticProps) { auto knownClsType = [&] { auto const it = known.find( PublicSPropMutations::KnownKey { cinfo.get(), kv.first } ); // If we didn't see a mutation, the type is TBottom. return it == end(known) ? TBottom : it->second; }(); auto unknownClsType = [&] { auto const it = unknown.find(kv.first); // If we didn't see a mutation, the type is TBottom. return it == end(unknown) ? TBottom : it->second; }(); // We can't keep context dependent types in public properties. auto newType = adjust_type_for_prop( *this, *cinfo->cls, &kv.second.prop->typeConstraint, unctx(union_of(std::move(knownClsType), std::move(unknownClsType))) ); if (!newType.is(BBottom)) { always_assert_flog( kv.second.everModified, "Static property index invariant violated on {}::{}:\n" " everModified flag went from false to true", cinfo->cls->name->data(), kv.first->data() ); } else { kv.second.everModified = false; } if (kv.second.initialValueResolved) { for (auto& prop : cinfo->cls->properties) { if (prop.name != kv.first) continue; kv.second.initializerType = from_cell(prop.val); kv.second.initialValueResolved = false; break; } assertx(!kv.second.initialValueResolved); } // The type from the indexer doesn't contain the in-class initializer // types. Add that here. auto effectiveType = union_of(std::move(newType), kv.second.initializerType); /* * We may only shrink the types we recorded for each property. (If a * property type ever grows, the interpreter could infer something * incorrect at some step.) */ always_assert_flog( effectiveType.subtypeOf(kv.second.inferredType), "Static property index invariant violated on {}::{}:\n" " {} is not a subtype of {}", cinfo->cls->name->data(), kv.first->data(), show(effectiveType), show(kv.second.inferredType) ); // Put a limit on the refinements to ensure termination. Since we only // ever refine types, we can stop at any point and still maintain // correctness. if (effectiveType.strictSubtypeOf(kv.second.inferredType)) { if (kv.second.refinements + 1 < options.publicSPropRefineLimit) { find_deps(*m_data, kv.second.prop, Dep::PublicSProp, deps); kv.second.inferredType = std::move(effectiveType); ++kv.second.refinements; } else { FTRACE( 1, "maxed out public static property refinements for {}:{}\n", cinfo->cls->name->data(), kv.first->data() ); } } } } } void Index::refine_bad_initial_prop_values(const php::Class* cls, bool value, DependencyContextSet& deps) { assertx(!is_used_trait(*cls)); auto const it = m_data->classInfo.find(cls->name); if (it == end(m_data->classInfo)) { return; } auto const cinfo = it->second; if (cinfo->cls != cls) { return; } always_assert_flog( cinfo->hasBadInitialPropValues || !value, "Bad initial prop values going from false to true on {}", cls->name->data() ); if (cinfo->hasBadInitialPropValues && !value) { cinfo->hasBadInitialPropValues = false; find_deps(*m_data, cls, Dep::PropBadInitialValues, deps); } } bool Index::frozen() const { return m_data->frozen; } void Index::freeze() { m_data->frozen = true; m_data->ever_frozen = true; } /* * Note that these functions run in separate threads, and * intentionally don't bump Trace::hhbbc_time. If you want to see * these times, set TRACE=hhbbc_time:1 */ #define CLEAR(x) \ { \ trace_time _{"clearing " #x}; \ (x).clear(); \ } void Index::cleanup_for_final() { trace_time _{"cleanup_for_final"}; CLEAR(m_data->dependencyMap); } void Index::cleanup_post_emit(php::ProgramPtr program) { trace_time _{"cleanup_post_emit"}; { trace_time t{"reset allClassInfos"}; parallel::for_each(m_data->allClassInfos, [] (auto& u) { u.reset(); }); } { trace_time t{"reset funcInfo"}; parallel::for_each( m_data->funcInfo, [] (auto& u) { u.returnTy = TBottom; u.families.clear(); } ); } { trace_time t{"reset program"}; parallel::for_each(program->units, [] (auto& u) { u.reset(); }); } std::vector<std::function<void()>> clearers; #define CLEAR_PARALLEL(x) clearers.push_back([&] CLEAR(x)); CLEAR_PARALLEL(m_data->classes); CLEAR_PARALLEL(m_data->methods); CLEAR_PARALLEL(m_data->method_inout_params_by_name); CLEAR_PARALLEL(m_data->method_readonly_by_name); CLEAR_PARALLEL(m_data->funcs); CLEAR_PARALLEL(m_data->typeAliases); CLEAR_PARALLEL(m_data->enums); CLEAR_PARALLEL(m_data->constants); CLEAR_PARALLEL(m_data->classClosureMap); CLEAR_PARALLEL(m_data->classExtraMethodMap); CLEAR_PARALLEL(m_data->allClassInfos); CLEAR_PARALLEL(m_data->classInfo); CLEAR_PARALLEL(m_data->funcInfo); CLEAR_PARALLEL(m_data->privatePropInfo); CLEAR_PARALLEL(m_data->privateStaticPropInfo); CLEAR_PARALLEL(m_data->publicSPropMutations); CLEAR_PARALLEL(m_data->funcFamilies); CLEAR_PARALLEL(m_data->ifaceSlotMap); CLEAR_PARALLEL(m_data->closureUseVars); CLEAR_PARALLEL(m_data->clsConstTypes); CLEAR_PARALLEL(m_data->clsConstLookupCache); CLEAR_PARALLEL(m_data->foldableReturnTypeMap); CLEAR_PARALLEL(m_data->contextualReturnTypes); parallel::for_each(clearers, [] (const std::function<void()>& f) { f(); }); } void Index::thaw() { m_data->frozen = false; } std::unique_ptr<ArrayTypeTable::Builder>& Index::array_table_builder() const { return m_data->arrTableBuilder; } ////////////////////////////////////////////////////////////////////// res::Func Index::do_resolve(const php::Func* f) const { auto const finfo = create_func_info(*m_data, f); return res::Func { finfo }; }; // Return true if we know for sure that one php::Class must derive // from another at runtime, in all possible instantiations. bool Index::must_be_derived_from(const php::Class* cls, const php::Class* parent) const { if (cls == parent) return true; auto const clsClass_it = m_data->classInfo.find(cls->name); auto const parentClass_it = m_data->classInfo.find(parent->name); if (clsClass_it == end(m_data->classInfo) || parentClass_it == end(m_data->classInfo)) { return true; } auto const rCls = res::Class { clsClass_it->second }; auto const rPar = res::Class { parentClass_it->second }; return rCls.mustBeSubtypeOf(rPar); } // Return true if any possible definition of one php::Class could // derive from another at runtime, or vice versa. bool Index::could_be_related(const php::Class* cls, const php::Class* parent) const { if (cls == parent) return true; auto const clsClass_it = m_data->classInfo.find(cls->name); auto const parentClass_it = m_data->classInfo.find(parent->name); if (clsClass_it == end(m_data->classInfo) || parentClass_it == end(m_data->classInfo)) { return false; } auto const rCls = res::Class { clsClass_it->second }; auto const rPar = res::Class { parentClass_it->second }; return rCls.couldBe(rPar); } ////////////////////////////////////////////////////////////////////// PublicSPropMutations::Data& PublicSPropMutations::get() { if (!m_data) m_data = std::make_unique<Data>(); return *m_data; } void PublicSPropMutations::mergeKnown(const ClassInfo* ci, const php::Prop& prop, const Type& val) { ITRACE(4, "PublicSPropMutations::mergeKnown: {} {} {}\n", ci->cls->name->data(), prop.name, show(val)); auto const res = get().m_known.emplace( KnownKey { const_cast<ClassInfo*>(ci), prop.name }, val ); if (!res.second) res.first->second |= val; } void PublicSPropMutations::mergeUnknownClass(SString prop, const Type& val) { ITRACE(4, "PublicSPropMutations::mergeUnknownClass: {} {}\n", prop, show(val)); auto const res = get().m_unknown.emplace(prop, val); if (!res.second) res.first->second |= val; } void PublicSPropMutations::mergeUnknown(Context ctx) { ITRACE(4, "PublicSPropMutations::mergeUnknown\n"); /* * We have a case here where we know neither the class nor the static * property name. This means we have to pessimize public static property * types for the entire program. * * We could limit it to pessimizing them by merging the `val' type, but * instead we just throw everything away---this optimization is not * expected to be particularly useful on programs that contain any * instances of this situation. */ std::fprintf( stderr, "NOTE: had to mark everything unknown for public static " "property types due to dynamic code. -fanalyze-public-statics " "will not help for this program.\n" "NOTE: The offending code occured in this context: %s\n", show(ctx).c_str() ); get().m_nothing_known = true; } ////////////////////////////////////////////////////////////////////// }