From 24d8934c28f4ce2cd3a753f581cb9c8dc7cbfcd1 Mon Sep 17 00:00:00 2001 From: Maxime Arthaud Date: Fri, 6 Oct 2023 06:06:39 -0700 Subject: [PATCH] Implement partition and environment abstract domains on top of PatriciaTreeHashMap Summary: The previous diff introduced `PatriciaTreeHashMap`, a generalized hash map on top of patricia trees. This diff implements partition and environment abstract domains, similar to `PatriciaTreeMapAbstractPartition` and `PatriciaTreeMapAbstractEnvironment`. Reviewed By: arnaudvenet Differential Revision: D49697554 fbshipit-source-id: 27dedb945ee1668311a0701393242a2958f9d170 --- include/sparta/FlattenIterator.h | 2 + .../PatriciaTreeHashMapAbstractEnvironment.h | 314 ++++++++++++++++++ .../PatriciaTreeHashMapAbstractPartition.h | 255 ++++++++++++++ .../PatriciaTreeMapAbstractEnvironment.h | 1 - ...iciaTreeHashMapAbstractEnvironmentTest.cpp | 264 +++++++++++++++ ...triciaTreeHashMapAbstractPartitionTest.cpp | 256 ++++++++++++++ 6 files changed, 1091 insertions(+), 1 deletion(-) create mode 100644 include/sparta/PatriciaTreeHashMapAbstractEnvironment.h create mode 100644 include/sparta/PatriciaTreeHashMapAbstractPartition.h create mode 100644 test/PatriciaTreeHashMapAbstractEnvironmentTest.cpp create mode 100644 test/PatriciaTreeHashMapAbstractPartitionTest.cpp diff --git a/include/sparta/FlattenIterator.h b/include/sparta/FlattenIterator.h index cfd0892..e90de4a 100644 --- a/include/sparta/FlattenIterator.h +++ b/include/sparta/FlattenIterator.h @@ -109,6 +109,8 @@ class FlattenIterator { reference operator*() { return *m_inner->begin; } + pointer operator->() const { return &*m_inner->begin; } + private: /* Advance the iterator until we find an element. */ void advance_empty() { diff --git a/include/sparta/PatriciaTreeHashMapAbstractEnvironment.h b/include/sparta/PatriciaTreeHashMapAbstractEnvironment.h new file mode 100644 index 0000000..be336f6 --- /dev/null +++ b/include/sparta/PatriciaTreeHashMapAbstractEnvironment.h @@ -0,0 +1,314 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#include +#include + +namespace sparta { + +namespace pthmae_impl { + +template +class MapValue; + +class value_is_bottom {}; + +} // namespace pthmae_impl + +/* + * An abstract environment based on `PatriciaTreeHashMap`. + * + * See `PatriciaTreeMapAbstractEnvironment` for more information. + */ +template +class PatriciaTreeHashMapAbstractEnvironment final + : public AbstractDomainScaffolding< + pthmae_impl::MapValue, + PatriciaTreeHashMapAbstractEnvironment> { + public: + using Value = pthmae_impl::MapValue; + + using MapType = + PatriciaTreeHashMap; + + /* + * The default constructor produces the Top value. + */ + PatriciaTreeHashMapAbstractEnvironment() + : AbstractDomainScaffolding() {} + + explicit PatriciaTreeHashMapAbstractEnvironment(AbstractValueKind kind) + : AbstractDomainScaffolding( + kind) {} + + explicit PatriciaTreeHashMapAbstractEnvironment( + std::initializer_list> 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(); + } + + 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 MapType& 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; + } + return this->get_value()->m_map.at(variable); + } + + PatriciaTreeHashMapAbstractEnvironment& set(const Variable& variable, + const Domain& value) { + return set_internal(variable, value); + } + + PatriciaTreeHashMapAbstractEnvironment& set(const Variable& variable, + Domain&& value) { + return set_internal(variable, std::move(value)); + } + + template // void(Domain*) + bool map(Operation&& f) { + if (this->is_bottom()) { + return false; + } + bool res = this->get_value()->map(std::forward(f)); + this->normalize(); + return res; + } + + PatriciaTreeHashMapAbstractEnvironment& clear() { + if (this->is_bottom()) { + return *this; + } + this->get_value()->clear(); + this->normalize(); + return *this; + } + + template // void(Domain*) + PatriciaTreeHashMapAbstractEnvironment& update(const Variable& variable, + Operation&& operation) { + if (this->is_bottom()) { + return *this; + } + try { + this->get_value()->m_map.update( + [operation = std::forward(operation)](Domain* x) -> void { + operation(x); + if (x->is_bottom()) { + throw pthmae_impl::value_is_bottom(); + } + }, + variable); + } catch (const pthmae_impl::value_is_bottom&) { + this->set_to_bottom(); + } + this->normalize(); + return *this; + } + + static PatriciaTreeHashMapAbstractEnvironment bottom() { + return PatriciaTreeHashMapAbstractEnvironment(AbstractValueKind::Bottom); + } + + static PatriciaTreeHashMapAbstractEnvironment top() { + return PatriciaTreeHashMapAbstractEnvironment(AbstractValueKind::Top); + } + + private: + template + PatriciaTreeHashMapAbstractEnvironment& set_internal(const Variable& variable, + D&& value) { + if (this->is_bottom()) { + return *this; + } + if (value.is_bottom()) { + this->set_to_bottom(); + return *this; + } + this->get_value()->insert_binding(variable, std::forward(value)); + this->normalize(); + return *this; + } +}; + +} // namespace sparta + +template +inline std::ostream& operator<<( + std::ostream& o, + const typename sparta::PatriciaTreeHashMapAbstractEnvironment& 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 << e.bindings(); + break; + } + } + return o; +} + +namespace sparta { + +namespace pthmae_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 PatriciaTreeHashMapAbstractEnvironment (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 +class MapValue final : public AbstractValue> { + public: + struct ValueInterface { + using type = Domain; + + static type default_value() { return type::top(); } + + static bool is_default_value(const type& x) { return x.is_top(); } + + static bool equals(const type& x, const type& y) { return x.equals(y); } + + static bool leq(const type& x, const type& y) { return x.leq(y); } + + constexpr static AbstractValueKind default_value_kind = + AbstractValueKind::Top; + }; + + MapValue() = default; + + MapValue(const Variable& variable, Domain value) { + insert_binding(variable, std::move(value)); + } + + void clear() { m_map.clear(); } + + AbstractValueKind kind() const { + // If the map is empty, then all variables are implicitly bound to Top, + // i.e., the abstract environment itself is Top. + return m_map.empty() ? AbstractValueKind::Top : AbstractValueKind::Value; + } + + bool leq(const MapValue& other) const { return m_map.leq(other.m_map); } + + bool equals(const MapValue& other) const { return m_map.equals(other.m_map); } + + AbstractValueKind join_with(const MapValue& other) { + return join_like_operation( + other, [](Domain* x, const Domain& y) { return x->join_with(y); }); + } + + AbstractValueKind widen_with(const MapValue& other) { + return join_like_operation( + other, [](Domain* x, const Domain& y) { return x->widen_with(y); }); + } + + AbstractValueKind meet_with(const MapValue& other) { + return meet_like_operation( + other, [](Domain* x, const Domain& y) { return x->meet_with(y); }); + } + + AbstractValueKind narrow_with(const MapValue& other) { + return meet_like_operation( + other, [](Domain* x, const Domain& y) { return x->narrow_with(y); }); + } + + private: + void insert_binding(const Variable& variable, Domain value) { + // The Bottom value is handled by the caller and should never occur here. + RUNTIME_CHECK(!value.is_bottom(), internal_error()); + m_map.insert_or_assign(variable, std::move(value)); + } + + template // void(Domain*) + bool map(Operation&& f) { + return m_map.map(std::forward(f)); + } + + template // void(Domain*, const Domain&) + AbstractValueKind join_like_operation(const MapValue& other, + Operation&& operation) { + m_map.intersection_with(std::forward(operation), other.m_map); + return kind(); + } + + template // void(Domain*, const Domain&) + AbstractValueKind meet_like_operation(const MapValue& other, + Operation&& operation) { + try { + m_map.union_with( + [operation = std::forward(operation)](Domain* x, + const Domain& y) { + operation(x, y); + if (x->is_bottom()) { + throw value_is_bottom(); + } + }, + other.m_map); + return kind(); + } catch (const value_is_bottom&) { + clear(); + return AbstractValueKind::Bottom; + } + } + + PatriciaTreeHashMap m_map; + + template + friend class sparta::PatriciaTreeHashMapAbstractEnvironment; +}; + +} // namespace pthmae_impl + +} // namespace sparta diff --git a/include/sparta/PatriciaTreeHashMapAbstractPartition.h b/include/sparta/PatriciaTreeHashMapAbstractPartition.h new file mode 100644 index 0000000..29bf854 --- /dev/null +++ b/include/sparta/PatriciaTreeHashMapAbstractPartition.h @@ -0,0 +1,255 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include + +namespace sparta { + +/* + * An abstract partition based on `PatriciaTreeHashMap`. + * + * See `PatriciaTreeMapAbstractPartition` for more information. + */ +template +class PatriciaTreeHashMapAbstractPartition final + : public AbstractDomain< + PatriciaTreeHashMapAbstractPartition> { + public: + struct ValueInterface { + using type = Domain; + + static type default_value() { return type::bottom(); } + + static bool is_default_value(const type& x) { return x.is_bottom(); } + + static bool equals(const type& x, const type& y) { return x.equals(y); } + + static bool leq(const type& x, const type& y) { return x.leq(y); } + + constexpr static AbstractValueKind default_value_kind = + AbstractValueKind::Bottom; + }; + + using MapType = PatriciaTreeHashMap; + + /* + * The default constructor produces the Bottom value. + */ + PatriciaTreeHashMapAbstractPartition() = default; + + explicit PatriciaTreeHashMapAbstractPartition( + std::initializer_list> l) { + for (const auto& p : l) { + set(p.first, p.second); + } + } + + /* + * Number of bindings not set to Bottom. This operation is not defined if the + * PatriciaTreeHashMapAbstractPartition is set to Top. + */ + size_t size() const { + RUNTIME_CHECK(!is_top(), undefined_operation()); + return m_map.size(); + } + + /* + * Get the bindings that are not set to Bottom. This operation is not defined + * if the PatriciaTreeHashMapAbstractPartition is set to Top. + */ + const MapType& bindings() const { + RUNTIME_CHECK(!is_top(), undefined_operation()); + return m_map; + } + + const Domain& get(const Label& label) const { + if (is_top()) { + static const Domain top = Domain::top(); + return top; + } + return m_map.at(label); + } + + /* + * This is a no-op if the partition is set to Top. + */ + PatriciaTreeHashMapAbstractPartition& set(const Label& label, + const Domain& value) { + return set_internal(label, value); + } + + /* + * This is a no-op if the partition is set to Top. + */ + PatriciaTreeHashMapAbstractPartition& set(const Label& label, + Domain&& value) { + return set_internal(label, std::move(value)); + } + + /* + * This is a no-op if the partition is set to Top. + */ + + template // void(Domain*) + PatriciaTreeHashMapAbstractPartition& update(const Label& label, + Operation&& operation) { + if (is_top()) { + return *this; + } + m_map.update(std::forward(operation), label); + return *this; + } + + template // void(Domain*) + bool map(Operation&& f) { + if (is_top()) { + return false; + } + return m_map.map(std::forward(f)); + } + + bool is_top() const { return m_is_top; } + + bool is_bottom() const { return !m_is_top && m_map.empty(); } + + void set_to_bottom() { + m_map.clear(); + m_is_top = false; + } + + void set_to_top() { + m_map.clear(); + m_is_top = true; + } + + bool leq(const PatriciaTreeHashMapAbstractPartition& other) const { + if (is_top()) { + return other.is_top(); + } + if (other.is_top()) { + return true; + } + return m_map.leq(other.m_map); + } + + bool equals(const PatriciaTreeHashMapAbstractPartition& other) const { + if (m_is_top != other.m_is_top) { + return false; + } + return m_map.equals(other.m_map); + } + + void join_with(const PatriciaTreeHashMapAbstractPartition& other) { + join_like_operation( + other, [](Domain* x, const Domain& y) { return x->join_with(y); }); + } + + void widen_with(const PatriciaTreeHashMapAbstractPartition& other) { + join_like_operation( + other, [](Domain* x, const Domain& y) { return x->widen_with(y); }); + } + + void meet_with(const PatriciaTreeHashMapAbstractPartition& other) { + meet_like_operation( + other, [](Domain* x, const Domain& y) { return x->meet_with(y); }); + } + + void narrow_with(const PatriciaTreeHashMapAbstractPartition& other) { + meet_like_operation( + other, [](Domain* x, const Domain& y) { return x->narrow_with(y); }); + } + + template // Domain(Domain*, const Domain&) + void join_like_operation(const PatriciaTreeHashMapAbstractPartition& other, + Operation&& operation) { + if (is_top()) { + return; + } + if (other.is_top()) { + set_to_top(); + return; + } + m_map.union_with(std::forward(operation), other.m_map); + } + + template // void(Domain*, const Domain&) + void meet_like_operation(const PatriciaTreeHashMapAbstractPartition& other, + Operation&& operation) { + if (is_top()) { + *this = other; + return; + } + if (other.is_top()) { + return; + } + m_map.intersection_with(std::forward(operation), other.m_map); + } + + template // void(Domain*, const Domain&) + void difference_like_operation( + const PatriciaTreeHashMapAbstractPartition& other, + Operation&& operation) { + if (other.is_top()) { + set_to_bottom(); + } else if (is_top()) { + return; + } else { + m_map.difference_with(std::forward(operation), other.m_map); + } + } + + static PatriciaTreeHashMapAbstractPartition bottom() { + return PatriciaTreeHashMapAbstractPartition(); + } + + static PatriciaTreeHashMapAbstractPartition top() { + auto part = PatriciaTreeHashMapAbstractPartition(); + part.m_is_top = true; + return part; + } + + private: + template + PatriciaTreeHashMapAbstractPartition& set_internal(const Label& label, + D&& value) { + if (is_top()) { + return *this; + } + m_map.insert_or_assign(label, std::forward(value)); + return *this; + } + + MapType m_map; + bool m_is_top{false}; +}; + +} // namespace sparta + +template +inline std::ostream& operator<<( + std::ostream& o, + const typename sparta::PatriciaTreeHashMapAbstractPartition& + partition) { + if (partition.is_bottom()) { + o << "_|_"; + } else if (partition.is_top()) { + o << "T"; + } else { + o << "[#" << partition.size() << "]"; + o << partition.bindings(); + } + return o; +} diff --git a/include/sparta/PatriciaTreeMapAbstractEnvironment.h b/include/sparta/PatriciaTreeMapAbstractEnvironment.h index 78cb8dd..3363b25 100644 --- a/include/sparta/PatriciaTreeMapAbstractEnvironment.h +++ b/include/sparta/PatriciaTreeMapAbstractEnvironment.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #include diff --git a/test/PatriciaTreeHashMapAbstractEnvironmentTest.cpp b/test/PatriciaTreeHashMapAbstractEnvironmentTest.cpp new file mode 100644 index 0000000..b4be41c --- /dev/null +++ b/test/PatriciaTreeHashMapAbstractEnvironmentTest.cpp @@ -0,0 +1,264 @@ +/* + * 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. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace sparta; + +using Domain = HashedSetAbstractDomain; +using Environment = PatriciaTreeHashMapAbstractEnvironment; + +class PatriciaTreeHashMapAbstractEnvironmentTest : public ::testing::Test { + protected: + PatriciaTreeHashMapAbstractEnvironmentTest() + : m_rd_device(), + m_generator(m_rd_device()), + m_size_dist(0, 50), + m_elem_dist(0, std::numeric_limits::max()) {} + + Environment generate_random_environment() { + Environment env; + size_t size = m_size_dist(m_generator); + for (size_t i = 0; i < size; ++i) { + auto rnd = m_elem_dist(m_generator); + auto rnd_string = std::to_string(m_elem_dist(m_generator)); + env.set(rnd, Domain({rnd_string})); + } + return env; + } + + std::random_device m_rd_device; + std::mt19937 m_generator; + std::uniform_int_distribution m_size_dist; + std::uniform_int_distribution m_elem_dist; +}; + +static HashedAbstractEnvironment hae_from_ptae( + const Environment& env) { + HashedAbstractEnvironment hae; + if (env.is_value()) { + for (const auto& pair : env.bindings()) { + hae.set(pair.first, pair.second); + } + } else if (env.is_top()) { + hae.set_to_top(); + } else { + hae.set_to_bottom(); + } + return hae; +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, latticeOperations) { + Environment e1({{1, Domain({"a", "b"})}, + {2, Domain("c")}, + {3, Domain({"d", "e", "f"})}, + {4, Domain({"a", "f"})}}); + Environment e2({{0, Domain({"c", "f"})}, + {2, Domain({"c", "d"})}, + {3, Domain({"d", "e", "g", "h"})}}); + + EXPECT_EQ(4, e1.size()); + EXPECT_EQ(3, e2.size()); + + EXPECT_TRUE(Environment::bottom().leq(e1)); + EXPECT_FALSE(e1.leq(Environment::bottom())); + EXPECT_FALSE(Environment::top().leq(e1)); + EXPECT_TRUE(e1.leq(Environment::top())); + EXPECT_FALSE(e1.leq(e2)); + EXPECT_FALSE(e2.leq(e1)); + + EXPECT_TRUE(e1.equals(e1)); + EXPECT_FALSE(e1.equals(e2)); + EXPECT_TRUE(Environment::bottom().equals(Environment::bottom())); + EXPECT_TRUE(Environment::top().equals(Environment::top())); + EXPECT_FALSE(Environment::bottom().equals(Environment::top())); + + Environment join = e1.join(e2); + EXPECT_TRUE(e1.leq(join)); + EXPECT_TRUE(e2.leq(join)); + EXPECT_EQ(2, join.size()); + EXPECT_THAT(join.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d")); + EXPECT_THAT(join.get(3).elements(), + ::testing::UnorderedElementsAre("d", "e", "f", "g", "h")); + EXPECT_TRUE(join.equals(e1.widening(e2))); + + EXPECT_TRUE(e1.join(Environment::top()).is_top()); + EXPECT_TRUE(e1.join(Environment::bottom()).equals(e1)); + + Environment meet = e1.meet(e2); + EXPECT_TRUE(meet.leq(e1)); + EXPECT_TRUE(meet.leq(e2)); + EXPECT_EQ(5, meet.size()); + EXPECT_THAT(meet.get(0).elements(), + ::testing::UnorderedElementsAre("c", "f")); + EXPECT_THAT(meet.get(1).elements(), + ::testing::UnorderedElementsAre("a", "b")); + EXPECT_THAT(meet.get(2).elements(), ::testing::ElementsAre("c")); + EXPECT_THAT(meet.get(3).elements(), + ::testing::UnorderedElementsAre("d", "e")); + EXPECT_THAT(meet.get(4).elements(), + ::testing::UnorderedElementsAre("a", "f")); + EXPECT_TRUE(meet.equals(e1.narrowing(e2))); + + EXPECT_TRUE(e1.meet(Environment::bottom()).is_bottom()); + EXPECT_TRUE(e1.meet(Environment::top()).equals(e1)); + + Environment s1({{7, Domain({"a", "b"})}}); + Environment s2({{7, Domain({"a", "b", "c"})}}); + Environment s3({{4, Domain({"a", "b", "c"})}}); + EXPECT_TRUE(s1.leq(s2)); + EXPECT_FALSE(s2.leq(s1)); + EXPECT_FALSE(s1.leq(s3)); + EXPECT_FALSE(s2.leq(s3)); + EXPECT_FALSE(s3.leq(s2)); +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, destructiveOperations) { + Environment e1({{1, Domain({"a", "b"})}}); + Environment e2({{2, Domain({"c", "d"})}, {3, Domain({"g", "h"})}}); + + e1.set(2, Domain({"c", "f"})).set(4, Domain({"e", "f", "g"})); + EXPECT_EQ(3, e1.size()); + EXPECT_THAT(e1.get(1).elements(), ::testing::UnorderedElementsAre("a", "b")); + EXPECT_THAT(e1.get(2).elements(), ::testing::UnorderedElementsAre("c", "f")); + EXPECT_THAT(e1.get(4).elements(), + ::testing::UnorderedElementsAre("e", "f", "g")); + + Environment join = e1; + join.join_with(e2); + EXPECT_EQ(1, join.size()) << join; + EXPECT_THAT(join.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d", "f")); + + Environment widening = e1; + widening.widen_with(e2); + EXPECT_TRUE(widening.equals(join)); + + Environment meet = e1; + meet.meet_with(e2); + EXPECT_EQ(4, meet.size()); + EXPECT_THAT(meet.get(1).elements(), + ::testing::UnorderedElementsAre("a", "b")); + EXPECT_THAT(meet.get(2).elements(), ::testing::ElementsAre("c")); + EXPECT_THAT(meet.get(3).elements(), + ::testing::UnorderedElementsAre("g", "h")); + EXPECT_THAT(meet.get(4).elements(), + ::testing::UnorderedElementsAre("e", "f", "g")); + + Environment narrowing = e1; + narrowing.narrow_with(e2); + EXPECT_TRUE(narrowing.equals(meet)); + + auto add_e = [](Domain* s) { s->add("e"); }; + e1.update(1, add_e).update(2, add_e); + EXPECT_EQ(3, e1.size()); + EXPECT_THAT(e1.get(1).elements(), + ::testing::UnorderedElementsAre("a", "b", "e")); + EXPECT_THAT(e1.get(2).elements(), + ::testing::UnorderedElementsAre("c", "e", "f")); + EXPECT_THAT(e1.get(4).elements(), + ::testing::UnorderedElementsAre("e", "f", "g")); + + Environment e3 = e2; + EXPECT_EQ(2, e3.size()); + e3.update(1, add_e).update(2, add_e); + EXPECT_EQ(2, e3.size()); + EXPECT_THAT(e3.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d", "e")); + EXPECT_THAT(e3.get(3).elements(), ::testing::UnorderedElementsAre("g", "h")); + + auto make_bottom = [](Domain* s) { s->set_to_bottom(); }; + Environment e4 = e2; + e4.update(1, make_bottom); + EXPECT_TRUE(e4.is_bottom()); + int counter = 0; + auto make_e = [&counter](Domain* s) { + ++counter; + *s = Domain({"e"}); + }; + e4.update(1, make_e).update(2, make_e); + EXPECT_TRUE(e4.is_bottom()); + // Since e4 is Bottom, make_e should have never been called. + EXPECT_EQ(0, counter); + + auto refine_de = [](Domain* s) { s->meet_with(Domain({"d", "e"})); }; + EXPECT_EQ(2, e2.size()); + e2.update(1, refine_de).update(2, refine_de); + EXPECT_EQ(3, e2.size()); + EXPECT_THAT(e2.get(1).elements(), ::testing::UnorderedElementsAre("d", "e")); + EXPECT_THAT(e2.get(2).elements(), ::testing::ElementsAre("d")); + EXPECT_THAT(e2.get(3).elements(), ::testing::UnorderedElementsAre("g", "h")); +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, robustness) { + for (size_t k = 0; k < 10; ++k) { + Environment e1 = this->generate_random_environment(); + Environment e2 = this->generate_random_environment(); + + auto ref_meet = hae_from_ptae(e1); + ref_meet.meet_with(hae_from_ptae(e2)); + auto meet = e1; + meet.meet_with(e2); + EXPECT_EQ(hae_from_ptae(meet), ref_meet); + EXPECT_TRUE(meet.leq(e1)); + EXPECT_TRUE(meet.leq(e2)); + + auto ref_join = hae_from_ptae(e1); + ref_join.join_with(hae_from_ptae(e2)); + auto join = e1; + join.join_with(e2); + EXPECT_EQ(hae_from_ptae(join), ref_join); + EXPECT_TRUE(e1.leq(join)); + EXPECT_TRUE(e2.leq(join)); + } +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, whiteBox) { + // The algorithms are designed in such a way that Patricia trees that are left + // unchanged by an operation are not reconstructed (i.e., the result of an + // operation shares structure with the operands whenever possible). This is + // what we check here. + Environment e({{1, Domain({"a"})}}); + const auto& before = e.bindings(); + e.update(1, [](Domain* x) { *x = Domain({"a"}); }); + EXPECT_TRUE(e.bindings().reference_equals(before)); + e.meet_with(e); + EXPECT_TRUE(e.bindings().reference_equals(before)); + e.join_with(e); + EXPECT_TRUE(e.bindings().reference_equals(before)); +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, map) { + Environment e1({{1, Domain({"a", "b"})}}); + bool any_changes = e1.map([](Domain*) {}); + EXPECT_FALSE(any_changes); + + any_changes = e1.map([](Domain* d) { d->set_to_top(); }); + EXPECT_TRUE(any_changes); + EXPECT_TRUE(e1.is_top()); +} + +TEST_F(PatriciaTreeHashMapAbstractEnvironmentTest, prettyPrinting) { + using StringEnvironment = + PatriciaTreeHashMapAbstractEnvironment; + std::string a = "a"; + StringEnvironment e({{&a, Domain("A")}}); + + std::ostringstream out; + out << e.bindings(); + EXPECT_EQ("{a -> [#1]{A}}", out.str()); +} diff --git a/test/PatriciaTreeHashMapAbstractPartitionTest.cpp b/test/PatriciaTreeHashMapAbstractPartitionTest.cpp new file mode 100644 index 0000000..ffa306c --- /dev/null +++ b/test/PatriciaTreeHashMapAbstractPartitionTest.cpp @@ -0,0 +1,256 @@ +/* + * 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. + */ + +#include +#include + +#include +#include +#include +#include + +#include "AbstractDomainPropertyTest.h" + +using namespace sparta; + +using Domain = HashedSetAbstractDomain; + +using Partition = PatriciaTreeHashMapAbstractPartition; + +INSTANTIATE_TYPED_TEST_CASE_P(PatriciaTreeHashMapAbstractPartition, + AbstractDomainPropertyTest, + Partition); + +template <> +std::vector +AbstractDomainPropertyTest::non_extremal_values() { + Partition p1({{1, Domain({"a", "b"})}, + {2, Domain("c")}, + {3, Domain({"d", "e", "f"})}, + {4, Domain({"a", "f"})}}); + Partition p2({{0, Domain({"c", "f"})}, + {2, Domain({"c", "d"})}, + {3, Domain({"d", "e", "g", "h"})}}); + return {p1, p2}; +} + +TEST(PatriciaTreeHashMapAbstractPartitionTest, basicPartialOrders) { + { + Partition p1; + EXPECT_TRUE(p1.leq(p1)); + } + + { + Partition p1; + Partition p2; + EXPECT_TRUE(p1.leq(p2)); + EXPECT_TRUE(p2.leq(p1)); + } + + { + Partition p1({{1, Domain({"a"})}}); + Partition p2({{1, Domain({"a"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_TRUE(p2.leq(p1)); + } + + { + Partition p1({{2, Domain({"a"})}, {3, Domain({"a"})}}); + Partition p2({{2, Domain({"a"})}, {3, Domain({"a"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_TRUE(p2.leq(p1)); + } + + { + Partition p1; + Partition p2({{1, Domain({"a"})}}); + Partition p3({{2, Domain({"a"})}, {3, Domain({"a"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + EXPECT_TRUE(p1.leq(p3)); + EXPECT_FALSE(p3.leq(p1)); + } + + { + Partition p1({{1, Domain({"a"})}}); + Partition p2({{1, Domain({"a"})}, {2, Domain({"a"})}}); + Partition p3({{2, Domain({"a"})}, {3, Domain({"a"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + EXPECT_FALSE(p1.leq(p3)); + EXPECT_FALSE(p3.leq(p1)); + } + + { + Partition p1; + p1.set_to_bottom(); + p1.set(1, Domain({"a"})); + p1.set(2, Domain({"a"})); + Partition p2; + p2.set_to_bottom(); + p2.set(1, Domain({"a"})); + EXPECT_FALSE(p1.leq(p2)); + EXPECT_TRUE(p2.leq(p1)); + } + + { + Partition p1({{1, Domain({"a"})}, {3, Domain({"a"})}}); + Partition p2({{1, Domain({"a"})}, {2, Domain({"a"})}, {3, Domain({"a"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + } + + { + Partition p1({{1, Domain({"a"})}, {3, Domain({"b"})}}); + Partition p2({{1, Domain({"a"})}, {2, Domain({"a"})}, {3, Domain({"a"})}}); + EXPECT_FALSE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + } + + { + Partition p1({{1, Domain({"a"})}, {3, Domain({"b"})}}); + Partition p2( + {{1, Domain({"a", "b"})}, {2, Domain({"a"})}, {3, Domain({"a", "b"})}}); + EXPECT_TRUE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + } +} + +TEST(PatriciaTreeHashMapAbstractPartitionTest, latticeOperations) { + Partition p1({{1, Domain({"a", "b"})}, + {2, Domain("c")}, + {3, Domain({"d", "e", "f"})}, + {4, Domain({"a", "f"})}}); + Partition p2({{0, Domain({"c", "f"})}, + {2, Domain({"c", "d"})}, + {3, Domain({"d", "e", "g", "h"})}}); + EXPECT_EQ(4, p1.size()); + EXPECT_EQ(3, p2.size()); + + EXPECT_FALSE(p1.leq(p2)); + EXPECT_FALSE(p2.leq(p1)); + + EXPECT_FALSE(p1.equals(p2)); + EXPECT_TRUE(Partition::bottom().equals(Partition())); + + Partition join = p1.join(p2); + EXPECT_EQ(5, join.size()); + EXPECT_EQ(join.get(0).elements(), p2.get(0).elements()); + EXPECT_EQ(join.get(1).elements(), p1.get(1).elements()); + EXPECT_THAT(join.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d")); + EXPECT_THAT(join.get(3).elements(), + ::testing::UnorderedElementsAre("d", "e", "f", "g", "h")); + EXPECT_EQ(join.get(4).elements(), p1.get(4).elements()); + EXPECT_TRUE(join.equals(p1.widening(p2))); + + Partition meet = p1.meet(p2); + EXPECT_EQ(2, meet.size()); + EXPECT_THAT(meet.get(2).elements(), ::testing::ElementsAre("c")); + EXPECT_THAT(meet.get(3).elements(), + ::testing::UnorderedElementsAre("d", "e")); + EXPECT_EQ(meet, p1.narrowing(p2)); +} + +TEST(PatriciaTreeHashMapAbstractPartitionTest, destructiveOperations) { + Partition p1({{1, Domain({"a", "b"})}}); + Partition p2({{2, Domain({"c", "d"})}, {3, Domain({"g", "h"})}}); + + p1.set(2, Domain({"c", "f"})).set(4, Domain({"e", "f", "g"})); + EXPECT_EQ(3, p1.size()); + EXPECT_THAT(p1.get(1).elements(), ::testing::UnorderedElementsAre("a", "b")); + EXPECT_THAT(p1.get(2).elements(), ::testing::UnorderedElementsAre("c", "f")); + EXPECT_THAT(p1.get(4).elements(), + ::testing::UnorderedElementsAre("e", "f", "g")); + + Partition join = p1; + join.join_with(p2); + EXPECT_EQ(4, join.size()); + EXPECT_EQ(join.get(1).elements(), p1.get(1).elements()); + EXPECT_EQ(join.get(4).elements(), p1.get(4).elements()); + EXPECT_THAT(join.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d", "f")); + EXPECT_EQ(join.get(3).elements(), p2.get(3).elements()); + + Partition widening = p1; + widening.widen_with(p2); + EXPECT_TRUE(widening.equals(join)); + + Partition meet = p1; + meet.meet_with(p2); + EXPECT_EQ(1, meet.size()); + EXPECT_THAT(meet.get(2).elements(), ::testing::ElementsAre("c")); + + Partition narrowing = p1; + narrowing.narrow_with(p2); + EXPECT_TRUE(narrowing.equals(meet)); + + auto add_e = [](Domain* s) { s->add("e"); }; + p1.update(1, add_e).update(2, add_e); + EXPECT_EQ(3, p1.size()); + EXPECT_THAT(p1.get(1).elements(), + ::testing::UnorderedElementsAre("a", "b", "e")); + EXPECT_THAT(p1.get(2).elements(), + ::testing::UnorderedElementsAre("c", "e", "f")); + EXPECT_THAT(p1.get(4).elements(), + ::testing::UnorderedElementsAre("e", "f", "g")); + + Partition p3 = p2; + EXPECT_EQ(2, p3.size()); + p3.update(1, add_e).update(2, add_e); + EXPECT_EQ(2, p3.size()); + EXPECT_THAT(p3.get(2).elements(), + ::testing::UnorderedElementsAre("c", "d", "e")); + EXPECT_THAT(p3.get(3).elements(), ::testing::UnorderedElementsAre("g", "h")); + + auto make_bottom = [](Domain* s) { s->set_to_bottom(); }; + Partition p4 = p2; + p4.update(2, make_bottom); + EXPECT_FALSE(p4.is_bottom()); + EXPECT_EQ(1, p4.size()); + + auto refine_de = [](Domain* s) { s->meet_with(Domain({"d", "e"})); }; + EXPECT_EQ(2, p2.size()); + p2.update(1, refine_de).update(2, refine_de); + EXPECT_EQ(2, p2.size()); + EXPECT_TRUE(p2.get(1).is_bottom()); + EXPECT_THAT(p2.get(2).elements(), ::testing::ElementsAre("d")); + EXPECT_THAT(p2.get(3).elements(), ::testing::UnorderedElementsAre("g", "h")); + + Partition p5({{0, Domain({"c", "d"})}, + {2, Domain::bottom()}, + {3, Domain({"a", "f", "g"})}}); + + EXPECT_EQ(2, p5.size()); + p5.set(0, Domain::bottom()); + p5.set(3, Domain::bottom()); + EXPECT_TRUE(p5.is_bottom()); + EXPECT_EQ(Partition::bottom(), p5); + EXPECT_TRUE(p5.get(4).is_bottom()); + + Partition p6{Partition::top()}; + EXPECT_TRUE(p6.get(0).is_top()); + + // All operations on Top are no-ops. + p6.set(1, Domain::bottom()); + EXPECT_TRUE(p6.get(1).is_top()); + EXPECT_TRUE(p6.is_top()); + + p6.update(1, make_bottom); + EXPECT_TRUE(p6.get(1).is_top()); + EXPECT_TRUE(p6.is_top()); +} + +TEST(PatriciaTreeHashMapAbstractPartitionTest, map) { + Partition p1({{1, Domain({"a", "b"})}}); + bool any_changes = p1.map([](Domain*) {}); + EXPECT_FALSE(any_changes); + + any_changes = p1.map([](Domain* d) { d->set_to_bottom(); }); + EXPECT_TRUE(any_changes); + EXPECT_TRUE(p1.is_bottom()); +}