include/HashedAbstractPartition.h (222 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 {
/*
* A partition is a mapping from a set of labels to elements in an abstract
* domain. It denotes a union of properties. A partition is Bottom iff all its
* bindings are set to Bottom, and it is Top iff all its bindings are set to
* Top.
*
* All lattice operations are applied componentwise.
*
* In order to minimize the size of the hashtable, we do not explicitly
* represent bindings to Bottom.
*
* This implementation differs slightly from the textbook definition of a
* partition: our Top partition cannot have its labels re-bound to anything
* other than Top. I.e. for all labels L and domains D,
*
* HashedAbstractPartition::top().set(L, D) == HashedAbstractPartition::top()
*
* This makes for a much simpler implementation.
*/
template <typename Label,
typename Domain,
typename LabelHash = std::hash<Label>,
typename LabelEqual = std::equal_to<Label>>
class HashedAbstractPartition final
: public AbstractDomain<
HashedAbstractPartition<Label, Domain, LabelHash, LabelEqual>> {
public:
/*
* The default constructor produces the Bottom value.
*/
HashedAbstractPartition() = default;
HashedAbstractPartition(std::initializer_list<std::pair<Label, Domain>> 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
* HashedAbstractPartition 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 HashedAbstractPartition is set to Top.
*/
const std::unordered_map<Label, Domain, LabelHash, LabelEqual>& 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;
}
auto binding = m_map.find(label);
if (binding == m_map.end()) {
static const Domain bottom = Domain::bottom();
return bottom;
}
return binding->second;
}
/*
* This is a no-op if the partition is set to Top.
*/
HashedAbstractPartition& set(const Label& label, const Domain& value) {
if (is_top()) {
return *this;
}
if (value.is_bottom()) {
m_map.erase(label);
} else {
m_map[label] = value;
}
return *this;
}
/*
* This is a no-op if the partition is set to Top.
*/
HashedAbstractPartition& update(const Label& label,
std::function<void(Domain*)> operation) {
if (is_top()) {
return *this;
}
auto binding = m_map.find(label);
Domain* value;
if (binding == m_map.end()) {
// This means it's an implicit binding. We explicitly construct the
// Bottom value in order to apply the operation.
value = &m_map[label];
value->set_to_bottom();
} else {
value = &binding->second;
}
operation(value);
if (value->is_bottom()) {
m_map.erase(label);
}
return *this;
}
bool is_top() const override { return m_is_top; }
bool is_bottom() const override { return !m_is_top && m_map.empty(); }
void set_to_bottom() override {
m_map.clear();
m_is_top = false;
}
void set_to_top() override {
m_map.clear();
m_is_top = true;
}
bool leq(const HashedAbstractPartition& other) const override {
if (is_top()) {
return other.is_top();
}
if (other.is_top()) {
return true;
}
if (m_map.size() > other.m_map.size()) {
// In this case, there is a label bound to a non-Bottom value in
// 'this' that is not defined in 'other' (and is therefore implicitly
// bound to Bottom).
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 Bottom.
return false;
}
if (!binding.second.leq(it->second)) {
return false;
}
}
return true;
}
bool equals(const HashedAbstractPartition& other) const override {
if (m_is_top != other.m_is_top || 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;
}
void join_with(const HashedAbstractPartition& other) override {
join_like_operation(other,
[](Domain* x, const Domain& y) { x->join_with(y); });
}
void widen_with(const HashedAbstractPartition& other) override {
join_like_operation(other,
[](Domain* x, const Domain& y) { x->widen_with(y); });
}
void meet_with(const HashedAbstractPartition& other) override {
meet_like_operation(other,
[](Domain* x, const Domain& y) { x->meet_with(y); });
}
void narrow_with(const HashedAbstractPartition& other) override {
meet_like_operation(other,
[](Domain* x, const Domain& y) { x->narrow_with(y); });
}
void join_like_operation(
const HashedAbstractPartition& other,
std::function<void(Domain*, const Domain&)> operation) {
if (is_top()) {
return;
}
if (other.is_top()) {
set_to_top();
return;
}
for (const auto& other_binding : other.m_map) {
auto binding = m_map.find(other_binding.first);
if (binding == m_map.end()) {
// The value is Bottom, we just insert the other value (Bottom is the
// identity for join-like operations).
m_map[other_binding.first] = other_binding.second;
} else {
// We compute the join-like combination of the values.
operation(&binding->second, other_binding.second);
// By construction, it's impossible to have Bottom in both operands,
// hence the result can never be Bottom.
RUNTIME_CHECK(!binding->second.is_bottom(), internal_error());
}
}
}
void meet_like_operation(
const HashedAbstractPartition& other,
std::function<void(Domain*, const Domain&)> operation) {
if (is_top()) {
*this = other;
return;
}
if (other.is_top()) {
return;
}
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 Bottom, 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 meet-like combination of the values.
operation(&it->second, other_binding->second);
if (it->second.is_bottom()) {
// If the result is Bottom, we erase the binding.
auto to_erase = it++;
m_map.erase(to_erase);
} else {
++it;
}
}
}
}
static HashedAbstractPartition bottom() { return HashedAbstractPartition(); }
static HashedAbstractPartition top() {
auto hap = HashedAbstractPartition();
hap.m_is_top = true;
return hap;
}
private:
std::unordered_map<Label, Domain, LabelHash, LabelEqual> m_map;
bool m_is_top{false};
};
} // namespace sparta
template <typename Label,
typename Domain,
typename LabelHash,
typename LabelEqual>
inline std::ostream& operator<<(
std::ostream& o,
const typename sparta::
HashedAbstractPartition<Label, Domain, LabelHash, LabelEqual>&
partition) {
if (partition.is_bottom()) {
o << "_|_";
} else if (partition.is_top()) {
o << "T";
} else {
o << "[#" << partition.size() << "]";
o << "{";
auto& bindings = partition.bindings();
for (auto it = bindings.begin(); it != bindings.end();) {
o << it->first << " -> " << it->second;
++it;
if (it != bindings.end()) {
o << ", ";
}
}
o << "}";
}
return o;
}