include/IntervalDomain.h (147 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 <cassert>
#include <limits>
#include <ostream>
#include "AbstractDomain.h"
namespace sparta {
/*
* Closed integer intervals with boundaries of type Num -- a bounded integral
* type.
*
* The minimal and maximal elements of `Num` are designated as MIN and MAX
* respectively. Finite intervals smaller than or equal to (MIN, MAX) can be
* represented precisely by this type. Any overhang below MIN or above MAX
* (inclusive) is approximated by "extending out to infinity":
*
* [min, min] is approximated by [-inf, min]
* [max, max] is approximated by [max, +inf]
*
* Because of the handling of extremal values, it is recommended that Num be a
* signed type, even when only non-negative values are interesting, as on an
* unsigned type, 0 will take the position of MIN, causing a loss of precision:
*
* [0, 0] + [1, 1] = [-inf, min] + [1, 1]
* = [-inf, 1]
*
* _|_ has a special encoding of [MAX, MIN], making it the only inhabitant of
* the type for which the upperbound is strictly smaller than the lowerbound.
* This property is exploited for the implementation of is_bottom() and means
* that code that assumes a sensible ordering of bounds must be guarded by a
* check for `!is_bottom()`.
*/
template <typename Num>
class IntervalDomain final : public AbstractDomain<IntervalDomain<Num>> {
static_assert(std::is_integral<Num>::value, "expecting integer bounds.");
public:
static constexpr Num MIN = std::numeric_limits<Num>::min();
static constexpr Num MAX = std::numeric_limits<Num>::max();
static_assert(MIN < MAX, "Encoding of Bottom requires this property.");
static IntervalDomain bottom() { return {MAX, MIN}; }
/* [lb, ub] */
static IntervalDomain finite(Num lb, Num ub) {
assert(MIN < lb && "interval not bounded below.");
assert(lb <= ub && "interval inverted.");
assert(ub < MAX && "interval not bounded above.");
return {lb, ub};
}
/* [lb, +inf] */
static IntervalDomain bounded_below(Num lb) {
assert(MIN < lb && "interval underflow");
return {lb, MAX};
}
/* [-inf, ub] */
static IntervalDomain bounded_above(Num ub) {
assert(ub < MAX && "interval overflow.");
return {MIN, ub};
}
/* [max, +inf] */
static IntervalDomain high() { return {MAX, MAX}; }
/* [-inf, min] */
static IntervalDomain low() { return {MIN, MIN}; }
/* [-inf, +inf] */
static IntervalDomain top() { return {MIN, MAX}; }
/* Default constructor produces Top. */
IntervalDomain() : IntervalDomain(MIN, MAX) {}
/* Inclusive lower-bound of the interval, assuming interval is not bottom. */
Num lower_bound() const {
assert(!is_bottom());
return m_lb;
}
/*
* Inclusive upper-bound of the interval, assuming interval is not bottom.
* Guaranteed to be greater than or equal to lower_bound().
*/
Num upper_bound() const {
assert(!is_bottom());
return m_ub;
}
IntervalDomain& operator+=(const IntervalDomain& that) {
if (that.is_bottom()) {
set_to_bottom();
} else if (!is_bottom()) {
m_lb = m_lb == MIN ? m_lb : clamped_add(m_lb, that.m_lb);
m_ub = m_ub == MAX ? m_ub : clamped_add(m_ub, that.m_ub);
}
return *this;
}
IntervalDomain& operator+=(Num b) { return *this += {b, b}; }
IntervalDomain operator+(const IntervalDomain& that) const {
auto cpy = *this;
cpy += that;
return cpy;
}
bool is_bottom() const override { return m_lb > m_ub; }
bool is_top() const override { return m_lb == MIN && m_ub == MAX; }
bool leq(const IntervalDomain& that) const override {
return is_bottom() || (that.m_lb <= m_lb && m_ub <= that.m_ub);
}
bool equals(const IntervalDomain& that) const override {
return m_lb == that.m_lb && m_ub == that.m_ub;
}
void set_to_bottom() override {
m_lb = MAX;
m_ub = MIN;
}
void set_to_top() override {
m_lb = MIN;
m_ub = MAX;
}
/*
* _|_ \/ [a,b] = [a, b]
* [a,b] \/ _|_ = [a, b]
* [a,b] \/ [c,d] = [min(a,c), max(b,d)]
*/
void join_with(const IntervalDomain& that) override {
m_lb = std::min(m_lb, that.m_lb);
m_ub = std::max(m_ub, that.m_ub);
}
/*
* _|_ W [a,b] = [a, b]
* [a,b] W _|_ = [a, b]
* [a,b] W [c,d] = [ c < a ? -inf : a
* , b < d ? +inf : b]
*/
void widen_with(const IntervalDomain& that) override {
if (is_bottom()) {
*this = that;
return;
}
if (that.m_lb < m_lb) {
m_lb = MIN;
}
if (m_ub < that.m_ub) {
m_ub = MAX;
}
}
/*
* _|_ /\ _ = _|_
* _ /\ _|_ = _|_
* [a,b] /\ [c,d] = [max(a,c), min(b,d)]
*/
void meet_with(const IntervalDomain& that) override {
m_lb = std::max(m_lb, that.m_lb);
m_ub = std::min(m_ub, that.m_ub);
if (is_bottom()) {
// Normalize the representation of bottom to simplify equality.
set_to_bottom();
}
}
/*
* _|_ N _ = _|_
* _ N _|_ = _|_
* [a,b] N [c,d] = [ a == -inf ? c : a
* , b == +inf ? d : b]
*/
void narrow_with(const IntervalDomain& that) override {
if (that.is_bottom()) {
set_to_bottom();
return;
}
if (m_lb == MIN) {
m_lb = that.m_lb;
}
if (m_ub == MAX) {
m_ub = that.m_ub;
}
if (is_bottom()) {
// Normalize the representation of bottom to simplify equality.
set_to_bottom();
}
}
private:
Num m_lb;
Num m_ub;
IntervalDomain(Num lb, Num ub) : m_lb(lb), m_ub(ub) {}
/*
* Addition with overflow and underflow protection.
*/
static Num clamped_add(Num a, Num b) {
// a + b > MAX
if (a > 0 && b > MAX - a) {
return MAX;
}
// a + b < MIN
if (a < 0 && b < MIN - a) {
return MIN;
}
return a + b;
}
};
template <typename Num>
inline std::ostream& operator<<(std::ostream& o,
const sparta::IntervalDomain<Num>& i) {
if (i.is_bottom()) {
return o << "_|_";
}
if (i.is_top()) {
return o << "T";
}
o << "[";
if (i.lower_bound() == sparta::IntervalDomain<Num>::MIN) {
o << "-inf";
} else {
// Unary plus promotes types narrower than `int` to be at least `int`
// avoiding the ostream interpreting them as characters.
o << +i.lower_bound();
}
o << ", ";
if (i.upper_bound() == sparta::IntervalDomain<Num>::MAX) {
o << "+inf";
} else {
o << +i.upper_bound();
}
return o << "]";
}
} // namespace sparta