blob: a957cde9d61580b4735c170d9d4ba93f198b3b9d [file] [edit]
#include "simplify.h"
#include <cassert>
#include <iostream>
#include <limits>
#include "evaluate.h"
#include "node_mutator.h"
#include "print.h"
#include "substitute.h"
namespace slinky {
namespace {
expr x = variable::make(0);
expr y = variable::make(1);
expr z = variable::make(2);
expr w = variable::make(3);
expr c0 = wildcard::make(10, as_constant);
expr c1 = wildcard::make(11, as_constant);
expr c2 = wildcard::make(12, as_constant);
// Check if a and b are out of (canonical) order.
bool should_commute(const expr& a, const expr& b) {
auto order = [](node_type t) {
switch (t) {
case node_type::constant: return 100;
case node_type::wildcard: return 99;
case node_type::variable: return 0;
case node_type::call: return -1;
default: return 1;
}
};
int ra = order(a.type());
int rb = order(b.type());
if (ra > rb) return true;
const call* ca = a.as<call>();
const call* cb = b.as<call>();
if (ca && cb) {
if (ca->intrinsic > cb->intrinsic) return true;
}
return false;
}
// Rules that are not in canonical order are unnecessary or may cause infinite loops.
// This visitor checks that all commutable operations are in canonical order.
class assert_canonical : public recursive_node_visitor {
public:
template <typename T>
void check(const T* x) {
if (should_commute(x->a, x->b)) {
std::cerr << "Non-canonical operands: " << expr(x) << std::endl;
std::abort();
}
x->a.accept(this);
x->b.accept(this);
}
void visit(const add* op) { check(op); };
void visit(const mul* op) { check(op); };
void visit(const class min* op) { check(op); };
void visit(const class max* op) { check(op); };
void visit(const equal* op) { check(op); };
void visit(const not_equal* op) { check(op); };
void visit(const logical_and* op) { check(op); };
void visit(const logical_or* op) { check(op); };
};
// We need to generate a lot of rules that are equivalent except for commutation.
// To avoid repetitive error-prone code, we can generate all the valid commutative
// equivalents of an expression.
class commute_variants : public node_visitor {
public:
std::vector<expr> results;
void visit(const variable* x) override { results = {x}; }
void visit(const wildcard* x) override { results = {x}; }
void visit(const constant* x) override { results = {x}; }
void visit(const let* x) override { std::abort(); }
template <typename T>
void visit_binary(bool commutative, const T* x) {
// TODO: I think some of these patterns are redundant, but finding them is tricky.
x->a.accept(this);
std::vector<expr> a = std::move(results);
x->b.accept(this);
std::vector<expr> b = std::move(results);
results.clear();
results.reserve(a.size() * b.size());
for (const expr& i : a) {
for (const expr& j : b) {
if (match(i, j)) {
results.push_back(T::make(i, j));
} else {
results.push_back(T::make(i, j));
if (commutative) {
results.push_back(T::make(j, i));
}
}
}
}
}
void visit(const add* x) override { visit_binary(true, x); }
void visit(const sub* x) override { visit_binary(false, x); }
void visit(const mul* x) override { visit_binary(true, x); }
void visit(const div* x) override { visit_binary(false, x); }
void visit(const mod* x) override { visit_binary(false, x); }
void visit(const class min* x) override { visit_binary(true, x); }
void visit(const class max* x) override { visit_binary(true, x); }
void visit(const equal* x) override { visit_binary(true, x); }
void visit(const not_equal* x) override { visit_binary(true, x); }
void visit(const less* x) override { visit_binary(false, x); }
void visit(const less_equal* x) override { visit_binary(false, x); }
void visit(const logical_and* x) override { visit_binary(true, x); }
void visit(const logical_or* x) override { visit_binary(true, x); }
void visit(const logical_not* x) override {
x->x.accept(this);
for (expr& i : results) {
i = !i;
}
}
void visit(const class select* x) override {
x->condition.accept(this);
std::vector<expr> c = std::move(results);
x->true_value.accept(this);
std::vector<expr> t = std::move(results);
x->false_value.accept(this);
std::vector<expr> f = std::move(results);
results.clear();
results.reserve(c.size() * t.size() * f.size());
for (const expr& i : c) {
for (const expr& j : t) {
for (const expr& k : f) {
results.push_back(select::make(i, j, k));
}
}
}
}
void visit(const call* x) override {
if (x->args.size() == 1) {
x->args.front().accept(this);
for (expr& i : results) {
i = call::make(x->intrinsic, {i});
}
} else {
results = {x};
}
}
void visit(const let_stmt* x) override { std::abort(); }
void visit(const block* x) override { std::abort(); }
void visit(const loop* x) override { std::abort(); }
void visit(const if_then_else* x) override { std::abort(); }
void visit(const call_func* x) override { std::abort(); }
void visit(const allocate* x) override { std::abort(); }
void visit(const make_buffer* x) override { std::abort(); }
void visit(const crop_buffer* x) override { std::abort(); }
void visit(const crop_dim* x) override { std::abort(); }
void visit(const slice_buffer* x) override { std::abort(); }
void visit(const slice_dim* x) override { std::abort(); }
void visit(const truncate_rank* x) override { std::abort(); }
void visit(const check* x) override { std::abort(); }
};
class rule_set {
public:
struct rule {
expr pattern;
expr replacement;
expr predicate;
rule(expr p, expr r, expr pr = expr())
: pattern(std::move(p)), replacement(std::move(r)), predicate(std::move(pr)) {
assert_canonical v;
replacement.accept(&v);
}
};
private:
std::vector<rule> rules_;
public:
rule_set(std::initializer_list<rule> rules) {
for (const rule& i : rules) {
commute_variants v;
i.pattern.accept(&v);
for (expr& p : v.results) {
rules_.emplace_back(std::move(p), i.replacement, i.predicate);
}
}
}
expr apply(expr x) {
// std::cerr << "apply_rules: " << x << std::endl;
symbol_map<expr> matches;
for (const rule& r : rules_) {
matches.clear();
// std::cerr << " Considering " << r.pattern << std::endl;
if (match(r.pattern, x, matches)) {
// std::cerr << " Matched:" << std::endl;
// for (const auto& i : matches) {
// std::cerr << " " << i.first << ": " << i.second << std::endl;
// }
if (!r.predicate.defined() || prove_true(substitute(r.predicate, matches))) {
// std::cerr << " Applied " << r.pattern << " -> " << r.replacement << std::endl;
x = substitute(r.replacement, matches);
// std::cerr << " Result: " << x << std::endl;
return x;
} else {
// std::cerr << " Failed predicate: " << r.predicate << std::endl;
}
}
}
// std::cerr << " Failed" << std::endl;
return x;
}
};
} // namespace
expr simplify(const class min* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return std::min(*ca, *cb);
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = min::make(std::move(a), std::move(b));
}
static rule_set rules = {
// Constant simplifications
{min(x, indeterminate()), indeterminate()},
{min(x, std::numeric_limits<index_t>::max()), x},
{min(x, positive_infinity()), x},
{min(x, std::numeric_limits<index_t>::min()), std::numeric_limits<index_t>::min()},
{min(x, negative_infinity()), negative_infinity()},
{min(min(x, c0), c1), min(x, min(c0, c1))},
{min(x, x + c0), x, c0 > 0},
{min(x, x + c0), x + c0, c0 < 0},
{min(x + c0, y + c1), min(x, y + (c1 - c0)) + c0},
{min(x + c0, c1), min(x, c1 - c0) + c0},
{min(c0 - x, c0 - y), c0 - max(x, y)},
// Algebraic simplifications
{min(x, x), x},
{min(x, min(x, y)), min(x, y)},
{min(max(x, y), min(x, z)), min(x, z)},
{min(min(x, y), min(x, z)), min(x, min(y, z))},
{min(max(x, y), max(x, z)), max(x, min(y, z))},
{min(x / z, y / z), min(x, y) / z, z > 0},
{min(x / z, y / z), max(x, y) / z, z < 0},
{min(x * z, y * z), z * min(x, y), z > 0},
{min(x * z, y * z), z * max(x, y), z < 0},
{min(x + z, y + z), z + min(x, y)},
{min(x - z, y - z), min(x, y) - z},
{min(z - x, z - y), z - max(x, y)},
// Buffer meta simplifications
// TODO: These rules are sketchy, they assume buffer_max(x, y) > buffer_min(x, y), which
// is true if we disallow empty buffers...
{min(buffer_min(x, y), buffer_max(x, y)), buffer_min(x, y)},
{min(buffer_min(x, y), buffer_max(x, y) + c0), buffer_min(x, y), c0 > 0},
{min(buffer_max(x, y), buffer_min(x, y) + c0), buffer_min(x, y) + c0, c0 < 0},
};
return rules.apply(e);
}
expr simplify(const class max* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return std::max(*ca, *cb);
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = max::make(std::move(a), std::move(b));
}
static rule_set rules = {
// Constant simplifications
{max(x, indeterminate()), indeterminate()},
{max(x, std::numeric_limits<index_t>::min()), x},
{max(x, negative_infinity()), x},
{max(x, std::numeric_limits<index_t>::max()), std::numeric_limits<index_t>::max()},
{max(x, positive_infinity()), positive_infinity()},
{max(max(x, c0), c1), max(x, max(c0, c1))},
{max(x, x + c0), x + c0, c0 > 0},
{max(x, x + c0), x, c0 < 0},
{max(x + c0, y + c1), max(x, y + (c1 - c0)) + c0},
{max(x + c0, c1), max(x, c1 - c0) + c0},
{max(c0 - x, c0 - y), c0 - min(x, y)},
// Algebraic simplifications
{max(x, x), x},
{max(x, max(x, y)), max(x, y)},
{max(min(x, y), max(x, z)), max(x, z)},
{max(max(x, y), max(x, z)), max(x, max(y, z))},
{max(min(x, y), min(x, z)), min(x, max(y, z))},
{max(x / z, y / z), max(x, y) / z, z > 0},
{max(x / z, y / z), min(x, y) / z, z < 0},
{max(x * z, y * z), z * max(x, y), z > 0},
{max(x * z, y * z), z * min(x, y), z < 0},
{max(x + z, y + z), z + max(x, y)},
{max(x - z, y - z), max(x, y) - z},
{max(z - x, z - y), z - min(x, y)},
// Buffer meta simplifications
{max(buffer_min(x, y), buffer_max(x, y)), buffer_max(x, y)},
{max(buffer_min(x, y), buffer_max(x, y) + c0), buffer_max(x, y) + c0, c0 > 0},
{max(buffer_max(x, y), buffer_min(x, y) + c0), buffer_max(x, y), c0 < 0},
};
return rules.apply(e);
}
expr simplify(const add* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca + *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = add::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x + indeterminate(), indeterminate()},
{positive_infinity() + indeterminate(), indeterminate()},
{negative_infinity() + positive_infinity(), indeterminate()},
{c0 + positive_infinity(), positive_infinity()},
{c0 + negative_infinity(), negative_infinity()},
{x + 0, x},
{x + x, x * 2},
{x + (x + y), y + x * 2},
{x + (x - y), x * 2 - y},
{x + (y - x), y},
//{x + x * y, x * (y + 1)}, // Needs x to be non-constant or it loops with c0 * (x + c1) -> c0 * x + c0 * c1... how?
{x * y + x * z, x * (y + z)},
{(x + c0) + c1, x + (c0 + c1)},
{(c0 - x) + c1, (c0 + c1) - x},
{x + (c0 - y), (x - y) + c0},
{x + (y + c0), (x + y) + c0},
{(x + c0) - y, (x - y) + c0},
{(x + c0) + (y + c1), (x + y) + (c0 + c1)},
{min(x + c0, y + c1) + c2, min(x + (c0 + c2), y + (c1 + c2))},
{max(x + c0, y + c1) + c2, max(x + (c0 + c2), y + (c1 + c2))},
{min(c0 - x, y + c1) + c2, min((c0 + c2) - x, y + (c1 + c2))},
{max(c0 - x, y + c1) + c2, max((c0 + c2) - x, y + (c1 + c2))},
{min(c0 - x, c1 - y) + c2, min((c0 + c2) - x, (c1 + c2) - y)},
{max(c0 - x, c1 - y) + c2, max((c0 + c2) - x, (c1 + c2) - y)},
{select(x, c0, c1) + c2, select(x, c0 + c2, c1 + c2)},
{select(x, y + c0, c1) + c2, select(x, y + (c0 + c2), c1 + c2)},
{select(x, c0 - y, c1) + c2, select(x, (c0 + c2) - y, c1 + c2)},
{select(x, c0, y + c1) + c2, select(x, c0 + c2, y + (c1 + c2))},
{select(x, c0, c1 - y) + c2, select(x, c0 + c2, (c1 + c2) - y)},
{select(x, y + c0, z + c1) + c2, select(x, y + (c0 + c2), z + (c1 + c2))},
{select(x, c0 - y, z + c1) + c2, select(x, (c0 + c2) - y, z + (c1 + c2))},
{select(x, y + c0, c1 - z) + c2, select(x, y + (c0 + c2), (c1 + c2) - z)},
{select(x, c0 - y, c1 - z) + c2, select(x, (c0 + c2) - y, (c1 + c2) - z)},
{buffer_min(x, y) + buffer_extent(x, y), buffer_max(x, y) + 1},
{buffer_min(x, y) + (z - buffer_max(x, y)), (z - buffer_extent(x, y)) + 1},
{buffer_max(x, y) + (z - buffer_min(x, y)), (buffer_extent(x, y) + z) - 1},
};
return rules.apply(e);
}
expr simplify(const sub* op, expr a, expr b) {
assert(a.defined());
assert(b.defined());
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca - *cb;
} else if (cb) {
// Canonicalize to addition with constants.
return simplify(static_cast<add*>(nullptr), a, -*cb);
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = sub::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x - indeterminate(), indeterminate()},
{indeterminate() - x, indeterminate()},
{positive_infinity() - positive_infinity(), indeterminate()},
{positive_infinity() - negative_infinity(), positive_infinity()},
{negative_infinity() - negative_infinity(), indeterminate()},
{negative_infinity() - positive_infinity(), negative_infinity()},
{c0 - positive_infinity(), negative_infinity()},
{c0 - negative_infinity(), positive_infinity()},
{x - x, 0},
{x - 0, x},
{x - (c0 - y), (x + y) - c0},
{c0 - (x - y), (y - x) + c0},
{x - (y + c0), (x - y) - c0},
{(c0 - x) - y, c0 - (x + y)},
{(x + c0) - y, (x - y) + c0},
{(x + y) - x, y},
{x - (x + y), -y},
{(c0 - x) - (y - z), ((z - x) - y) + c0},
{(x + c0) - (y + c1), (x - y) + (c0 - c1)},
{c2 - select(x, c0, c1), select(x, c2 - c0, c2 - c1)},
{c2 - select(x, y + c0, c1), select(x, (c2 - c0) - y, c2 - c1)},
{c2 - select(x, c0 - y, c1), select(x, y + (c2 - c0), c2 - c1)},
{c2 - select(x, c0, y + c1), select(x, c2 - c0, (c2 - c1) - y)},
{c2 - select(x, c0, c1 - y), select(x, c2 - c0, y + (c2 - c1))},
{c2 - select(x, y + c0, z + c1), select(x, (c2 - c0) - y, (c2 - c1) - z)},
{c2 - select(x, c0 - y, z + c1), select(x, y + (c2 - c0), (c2 - c1) - z)},
{c2 - select(x, y + c0, c1 - z), select(x, (c2 - c0) - y, z + (c2 - c1))},
{c2 - select(x, c0 - y, c1 - z), select(x, y + (c2 - c0), z + (c2 - c1))},
{buffer_max(x, y) - buffer_min(x, y), buffer_extent(x, y) - 1},
{buffer_max(x, y) - (buffer_min(x, y) + z), (buffer_extent(x, y) - z) - 1},
{(buffer_max(x, y) + z) - buffer_min(x, y), (buffer_extent(x, y) + z) - 1},
};
return rules.apply(e);
}
expr simplify(const mul* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca * *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = mul::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x * indeterminate(), indeterminate()},
{positive_infinity() * positive_infinity(), positive_infinity()},
{negative_infinity() * positive_infinity(), negative_infinity()},
{negative_infinity() * negative_infinity(), positive_infinity()},
{c0 * positive_infinity(), positive_infinity(), c0 > 0},
{c0 * negative_infinity(), negative_infinity(), c0 > 0},
{c0 * positive_infinity(), negative_infinity(), c0 < 0},
{c0 * negative_infinity(), positive_infinity(), c0 < 0},
{x * 0, 0},
{x * 1, x},
{(x * c0) * c1, x * (c0 * c1)},
{(x + c0) * c1, x * c1 + c0 * c1},
{(0 - x) * c1, x * (-c1)},
{(c0 - x) * c1, c0 * c1 - x * c1},
};
return rules.apply(e);
}
expr simplify(const div* op, expr a, expr b) {
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return euclidean_div(*ca, *cb);
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = div::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x / indeterminate(), indeterminate()},
{indeterminate() / x, indeterminate()},
{positive_infinity() / positive_infinity(), indeterminate()},
{positive_infinity() / negative_infinity(), indeterminate()},
{negative_infinity() / positive_infinity(), indeterminate()},
{negative_infinity() / negative_infinity(), indeterminate()},
{c0 / positive_infinity(), 0},
{c0 / negative_infinity(), 0},
{positive_infinity() / c0, positive_infinity(), c0 > 0},
{negative_infinity() / c0, negative_infinity(), c0 > 0},
{positive_infinity() / c0, negative_infinity(), c0 < 0},
{negative_infinity() / c0, positive_infinity(), c0 < 0},
{x / 0, 0},
{0 / x, 0},
{x / 1, x},
{x / x, x != 0},
};
return rules.apply(e);
}
expr simplify(const mod* op, expr a, expr b) {
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return euclidean_mod(*ca, *cb);
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = mod::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x % 1, 0},
{x % 0, 0},
{x % x, 0},
};
return rules.apply(e);
}
expr simplify(const less* op, expr a, expr b) {
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca < *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = less::make(std::move(a), std::move(b));
}
static rule_set rules = {
{positive_infinity() < c0, false},
{negative_infinity() < c0, true},
{x < x, false},
{x + c0 < c1, x < c1 - c0},
{x < x + y, 0 < y},
{x + y < x, y < 0},
{x - y < x, 0 < y},
{0 - x < c0, -c0 < x},
{c0 - x < c1, c0 - c1 < x},
{c0 < c1 - x, x < c1 - c0},
{x + y < x + z, y < z},
{buffer_extent(x, y) < c0, false, c0 < 0},
{c0 < buffer_extent(x, y), true, c0 < 0},
};
return rules.apply(e);
}
expr simplify(const less_equal* op, expr a, expr b) {
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca <= *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = less_equal::make(std::move(a), std::move(b));
}
static rule_set rules = {
{positive_infinity() <= c0, false},
{negative_infinity() <= c0, true},
{c0 <= positive_infinity(), true},
{c0 <= negative_infinity(), false},
{x <= x, true},
{x <= x + y, 0 <= y},
{x + y <= x, y <= 0},
{x - y <= x, 0 <= y},
{0 - x <= c0, -c0 <= x},
{c0 - x <= y, c0 <= y + x},
{x <= c1 - y, x + y <= c1},
{x + c0 <= y + c1, x - y <= c1 - c0},
{x + y <= x + z, y <= z},
{buffer_extent(x, y) <= c0, false, c0 <= 0},
{c0 <= buffer_extent(x, y), true, c0 <= 0},
};
return rules.apply(e);
}
expr simplify(const equal* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca == *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = equal::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x == x, true},
{x + c0 == c1, x == c1 - c0},
{c0 - x == c1, -x == c1 - c0, c0 != 0},
};
return rules.apply(e);
}
expr simplify(const not_equal* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca != *cb;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = not_equal::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x != x, false},
{x + c0 != c1, x != c1 - c0},
{c0 - x != c1, -x != c1 - c0, c0 != 0},
};
return rules.apply(e);
}
expr simplify(const logical_and* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca != 0 && *cb != 0;
} else if (cb) {
return *cb ? a : b;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = logical_and::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x && x, x},
{x && !x, false},
{!x && !y, !(x || y)},
{x && (x && y), x && y},
{x && (x || y), x},
};
return rules.apply(e);
}
expr simplify(const logical_or* op, expr a, expr b) {
if (should_commute(a, b)) {
std::swap(a, b);
}
const index_t* ca = as_constant(a);
const index_t* cb = as_constant(b);
if (ca && cb) {
return *ca != 0 || *cb != 0;
} else if (cb) {
return *cb ? b : a;
}
expr e;
if (op && a.same_as(op->a) && b.same_as(op->b)) {
e = op;
} else {
e = logical_or::make(std::move(a), std::move(b));
}
static rule_set rules = {
{x || x, x},
{x || !x, true},
{!x || !y, !(x && y)},
{x || (x && y), x},
{x || (x || y), x || y},
};
return rules.apply(e);
}
expr simplify(const logical_not* op, expr value) {
const index_t* cv = as_constant(value);
if (cv) {
return *cv == 0;
}
expr e;
if (op && value.same_as(op->x)) {
e = op;
} else {
e = logical_not::make(std::move(value));
}
static rule_set rules = {
{!!x, x},
{!(x == y), x != y},
{!(x != y), x == y},
{!(x < y), y <= x},
{!(x <= y), y < x},
};
return rules.apply(e);
}
expr simplify(const class select* op, expr c, expr t, expr f) {
std::optional<bool> const_c = attempt_to_prove(c);
if (const_c) {
if (*const_c) {
return op->true_value;
} else {
return op->false_value;
}
}
expr e;
if (match(t, f)) {
return t;
} else if (op && c.same_as(op->condition) && t.same_as(op->true_value) && f.same_as(op->false_value)) {
e = op;
} else {
e = select::make(std::move(c), std::move(t), std::move(f));
}
static rule_set rules = {
{select(!x, y, z), select(x, z, y)},
// Pull common expressions out
{select(x, y, y + z), y + select(x, 0, z)},
{select(x, y + z, y), y + select(x, z, 0)},
{select(x, y + z, y + w), y + select(x, z, w)},
{select(x, z - y, w - y), select(x, z, w) - y},
};
return rules.apply(e);
}
expr simplify(const call* op, std::vector<expr> args) {
bool constant = true;
bool changed = false;
assert(op->args.size() == args.size());
for (std::size_t i = 0; i < args.size(); ++i) {
constant = constant && as_constant(args[i]);
changed = changed || !args[i].same_as(op->args[i]);
}
if (op->intrinsic == intrinsic::buffer_at) {
// Trailing undefined indices can be removed.
for (index_t d = 1; d < args.size(); ++d) {
// buffer_at(b, buffer_min(b, 0)) is equivalent to buffer_base(b)
if (args[d].defined() && match(args[d], buffer_min(args[0], d - 1))) {
args[d] = expr();
changed = true;
}
}
// Trailing undefined args have no effect.
while (args.size() > 1 && !args.back().defined()) {
args.pop_back();
changed = true;
}
if (args.size() == 1) {
return call::make(intrinsic::buffer_base, std::move(args));
}
}
expr e;
if (op && !changed) {
e = op;
} else {
e = call::make(op->intrinsic, std::move(args));
}
if (can_evaluate(op->intrinsic) && constant) {
return evaluate(e);
}
static rule_set rules = {
{abs(negative_infinity()), positive_infinity()},
{abs(-x), abs(x)},
{abs(abs(x)), abs(x)},
};
return rules.apply(e);
}
namespace {
class simplifier : public node_mutator {
symbol_map<int> references;
symbol_map<box_expr> buffer_bounds;
bounds_map expr_bounds;
public:
simplifier(const bounds_map& expr_bounds) : expr_bounds(expr_bounds) {}
interval_expr mutate(const interval_expr& x) {
interval_expr result = {mutate(x.min), mutate(x.max)};
if (!result.min.same_as(result.max) && match(result.min, result.max)) {
// If the bounds are the same, make sure same_as returns true.
result.max = result.min;
}
return result;
}
using node_mutator::mutate;
void visit(const variable* op) override {
auto& ref_count = references[op->name];
if (!ref_count) {
ref_count = 1;
} else {
*ref_count += 1;
}
set_result(op);
}
template <typename T>
void visit_binary(const T* op) {
expr a = mutate(op->a);
expr b = mutate(op->b);
expr e = simplify(op, std::move(a), std::move(b));
if (!e.same_as(op)) {
e = mutate(e);
}
set_result(e);
}
void visit(const class min* op) override { visit_binary(op); }
void visit(const class max* op) override { visit_binary(op); }
void visit(const add* op) override { visit_binary(op); }
void visit(const sub* op) override {
expr a = mutate(op->a);
expr b = mutate(op->b);
const index_t* cb = as_constant(b);
if (cb && *cb < 0) {
// Canonicalize to addition with constants.
set_result(mutate(a + -*cb));
} else {
expr e = simplify(op, std::move(a), std::move(b));
if (!e.same_as(op)) {
e = mutate(e);
}
set_result(e);
}
}
void visit(const mul* op) override { visit_binary(op); }
void visit(const div* op) override { visit_binary(op); }
void visit(const mod* op) override { visit_binary(op); }
void visit(const less* op) override { visit_binary(op); }
void visit(const less_equal* op) override { visit_binary(op); }
void visit(const equal* op) override { visit_binary(op); }
void visit(const not_equal* op) override { visit_binary(op); }
void visit(const logical_and* op) override { visit_binary(op); }
void visit(const logical_or* op) override { visit_binary(op); }
void visit(const logical_not* op) override {
expr x = mutate(op->x);
expr e = simplify(op, std::move(x));
if (!e.same_as(op)) {
e = mutate(e);
}
set_result(e);
}
void visit(const class select* op) override {
expr c = mutate(op->condition);
std::optional<bool> const_c = attempt_to_prove(c, expr_bounds);
if (const_c) {
if (*const_c) {
set_result(mutate(op->true_value));
} else {
set_result(mutate(op->false_value));
}
return;
}
expr t = mutate(op->true_value);
expr f = mutate(op->false_value);
expr e = simplify(op, std::move(c), std::move(t), std::move(f));
if (!e.same_as(op)) {
e = mutate(e);
}
set_result(e);
}
void visit(const call* op) override {
std::vector<expr> args;
args.reserve(op->args.size());
for (const expr& i : op->args) {
args.push_back(mutate(i));
}
expr e = simplify(op, std::move(args));
if (!e.same_as(op)) {
e = mutate(e);
}
set_result(e);
}
template <typename T>
auto visit_let(const T* op) {
expr value = mutate(op->value);
auto set_bounds = set_value_in_scope(expr_bounds, op->name, bounds_of(value, expr_bounds));
auto ref_count = set_value_in_scope(references, op->name, 0);
auto body = mutate(op->body);
int refs = *references[op->name];
if (refs == 0) {
// This let is dead
return body;
} else if (refs == 1 || value.as<constant>() || value.as<variable>()) {
return mutate(substitute(body, op->name, value));
} else if (value.same_as(op->value) && body.same_as(op->body)) {
return decltype(body){op};
} else {
return T::make(op->name, std::move(value), std::move(body));
}
}
void visit(const let* op) override { set_result(visit_let(op)); }
void visit(const let_stmt* op) override { set_result(visit_let(op)); }
void visit(const loop* op) override {
interval_expr bounds = mutate(op->bounds);
auto set_bounds = set_value_in_scope(expr_bounds, op->name, bounds);
stmt body = mutate(op->body);
if (bounds.same_as(op->bounds) && body.same_as(op->body)) {
set_result(op);
} else {
set_result(loop::make(op->name, std::move(bounds), std::move(body)));
}
}
void visit(const if_then_else* op) override {
expr c = mutate(op->condition);
std::optional<bool> const_c = attempt_to_prove(c, expr_bounds);
if (const_c) {
if (*const_c) {
set_result(mutate(op->true_body));
} else {
set_result(mutate(op->false_body));
}
return;
}
stmt t = mutate(op->true_body);
stmt f = mutate(op->false_body);
if (const logical_not* n = c.as<logical_not>()) {
c = n->x;
std::swap(t, f);
}
if (!t.defined() && !f.defined()) {
set_result(t);
} else if (t.defined() && f.defined() && match(t, f)) {
set_result(t);
} else if (c.same_as(op->condition) && t.same_as(op->true_body) && f.same_as(op->false_body)) {
set_result(op);
} else {
set_result(if_then_else::make(std::move(c), std::move(t), std::move(f)));
}
}
void visit(const block* op) override {
stmt a = mutate(op->a);
stmt b = mutate(op->b);
const if_then_else* a_if = a.as<if_then_else>();
const if_then_else* b_if = b.as<if_then_else>();
if (a_if && b_if && match(a_if->condition, b_if->condition)) {
stmt true_body = mutate(block::make({a_if->true_body, b_if->true_body}));
stmt false_body = mutate(block::make({a_if->false_body, b_if->false_body}));
set_result(if_then_else::make(a_if->condition, true_body, false_body));
} else if (!a.defined() && !b.defined()) {
set_result(stmt());
} else if (!a.defined()) {
set_result(b);
} else if (!b.defined()) {
set_result(a);
} else if (a.same_as(op->a) && b.same_as(op->b)) {
set_result(op);
} else {
set_result(block::make(std::move(a), std::move(b)));
}
}
void visit(const allocate* op) override {
std::vector<dim_expr> dims;
box_expr bounds;
dims.reserve(op->dims.size());
for (const dim_expr& i : op->dims) {
interval_expr bounds_i = mutate(i.bounds);
dims.emplace_back(bounds_i, mutate(i.stride), mutate(i.fold_factor));
bounds.push_back(bounds_i);
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, std::move(bounds));
stmt body = mutate(op->body);
set_result(allocate::make(op->storage, op->name, op->elem_size, std::move(dims), std::move(body)));
}
void visit(const make_buffer* op) override {
expr base = mutate(op->base);
expr elem_size = mutate(op->elem_size);
std::vector<dim_expr> dims;
box_expr bounds;
dims.reserve(op->dims.size());
bounds.reserve(op->dims.size());
bool changed = false;
for (const dim_expr& d : op->dims) {
interval_expr new_bounds = mutate(d.bounds);
dim_expr new_dim = {new_bounds, mutate(d.stride), mutate(d.fold_factor)};
changed = changed || !new_dim.same_as(d);
dims.push_back(std::move(new_dim));
bounds.push_back(std::move(new_bounds));
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, std::move(bounds));
stmt body = mutate(op->body);
// Check if this make_buffer is equivalent to truncate_rank
var buf(op->name);
if (match(base, buffer_base(buf)) && match(elem_size, buffer_elem_size(buf))) {
bool is_truncate = true;
for (index_t d = 0; d < static_cast<index_t>(dims.size()); ++d) {
is_truncate = is_truncate && match(dims[d], buffer_dim(buf, d));
}
if (is_truncate) {
set_result(truncate_rank::make(op->name, dims.size(), std::move(body)));
return;
}
}
// Check if this make_buffer is equivalent to slice_buffer or crop_buffer
if (const call* bc = base.as<call>()) {
if (bc->intrinsic == intrinsic::buffer_at && match(bc->args[0], buf) && match(elem_size, buffer_elem_size(buf))) {
// To be a slice, we need every dimension that is present in the buffer_at call to be skipped, and the rest of
// the dimensions to be identity.
index_t dim = 0;
bool is_slice = true;
for (index_t d = 0; d < static_cast<index_t>(dims.size()); ++d) {
if (d + 1 < static_cast<index_t>(bc->args.size()) && bc->args[d + 1].defined()) {
// Skip this dimension.
++dim;
} else {
// This arg is undefined. We need to find the next dimension here to be a slice.
is_slice = is_slice && match(dims[dim], buffer_dim(buf, d));
}
}
if (is_slice) {
std::vector<expr> at(bc->args.begin() + 1, bc->args.end());
set_result(slice_buffer::make(op->name, std::move(at), std::move(body)));
return;
}
// To be a crop, we need dimensions to either be identity, or the buffer_at argument is the same as the min.
bool is_crop = true;
box_expr crop_bounds(dims.size());
for (index_t d = 0; d < static_cast<index_t>(dims.size()); ++d) {
if (!match(dims[d].stride, buffer_stride(buf, d)) ||
!match(dims[d].fold_factor, buffer_fold_factor(buf, d))) {
is_crop = false;
break;
}
// If the argument is defined, we need the min to be the same as the argument.
// If it is not defined, it must be buffer_min(buf, d).
bool has_at_d = d + 1 < static_cast<index_t>(bc->args.size()) && bc->args[d + 1].defined();
expr crop_min = has_at_d ? bc->args[d + 1] : buffer_min(buf, d);
if (match(dims[d].bounds.min, crop_min)) {
crop_bounds[d] = dims[d].bounds;
} else {
is_crop = false;
break;
}
}
if (is_crop) {
set_result(mutate(crop_buffer::make(op->name, std::move(crop_bounds), std::move(body))));
return;
}
}
}
if (changed || !base.same_as(op->base) || !elem_size.same_as(op->elem_size) || !body.same_as(op->body)) {
set_result(make_buffer::make(op->name, std::move(base), std::move(elem_size), std::move(dims), std::move(body)));
} else {
set_result(op);
}
}
void visit(const crop_buffer* op) override {
// This is the bounds of the buffer as we understand them, for simplifying the inner scope.
box_expr bounds(op->bounds.size());
// This is the new bounds of the crop operation. Crops that are no-ops become undefined here.
box_expr new_bounds(op->bounds.size());
// If possible, rewrite crop_buffer of one dimension to crop_dim.
std::optional<box_expr> prev_bounds = buffer_bounds[op->name];
index_t dims_count = 0;
bool changed = false;
for (index_t i = 0; i < static_cast<index_t>(op->bounds.size()); ++i) {
interval_expr bounds_i = mutate(op->bounds[i]);
changed = changed || !bounds_i.same_as(op->bounds[i]);
bounds[i] = bounds_i;
// If the new bounds are the same as the existing bounds, set the crop in this dimension to
// be undefined.
if (prev_bounds && i < static_cast<index_t>(prev_bounds->size())) {
if (prove_true(bounds_i.min == (*prev_bounds)[i].min) && prove_true(bounds_i.max == (*prev_bounds)[i].max)) {
bounds_i.min = expr();
bounds_i.max = expr();
}
}
new_bounds[i] = bounds_i;
dims_count += bounds_i.min.defined() && bounds_i.max.defined() ? 1 : 0;
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, bounds);
stmt body = mutate(op->body);
// Remove trailing undefined bounds.
while (new_bounds.size() > 0 && !new_bounds.back().min.defined() && !new_bounds.back().max.defined()) {
new_bounds.pop_back();
}
if (new_bounds.empty()) {
// This crop was a no-op.
set_result(std::move(body));
} else if (dims_count == 1) {
// This crop is of one dimension, replace it with crop_dim.
// We removed undefined trailing bounds, so this must be the dim we want.
int d = static_cast<int>(new_bounds.size()) - 1;
set_result(crop_dim::make(op->name, d, std::move(new_bounds[d]), std::move(body)));
} else if (changed || !body.same_as(op->body)) {
set_result(crop_buffer::make(op->name, std::move(new_bounds), std::move(body)));
} else {
set_result(op);
}
}
void visit(const crop_dim* op) override {
interval_expr bounds = mutate(op->bounds);
if (!bounds.min.defined() && !bounds.max.defined()) {
set_result(mutate(op->body));
return;
}
std::optional<box_expr> buf_bounds = buffer_bounds[op->name];
if (buf_bounds && op->dim < static_cast<index_t>(buf_bounds->size())) {
interval_expr& dim = (*buf_bounds)[op->dim];
if (prove_true(bounds.min == dim.min) && prove_true(bounds.max == dim.max)) {
// This crop is a no-op.
set_result(mutate(op->body));
return;
}
(*buf_bounds)[op->dim] = bounds;
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, buf_bounds);
stmt body = mutate(op->body);
if (bounds.same_as(op->bounds) && body.same_as(op->body)) {
set_result(op);
} else {
set_result(crop_dim::make(op->name, op->dim, std::move(bounds), std::move(body)));
}
}
void visit(const slice_buffer* op) override {
// Update the bounds for the slice. Sliced dimensions are removed from the bounds.
std::optional<box_expr> bounds = buffer_bounds[op->name];
std::vector<expr> at(op->at.size());
std::size_t dims_count = 0;
bool changed = false;
for (index_t i = 0; i < static_cast<index_t>(op->at.size()); ++i) {
if (op->at[i].defined()) {
at[i] = mutate(op->at[i]);
changed = changed || !at[i].same_as(op->at[i]);
// We sliced this dimension. Remove it from the bounds.
if (bounds && i < static_cast<index_t>(bounds->size())) {
bounds->erase(bounds->begin() + i);
}
++dims_count;
}
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, bounds);
stmt body = mutate(op->body);
// Remove trailing undefined bounds.
while (at.size() > 0 && !at.back().defined()) {
at.pop_back();
}
changed = changed || at.size() != op->at.size();
if (at.empty()) {
// This slice was a no-op.
set_result(std::move(body));
} else if (dims_count == 1) {
// This slice is of one dimension, replace it with slice_dim.
// We removed undefined trailing bounds, so this must be the dim we want.
int d = static_cast<int>(at.size()) - 1;
set_result(slice_dim::make(op->name, d, std::move(at[d]), std::move(body)));
} else if (changed || !body.same_as(op->body)) {
set_result(slice_buffer::make(op->name, std::move(at), std::move(body)));
} else {
set_result(op);
}
}
void visit(const slice_dim* op) override {
expr at = mutate(op->at);
std::optional<box_expr> bounds = buffer_bounds[op->name];
if (bounds && op->dim < static_cast<index_t>(bounds->size())) {
bounds->erase(bounds->begin() + op->dim);
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, bounds);
stmt body = mutate(op->body);
if (at.same_as(op->at) && body.same_as(op->body)) {
set_result(op);
} else {
set_result(slice_dim::make(op->name, op->dim, std::move(at), std::move(body)));
}
}
void visit(const truncate_rank* op) override {
std::optional<box_expr> bounds = buffer_bounds[op->name];
if (bounds && static_cast<int>(bounds->size()) > op->rank) {
bounds->resize(op->rank);
}
auto set_bounds = set_value_in_scope(buffer_bounds, op->name, bounds);
stmt body = mutate(op->body);
if (body.same_as(op->body)) {
set_result(op);
} else {
set_result(truncate_rank::make(op->name, op->rank, std::move(body)));
}
}
void visit(const check* op) override {
if (!op->condition.defined()) {
set_result(op);
return;
}
expr c = mutate(op->condition);
std::optional<bool> const_c = attempt_to_prove(c, expr_bounds);
if (const_c) {
if (*const_c) {
set_result(stmt());
} else {
std::cerr << op->condition << " is statically false." << std::endl;
std::abort();
}
} else if (c.same_as(op->condition)) {
set_result(op);
} else {
set_result(check::make(std::move(c)));
}
}
};
} // namespace
expr simplify(const expr& e, const bounds_map& bounds) { return simplifier(bounds).mutate(e); }
stmt simplify(const stmt& s, const bounds_map& bounds) { return simplifier(bounds).mutate(s); }
namespace {
class find_bounds : public node_visitor {
bounds_map bounds;
public:
find_bounds(const bounds_map& bounds) : bounds(bounds) {}
interval_expr result;
template <typename T>
void visit_variable(const T* x) {
if (bounds.contains(x->name)) {
result = *bounds.lookup(x->name);
} else {
result = {x, x};
}
}
void visit(const variable* x) override { visit_variable(x); }
void visit(const wildcard* x) override { visit_variable(x); }
void visit(const constant* x) override { result = point(x); }
void visit(const let* x) override {
x->value.accept(this);
auto s = set_value_in_scope(bounds, x->name, result);
x->body.accept(this);
}
struct binary_result {
interval_expr a;
interval_expr b;
};
template <typename T>
binary_result binary_bounds(const T* x) {
x->a.accept(this);
interval_expr ba = result;
x->b.accept(this);
return {ba, result};
}
template <typename T>
void visit_linear(const T* x) {
binary_result r = binary_bounds(x);
result = {simplify(x, r.a.min, r.b.min), simplify(x, r.a.max, r.b.max)};
}
void visit(const add* x) override { visit_linear(x); }
void visit(const sub* x) override {
binary_result r = binary_bounds(x);
result = {simplify(x, r.a.min, r.b.max), simplify(x, r.a.max, r.b.min)};
}
void visit(const mul* x) override {
binary_result r = binary_bounds(x);
// TODO: I'm pretty sure there are cases missing here that would produce simpler bounds than the fallback cases.
if (is_non_negative(r.a.min) && is_non_negative(r.b.min)) {
// Both are >= 0, neither intervals flip.
result = {simplify(x, r.a.min, r.b.min), simplify(x, r.a.max, r.b.max)};
} else if (is_non_positive(r.a.max) && is_non_positive(r.b.max)) {
// Both are <= 0, both intervals flip.
result = {simplify(x, r.a.max, r.b.max), simplify(x, r.a.min, r.b.min)};
} else if (r.b.is_single_point()) {
if (is_non_negative(r.b.min)) {
result = {simplify(x, r.a.min, r.b.min), simplify(x, r.a.max, r.b.min)};
} else if (is_non_positive(r.b.min)) {
result = {simplify(x, r.a.max, r.b.min), simplify(x, r.a.min, r.b.min)};
} else {
expr corners[] = {
simplify(x, r.a.min, r.b.min),
simplify(x, r.a.max, r.b.min),
};
result = {min(corners), max(corners)};
}
} else if (r.a.is_single_point()) {
if (is_non_negative(r.a.min)) {
result = {simplify(x, r.a.min, r.b.min), simplify(x, r.a.min, r.b.max)};
} else if (is_non_positive(r.a.min)) {
result = {simplify(x, r.a.min, r.b.max), simplify(x, r.a.min, r.b.min)};
} else {
expr corners[] = {
simplify(x, r.a.min, r.b.min),
simplify(x, r.a.min, r.b.max),
};
result = {min(corners), max(corners)};
}
} else {
// We don't know anything. The results is the union of all 4 possible intervals.
expr corners[] = {
simplify(x, r.a.min, r.b.min),
simplify(x, r.a.min, r.b.max),
simplify(x, r.a.max, r.b.min),
simplify(x, r.a.max, r.b.max),
};
result = {min(corners), max(corners)};
}
}
void visit(const div* x) override {
binary_result r = binary_bounds(x);
// Because b is an integer, the bounds of a will only be shrunk
// (we define division by 0 to be 0). The absolute value of the
// bounds are maximized when b is 1 or -1.
if (r.b.is_single_point() && is_zero(r.b.min)) {
result = {0, 0};
} else if (is_positive(r.b.min)) {
// b > 0 => the biggest result in absolute value occurs at the min of b.
result = (r.a | -r.a) / r.b.min;
} else if (is_negative(r.b.max)) {
// b < 0 => the biggest result in absolute value occurs at the max of b.
result = (r.a | -r.a) / r.b.max;
} else {
result = r.a | -r.a;
}
}
void visit(const mod* x) override {
binary_result r = binary_bounds(x);
result = {0, max(abs(r.b.min), abs(r.b.max))};
}
void visit(const class min* x) override { visit_linear(x); }
void visit(const class max* x) override { visit_linear(x); }
template <typename T>
void visit_less(const T* x) {
binary_result r = binary_bounds(x);
// This bit of genius comes from
// https://github.com/halide/Halide/blob/61b8d384b2b799cd47634e4a3b67aa7c7f580a46/src/Bounds.cpp#L829
result = {simplify(x, r.a.max, r.b.min), simplify(x, r.a.min, r.b.max)};
}
void visit(const less* x) override { visit_less(x); }
void visit(const less_equal* x) override { visit_less(x); }
void visit(const equal* x) override {
binary_result r = binary_bounds(x);
result.min = 0;
result.max = r.a.min <= r.b.max && r.b.min <= r.a.max;
}
void visit(const not_equal* x) override {
binary_result r = binary_bounds(x);
result.min = r.a.max < r.b.min || r.b.max < r.a.min;
result.max = 1;
}
void visit(const logical_and* x) override { visit_linear(x); }
void visit(const logical_or* x) override { visit_linear(x); }
void visit(const logical_not* x) override {
x->x.accept(this);
result = {simplify(x, result.max), simplify(x, result.min)};
}
void visit(const class select* x) override {
x->condition.accept(this);
interval_expr cb = result;
x->true_value.accept(this);
interval_expr tb = result;
x->false_value.accept(this);
interval_expr fb = result;
if (is_true(cb.min)) {
result = tb;
} else if (is_false(cb.max)) {
result = fb;
} else {
result = fb | tb;
}
}
void visit(const call* x) override {
switch (x->intrinsic) {
case intrinsic::abs: result = {0, x}; return;
default: result = {x, x}; return;
}
}
void visit(const let_stmt* x) override { std::abort(); }
void visit(const block* x) override { std::abort(); }
void visit(const loop* x) override { std::abort(); }
void visit(const if_then_else* x) override { std::abort(); }
void visit(const call_func* x) override { std::abort(); }
void visit(const allocate* x) override { std::abort(); }
void visit(const make_buffer* x) override { std::abort(); }
void visit(const crop_buffer* x) override { std::abort(); }
void visit(const crop_dim* x) override { std::abort(); }
void visit(const slice_buffer* x) override { std::abort(); }
void visit(const slice_dim* x) override { std::abort(); }
void visit(const truncate_rank* x) override { std::abort(); }
void visit(const check* x) override { std::abort(); }
};
} // namespace
interval_expr bounds_of(const expr& e, const bounds_map& bounds) {
find_bounds fb(bounds);
e.accept(&fb);
return fb.result;
}
std::optional<bool> attempt_to_prove(const expr& condition, const bounds_map& expr_bounds) {
simplifier s(expr_bounds);
expr c = s.mutate(condition);
interval_expr bounds = bounds_of(c, expr_bounds);
if (is_true(s.mutate(bounds.min))) {
return true;
} else if (is_false(s.mutate(bounds.max))) {
return false;
} else {
return {};
}
}
bool prove_true(const expr& condition, const bounds_map& bounds) {
std::optional<bool> r = attempt_to_prove(condition, bounds);
return r && *r;
}
bool prove_false(const expr& condition, const bounds_map& bounds) {
std::optional<bool> r = attempt_to_prove(condition, bounds);
return r && !*r;
}
interval_expr where_true(const expr& condition, symbol_id var) {
// TODO: This needs a proper implementation. For now, a ridiculous hack: trial and error.
// We use the leaves of the expression as guesses around which to search.
// We could use every node in the expression...
class initial_guesses : public recursive_node_visitor {
public:
std::vector<expr> leaves;
void visit(const variable* x) { leaves.push_back(x); }
void visit(const constant* x) { leaves.push_back(x); }
};
initial_guesses v;
condition.accept(&v);
std::vector<expr> offsets;
offsets.push_back(negative_infinity());
for (index_t i = -10; i <= 10; ++i) {
offsets.push_back(i);
}
offsets.push_back(positive_infinity());
interval_expr result = interval_expr::none();
for (const expr& i : v.leaves) {
interval_expr result_i;
for (const expr& j : offsets) {
if (!result_i.min.defined()) {
// Find the first offset where the expression is true.
if (prove_true(substitute(condition, var, i + j))) {
result_i.min = i + j;
result_i.max = result_i.min;
}
} else if (prove_true(substitute(condition, var, i + j))) {
// Find the last offset where the expression is true.
result_i.max = i + j;
}
}
if (result_i.min.defined()) {
result.min = simplify(min(result.min, result_i.min));
}
if (result_i.max.defined()) {
result.max = simplify(max(result.max, result_i.max));
}
}
return result;
}
namespace {
class derivative : public node_mutator {
symbol_id dx;
public:
derivative(symbol_id dx) : dx(dx) {}
template <typename T>
void visit_variable(const T* x) {
if (x->name == dx) {
set_result(1);
} else {
set_result(expr(0));
}
}
void visit(const variable* x) override { visit_variable(x); }
void visit(const wildcard* x) override { visit_variable(x); }
void visit(const constant*) override { set_result(expr(0)); }
void visit(const mul* x) override {
if (depends_on(x->a, dx) && depends_on(x->b, dx)) {
expr da = mutate(x->a);
expr db = mutate(x->b);
set_result(simplify(x, x->a, db) + simplify(x, da, x->b));
} else if (depends_on(x->a, dx)) {
set_result(simplify(x, mutate(x->a), x->b));
} else if (depends_on(x->b, dx)) {
set_result(simplify(x, x->a, mutate(x->b)));
} else {
set_result(expr(0));
}
}
void visit(const div* x) override {
if (depends_on(x->a, dx) && depends_on(x->b, dx)) {
expr da = mutate(x->a);
expr db = mutate(x->b);
set_result((da * x->b - x->a * db) / (x->b * x->b));
} else if (depends_on(x->a, dx)) {
set_result(mutate(x->a) / x->b);
} else if (depends_on(x->b, dx)) {
expr db = mutate(x->b);
set_result(-x->a / (x->b * x->b));
} else {
set_result(expr(0));
}
}
virtual void visit(const mod* x) override { set_result(indeterminate()); }
virtual void visit(const class min* x) override { set_result(select(x->a < x->b, mutate(x->a), mutate(x->b))); }
virtual void visit(const class max* x) override { set_result(select(x->b < x->a, mutate(x->a), mutate(x->b))); }
template <typename T>
void visit_compare(const T* x) {
if (depends_on(x->a, dx) || depends_on(x->b, dx)) {
set_result(indeterminate());
} else {
set_result(expr(0));
}
}
virtual void visit(const equal* x) override { visit_compare(x); }
virtual void visit(const not_equal* x) override { visit_compare(x); }
virtual void visit(const less* x) override { visit_compare(x); }
virtual void visit(const less_equal* x) override { visit_compare(x); }
virtual void visit(const logical_and* x) override {}
virtual void visit(const logical_or* x) override {}
virtual void visit(const logical_not* x) override { set_result(-mutate(x->x)); }
virtual void visit(const class select* x) override {
set_result(select(x->condition, mutate(x->true_value), mutate(x->false_value)));
}
virtual void visit(const call* x) override { std::abort(); }
};
} // namespace
expr differentiate(const expr& f, symbol_id x) { return derivative(x).mutate(f); }
} // namespace slinky