include/ReducedProductAbstractDomain.h (82 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 <algorithm> #include <cstddef> #include <functional> #include <initializer_list> #include <sstream> #include <tuple> #include <type_traits> #include "DirectProductAbstractDomain.h" namespace sparta { /* * The reduced cartesian product of abstract domains D1 x ... x Dn consists of * tuples of abstract values (v1, ..., vn) that represent the intersection of * the denotations of v1, ..., vn. Hence, all tuples that have at least one _|_ * component are equated to _|_ (this is similar to abstract environments). * However, the intersection of the denotations may be empty even though none of * the components is equal to _|_. * * The reduction operation of the reduced product (usually denoted by the * Greek letter sigma in the literature) is used to decide whether the * intersection of the denotations of the components is empty when no * component is _|_. This occurs when the component domains have overlapping * denotations and can refine each other. For example, one could implement * Granger's local iterations to propagate information across components. The * reduction operation is specific to the abstract domains used in the product * and should be implemented in the derived class as the `reduce_product()` * method. * * The interface uses the curiously recurring template pattern, thus allowing * the product domain to define additional operations. This comes handy when * each component domain defines a common operation that has to be lifted to the * product. * * Example usage: * * class D0 final : public AbstractDomain<D0> { * public: * D0 operation1(...) { ... } * void operation2(...) { ... } * ... * }; * * class D1 final : public AbstractDomain<D1> { * public: * D1 operation1(...) { ... } * void operation2(...) { ... } * ... * }; * * class D0xD1 final : public ProductAbstractDomain<D0xD1, D0, D1> { * public: * D0xD1 operation1(...) { ... } * void operation2(...) { * D0& x = get<0>(); * D1& y = get<1>(); * x.operation2(); * y.operation2(); * ... * // Explicitly call the reduction operation * reduce(); * } * // The reduce_product method implements the mechanics of the reduction * // operation. It is required by the API, even though it does nothing. The * // reduction operates by changing the contents of the given tuple. * static void reduce_product(std::tuple<D0, D1>& product) { ... } * static D0xD1 bottom() { * D0xD1 x; * x.set_to_bottom(); * return x; * } * static D0xD1 top() { * D0xD1 x; * x.set_to_top(); * return x; * } * ... * }; * */ template <typename Derived, typename... Domains> class ReducedProductAbstractDomain : public DirectProductAbstractDomain<Derived, Domains...> { public: ~ReducedProductAbstractDomain() { // The destructor is the only method that is guaranteed to be created when a // class template is instantiated. This is a good place to perform all the // sanity checks on the template parameters. static_assert( sizeof...(Domains) >= 2, "ReducedProductAbstractDomain requires at least two parameters"); static_assert(std::is_void<decltype(Derived::reduce_product( std::declval<std::tuple<Domains...>&>()))>::value, "Derived::reduce_product() does not exist"); } /* * Defining a public variadic constructor will invariably lead to instances of * the "Most Vexing Parse". Passing a tuple of elements as a single argument * to the constructor circumvents the issue without sacrificing readability. */ explicit ReducedProductAbstractDomain(std::tuple<Domains...> product) : DirectProductAbstractDomain<Derived, Domains...>(product) { // Since one or more components can be _|_, we need to normalize the // representation. normalize(); if (is_bottom()) { // No need to reduce the product any further. return; } // Even though no component is _|_, the intersection of the denotations of // the components might still be empty. Deciding the emptiness of the // intersection usually involves more sophisticated techniques that are // specific to the abstract domains in the product. This step is performed // by the reduce() method. reduce(); } /* * We need to define the default constructor in terms of the tuple-taking * constructor because the normalized product of the default-constructed * component Domains may be _|_. */ ReducedProductAbstractDomain() : ReducedProductAbstractDomain(std::tuple<Domains...>{}) {} // This method allows the user to explicitly call the reduction operation at // any time during the analysis. The `reduce_product()` method implements the // mechanics of the reduction operation and should never be called explicitly. // Note that this method cannot be virtual, because it would otherwise be // impossible to call it from the constructor (dynamic binding is disabled in // the body of constructors). void reduce() { Derived::reduce_product(this->m_product); // We don't assume that the reduction operation leaves the representation in // a normalized state. normalize(); } /* * Returns a read-only reference to a component in the tuple. */ template <size_t Index> const typename std::tuple_element<Index, std::tuple<Domains...>>::type& get() const { return std::get<Index>(this->m_product); } /* * Updates a component of the tuple by applying a user-defined operation. * Since the reduction may involve costly computations and is not always * required depending on the nature of the operation performed, we leave it as * an optional step. */ template <size_t Index> void apply( std::function<void( typename std::tuple_element<Index, std::tuple<Domains...>>::type*)> operation, bool do_reduction = false) { if (is_bottom()) { return; } operation(&std::get<Index>(this->m_product)); if (this->get<Index>().is_bottom()) { this->set_to_bottom(); return; } if (do_reduction) { reduce(); } } bool is_bottom() const override { // The normalized _|_ element in the product domain has all its components // set to _|_, so that we just need to check the first component. return this->get<0>().is_bottom(); } // We leave the Meet and Narrowing methods virtual, because one might want // to refine the result of these operations by applying reduce(). The default // implementation doesn't call reduce() as it might be too costly to perform // this operation after each Meet, or it might even break the termination // property of the Narrowing. virtual void meet_with(const Derived& other_domain) override { this->combine_with( other_domain, [](auto&& self, auto&& other) { self.meet_with(other); }, /* smash_bottom */ true); } virtual void narrow_with(const Derived& other_domain) override { this->combine_with( other_domain, [](auto&& self, auto&& other) { self.narrow_with(other); }, /* smash_bottom */ true); } private: // Performs the smash-bottom normalization of a tuple of abstract values. void normalize() { if (this->any_of([](auto&& component) { return component.is_bottom(); })) { this->set_to_bottom(); } } }; } // namespace sparta