include/HashedAbstractEnvironment.h (280 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #include <cstddef> #include <functional> #include <initializer_list> #include <ostream> #include <sstream> #include <unordered_map> #include <utility> #include "AbstractDomain.h" namespace sparta { namespace hae_impl { template <typename Variable, typename Domain, typename VariableHash, typename VariableEqual> class MapValue; } // namespace hae_impl /* * An abstract environment is a type of abstract domain that maps the variables * of a program to elements of a common abstract domain. For example, to perform * range analysis one can use an abstract environment that maps variable names * to intervals: * * {"x" -> [-1, 1], "i" -> [0, 10], ...} * * Another example is descriptive type analysis for Dex code, where one computes * the set of all possible Java classes a register can hold a reference to at * any point in the code: * * {"v0" -> {android.app.Fragment, java.lang.Object}, "v1" -> {...}, ...} * * This type of domain is commonly used for nonrelational (also called * attribute-independent) analyses that do not track relationships among * program variables. Please note that by definition of an abstract * environment, if the value _|_ appears in a variable binding, then no valid * execution state can ever be represented by this abstract environment. Hence, * assigning _|_ to a variable is equivalent to setting the entire environment * to _|_. * * This implementation of abstract environments is based on hashtables and is * well suited for intraprocedural analysis. It is not intended to handle very * large variable sets in the thousands. We use the AbstractDomainScaffolding * template to build the domain. In order to minimize the size of the underlying * hashtable, we do not explicitly represent bindings of a variable to the Top * element. Hence, any variable that is not explicitly represented in the * environment has a default value of Top. This representation is quite * convenient in practice. It also allows us to manipulate large (or possibly * infinite) variable sets with sparse assignments of non-Top values. */ template <typename Variable, typename Domain, typename VariableHash = std::hash<Variable>, typename VariableEqual = std::equal_to<Variable>> class HashedAbstractEnvironment final : public AbstractDomainScaffolding< hae_impl::MapValue<Variable, Domain, VariableHash, VariableEqual>, HashedAbstractEnvironment<Variable, Domain, VariableHash, VariableEqual>> { public: using Value = hae_impl::MapValue<Variable, Domain, VariableHash, VariableEqual>; /* * The default constructor produces the Top value. */ HashedAbstractEnvironment() : AbstractDomainScaffolding<Value, HashedAbstractEnvironment>() {} HashedAbstractEnvironment(AbstractValueKind kind) : AbstractDomainScaffolding<Value, HashedAbstractEnvironment>(kind) {} HashedAbstractEnvironment( std::initializer_list<std::pair<Variable, Domain>> l) { for (const auto& p : l) { if (p.second.is_bottom()) { this->set_to_bottom(); return; } this->get_value()->insert_binding(p.first, p.second); } this->normalize(); } bool is_value() const { return this->kind() == AbstractValueKind::Value; } size_t size() const { RUNTIME_CHECK(this->kind() == AbstractValueKind::Value, invalid_abstract_value() << expected_kind(AbstractValueKind::Value) << actual_kind(this->kind())); return this->get_value()->m_map.size(); } const std::unordered_map<Variable, Domain, VariableHash, VariableEqual>& bindings() const { RUNTIME_CHECK(this->kind() == AbstractValueKind::Value, invalid_abstract_value() << expected_kind(AbstractValueKind::Value) << actual_kind(this->kind())); return this->get_value()->m_map; } const Domain& get(const Variable& variable) const { if (this->is_bottom()) { static const Domain bottom = Domain::bottom(); return bottom; } auto binding = this->get_value()->m_map.find(variable); if (binding == this->get_value()->m_map.end()) { static const Domain top = Domain::top(); return top; } return binding->second; } HashedAbstractEnvironment& set(const Variable& variable, const Domain& value) { if (this->is_bottom()) { return *this; } if (value.is_bottom()) { this->set_to_bottom(); return *this; } this->get_value()->insert_binding(variable, value); this->normalize(); return *this; } HashedAbstractEnvironment& update(const Variable& variable, std::function<void(Domain*)> operation) { if (this->is_bottom()) { return *this; } auto& map = this->get_value()->m_map; auto binding = map.find(variable); Domain* value; if (binding == map.end()) { // This means it's an implicit binding (variable, Top). We explicitly // construct the Top value in order to apply the operation. value = &map[variable]; value->set_to_top(); } else { value = &binding->second; } operation(value); // We normalize the abstract environment after the operation has been // completed. if (value->is_bottom()) { this->set_to_bottom(); return *this; } if (value->is_top()) { map.erase(variable); } this->normalize(); return *this; } static HashedAbstractEnvironment bottom() { return HashedAbstractEnvironment(AbstractValueKind::Bottom); } static HashedAbstractEnvironment top() { return HashedAbstractEnvironment(AbstractValueKind::Top); } }; } // namespace sparta template <typename Variable, typename Domain, typename VariableHash, typename VariableEqual> inline std::ostream& operator<<( std::ostream& o, const typename sparta::HashedAbstractEnvironment<Variable, Domain, VariableHash, VariableEqual>& e) { using namespace sparta; switch (e.kind()) { case AbstractValueKind::Bottom: { o << "_|_"; break; } case AbstractValueKind::Top: { o << "T"; break; } case AbstractValueKind::Value: { o << "[#" << e.size() << "]"; o << "{"; auto& bindings = e.bindings(); for (auto it = bindings.begin(); it != bindings.end();) { o << it->first << " -> " << it->second; ++it; if (it != bindings.end()) { o << ", "; } } o << "}"; break; } } return o; } namespace sparta { namespace hae_impl { /* * The definition of an element of an abstract environment, i.e., a map from a * (possibly infinite) set of variables to an abstract domain implemented as a * hashtable. Variable bindings with the Top value are not stored in the * hashtable. The hashtable can never contain bindings with Bottom, as those are * filtered out in HashedAbstractEnvironment (the whole environment is set to * Bottom in that case). The Meet and Narrowing operations abort and return * AbstractValueKind::Bottom whenever a binding with Bottom is about to be * created. */ template <typename Variable, typename Domain, typename VariableHash, typename VariableEqual> class MapValue final : public AbstractValue< MapValue<Variable, Domain, VariableHash, VariableEqual>> { public: MapValue() = default; MapValue(const Variable& variable, const Domain& value) { insert_binding(variable, value); } void clear() override { m_map.clear(); } AbstractValueKind kind() const override { // If the map is empty, then all variables are implicitly bound to Top, // i.e., the abstract environment itself is Top. return (m_map.size() == 0) ? AbstractValueKind::Top : AbstractValueKind::Value; } bool leq(const MapValue& other) const override { if (other.m_map.size() > m_map.size()) { // In this case, there is a variable bound to a non-Top value in 'other' // that is not defined in 'this' (and is therefore implicitly bound to // Top). return false; } for (const auto& binding : m_map) { auto it = other.m_map.find(binding.first); if (it == other.m_map.end()) { // The other value is Top. continue; } if (!binding.second.leq(it->second)) { return false; } } // Now we look for a variable appearing in 'other' that is not defined in // 'this' (and thus bound to Top). for (const auto& binding : other.m_map) { auto it = m_map.find(binding.first); if (it == m_map.end()) { // The value is Top, but we know by construction that binding.second is // not Top. return false; } } return true; } bool equals(const MapValue& other) const override { if (m_map.size() != other.m_map.size()) { return false; } for (const auto& binding : m_map) { auto it = other.m_map.find(binding.first); if (it == other.m_map.end()) { return false; } if (!binding.second.equals(it->second)) { return false; } } return true; } AbstractValueKind join_with(const MapValue& other) override { return join_like_operation( other, [](Domain* x, const Domain& y) { x->join_with(y); }); } AbstractValueKind widen_with(const MapValue& other) override { return join_like_operation( other, [](Domain* x, const Domain& y) { x->widen_with(y); }); } AbstractValueKind meet_with(const MapValue& other) override { return meet_like_operation( other, [](Domain* x, const Domain& y) { x->meet_with(y); }); } AbstractValueKind narrow_with(const MapValue& other) override { return meet_like_operation( other, [](Domain* x, const Domain& y) { x->narrow_with(y); }); } private: void insert_binding(const Variable& variable, const Domain& value) { // The Bottom value is handled in HashedAbstractEnvironment and should // never occur here. RUNTIME_CHECK(!value.is_bottom(), internal_error()); if (value.is_top()) { // Bindings with the Top value are not explicitly represented. m_map.erase(variable); } else { m_map[variable] = value; } } AbstractValueKind join_like_operation( const MapValue& other, std::function<void(Domain*, const Domain&)> operation) { for (auto it = m_map.begin(); it != m_map.end();) { auto other_binding = other.m_map.find(it->first); if (other_binding == other.m_map.end()) { // The other value is Top, we just erase the binding. We need to use a // different iterator, because all iterators to an erased binding are // invalidated. auto to_erase = it++; m_map.erase(to_erase); } else { // We compute the join-like combination of the values. operation(&it->second, other_binding->second); if (it->second.is_top()) { // If the result is Top, we erase the binding. auto to_erase = it++; m_map.erase(to_erase); } else { ++it; } } } return kind(); } AbstractValueKind meet_like_operation( const MapValue& other, std::function<void(Domain*, const Domain&)> operation) { for (const auto& other_binding : other.m_map) { auto binding = m_map.find(other_binding.first); if (binding == m_map.end()) { // The value is Top, we just insert the other value (Top is the identity // for meet-like operations). m_map[other_binding.first] = other_binding.second; } else { // We compute the meet-like combination of the values. operation(&binding->second, other_binding.second); if (binding->second.is_bottom()) { // If the result is Bottom, the entire environment becomes Bottom. clear(); return AbstractValueKind::Bottom; } } } return kind(); } std::unordered_map<Variable, Domain, VariableHash, VariableEqual> m_map; template <typename T1, typename T2, typename T3, typename T4> friend class sparta::HashedAbstractEnvironment; }; } // namespace hae_impl } // namespace sparta